Optionally inject Content Script
Solution 1:
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:
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.Use the
webNavigation
API (e.g.chrome.webNavigation.onCommitted
) to detect when the user has navigated to the page, then usechrome.tabs.executeScript
to insert the content script in the tab (orchrome.tabs.insertCSS
to insert styles).Use the
tabs
API (chrome.tabs.onUpdated
) to detect that a page might have changed, and insert a content script in the page usingchrome.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).
Solution 2:
I'll split this 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. In the future you will be able to use chrome.scripting.registerContentScript
but it's not yet implemented.
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": [
"*://*/*"
]
}
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.