w3c / webextensions

Charter and administrivia for the WebExtensions Community Group (WECG)
Other
599 stars 56 forks source link

Proposal: Ability to insert CSS from content script context #403

Open gorhill opened 1 year ago

gorhill commented 1 year ago

Background

I have been working on a MV3-compatible lite version of uBlock Origin, named uBlock Origin Lite, or uBOL. The goal with the extension is to be fully declarative, such that no service worker is needed for the filtering to occur. The service worker is required only when the user interacts with the extension settings (either from the popup panel or options page.

The fully declarative nature of the extension is fulfilled, but there is currently a negative side effect as a result: the inability to reliably enforce cosmetic filtering on web pages.

This issue arises because the CSS styles to inject in a web page do not have precedence over the webpage's own CSS styles. This CSS styles precedence issue is normally resolved using the scripting.insertCSS(), but since uBOL is entirely declarative, it can't use this approach, as this would require the content script to send a message to the service worker, causing the service worker to wake up as a result. This would defeat the goal of being entirely declarative to avoid the cost of having to constantly wake up the service worker each time a webpage navigation occurs.

Solution

This issue would be solved if it was possible to call scripting.insertCSS() (or equivalent) from the content script context. The API in the context of a content script could be made simpler, as its purpose would be always to inject CSS targeting the current document (so no need for tab id, frame id, etc.)

chrome.scripting.insertCSS(
  injection: CSSContentScriptInjection
)

injection: The details of the styles to insert.

CSSContentScriptInjection

css: string: A string containing the CSS to inject.

origin?: StyleOrigin. The style origin for the injection. Defaults to AUTHOR.

Notes

There are already a set of API methods available from content script contexts: https://developer.chrome.com/docs/extensions/mv3/content_scripts/#capabilities, so this is not a new approach.

Using the browser dev tools, I notice that there is already an instance of chrome.scripting object in the context of the content script (possibly as a result of the content script being injected through scripting.registerContentScripts), so it would be a matter of adding insertCSS method to it.

Essentially the API method would accomplish in a more efficient way what can be already accomplished by sending a message to the service worker, and security-wise, I cannot see a difference.

This is sort of related to Proposal: Declarative Cosmetic Rules, but adding API method scripting.insertCSS in the context of a content script would not make the issues raised in #362 a roadblock while reaching the same goal of being fully declarative.

References

Related issue in uBOL repo: https://github.com/uBlockOrigin/uBOL-issues/issues/5#issuecomment-1575425913

uBOL summary description: https://github.com/gorhill/uBlock/blob/master/platform/mv3/description/en.md

Chrome Web Store: https://chrome.google.com/webstore/detail/ublock-origin-lite/ddkjiahejlhfcafbddmgiahcphecmpfh


Updates

tophf commented 1 year ago

The method can be inside chrome.dom namespace. It'd be synchronous for the proposed use case of a literal css string.

there is already an instance of chrome.scripting object in the context of the content script

AFAIK it's an artifact from the currently abandoned implementation of an API method to configure parameters of the content script via chrome.scripting API in a full chrome-extension:// context, i.e. when it'll be finally implemented it may have a different namespace.

Rob--W commented 1 year ago

This issue was discussed during today's meeting; the meeting notes are pending publication at #408.

This issue arises because the CSS styles to inject in a web page do not have precedence over the webpage's own CSS styles. This CSS styles precedence issue is normally resolved using the scripting.insertCSS(),

I suppose that this is your main point of the feature request: as an extension you want the highest control over the appearance, and currently the only way to do that is by loading a stylesheet with "user" origin. While web pages (with "author") origin ordinarily take precedent over the "user" style sheets, the opposite is true when !important is added: https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade#cascading_order

there is already an instance of chrome.scripting object in the context of the content script

AFAIK it's an artifact from the currently abandoned implementation of an API method to configure parameters of the content script via chrome.scripting API in a full chrome-extension:// context, i.e. when it'll be finally implemented it may have a different namespace.

Specifically, globalParams.

gorhill commented 1 year ago

you want the highest control over the appearance, and currently the only way to do that is by loading a stylesheet with "user" origin

Yes. There is no guarantee that a constructable stylesheet will override the styles we want to override, even more so when those styles to override are inlined using the style attribute on an element.

Overriding using inline style is also problematic because this changes the style attribute, and some cosmetic filters do use the style attribute as a condition, for example:

##div[style="display:flex !important"] > div

And for reliability this also requires a mutation observer, so in the end beside being unreliable, it's inefficient.

Since this can already be accomplished by waking up the service worker so that it can inject the USER styles using scripting.injectCSS, the request is really for a way to do the same without having to wake up the service worker -- it does feel like an avoidable roundtrip to the service worker since the USER styles are meant to be injected in the current context.

I did modify the current code in my extension in order to inject USER styles through the service worker, and I have to admit this works rather well in Chromium, though I did take care to minimize initialization work performed by the service worker.

This doesn't work as well in Firefox though, there is a visible delay before the user styles are injected, and there seem to be other issues with reliability as the user styles are seemingly not always injected (reason unknown).

oliverdunk commented 1 year ago

I had some discussions about this on the Chrome side. Given scripting.insertCSS supports a target property we wouldn't want to expose in content scripts, exposing the same API namespace seems confusing. That said, we would be supportive of adding something to the browser.dom namespace, which hasn't previously been used outside of Chrome but feels like a nice place for this sort of functionality.

We also discussed if we would want to support the files parameter, or if we would require CSS as a string. We didn't have a definite answer there but were leaning towards not supporting files as this could allow a compromised content script to access files it otherwise wouldn't be able to.

oliverdunk commented 1 year ago

Adding https://github.com/w3c/webextensions/labels/supportive%3A%20chrome with the context above.