3

Can Chrome/ium or Firefox browser extensions (also called add-ons/WebExtensions) actually somehow overwrite the result of a media query?

I mean, for JS window.match​Media() this is likely easy: Just inject a content script that overwrites that JS function.

But if "real" CSS media queries (inside css files) are used, this is not really possible, is it?

I could obviously inject my own CSS, but this is not what I want: I just want to trigger the website's CSS to do something different, i.e. assume a different media query result.

Some background: If you wonder why, my use case would be to overwrite the new prefers-color-scheme CSS media feature introduced in Firefox 67. (It's not currently available for any other browsers that support browser/WebExtensions.)


Cross-posted on Mozilla's Discourse instance.

rugk
  • 4,755
  • 2
  • 28
  • 55
  • 1
    All the CSS gets loaded into `document.stylesheets`, which JavaScript can read and modify, just like it can modify the rest of the DOM. I'd be surprised if an extension can't get into it. – Barmar Apr 24 '19 at 20:59
  • Thanks, though quickly looking into it, this seems to be quite hard: There is no pure CSS there, there are list's of CSS and remote locations. In the end, it looks like I would need to re-download files or even write my own CSS parser in my add-on to do this. – rugk Apr 24 '19 at 21:03
  • The `styleSheet` object contains the parsed styles, analogous to the way the DOM contains parsed HTML. You can iterate through it and modify properties directly, you don't need to parse the file yourself. – Barmar Apr 24 '19 at 21:10
  • @Barmar Okay, so if you want, feel free to submit a real answer (with some details). It makes no sense to discuss this in the comments. – rugk Apr 24 '19 at 21:11
  • I'm not going to work out the precise details for what you're trying to do, but here's an example of code that modifies stylesheets on the fly: https://stackoverflow.com/a/8630641/1491895 – Barmar Apr 24 '19 at 21:12
  • It's not as easy as DOM modification, because there's nothing analogous to `getElementById` or `querySelector`. You have to write your own code that loops over the `styleSheets` list and drill down to find the item you want to modify. – Barmar Apr 24 '19 at 21:13
  • There's currently no way to force-activate a specific media query so you'll have to inject your own CSS or process the site's one - parsing can be performed by creating a dummy style element in the background script. You can suggest to Mozilla's devs to implement an extension API to control the prefers-color-scheme feature. – wOxxOm Apr 25 '19 at 04:24

1 Answers1

1

Workaround

Thanks for all others, who pointed me to the right direction. Basically, there is no easy way, but you can:

  • inject a content script and iterate document.styleSheets, where all parsed style sheets are already loaded. That's the hard task. However, this is all read-only, so you cannot modify it directly.
  • Then you need to send that result back to your background script (as content scripts do not have access to the required API) and apply the CSS manually via browser.tabs.insertCSS.

As for the first task, here is the code snippet (one time written in functional way and one time just structural) that returns all the CSS, given a media query.
You e.g. can call getCssForMediaQueryFunc("(prefers-color-scheme: dark)") and get all the CSS applied for the dark color scheme.

/**
 * Return CSS from the website for a specific query string.
 *
 * (functional implementation)
 *
 * @private
 * @param {string} queryString
 * @returns {string}
 */
function getCssForMediaQueryFunc(queryString) {
    return Array.from(document.styleSheets).reduce((prev, styleSheet) => {
        /* workaround for crazy HTML spec throwing an SecurityError here,
         * see https://discourse.mozilla.org/t/accessing-some-fonts-css-style-sheet-via-stylesheet/38717?u=rugkx
         * and https://stackoverflow.com/questions/21642277/security-error-the-operation-is-insecure-in-firefox-document-stylesheets */
        try {
            styleSheet.cssRules; // eslint-disable-line no-unused-expressions
        } catch (e) {
            return prev;
        }

        return Array.from(styleSheet.cssRules).reduce((prev, cssRule) => {
            if (cssRule instanceof CSSMediaRule) {
                if (cssRule.conditionText === queryString) {
                    return Array.from(cssRule.cssRules).reduce((prev, subCssRule) => {
                        return prev + subCssRule.cssText;
                    }, prev);
                }
            }
            return prev;
        }, prev);
    }, "");
}

/**
 * Return CSS from the website for a specific query string.
 *
 * @private
 * @param {string} queryString
 * @returns {string}
 */
function getCssForMediaQuery(queryString) { // eslint-disable-line no-unused-vars
    let cssRules = "";
    for (const styleSheet of document.styleSheets) {
        /* workaround for crazy HTML spec throwing an SecurityError here,
         * see https://discourse.mozilla.org/t/accessing-some-fonts-css-style-sheet-via-stylesheet/38717?u=rugkx
         * and https://stackoverflow.com/questions/21642277/security-error-the-operation-is-insecure-in-firefox-document-stylesheets */
        try {
            styleSheet.cssRules; // eslint-disable-line no-unused-expressions
        } catch (e) {
            continue;
        }

        for (const cssRule of styleSheet.cssRules) {
            if (cssRule instanceof CSSMediaRule) {
                if (cssRule.conditionText === queryString) {
                    for (const subCssRule of cssRule.cssRules) {
                        cssRules = cssRules + subCssRule.cssText;
                    }
                }
            }
        }
    }
    return cssRules;
}

(up-to-date code should be accessible here)

  • As mentioned before, you also need to overwrite window.match​Media() to also fake the result of the websites that could use JS for the detection. However, this is also it's own non-trivial task and requires exporting this function from the content script to the website. (Also faking that is hard.)

Proof of concept

I've implemented this whole thing as a more or less proof-of-concept in an add-on here, also available on addons.mozilla.org (AMO). (I've used permalinks here, but the add-on may of course be updated in the future.)

Future

Obviously, this is not a nice method, so I've created a new Bugzilla issue to find a better solution for that, e.g. a special Firefox API.

rugk
  • 4,755
  • 2
  • 28
  • 55