From 7414e67505d8f8579db33b556645c31890402f70 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 5 Dec 2025 16:37:52 -0500 Subject: [PATCH] experimental --- platform/mv3/extension/js/background.js | 3 + platform/mv3/extension/js/ext.js | 8 +++ .../mv3/extension/js/scripting-manager.js | 70 ++++++++++++++++--- .../extension/js/scripting/css-procedural.js | 51 +++++++++----- .../extension/js/scripting/css-specific.js | 51 +++++++++----- .../extension/js/scripting/isolated-api.js | 41 +++++++++++ platform/mv3/make-rulesets.js | 37 +++++----- .../mv3/scriptlets/css-procedural.template.js | 12 ++-- .../mv3/scriptlets/css-specific.template.js | 11 ++- 9 files changed, 209 insertions(+), 75 deletions(-) diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index dff432524..840cc4030 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -63,6 +63,7 @@ import { browser, localRead, localRemove, localWrite, runtime, + sessionAccessLevel, webextFlavor, } from './ext.js'; @@ -667,6 +668,8 @@ async function startSession() { // launch time whether content css/scripts are properly registered. registerInjectables(); + sessionAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' }); + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest // Firefox API does not support `dnr.setExtensionActionOptions` if ( canShowBlockedCount ) { diff --git a/platform/mv3/extension/js/ext.js b/platform/mv3/extension/js/ext.js index 78fb2c53b..a615aaf1c 100644 --- a/platform/mv3/extension/js/ext.js +++ b/platform/mv3/extension/js/ext.js @@ -100,6 +100,14 @@ export async function sessionRemove(key) { return browser.storage.session.remove(key); } +export async function sessionAccessLevel(level) { + try { + browser.storage.session.setAccessLevel(level); + } catch { + } + +} + /******************************************************************************/ export async function adminRead(key) { diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 43b02efc9..8ae4fef97 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -21,7 +21,11 @@ import * as ut from './utils.js'; -import { browser, localRemove } from './ext.js'; +import { + browser, + localKeys, localRemove, localWrite, + sessionRemove, +} from './ext.js'; import { ubolErr, ubolLog } from './debug.js'; import { fetchJSON } from './fetch.js'; @@ -286,16 +290,25 @@ function registerGeneric(context, genericDetails) { /******************************************************************************/ -function registerProcedural(context) { +async function registerProcedural(context) { const { before, filteringModeDetails, rulesetsDetails } = context; - const js = []; + { + const keys = await localKeys(); + for ( const key of keys ) { + if ( key.startsWith('css.procedural.data.') === false ) { continue; } + sessionRemove(key); + localRemove(key); + } + } + + const rulesetIds = []; for ( const rulesetDetails of rulesetsDetails ) { const count = rulesetDetails.css?.procedural || 0; if ( count === 0 ) { continue; } - js.push(`/rulesets/scripting/procedural/${rulesetDetails.id}.js`); + rulesetIds.push(rulesetDetails.id); } - if ( js.length === 0 ) { return; } + if ( rulesetIds.length === 0 ) { return; } const { none, basic, optimal, complete } = filteringModeDetails; const matches = [ @@ -304,8 +317,21 @@ function registerProcedural(context) { ]; if ( matches.length === 0 ) { return; } + { + const promises = []; + for ( const id of rulesetIds ) { + promises.push( + fetchJSON(`/rulesets/scripting/procedural/data/${id}`).then(data => { + return localWrite(`css.procedural.data.${id}`, data); + }) + ); + } + await Promise.all(promises); + } + normalizeMatches(matches); + const js = rulesetIds.map(id => `/rulesets/scripting/procedural/${id}.js`); js.unshift('/js/scripting/css-api.js', '/js/scripting/isolated-api.js'); js.push('/js/scripting/css-procedural.js'); @@ -353,16 +379,25 @@ function registerProcedural(context) { /******************************************************************************/ -function registerSpecific(context) { +async function registerSpecific(context) { const { before, filteringModeDetails, rulesetsDetails } = context; - const js = []; + { + const keys = await localKeys(); + for ( const key of keys ) { + if ( key.startsWith('css.specific.data.') === false ) { continue; } + sessionRemove(key); + localRemove(key); + } + } + + const rulesetIds = []; for ( const rulesetDetails of rulesetsDetails ) { const count = rulesetDetails.css?.specific || 0; if ( count === 0 ) { continue; } - js.push(`/rulesets/scripting/specific/${rulesetDetails.id}.js`); + rulesetIds.push(rulesetDetails.id); } - if ( js.length === 0 ) { return; } + if ( rulesetIds.length === 0 ) { return; } const { none, basic, optimal, complete } = filteringModeDetails; const matches = [ @@ -371,8 +406,21 @@ function registerSpecific(context) { ]; if ( matches.length === 0 ) { return; } + { + const promises = []; + for ( const id of rulesetIds ) { + promises.push( + fetchJSON(`/rulesets/scripting/specific/data/${id}`).then(data => { + return localWrite(`css.specific.data.${id}`, data); + }) + ); + } + await Promise.all(promises); + } + normalizeMatches(matches); + const js = rulesetIds.map(id => `/rulesets/scripting/specific/${id}.js`); js.unshift('/js/scripting/css-api.js', '/js/scripting/isolated-api.js'); js.push('/js/scripting/css-specific.js'); @@ -533,9 +581,9 @@ async function registerInjectables() { }; await Promise.all([ - registerProcedural(context), - registerScriptlet(context, scriptletDetails), registerSpecific(context), + registerScriptlet(context, scriptletDetails), + registerProcedural(context), registerGeneric(context, genericDetails), registerHighGeneric(context, genericDetails), registerCustomFilters(context), diff --git a/platform/mv3/extension/js/scripting/css-procedural.js b/platform/mv3/extension/js/scripting/css-procedural.js index a17da0020..eca031c79 100644 --- a/platform/mv3/extension/js/scripting/css-procedural.js +++ b/platform/mv3/extension/js/scripting/css-procedural.js @@ -21,7 +21,7 @@ // Important! // Isolate from global scope -(function uBOL_cssProcedural() { +(async function uBOL_cssProcedural() { /******************************************************************************/ @@ -31,25 +31,12 @@ self.proceduralImports = undefined; /******************************************************************************/ const isolatedAPI = self.isolatedAPI; -const selectors = new Set(); -const exceptions = new Set(); const lookupHostname = (hostname, details) => { const listref = isolatedAPI.binarySearch(details.hostnames, hostname); if ( listref === -1 ) { return; } - if ( Array.isArray(details.selectorLists) === false ) { - details.selectorLists = details.selectorLists.split(';'); - details.selectorListRefs = JSON.parse(`[${details.selectorListRefs}]`); - } - const ilist = details.selectorListRefs[listref]; - const list = JSON.parse(`[${details.selectorLists[ilist]}]`); - for ( const iselector of list ) { - if ( iselector >= 0 ) { - selectors.add(details.selectors[iselector]); - } else { - exceptions.add(details.selectors[~iselector]); - } - } + details.listrefs ||= []; + details.listrefs.push(listref); }; const lookupAll = hostname => { @@ -62,6 +49,38 @@ isolatedAPI.forEachHostname(lookupAll, { hasEntities: proceduralImports.some(a => a.hasEntities) }); +const toLookup = proceduralImports.filter(a => Array.isArray(a.listrefs)); +if ( toLookup.length === 0 ) { return; } + +const selectors = new Set(); +const exceptions = new Set(); + +const lookupSelectors = async details => { + const { rulesetId } = details; + const key = `css.procedural.data.${rulesetId}`; + const data = await isolatedAPI.storageGet(key); + if ( Boolean(data) === false ) { return; } + if ( data.signature !== details.signature ) { return; } + for ( const listref of details.listrefs ) { + const ilist = data.selectorListRefs[listref]; + const list = JSON.parse(`[${data.selectorLists[ilist]}]`); + for ( const iselector of list ) { + if ( iselector >= 0 ) { + selectors.add(data.selectors[iselector]); + } else { + exceptions.add(data.selectors[~iselector]); + } + } + } +}; + +const promises = []; +for ( const details of toLookup ) { + promises.push(lookupSelectors(details)); +} + +await Promise.all(promises); + proceduralImports.length = 0; for ( const selector of exceptions ) { diff --git a/platform/mv3/extension/js/scripting/css-specific.js b/platform/mv3/extension/js/scripting/css-specific.js index 4f2c878e1..e8248ab03 100644 --- a/platform/mv3/extension/js/scripting/css-specific.js +++ b/platform/mv3/extension/js/scripting/css-specific.js @@ -21,7 +21,7 @@ // Important! // Isolate from global scope -(function uBOL_cssSpecific() { +(async function uBOL_cssSpecific() { /******************************************************************************/ @@ -31,25 +31,12 @@ self.specificImports = undefined; /******************************************************************************/ const isolatedAPI = self.isolatedAPI; -const selectors = new Set(); -const exceptions = new Set(); const lookupHostname = (hostname, details) => { const listref = isolatedAPI.binarySearch(details.hostnames, hostname); if ( listref === -1 ) { return; } - if ( Array.isArray(details.selectorLists) === false ) { - details.selectorLists = details.selectorLists.split(';'); - details.selectorListRefs = JSON.parse(`[${details.selectorListRefs}]`); - } - const ilist = details.selectorListRefs[listref]; - const list = JSON.parse(`[${details.selectorLists[ilist]}]`); - for ( const iselector of list ) { - if ( iselector >= 0 ) { - selectors.add(details.selectors[iselector]); - } else { - exceptions.add(details.selectors[~iselector]); - } - } + details.listrefs ||= []; + details.listrefs.push(listref); }; const lookupAll = hostname => { @@ -62,7 +49,37 @@ isolatedAPI.forEachHostname(lookupAll, { hasEntities: specificImports.some(a => a.hasEntities) }); -specificImports.length = 0; +const toLookup = specificImports.filter(a => Array.isArray(a.listrefs)); +if ( toLookup.length === 0 ) { return; } + +const selectors = new Set(); +const exceptions = new Set(); + +const lookupSelectors = async details => { + const { rulesetId } = details; + const key = `css.specific.data.${rulesetId}`; + const data = await isolatedAPI.storageGet(key); + if ( Boolean(data) === false ) { return; } + if ( data.signature !== details.signature ) { return; } + for ( const listref of details.listrefs ) { + const ilist = data.selectorListRefs[listref]; + const list = JSON.parse(`[${data.selectorLists[ilist]}]`); + for ( const iselector of list ) { + if ( iselector >= 0 ) { + selectors.add(data.selectors[iselector]); + } else { + exceptions.add(data.selectors[~iselector]); + } + } + } +}; + +const promises = []; +for ( const details of toLookup ) { + promises.push(lookupSelectors(details)); +} + +await Promise.all(promises); for ( const selector of exceptions ) { selectors.delete(selector); diff --git a/platform/mv3/extension/js/scripting/isolated-api.js b/platform/mv3/extension/js/scripting/isolated-api.js index 4cf251548..252a5169e 100644 --- a/platform/mv3/extension/js/scripting/isolated-api.js +++ b/platform/mv3/extension/js/scripting/isolated-api.js @@ -95,6 +95,47 @@ return -1; }; + const sessionGet = async function(key) { + let data; + try { + const bin = await chrome.storage.session.get(key); + data = bin?.[key] ?? undefined; + } catch (error) { + console.trace(error); + } + return data; + }; + + const sessionSet = function(key, data) { + try { + chrome.storage.session.set({ [key]: data }); + } catch (error) { + console.trace(error); + } + }; + + const localGet = async function(key) { + let data; + try { + const bin = await chrome.storage.local.get(key); + data = bin?.[key] ?? undefined; + } catch (error) { + console.trace(error); + } + return data; + }; + + isolatedAPI.storageGet = async function(key) { + let data = await sessionGet(key); + if ( data === undefined ) { + data = await localGet(key); + if ( data !== undefined ) { + sessionSet(key, data); + } + } + return data; + }; + })(self.isolatedAPI); /******************************************************************************/ diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 9443e5ac9..d8cb80137 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -863,30 +863,31 @@ async function processCosmeticFilters(assetDetails, realm, mapin) { allHostnames.set(hn, allSelectorLists.get(list)); } - // The cosmetic filters will be injected programmatically as content - // script and the decisions to activate the cosmetic filters will be - // done at injection time according to the document's hostname. - const originalScriptletMap = await loadAllSourceScriptlets(); - let patchedScriptlet = originalScriptletMap.get(`css-${realm}`).replace( - '$rulesetId$', - assetDetails.id - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$selectors\$/, - `/* ${allSelectors.size} */ ${JSON.stringify(Array.from(allSelectors.keys()))}` - ); - patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$selectorLists\$/, - `/* ${allSelectorLists.size} */ ${JSON.stringify(Array.from(allSelectorLists.keys()).join(';'))}` - ); const sortedHostnames = Array.from(allHostnames.keys()).toSorted((a, b) => { const d = a.length - b.length; if ( d !== 0 ) { return d; } return a < b ? -1 : 1; }); + + const data = JSON.stringify({ + signature: secret, + selectors: Array.from(allSelectors.keys()), + selectorLists: Array.from(allSelectorLists.keys()), + selectorListRefs: sortedHostnames.map(a => allHostnames.get(a)), + }); + writeFile(`${scriptletDir}/${realm}/data/${assetDetails.id}.json`, data); + + // The cosmetic filters will be injected programmatically as content + // script and the decisions to activate the cosmetic filters will be + // done at injection time according to the document's hostname. + const originalScriptletMap = await loadAllSourceScriptlets(); + let patchedScriptlet = originalScriptletMap.get(`css-${realm}`).replace( + 'self.$rulesetId$', + JSON.stringify(assetDetails.id) + ); patchedScriptlet = safeReplace(patchedScriptlet, - /\bself\.\$selectorListRefs\$/, - `/* ${sortedHostnames.length} */ "${JSON.stringify(sortedHostnames.map(a => allHostnames.get(a))).slice(1, -1)}"` + 'self.$signature$', + JSON.stringify(secret) ); patchedScriptlet = safeReplace(patchedScriptlet, /\bself\.\$hostnames\$/, diff --git a/platform/mv3/scriptlets/css-procedural.template.js b/platform/mv3/scriptlets/css-procedural.template.js index 1456dc1be..58229fa86 100644 --- a/platform/mv3/scriptlets/css-procedural.template.js +++ b/platform/mv3/scriptlets/css-procedural.template.js @@ -19,23 +19,21 @@ Home: https://github.com/gorhill/uBlock */ -// ruleset: $rulesetId$ - // Important! // Isolate from global scope (function uBOL_cssProceduralImport() { +if ( Boolean(chrome?.storage?.local) === false ) { return; } + /******************************************************************************/ - -const selectors = self.$selectors$; -const selectorLists = self.$selectorLists$; -const selectorListRefs = self.$selectorListRefs$; +const rulesetId = self.$rulesetId$; +const signature = self.$signature$; const hostnames = self.$hostnames$; const hasEntities = self.$hasEntities$; self.proceduralImports = self.proceduralImports || []; -self.proceduralImports.push({ selectors, selectorLists, selectorListRefs, hostnames, hasEntities }); +self.proceduralImports.push({ rulesetId, signature, hostnames, hasEntities }); /******************************************************************************/ diff --git a/platform/mv3/scriptlets/css-specific.template.js b/platform/mv3/scriptlets/css-specific.template.js index cfe84f385..e7e4778ae 100644 --- a/platform/mv3/scriptlets/css-specific.template.js +++ b/platform/mv3/scriptlets/css-specific.template.js @@ -19,22 +19,21 @@ Home: https://github.com/gorhill/uBlock */ -// ruleset: $rulesetId$ - // Important! // Isolate from global scope (function uBOL_cssSpecificImports() { +if ( Boolean(chrome?.storage?.local) === false ) { return; } + /******************************************************************************/ -const selectors = self.$selectors$; -const selectorLists = self.$selectorLists$; -const selectorListRefs = self.$selectorListRefs$; +const rulesetId = self.$rulesetId$; +const signature = self.$signature$; const hostnames = self.$hostnames$; const hasEntities = self.$hasEntities$; self.specificImports = self.specificImports || []; -self.specificImports.push({ selectors, selectorLists, selectorListRefs, hostnames, hasEntities }); +self.specificImports.push({ rulesetId, signature, hostnames, hasEntities }); /******************************************************************************/