diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 6af564822..5afa975f2 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -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` diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index ee735b21b..1b95b11ec 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -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); +})(); /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 8ff9a5c36..37c754e34 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -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(''); + 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;