From 0101a47d2550090444f058acb765b481aaeb49c3 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 27 Dec 2025 14:12:31 -0500 Subject: [PATCH] [mv3] Try to address Safari's webextensions API issues 1) Reset "seen realms" when modifying enabled rulesets. The "seen realms" code path is an attempt at mitigating an issue whereas the DNR rules would not be enforced under certain circumstances. 2) Re-write the scripting manager to always wholly unregister/register all content scripts instead of only the differences before/after following a change affecting content scripts. --- platform/mv3/extension/js/action.js | 12 +- platform/mv3/extension/js/filter-manager.js | 11 +- .../mv3/extension/js/scripting-manager.js | 148 ++---------------- platform/mv3/safari/ext-compat.js | 87 +++++----- 4 files changed, 63 insertions(+), 195 deletions(-) diff --git a/platform/mv3/extension/js/action.js b/platform/mv3/extension/js/action.js index 811fe101c..7e194dd08 100644 --- a/platform/mv3/extension/js/action.js +++ b/platform/mv3/extension/js/action.js @@ -19,8 +19,8 @@ Home: https://github.com/gorhill/uBlock */ -import { matchesFromHostnames, strArrayEq } from './utils.js'; import { browser } from './ext.js'; +import { matchesFromHostnames } from './utils.js'; /******************************************************************************/ @@ -91,9 +91,6 @@ export async function registerToolbarIconToggler(context) { if ( toToggle.size === 0 ) { return; } - const registered = context.before.get('toolbar-icon'); - context.before.delete('toolbar-icon'); // Important! - const directive = { id: 'toolbar-icon', js: [ '/js/scripting/toolbar-icon.js' ], @@ -101,10 +98,5 @@ export async function registerToolbarIconToggler(context) { runAt: 'document_start', }; - if ( registered === undefined ) { - context.toAdd.push(directive); - } else if ( strArrayEq(registered.matches, directive.matches) === false ) { - context.toRemove.push('toolbar-icon'); - context.toAdd.push(directive); - } + context.toAdd.push(directive); } diff --git a/platform/mv3/extension/js/filter-manager.js b/platform/mv3/extension/js/filter-manager.js index c7deaab93..b1ec9464d 100644 --- a/platform/mv3/extension/js/filter-manager.js +++ b/platform/mv3/extension/js/filter-manager.js @@ -30,7 +30,6 @@ import { import { intersectHostnameIters, matchesFromHostnames, - strArrayEq, subtractHostnameIters, } from './utils.js'; @@ -189,9 +188,6 @@ export async function registerCustomFilters(context) { } if ( hostnames.length === 0 ) { return; } - const registered = context.before.get('css-user'); - context.before.delete('css-user'); // Important! - const directive = { id: 'css-user', js: [ '/js/scripting/css-user.js' ], @@ -199,12 +195,7 @@ export async function registerCustomFilters(context) { runAt: 'document_start', }; - if ( registered === undefined ) { - context.toAdd.push(directive); - } else if ( strArrayEq(registered.matches, directive.matches) === false ) { - context.toRemove.push('css-user'); - context.toAdd.push(directive); - } + context.toAdd.push(directive); } /******************************************************************************/ diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 338c0cdfc..6e5aa13f1 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -71,29 +71,6 @@ const normalizeMatches = matches => { /******************************************************************************/ -// The extensions API does not always return exactly what we fed it, so we -// need to normalize some entries to be sure we properly detect changes when -// comparing registered entries vs. entries to register. - -const normalizeRegisteredContentScripts = registered => { - for ( const entry of registered ) { - const { css = [], js = [] } = entry; - for ( let i = 0; i < css.length; i++ ) { - const path = css[i]; - if ( path.startsWith('/') ) { continue; } - css[i] = `/${path}`; - } - for ( let i = 0; i < js.length; i++ ) { - const path = js[i]; - if ( path.startsWith('/') ) { continue; } - js[i] = `/${path}`; - } - } - return registered; -}; - -/******************************************************************************/ - async function resetCSSCache() { const keys = await sessionKeys(); return Promise.all( @@ -104,7 +81,7 @@ async function resetCSSCache() { /******************************************************************************/ function registerHighGeneric(context, genericDetails) { - const { before, filteringModeDetails, rulesetsDetails } = context; + const { filteringModeDetails, rulesetsDetails } = context; const excludeHostnames = []; const includeHostnames = []; @@ -145,12 +122,8 @@ function registerHighGeneric(context, genericDetails) { ) ); } - if ( matches.length === 0 ) { return; } - const registered = before.get('css-generichigh'); - before.delete('css-generichigh'); // Important! - // https://github.com/w3c/webextensions/issues/414#issuecomment-1623992885 // Once supported, add: // cssOrigin: 'USER', @@ -166,26 +139,13 @@ function registerHighGeneric(context, genericDetails) { } // register - if ( registered === undefined ) { - context.toAdd.push(directive); - return; - } - - // update - if ( - ut.strArrayEq(registered.css, css, false) === false || - ut.strArrayEq(registered.matches, matches) === false || - ut.strArrayEq(registered.excludeMatches, excludeMatches) === false - ) { - context.toRemove.push('css-generichigh'); - context.toAdd.push(directive); - } + context.toAdd.push(directive); } /******************************************************************************/ function registerGeneric(context, genericDetails) { - const { before, filteringModeDetails, rulesetsDetails } = context; + const { filteringModeDetails, rulesetsDetails } = context; const excludedByFilter = []; const includedByFilter = []; @@ -224,8 +184,6 @@ function registerGeneric(context, genericDetails) { ), ]; if ( matches.length === 0 ) { return; } - const registered = before.get('css-generic-some'); - before.delete('css-generic-some'); // Important! const directive = { id: 'css-generic-some', js, @@ -233,15 +191,7 @@ function registerGeneric(context, genericDetails) { matches, runAt: 'document_idle', }; - if ( registered === undefined ) { // register - context.toAdd.push(directive); - } else if ( // update - ut.strArrayEq(registered.js, js, false) === false || - ut.strArrayEq(registered.matches, directive.matches) === false - ) { - context.toRemove.push('css-generic-some'); - context.toAdd.push(directive); - } + context.toAdd.push(directive); return; } @@ -249,8 +199,6 @@ function registerGeneric(context, genericDetails) { ...ut.matchesFromHostnames(excludedByMode), ...ut.matchesFromHostnames(excludedByFilter), ]; - const registeredAll = before.get('css-generic-all'); - before.delete('css-generic-all'); // Important! const directiveAll = { id: 'css-generic-all', js, @@ -261,24 +209,14 @@ function registerGeneric(context, genericDetails) { if ( excludeMatches.length !== 0 ) { directiveAll.excludeMatches = excludeMatches; } + context.toAdd.push(directiveAll); - if ( registeredAll === undefined ) { // register - context.toAdd.push(directiveAll); - } else if ( // update - ut.strArrayEq(registeredAll.js, js, false) === false || - ut.strArrayEq(registeredAll.excludeMatches, directiveAll.excludeMatches) === false - ) { - context.toRemove.push('css-generic-all'); - context.toAdd.push(directiveAll); - } const matches = [ ...ut.matchesFromHostnames( ut.subtractHostnameIters(includedByFilter, excludedByMode) ), ]; if ( matches.length === 0 ) { return; } - const registeredSome = before.get('css-generic-some'); - before.delete('css-generic-some'); // Important! const directiveSome = { id: 'css-generic-some', js, @@ -286,21 +224,13 @@ function registerGeneric(context, genericDetails) { matches, runAt: 'document_idle', }; - if ( registeredSome === undefined ) { // register - context.toAdd.push(directiveSome); - } else if ( // update - ut.strArrayEq(registeredSome.js, js, false) === false || - ut.strArrayEq(registeredSome.matches, directiveSome.matches) === false - ) { - context.toRemove.push('css-generic-some'); - context.toAdd.push(directiveSome); - } + context.toAdd.push(directiveSome); } /******************************************************************************/ async function registerCosmetic(realm, context) { - const { before, filteringModeDetails, rulesetsDetails } = context; + const { filteringModeDetails, rulesetsDetails } = context; { const keys = await localKeys(); @@ -355,9 +285,6 @@ async function registerCosmetic(realm, context) { } } - const registered = before.get(realmid); - before.delete(realmid); // Important! - const directive = { id: realmid, js, @@ -370,26 +297,13 @@ async function registerCosmetic(realm, context) { } // register - if ( registered === undefined ) { - context.toAdd.push(directive); - return; - } - - // update - if ( - ut.strArrayEq(registered.js, js, false) === false || - ut.strArrayEq(registered.matches, matches) === false || - ut.strArrayEq(registered.excludeMatches, excludeMatches) === false - ) { - context.toRemove.push(realmid); - context.toAdd.push(directive); - } + context.toAdd.push(directive); } /******************************************************************************/ function registerScriptlet(context, scriptletDetails) { - const { before, filteringModeDetails, rulesetsDetails } = context; + const { filteringModeDetails, rulesetsDetails } = context; const hasBroadHostPermission = filteringModeDetails.optimal.has('all-urls') || @@ -431,9 +345,6 @@ function registerScriptlet(context, scriptletDetails) { matches.push(...ut.matchesFromHostnames(targetHostnames)); normalizeMatches(matches); - const registered = before.get(id); - before.delete(id); // Important! - const directive = { id, js: [ `/rulesets/scripting/scriptlet/${world.toLowerCase()}/${rulesetId}.js` ], @@ -448,19 +359,7 @@ function registerScriptlet(context, scriptletDetails) { } // register - if ( registered === undefined ) { - context.toAdd.push(directive); - continue; - } - - // update - if ( - ut.strArrayEq(registered.matches, matches) === false || - ut.strArrayEq(registered.excludeMatches, excludeMatches) === false - ) { - context.toRemove.push(id); - context.toAdd.push(directive); - } + context.toAdd.push(directive); } } } @@ -481,26 +380,17 @@ export async function registerInjectables() { rulesetsDetails, scriptletDetails, genericDetails, - registered, ] = await Promise.all([ getFilteringModeDetails(), getEnabledRulesetsDetails(), getScriptletDetails(), getGenericDetails(), - browser.scripting.getRegisteredContentScripts(), ]); - const before = new Map( - normalizeRegisteredContentScripts(registered).map( - entry => [ entry.id, entry ] - ) - ); - const toAdd = [], toRemove = []; + const toAdd = []; const context = { filteringModeDetails, rulesetsDetails, - before, toAdd, - toRemove, }; await Promise.all([ @@ -513,23 +403,17 @@ export async function registerInjectables() { registerToolbarIconToggler(context), ]); - toRemove.push(...Array.from(before.keys())); - - if ( toRemove.length !== 0 ) { - ubolLog(`Unregistered ${toRemove} content (css/js)`); - try { - await browser.scripting.unregisterContentScripts({ ids: toRemove }); - localRemove('$scripting.unregisterContentScripts'); - } catch(reason) { - ubolErr(`unregisterContentScripts/${reason}`); - } + ubolLog(`Unregistered all content (css/js)`); + try { + await browser.scripting.unregisterContentScripts(); + } catch(reason) { + ubolErr(`unregisterContentScripts/${reason}`); } if ( toAdd.length !== 0 ) { ubolLog(`Registered ${toAdd.map(v => v.id)} content (css/js)`); try { await browser.scripting.registerContentScripts(toAdd); - localRemove('$scripting.registerContentScripts'); } catch(reason) { ubolErr(`registerContentScripts/${reason}`); } diff --git a/platform/mv3/safari/ext-compat.js b/platform/mv3/safari/ext-compat.js index be095c48b..9fe21bae7 100644 --- a/platform/mv3/safari/ext-compat.js +++ b/platform/mv3/safari/ext-compat.js @@ -24,6 +24,46 @@ export const webext = self.browser; /******************************************************************************/ +// Workaround for: +// https://github.com/uBlockOrigin/uBOL-home/issues/515 +// https://bugs.webkit.org/show_bug.cgi?id=300236 +// +// For each realm, we will force-reload registered rulesets once. + +const { windows } = webext; +const NORMAL_REALM = 0b01; +const PRIVATE_REALM = 0b10; +const ALL_REALMS = NORMAL_REALM | PRIVATE_REALM; + +let seenRealms = 0b00; +let seenRealmsReady = webext.storage.session.get('safari.seenRealms').then(bin => { + seenRealms |= bin?.['safari.seenRealms'] ?? 0; +}).catch(( ) => { +}); + +async function forceEnableRulesets(windowId) { + await seenRealmsReady; + if ( seenRealms === ALL_REALMS ) { return; } + if ( windowId === windows.WINDOW_ID_NONE ) { return; } + const details = await windows.get(windowId, { windowTypes: [ 'normal' ] }); + const incognito = details?.incognito; + if ( typeof incognito !== 'boolean' ) { return; } + const currentRealm = incognito ? PRIVATE_REALM : NORMAL_REALM; + if ( (seenRealms & currentRealm) !== 0 ) { return; } + seenRealms |= currentRealm; + webext.storage.session.set({ 'safari.seenRealms': seenRealms }); + const ids = await nativeDNR.getEnabledRulesets(); + if ( ids.length === 0 ) { return; } + nativeDNR.updateEnabledRulesets({ + disableRulesetIds: ids.slice(), + enableRulesetIds: ids.slice(), + }); +} + +windows.onFocusChanged.addListener(forceEnableRulesets); + +/******************************************************************************/ + // https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest/ const nativeDNR = webext.declarativeNetRequest; @@ -142,8 +182,10 @@ export const dnr = { if ( optionsAfter === undefined ) { return; } return nativeDNR.updateDynamicRules(optionsAfter); }, - updateEnabledRulesets(...args) { - return nativeDNR.updateEnabledRulesets(...args); + async updateEnabledRulesets(...args) { + await nativeDNR.updateEnabledRulesets(...args); + seenRealms = 0b00; + await webext.storage.session.remove('safari.seenRealms'); }, async updateSessionRules(optionsBefore) { const optionsAfter = prepareUpdateRules(optionsBefore); @@ -181,44 +223,3 @@ export const dnr = { return nativeDNR.setExtensionActionOptions(...args); }, }; - -/******************************************************************************/ - -// Workaround for: -// https://github.com/uBlockOrigin/uBOL-home/issues/515 -// https://bugs.webkit.org/show_bug.cgi?id=300236 -// -// For each realm, we will force-reload registered rulesets once. - -const { windows } = webext; -const NORMAL_REALM = 0b01; -const PRIVATE_REALM = 0b10; -const ALL_REALMS = NORMAL_REALM | PRIVATE_REALM; -let seenRealms = 0b00; - -async function forceEnableRulesets(windowId) { - await seenRealmsReady; - if ( seenRealms === ALL_REALMS ) { return; } - if ( windowId === windows.WINDOW_ID_NONE ) { return; } - const details = await windows.get(windowId, { windowTypes: [ 'normal' ] }); - const incognito = details?.incognito; - if ( typeof incognito !== 'boolean' ) { return; } - const currentRealm = incognito ? PRIVATE_REALM : NORMAL_REALM; - if ( (seenRealms & currentRealm) !== 0 ) { return; } - seenRealms |= currentRealm; - webext.storage.session.set({ 'safari.seenRealms': seenRealms }); - const ids = await nativeDNR.getEnabledRulesets(); - if ( ids.length === 0 ) { return; } - nativeDNR.updateEnabledRulesets({ - disableRulesetIds: ids.slice(), - enableRulesetIds: ids.slice(), - }); -} - -windows.onFocusChanged.addListener(forceEnableRulesets); - -const seenRealmsReady = webext.storage.session.get('safari.seenRealms').then(bin => { - seenRealms |= bin?.['safari.seenRealms'] ?? 0; -}).catch(( ) => { -}); -