diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 78200d47b..43b02efc9 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -434,29 +434,24 @@ function registerScriptlet(context, scriptletDetails) { ]; for ( const rulesetId of rulesetsDetails.map(v => v.id) ) { - const scriptletList = scriptletDetails.get(rulesetId); - if ( scriptletList === undefined ) { continue; } - - for ( const [ token, details ] of scriptletList ) { - const id = `${rulesetId}.${token}`; - const registered = before.get(id); + const worlds = scriptletDetails.get(rulesetId); + if ( worlds === undefined ) { continue; } + for ( const world of Object.keys(worlds) ) { + const id = `${rulesetId}.${world.toLowerCase()}`; const matches = []; const excludeMatches = []; + const hostnames = worlds[world]; let targetHostnames = []; if ( hasBroadHostPermission ) { excludeMatches.push(...permissionRevokedMatches); - if ( details.hostnames.length > 100 ) { - targetHostnames = [ '*' ]; - } else { - targetHostnames = details.hostnames; - } + targetHostnames = hostnames; } else if ( permissionGrantedHostnames.length !== 0 ) { - if ( details.hostnames.includes('*') ) { + if ( hostnames.includes('*') ) { targetHostnames = permissionGrantedHostnames; } else { targetHostnames = ut.intersectHostnameIters( - details.hostnames, + hostnames, permissionGrantedHostnames ); } @@ -465,16 +460,17 @@ 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/${id}.js` ], + js: [ `/rulesets/scripting/scriptlet/${world.toLowerCase()}/${rulesetId}.js` ], matches, allFrames: true, matchOriginAsFallback: true, runAt: 'document_start', - world: details.world, + world, }; if ( excludeMatches.length !== 0 ) { directive.excludeMatches = excludeMatches; diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 515948fbd..9f307232c 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -101,6 +101,7 @@ const consoleLog = console.log; const stdOutput = []; const log = (text, silent = true) => { + silent = silent && text.startsWith('!!!') === false; stdOutput.push(text); if ( silent === false ) { consoleLog(text); diff --git a/platform/mv3/make-scriptlets.js b/platform/mv3/make-scriptlets.js index cf356c5ea..ae405c333 100644 --- a/platform/mv3/make-scriptlets.js +++ b/platform/mv3/make-scriptlets.js @@ -27,24 +27,37 @@ import { safeReplace } from './safe-replace.js'; const resourceDetails = new Map(); const resourceAliases = new Map(); -const scriptletFiles = new Map(); +const worldTemplate = { + scriptletFunctions: new Map(), + allFunctions: new Map(), + args: new Map(), + arglists: new Map(), + hostnames: new Map(), + matches: new Set(), + hasEntities: false, + hasAncestors: false, +}; +const worlds = { + ISOLATED: structuredClone(worldTemplate), + MAIN: structuredClone(worldTemplate), +}; /******************************************************************************/ -function createScriptletCoreCode(scriptletToken) { - const details = resourceDetails.get(scriptletToken); - const components = new Map([ [ scriptletToken, details.code ] ]); - const dependencies = details.dependencies && details.dependencies.slice() || []; +function createScriptletCoreCode(worldDetails, resourceEntry) { + const { allFunctions } = worldDetails; + allFunctions.set(resourceEntry.name, resourceEntry.code); + const dependencies = resourceEntry.dependencies && + resourceEntry.dependencies.slice() || []; while ( dependencies.length !== 0 ) { const token = dependencies.shift(); - if ( components.has(token) ) { continue; } const details = resourceDetails.get(token); if ( details === undefined ) { continue; } - components.set(token, details.code); + if ( allFunctions.has(details.name) ) { continue; } + allFunctions.set(details.name, details.code); if ( Array.isArray(details.dependencies) === false ) { continue; } dependencies.push(...details.dependencies); } - return Array.from(components.values()).join('\n\n'); } /******************************************************************************/ @@ -70,7 +83,8 @@ export function init() { /******************************************************************************/ export function reset() { - scriptletFiles.clear(); + worlds.ISOLATED = structuredClone(worldTemplate); + worlds.MAIN = structuredClone(worldTemplate); } /******************************************************************************/ @@ -85,56 +99,58 @@ export function compile(assetDetails, details) { const scriptletToken = details.args[0]; const resourceEntry = resourceDetails.get(scriptletToken); if ( resourceEntry === undefined ) { return; } - const argsToken = JSON.stringify(details.args.slice(1)); if ( resourceEntry.requiresTrust && details.trustedSource !== true ) { - console.log(`Rejecting +js(${scriptletToken},${argsToken.slice(1,-1)}): ${assetDetails.id} is not trusted`); + console.log(`Rejecting +js(${details.args.join()}): ${assetDetails.id} is not trusted`); return; } - if ( scriptletFiles.has(scriptletToken) === false ) { - scriptletFiles.set(scriptletToken, { - name: resourceEntry.name, - code: createScriptletCoreCode(scriptletToken), - world: resourceEntry.world, - args: new Map(), - hostnames: new Map(), - exceptions: new Map(), - hasEntities: false, - hasAncestors: false, - matches: new Set(), - }); + const worldDetails = worlds[resourceEntry.world]; + const { scriptletFunctions } = worldDetails; + if ( scriptletFunctions.has(resourceEntry.name) === false ) { + scriptletFunctions.set(resourceEntry.name, scriptletFunctions.size); + createScriptletCoreCode(worldDetails, resourceEntry); } - const scriptletDetails = scriptletFiles.get(scriptletToken); - if ( scriptletDetails.args.has(argsToken) === false ) { - scriptletDetails.args.set(argsToken, scriptletDetails.args.size); + // Convert args to arg indices + const arglist = details.args.slice(); + arglist[0] = scriptletFunctions.get(resourceEntry.name); + for ( let i = 1; i < arglist.length; i++ ) { + const arg = arglist[i]; + if ( worldDetails.args.has(arg) === false ) { + worldDetails.args.set(arg, worldDetails.args.size); + } + arglist[i] = worldDetails.args.get(arg); } - const iArgs = scriptletDetails.args.get(argsToken); + const arglistKey = JSON.stringify(arglist).slice(1, -1); + if ( worldDetails.arglists.has(arglistKey) === false ) { + worldDetails.arglists.set(arglistKey, worldDetails.arglists.size); + } + const arglistIndex = worldDetails.arglists.get(arglistKey); if ( details.matches ) { for ( const hn of details.matches ) { const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>'); - scriptletDetails.hasEntities ||= isEntity; + worldDetails.hasEntities ||= isEntity; const isAncestor = hn.endsWith('>>') - scriptletDetails.hasAncestors ||= isAncestor; + worldDetails.hasAncestors ||= isAncestor; if ( isEntity || isAncestor ) { - scriptletDetails.matches.clear(); - scriptletDetails.matches.add('*'); + worldDetails.matches.clear(); + worldDetails.matches.add('*'); } - if ( scriptletDetails.matches.has('*') === false ) { - scriptletDetails.matches.add(hn); + if ( worldDetails.matches.has('*') === false ) { + worldDetails.matches.add(hn); } - if ( scriptletDetails.hostnames.has(hn) === false ) { - scriptletDetails.hostnames.set(hn, new Set()); + if ( worldDetails.hostnames.has(hn) === false ) { + worldDetails.hostnames.set(hn, new Set()); } - scriptletDetails.hostnames.get(hn).add(iArgs); + worldDetails.hostnames.get(hn).add(arglistIndex); } } else { - scriptletDetails.matches.add('*'); + worldDetails.matches.add('*'); } if ( details.excludeMatches ) { for ( const hn of details.excludeMatches ) { - if ( scriptletDetails.exceptions.has(hn) === false ) { - scriptletDetails.exceptions.set(hn, []); + if ( worldDetails.hostnames.has(hn) === false ) { + worldDetails.hostnames.set(hn, new Set()); } - scriptletDetails.exceptions.get(hn).push(iArgs); + worldDetails.hostnames.get(hn).add(~arglistIndex); } } } @@ -146,51 +162,57 @@ export async function commit(rulesetId, path, writeFn) { './scriptlets/scriptlet.template.js', { encoding: 'utf8' } ); - const patchHnMap = hnmap => { - const out = Array.from(hnmap); - out.forEach(a => { - const values = Array.from(a[1]); - a[1] = values.length === 1 ? values[0] : values; - }); - return out; - }; - const scriptletStats = []; - for ( const [ name, details ] of scriptletFiles ) { - let content = safeReplace(scriptletTemplate, - 'function $scriptletName$(){}', - details.code - ); - content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0); - content = safeReplace(content, /\$scriptletName\$/, details.name, 0); + const stats = {}; + for ( const world of Object.keys(worlds) ) { + const worldDetails = worlds[world]; + const { scriptletFunctions, allFunctions, args, arglists } = worldDetails; + if ( scriptletFunctions.size === 0 ) { continue; } + const hostnames = Array.from(worldDetails.hostnames).toSorted((a, b) => { + const d = a[0].length - b[0].length; + if ( d !== 0 ) { return d; } + return a[0] < b[0] ? -1 : 1; + }).map(a => ([ a[0], JSON.stringify(Array.from(a[1]).map(a => JSON.parse(a))).slice(1,-1)])); + let content = safeReplace(scriptletTemplate, /\$rulesetId\$/, rulesetId, 0); + if ( worldDetails.hasEntities ) { + content = safeReplace(content, + 'const $hasEntities$ = false;', + 'const $hasEntities$ = true;' + ); + } + if ( worldDetails.hasAncestors ) { + content = safeReplace(content, + 'const $hasAncestors$ = false;', + 'const $hasAncestors$ = true;' + ); + }; content = safeReplace(content, - 'self.$argsList$', - JSON.stringify(Array.from(details.args.keys()).map(a => JSON.parse(a))) + 'const $scriptletHostnames$ = [];', + `const $scriptletHostnames$ = /* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[0]))};` ); content = safeReplace(content, - 'self.$hostnamesMap$', - JSON.stringify(patchHnMap(details.hostnames)) + 'const $scriptletArglistRefs$ = [];', + `const $scriptletArglistRefs$ = /* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[1]).join(';'))};` ); content = safeReplace(content, - 'self.$hasEntities$', - JSON.stringify(details.hasEntities) + 'const $scriptletArglists$ = [];', + `const $scriptletArglists$ = /* ${arglists.size} */ ${JSON.stringify(Array.from(arglists.keys()).join(';'))};` ); content = safeReplace(content, - 'self.$hasAncestors$', - JSON.stringify(details.hasAncestors) + 'const $scriptletArgs$ = [];', + `const $scriptletArgs$ = /* ${args.size} */ ${JSON.stringify(Array.from(args.keys()).join('\n'))};` ); content = safeReplace(content, - 'self.$exceptionsMap$', - JSON.stringify(Array.from(details.exceptions)) + 'const $scriptletFunctions$ = [];', + `const $scriptletFunctions$ = /* ${scriptletFunctions.size} */\n[${Array.from(scriptletFunctions.keys()).join(',')}];` ); - writeFn(`${path}/${rulesetId}.${name}`, content); - scriptletStats.push([ - name.slice(0, -3), { - hostnames: Array.from(details.matches).sort(), - world: details.world, - } - ]); + content = safeReplace(content, + 'function $scriptletCode$(){} // eslint-disable-line', + Array.from(allFunctions.values()).join('\n\n') + ); + writeFn(`${path}/${world.toLowerCase()}/${rulesetId}.js`, content); + stats[world] = Array.from(worldDetails.matches).sort(); } - return scriptletStats; + return stats; } /******************************************************************************/ diff --git a/platform/mv3/scriptlets/scriptlet.template.js b/platform/mv3/scriptlets/scriptlet.template.js index fb5ace818..63c80f8cd 100644 --- a/platform/mv3/scriptlets/scriptlet.template.js +++ b/platform/mv3/scriptlets/scriptlet.template.js @@ -26,53 +26,30 @@ // Isolate from global scope // Start of local scope -(function uBOL_$scriptletName$() { +(function uBOL_scriptlets() { /******************************************************************************/ -function $scriptletName$(){} +function $scriptletCode$(){} // eslint-disable-line /******************************************************************************/ const scriptletGlobals = {}; // eslint-disable-line -const argsList = self.$argsList$; -const hostnamesMap = new Map(self.$hostnamesMap$); -const exceptionsMap = new Map(self.$exceptionsMap$); -const hasEntities = self.$hasEntities$; -const hasAncestors = self.$hasAncestors$; -const collectArgIndices = (hn, map, out) => { - let argsIndices = map.get(hn); - if ( argsIndices === undefined ) { return; } - if ( typeof argsIndices !== 'number' ) { - for ( const argsIndex of argsIndices ) { - out.add(argsIndex); - } - } else { - out.add(argsIndices); - } -}; +const $scriptletFunctions$ = []; -const indicesFromHostname = (hostname, suffix = '') => { - const hnParts = hostname.split('.'); - const hnpartslen = hnParts.length; - if ( hnpartslen === 0 ) { return; } - for ( let i = 0; i < hnpartslen; i++ ) { - const hn = `${hnParts.slice(i).join('.')}${suffix}`; - collectArgIndices(hn, hostnamesMap, todoIndices); - collectArgIndices(hn, exceptionsMap, tonotdoIndices); - } - if ( hasEntities ) { - const n = hnpartslen - 1; - for ( let i = 0; i < n; i++ ) { - for ( let j = n; j > i; j-- ) { - const en = `${hnParts.slice(i,j).join('.')}.*${suffix}`; - collectArgIndices(en, hostnamesMap, todoIndices); - collectArgIndices(en, exceptionsMap, tonotdoIndices); - } - } - } -}; +const $scriptletArgs$ = []; + +const $scriptletArglists$ = []; + +const $scriptletArglistRefs$ = []; + +const $scriptletHostnames$ = []; + +const $hasEntities$ = false; +const $hasAncestors$ = false; + +/******************************************************************************/ const entries = (( ) => { const docloc = document.location; @@ -81,31 +58,107 @@ const entries = (( ) => { origins.push(...docloc.ancestorOrigins); } return origins.map((origin, i) => { - const beg = origin.lastIndexOf('://'); + const beg = origin.indexOf('://'); if ( beg === -1 ) { return; } - const hn = origin.slice(beg+3) - const end = hn.indexOf(':'); - return { hn: end === -1 ? hn : hn.slice(0, end), i }; + const hn1 = origin.slice(beg+3) + const end = hn1.indexOf(':'); + const hn2 = end === -1 ? hn1 : hn1.slice(0, end); + const hnParts = hn2.split('.'); + if ( hn2.length === 0 ) { return; } + const hns = []; + for ( let i = 0; i < hnParts.length; i++ ) { + hns.push(`${hnParts.slice(i).join('.')}`); + } + const ens = []; + if ( $hasEntities$ ) { + const n = hnParts.length - 1; + for ( let i = 0; i < n; i++ ) { + for ( let j = n; j > i; j-- ) { + ens.push(`${hnParts.slice(i,j).join('.')}.*`); + } + } + ens.sort((a, b) => { + const d = b.length - a.length; + if ( d !== 0 ) { return d; } + return a > b ? -1 : 1; + }); + } + return { hns, ens, i }; }).filter(a => a !== undefined); })(); if ( entries.length === 0 ) { return; } -const todoIndices = new Set(); -const tonotdoIndices = new Set(); +const collectArglistRefIndices = (out, hn, r) => { + let l = 0, i = 0, d = 0; + let candidate = ''; + while ( l < r ) { + i = l + r >>> 1; + candidate = $scriptletHostnames$[i]; + d = hn.length - candidate.length; + if ( d === 0 ) { + if ( hn === candidate ) { + out.add(i); break; + } + d = hn < candidate ? -1 : 1; + } + if ( d < 0 ) { + r = i; + } else { + l = i + 1; + } + } + return i; +}; -indicesFromHostname(entries[0].hn); -if ( hasAncestors ) { +const indicesFromHostname = (out, hnDetails, suffix = '') => { + if ( hnDetails.hns.length === 0 ) { return; } + let r = $scriptletHostnames$.length; + for ( const hn of hnDetails.hns ) { + r = collectArglistRefIndices(out, `${hn}${suffix}`, r); + } + if ( $hasEntities$ ) { + let r = $scriptletHostnames$.length; + for ( const en of hnDetails.ens ) { + r = collectArglistRefIndices(out, `${en}${suffix}`, r); + } + } +}; + +const todoIndices = new Set(); +indicesFromHostname(todoIndices, entries[0]); +if ( $hasAncestors$ ) { for ( const entry of entries ) { if ( entry.i === 0 ) { continue; } - indicesFromHostname(entry.hn, '>>'); + indicesFromHostname(todoIndices, entry, '>>'); + } +} +$scriptletHostnames$.length = 0; + +if ( todoIndices.size === 0 ) { return; } + +// Collect arglist references +const todo = new Set(); +{ + const arglistRefs = $scriptletArglistRefs$.split(';'); + for ( const i of todoIndices ) { + for ( const ref of JSON.parse(`[${arglistRefs[i]}]`) ) { + todo.add(ref); + } } } -// Apply scriplets -for ( const i of todoIndices ) { - if ( tonotdoIndices.has(i) ) { continue; } - try { $scriptletName$(...argsList[i]); } - catch { } +// Execute scriplets +{ + const arglists = $scriptletArglists$.split(';'); + const args = $scriptletArgs$.split('\n'); + for ( const ref of todo ) { + if ( ref < 0 ) { continue; } + if ( todo.has(~ref) ) { continue; } + const arglist = JSON.parse(`[${arglists[ref]}]`); + const fn = $scriptletFunctions$[arglist[0]]; + try { fn(...arglist.slice(1).map(a => args[a])); } + catch { } + } } /******************************************************************************/ diff --git a/src/js/resources/localstorage.js b/src/js/resources/localstorage.js index 8a6b76293..3e33ca239 100644 --- a/src/js/resources/localstorage.js +++ b/src/js/resources/localstorage.js @@ -47,6 +47,7 @@ export function setLocalStorageItemFn( trusted = false, key = '', value = '', + options = {} ) { if ( key === '' ) { return; } @@ -86,6 +87,8 @@ export function setLocalStorageItemFn( } } + let modified = false; + try { const storage = self[`${which}Storage`]; if ( value === '$remove$' ) { @@ -96,14 +99,25 @@ export function setLocalStorageItemFn( const key = storage.key(i); if ( pattern.test(key) ) { toRemove.push(key); } } + modified = toRemove.length !== 0; for ( const key of toRemove ) { storage.removeItem(key); } } else { - storage.setItem(key, `${value}`); + + const before = storage.getItem(key); + const after = `${value}`; + modified = after !== before; + if ( modified ) { + storage.setItem(key, after); + } } } catch { } + + if ( modified && typeof options.reload === 'number' ) { + setTimeout(( ) => { window.location.reload(); }, options.reload); + } } registerScriptlet(setLocalStorageItemFn, { name: 'set-local-storage-item.fn', @@ -178,23 +192,29 @@ registerScriptlet(removeCacheStorageItem, { **/ export function setLocalStorageItem(key = '', value = '') { - setLocalStorageItemFn('local', false, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('local', false, key, value, options); } registerScriptlet(setLocalStorageItem, { name: 'set-local-storage-item.js', world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); export function setSessionStorageItem(key = '', value = '') { - setLocalStorageItemFn('session', false, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('session', false, key, value, options); } registerScriptlet(setSessionStorageItem, { name: 'set-session-storage-item.js', world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); @@ -211,25 +231,31 @@ registerScriptlet(setSessionStorageItem, { **/ export function trustedSetLocalStorageItem(key = '', value = '') { - setLocalStorageItemFn('local', true, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('local', true, key, value, options); } registerScriptlet(trustedSetLocalStorageItem, { name: 'trusted-set-local-storage-item.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], }); export function trustedSetSessionStorageItem(key = '', value = '') { - setLocalStorageItemFn('session', true, key, value); + const safe = safeSelf(); + const options = safe.getExtraArgs(Array.from(arguments), 2) + setLocalStorageItemFn('session', true, key, value, options); } registerScriptlet(trustedSetSessionStorageItem, { name: 'trusted-set-session-storage-item.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ + safeSelf, setLocalStorageItemFn, ], });