21

Content Script can be injected programatically or permanently by declaring in Extension manifest file. Programatic injection require host permission, which is generally grant by browser or page action.

In my use case, I want to inject gmail, outlook.com and yahoo mail web site without user action. I can do by declaring all of them manifest, but by doing so require all data access to those account. Some use may want to grant only outlook.com, but not gmail. Programatic injection does not work because I need to know when to inject. Using tabs permission is also require another permission.

Is there any good way to optionally inject web site?

Kyaw Tun
  • 12,447
  • 10
  • 56
  • 83

3 Answers3

18

You cannot run code on a site without the appropriate permissions. Fortunately, you can add the host permissions to optional_permissions in the manifest file to declare them optional and still allow the extension to use them.

In response to a user gesture, you can use chrome.permission.request to request additional permissions. This API can only be used in extension pages (background page, popup page, options page, ...). As of Chrome 36.0.1957.0, the required user gesture also carries over from content scripts, so if you want to, you could add a click event listener from a content script and use chrome.runtime.sendMessage to send the request to the background page, which in turn calls chrome.permissions.request.

Optional code execution in tabs

After obtaining the host permissions (optional or mandatory), you have to somehow inject the content script (or CSS style) in the matching pages. There are a few options, in order of my preference:

  1. Use the chrome.declarativeContent.RequestContentScript action to insert a content script in the page. Read the documentation if you want to learn how to use this API.

  2. Use the webNavigation API (e.g. chrome.webNavigation.onCommitted) to detect when the user has navigated to the page, then use chrome.tabs.executeScript to insert the content script in the tab (or chrome.tabs.insertCSS to insert styles).

  3. Use the tabs API (chrome.tabs.onUpdated) to detect that a page might have changed, and insert a content script in the page using chrome.tabs.executeScript.

I strongly recommend option 1, because it was specifically designed for this use case. Note: This API was added in Chrome 38, but only worked with optional permissions since Chrome 39. Despite the "WARNING: This action is still experimental and is not supported on stable builds of Chrome." in the documentation, the API is actually supported on stable. Initially the idea was to wait for a review before publishing the API on stable, but that review never came and so now this API has been working fine for almost two years.

The second and third options are similar. The difference between the two is that using the webNavigation API adds an additional permission warning ("Read your browsing history"). For this warning, you get an API that can efficiently filter the navigations, so the number of chrome.tabs.executeScript calls can be minimized.

If you don't want to put this extra permission warning in your permission dialog, then you could blindly try to inject on every tab. If your extension has the permission, then the injection will succeed. Otherwise, it fails. This doesn't sound very efficient, and it is not... ...on the bright side, this method does not require any additional permissions.

By using either of the latter two methods, your content script must be designed in such a way that it can handle multiple insertions (e.g. with a guard). Inserting in frames is also supported (allFrames:true), but only if your extension is allowed to access the tab's URL (or the frame's URL if frameId is set).

