[mv3] Programmatically inject content scripts on version change

Related issue:
https://github.com/w3c/webextensions/issues/617
This commit is contained in:
Raymond Hill 2025-04-20 16:44:08 -04:00
parent 782fff35ea
commit d0e32a5f47
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
3 changed files with 96 additions and 9 deletions

View file

@ -463,10 +463,9 @@ async function launch() {
// Permissions may have been removed while the extension was disabled
await syncWithBrowserPermissions();
// Unsure whether the browser remembers correctly registered css/scripts
// after we quit the browser. For now uBOL will check unconditionally at
// launch time whether content css/scripts are properly registered.
registerInjectables();
// Ensure that scriplets are registered. Force-execute them when a new
// version is detected.
registerInjectables(isNewVersion);
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest
// Firefox API does not support `dnr.setExtensionActionOptions`

View file

@ -27,6 +27,12 @@ export const browser = webext;
export const i18n = browser.i18n;
export const runtime = browser.runtime;
export const windows = browser.windows;
export const vendor = (( ) => {
const url = browser.runtime.getURL('');
const pos = url.indexOf(':');
if ( pos === -1 ) { return ''; }
return url.slice(0, pos);
})();
/******************************************************************************/

View file

@ -21,7 +21,7 @@
import * as ut from './utils.js';
import { browser } from './ext.js';
import { browser, vendor } from './ext.js';
import { fetchJSON } from './fetch.js';
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
import { getFilteringModeDetails } from './mode-manager.js';
@ -168,6 +168,7 @@ function registerHighGeneric(context, genericDetails) {
// update
if (
context.isNewVersion ||
arrayEq(registered.css, css, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
@ -234,6 +235,7 @@ function registerGeneric(context, genericDetails) {
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, directive.matches) === false
) {
context.isNewVersion ||
context.toRemove.push('css-generic-some');
context.toAdd.push(directive);
}
@ -260,6 +262,7 @@ function registerGeneric(context, genericDetails) {
arrayEq(registeredAll.js, js, false) === false ||
arrayEq(registeredAll.excludeMatches, directiveAll.excludeMatches) === false
) {
context.isNewVersion ||
context.toRemove.push('css-generic-all');
context.toAdd.push(directiveAll);
}
@ -284,6 +287,7 @@ function registerGeneric(context, genericDetails) {
arrayEq(registeredSome.js, js, false) === false ||
arrayEq(registeredSome.matches, directiveSome.matches) === false
) {
context.isNewVersion ||
context.toRemove.push('css-generic-some');
context.toAdd.push(directiveSome);
}
@ -342,6 +346,7 @@ function registerProcedural(context) {
// update
if (
context.isNewVersion ||
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
@ -404,6 +409,7 @@ function registerDeclarative(context) {
// update
if (
context.isNewVersion ||
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
@ -466,6 +472,7 @@ function registerSpecific(context) {
// update
if (
context.isNewVersion ||
arrayEq(registered.js, js, false) === false ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
@ -545,6 +552,7 @@ function registerScriptlet(context, scriptletDetails) {
// update
if (
context.isNewVersion ||
arrayEq(registered.matches, matches) === false ||
arrayEq(registered.excludeMatches, excludeMatches) === false
) {
@ -557,10 +565,77 @@ function registerScriptlet(context, scriptletDetails) {
/******************************************************************************/
// Issue: Safari appears to completely ignore excludeMatches
// https://github.com/radiolondra/ExcludeMatches-Test
async function injectImmediately(tabId, info) {
try {
const results = await browser.scripting.executeScript({
args: [ info.matches || [], info.excludeMatches || [] ],
func: injectImmediately.targetMatches,
target: { tabId },
});
if ( Array.isArray(results) === false ) { return; }
if ( results.length === 0 ) { return; }
const { frameId, result } = results[0]
if ( result !== true ) { return; }
if ( Array.isArray(info.js) && info.js.length !== 0 ) {
browser.scripting.executeScript({
files: info.js,
injectImmediately: info.runAt === 'document_start',
world: info.world || 'ISOLATED',
target: { tabId, frameIds: [ frameId ] },
}).catch(( ) => { });
} else if ( Array.isArray(info.css) && info.css.length !== 0 ) {
browser.scripting.insertCSS({
files: info.css,
origin: info.origin,
target: { tabId, frameIds: [ frameId ] },
}).catch(( ) => { });
}
} catch {
return;
}
return true;
}
async function registerInjectables() {
injectImmediately.targetMatches = function(matches, excludeMatches) {
let matched = matches.includes('<all_urls>');
if ( matched === false ) {
let hn = document.location.hostname;
for (;;) {
matched = matches.includes(`*://*.${hn}/*`);
if ( matched ) { break; }
const pos = hn.indexOf('.');
if ( pos === -1 ) { break; }
hn = hn.slice(pos + 1);
}
if ( matched === false ) { return false; }
}
let hn = document.location.hostname;
for (;;) {
if ( excludeMatches.includes(`*://*.${hn}/*`) ) { return false; }
const pos = hn.indexOf('.');
if ( pos === -1 ) { break; }
hn = hn.slice(pos + 1);
}
return true;
};
async function installContentScripts(toInject) {
const tabs = await browser.tabs.query({ discarded: false });
const promises = [];
for ( const tab of tabs ) {
if ( tab.status === 'unloaded' ) { continue; }
for ( const info of toInject ) {
promises.push(injectImmediately(tab.id, info));
}
}
const results = await Promise.all(promises);
const count = results.reduce((a, b) => b ? a+1 : a, 0);
ubolLog(`Injected ${count} scriptlets into already opened tabs`);
}
/******************************************************************************/
async function registerInjectables(isNewVersion = false) {
if ( browser.scripting === undefined ) { return false; }
if ( registerInjectables.barrier ) { return true; }
@ -586,6 +661,7 @@ async function registerInjectables() {
);
const toAdd = [], toRemove = [];
const context = {
isNewVersion,
filteringModeDetails,
rulesetsDetails,
before,
@ -611,7 +687,13 @@ async function registerInjectables() {
if ( toAdd.length !== 0 ) {
ubolLog(`Registered ${toAdd.map(v => v.id)} content (css/js)`);
await browser.scripting.registerContentScripts(toAdd)
.catch(reason => { console.info(reason); });
.catch(reason => { ubolLog(reason); });
// Chromium-based browsers do not inject newly registered scripts into
// already opened tabs, so we do this manually.
// https://github.com/w3c/webextensions/issues/617
if ( isNewVersion && vendor === 'chrome-extension' ) {
installContentScripts(toAdd);
}
}
registerInjectables.barrier = false;