IvanRF
  • 7,115
  • 5
  • 47
  • 71
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • Woo, new `declarativeContent` actions! Cool. – Xan Oct 31 '14 at 16:27
  • 1
    Contradictory information in the docs though. "Since Chrome 38" and "WARNING: This action is still experimental and is not supported on stable builds of Chrome." – Xan Oct 31 '14 at 16:27
  • @Xan Yes, see the linked bug in my answer: https://crbug.com/409147. The intent was to keep it away from stable until the review was completed, but the feature landed anyway. And the docs is not completely contradictory, because features can be disabled on stable builds (e.g. look at [infobars](https://developer.chrome.com/extensions/infobars) or [`chrome.declarativeWebRequest`](https://developer.chrome.com/extensions/declarativeWebRequest)). – Rob W Oct 31 '14 at 16:28
  • @RobW Thanks for excellent answer. I learn many new things from your answer. You answer hint that, I can use content script declearing with optional permissions. However Chrome still request host permission during the installation for using them in content script, even though I declear them as optional. – Kyaw Tun Nov 02 '14 at 05:04
  • @KyawTun Make sure that the optional permissions are ONLY declared in `"optional_permissions"`, and not in `"permissions"` or in `content_scripts[*].matches`. – Rob W Nov 02 '14 at 08:29
  • Thx. It works. Having another issue for hiding page icon https://code.google.com/p/chromium/issues/detail?id=429603 – Kyaw Tun Nov 03 '14 at 10:27
  • @KyawTun I triaged that bug report for you. The only way to work around that issue is to unregister the page action, or use a different method of showing the page action (e.g. in response to the webNavigation API, see e.g. https://stackoverflow.com/a/20856789/938089). – Rob W Nov 03 '14 at 10:38
  • @RobW are you saying that the RequestContentScript action is or is not available in stable builds? It's been a while since your reply so this may be out-dated, but I can't seem to get my scripts to be injected. No errors even if the script path is bogus. – Tristan Lee May 04 '16 at 22:48
  • @TristanLee I'm saying that it was not intended to go stable, but it did anyway and no-one ever reverted it. So even though the documentation of RequestContentScript says that it might not work on stable, it *does* work on stable. – Rob W May 04 '16 at 22:54
  • 1
    @RobW are you able to get stylesheets to inject using the chrome.declarativeContent.RequestContentScript method? My js files are injecting properly but my css files don't seem to be injected at all via RequestContentScript({js: [...], css: [...]}) – istan Apr 04 '17 at 06:17
  • 4
    @istan "css" does not appear to work, I reported it here: https://crbug.com/708115 As a work-around, you could use "js" to insert a ` – Rob W Apr 04 '17 at 09:32
  • After three years, `RequestContentScript` is still experimental. – Kyaw Tun Jun 06 '17 at 03:36
  • @RobW I've tried every option now without any luck, I wrote all the details in [this new question](https://stackoverflow.com/q/44425102/1718678) – IvanRF Jun 08 '17 at 01:15
  • @RobW, But does `RequestContentScript` activates before any script has run? Even before `document_start`? – Pacerier Jul 16 '17 at 04:24
  • @Pacerier No it does not necessarily run before other scripts have run. Its implementation appears to be comparable to `chrome.tabs.executeScript`. – Rob W Jul 16 '17 at 08:22
  • In my real version test, I found that version >=60 is only available. And the allFrames option does not seem to be available. – weiya ou Oct 26 '20 at 14:39
  • I went with option 2(webNavigation), it feels like it executes the same way as how content scripts via declaration does. Sure the user has to accept the web navigation permission but that's only once in the beginning, after that, its just the optional_parameters that don't trigger extension disabled like content_scripts matches :) – AEQ Dec 22 '20 at 00:28
7

I advise against using declarativeContent APIs because they're deprecated and buggy with CSS, as described by the last comment on https://bugs.chromium.org/p/chromium/issues/detail?id=708115.

Use the new content script registration APIs instead. Here's what you need, in two parts:

Programmatic script injection

There's a new contentScripts.register() API which can programmatically register content scripts and they'll be loaded exactly like content_scripts defined in the manifest:

browser.contentScripts.register({
    matches: ['https://your-dynamic-domain.example.com/*'],
    js: [{file: 'content.js'}]
});

This API is only available in Firefox but there's a Chrome polyfill you can use. If you're using Manifest v3, there's the native chrome.scripting.registerContentScript which does the same thing but slightly differently.

Acquiring new permissions

By using chrome.permissions.request you can add new domains on which you can inject content scripts. An example would be:

// In a content script or options page
document.querySelector('button').addEventListener('click', () => {
    chrome.permissions.request({
        origins: ['https://your-dynamic-domain.example.com/*']
    }, granted => {
        if (granted) {
            /* Use contentScripts.register */
        }
    });
});

And you'll have to add optional_permissions in your manifest.json to allow new origins to be requested:

{
    "optional_permissions": [
        "*://*/*"
    ]
}

In Manifest v3 this property was renamed to optional_host_permissions.


I also wrote some tools to further simplify this for you and for the end user, such as webext-domain-permission-toggle and webext-dynamic-content-scripts. They will automatically register your scripts in the next browser launches and allow the user the remove the new permissions and scripts.

fregante
  • 29,050
  • 14
  • 119
  • 159
  • that you for your answer! Just want to clarify, if I register content scripts programmatically, will chrome display a warning message "Can read and change all website data" right before installing the extension? My script is designed to run on every page. – sheriff_paul Oct 15 '19 at 20:19
  • No, as long as you don’t include `tabs` or `https://*/*` in `permissions` in the manifest. If needed they can only be `optional_permissions`. To clarify, with this setup your script will run on *any* page the user explicitly granted permission for via `chrome.permissions.request` – fregante Oct 21 '19 at 17:29
4

Since the existing answer is now a few years old, optional injection is now much easier and is described here. It says that to inject a new file conditionally, you can use the following code:

// The lines I have commented are in the documentation, but the uncommented
// lines are the important part

//chrome.runtime.onMessage.addListener((message, callback) => {
//   if (message == “runContentScript”){

        chrome.tabs.executeScript({
          file: 'contentScript.js'
        });

//   }
//});

You will need the Active Tab Permission to do this.

Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124