mirror of https://github.com/gorhill/uBlock.git
Support injecting scriptlet in MAIN or ISOLATED world
This reflects the _world_ of the MV3 scripting API: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld MAIN: page's world ISOLATED: extension's content script world Some scriptlets are best executed in either world, so this commit allows to pick in which world a scriptlet should execute (default to MAIN). For instance, the new sed.js scriptlet will now execute in the ISOLATED world.
This commit is contained in:
parent
9f465f503a
commit
1a863a877d
|
@ -2218,6 +2218,7 @@ builtinScriptlets.push({
|
|||
name: 'sed.js',
|
||||
requiresTrust: true,
|
||||
fn: sed,
|
||||
world: 'ISOLATED',
|
||||
dependencies: [
|
||||
'pattern-to-regex.fn',
|
||||
'run-at.fn',
|
||||
|
@ -2229,7 +2230,6 @@ function sed(
|
|||
pattern = '',
|
||||
replacement = ''
|
||||
) {
|
||||
if ( document.documentElement === null ) { return; }
|
||||
const reNodeName = patternToRegex(nodeName, 'i');
|
||||
const rePattern = patternToRegex(pattern, 'gms');
|
||||
const extraArgs = new Map(
|
||||
|
@ -2274,22 +2274,18 @@ function sed(
|
|||
}
|
||||
};
|
||||
const observer = new MutationObserver(handleMutations);
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||
{
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
if ( document.documentElement ) {
|
||||
const treeWalker = document.createTreeWalker(
|
||||
document.documentElement,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
|
||||
);
|
||||
const currentScriptNode = document.currentScript;
|
||||
const currentTextNode = currentScriptNode.firstChild;
|
||||
let count = 0;
|
||||
for (;;) {
|
||||
const node = treeWalker.nextNode();
|
||||
count += 1;
|
||||
if ( node === null ) { break; }
|
||||
if ( reNodeName.test(node.nodeName) === false ) { continue; }
|
||||
if ( node === currentScriptNode ) { continue; }
|
||||
if ( node === currentTextNode ) { continue; }
|
||||
if ( handleNode(node) ) { continue; }
|
||||
stop(); break;
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ vAPI.scriptletsInjector = ((doc, details) => {
|
|||
script = doc.createElement('script');
|
||||
script.async = false;
|
||||
script.src = url;
|
||||
(doc.head || doc.documentElement).appendChild(script);
|
||||
doc.append(script);
|
||||
self.uBO_scriptletsInjected = details.filters;
|
||||
} catch (ex) {
|
||||
}
|
||||
|
|
|
@ -1324,9 +1324,9 @@ vAPI.DOMFilterer = class {
|
|||
// https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt
|
||||
if ( scriptletDetails && typeof self.uBO_scriptletsInjected !== 'string' ) {
|
||||
self.uBO_scriptletsInjected = scriptletDetails.filters;
|
||||
if ( scriptletDetails.scriptlets ) {
|
||||
vAPI.injectScriptlet(document, scriptletDetails.scriptlets);
|
||||
vAPI.injectedScripts = scriptletDetails.scriptlets;
|
||||
if ( scriptletDetails.mainWorld ) {
|
||||
vAPI.injectScriptlet(document, scriptletDetails.mainWorld);
|
||||
vAPI.injectedScripts = scriptletDetails.mainWorld;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ class RedirectEntry {
|
|||
this.warURL = undefined;
|
||||
this.params = undefined;
|
||||
this.requiresTrust = false;
|
||||
this.world = 'MAIN';
|
||||
this.dependencies = [];
|
||||
}
|
||||
|
||||
|
@ -157,6 +158,7 @@ class RedirectEntry {
|
|||
r.requiresTrust = details.requiresTrust === true;
|
||||
r.warURL = details.warURL !== undefined && details.warURL || undefined;
|
||||
r.params = details.params !== undefined && details.params || undefined;
|
||||
r.world = details.world || 'MAIN';
|
||||
if ( Array.isArray(details.dependencies) ) {
|
||||
r.dependencies.push(...details.dependencies);
|
||||
}
|
||||
|
@ -227,6 +229,7 @@ class RedirectEngine {
|
|||
if ( entry.mime.startsWith(mime) === false ) { return; }
|
||||
return {
|
||||
js: entry.toContent(),
|
||||
world: entry.world,
|
||||
dependencies: entry.dependencies.slice(),
|
||||
};
|
||||
}
|
||||
|
@ -320,6 +323,7 @@ class RedirectEngine {
|
|||
data: fn.toString(),
|
||||
dependencies: scriptlet.dependencies,
|
||||
requiresTrust: scriptlet.requiresTrust === true,
|
||||
world: scriptlet.world || 'MAIN',
|
||||
});
|
||||
this.resources.set(name, entry);
|
||||
if ( Array.isArray(aliases) === false ) { continue; }
|
||||
|
|
|
@ -82,7 +82,7 @@ const scriptletFilteringEngine = {
|
|||
// Consequently, the programmatic-injection code path is taken only with
|
||||
// Chromium-based browsers.
|
||||
|
||||
const contentscriptCode = (( ) => {
|
||||
const mainWorldInjector = (( ) => {
|
||||
const parts = [
|
||||
'(',
|
||||
function(injector, details) {
|
||||
|
@ -95,7 +95,7 @@ const contentscriptCode = (( ) => {
|
|||
return;
|
||||
}
|
||||
injector(doc, details);
|
||||
if ( typeof self.uBO_scriptletsInjected === 'string' ) { return 0; }
|
||||
return 0;
|
||||
}.toString(),
|
||||
')(',
|
||||
vAPI.scriptletsInjector, ', ',
|
||||
|
@ -112,7 +112,39 @@ const contentscriptCode = (( ) => {
|
|||
filters,
|
||||
});
|
||||
return this.parts.join('');
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
const isolatedWorldInjector = (( ) => {
|
||||
const parts = [
|
||||
'(',
|
||||
function(details) {
|
||||
const doc = document;
|
||||
if (
|
||||
doc.location === null ||
|
||||
details.hostname !== doc.location.hostname ||
|
||||
self.uBO_isolatedScriptlets === 'done'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const isolatedScriptlets = function(){};
|
||||
isolatedScriptlets();
|
||||
self.uBO_isolatedScriptlets = 'done';
|
||||
return 0;
|
||||
}.toString(),
|
||||
')(',
|
||||
'json-slot',
|
||||
');',
|
||||
];
|
||||
return {
|
||||
parts,
|
||||
jsonSlot: parts.indexOf('json-slot'),
|
||||
scriptletSlot: parts.indexOf('scriptlet-slot'),
|
||||
assemble: function(hostname, scriptlets) {
|
||||
this.parts[this.jsonSlot] = JSON.stringify({ hostname });
|
||||
return this.parts.join('').replace('function(){}', scriptlets);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
|
@ -147,8 +179,8 @@ const normalizeRawFilter = function(parser, sourceIsTrusted = false) {
|
|||
return `+js(${args.join(', ')})`;
|
||||
};
|
||||
|
||||
const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) {
|
||||
if ( scriptletMap.has(rawToken) ) { return; }
|
||||
const lookupScriptlet = function(rawToken, mainMap, isolatedMap) {
|
||||
if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; }
|
||||
const pos = rawToken.indexOf(',');
|
||||
let token, args = '';
|
||||
if ( pos === -1 ) {
|
||||
|
@ -157,8 +189,6 @@ const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) {
|
|||
token = rawToken.slice(0, pos).trim();
|
||||
args = rawToken.slice(pos + 1).trim();
|
||||
}
|
||||
// TODO: The alias lookup can be removed once scriptlet resources
|
||||
// with obsolete name are converted to their new name.
|
||||
if ( reng.aliases.has(token) ) {
|
||||
token = reng.aliases.get(token);
|
||||
} else {
|
||||
|
@ -166,18 +196,19 @@ const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) {
|
|||
}
|
||||
const details = reng.contentFromName(token, 'text/javascript');
|
||||
if ( details === undefined ) { return; }
|
||||
const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap;
|
||||
const content = patchScriptlet(details.js, args);
|
||||
const dependencies = details.dependencies || [];
|
||||
while ( dependencies.length !== 0 ) {
|
||||
const token = dependencies.shift();
|
||||
if ( dependencyMap.has(token) ) { continue; }
|
||||
if ( targetWorldMap.has(token) ) { continue; }
|
||||
const details = reng.contentFromName(token, 'fn/javascript');
|
||||
if ( details === undefined ) { continue; }
|
||||
dependencyMap.set(token, details.js);
|
||||
targetWorldMap.set(token, details.js);
|
||||
if ( Array.isArray(details.dependencies) === false ) { continue; }
|
||||
dependencies.push(...details.dependencies);
|
||||
}
|
||||
scriptletMap.set(rawToken, [
|
||||
targetWorldMap.set(rawToken, [
|
||||
'try {',
|
||||
'// >>>> scriptlet start',
|
||||
content,
|
||||
|
@ -314,24 +345,14 @@ scriptletFilteringEngine.fromCompiledContent = function(reader) {
|
|||
|
||||
const $scriptlets = new Set();
|
||||
const $exceptions = new Set();
|
||||
const $scriptletMap = new Map();
|
||||
const $scriptletDependencyMap = new Map();
|
||||
const $mainWorldMap = new Map();
|
||||
const $isolatedWorldMap = new Map();
|
||||
|
||||
scriptletFilteringEngine.retrieve = function(request) {
|
||||
if ( scriptletDB.size === 0 ) { return; }
|
||||
|
||||
const hostname = request.hostname;
|
||||
|
||||
$scriptlets.clear();
|
||||
$exceptions.clear();
|
||||
|
||||
scriptletDB.retrieve(hostname, [ $scriptlets, $exceptions ]);
|
||||
const entity = request.entity !== ''
|
||||
? `${hostname.slice(0, -request.domain.length)}${request.entity}`
|
||||
: '*';
|
||||
scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
|
||||
if ( $scriptlets.size === 0 ) { return; }
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2835
|
||||
// Do not inject scriptlets if the site is under an `allow` rule.
|
||||
if (
|
||||
|
@ -341,6 +362,22 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( scriptletCache.resetTime < reng.modifyTime ) {
|
||||
scriptletCache.reset();
|
||||
}
|
||||
|
||||
let cacheDetails = scriptletCache.lookup(hostname);
|
||||
if ( cacheDetails === undefined ) {
|
||||
$scriptlets.clear();
|
||||
$exceptions.clear();
|
||||
|
||||
scriptletDB.retrieve(hostname, [ $scriptlets, $exceptions ]);
|
||||
const entity = request.entity !== ''
|
||||
? `${hostname.slice(0, -request.domain.length)}${request.entity}`
|
||||
: '*';
|
||||
scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
|
||||
if ( $scriptlets.size === 0 ) { return; }
|
||||
|
||||
// Wholly disable scriptlet injection?
|
||||
if ( $exceptions.has('') ) {
|
||||
return {
|
||||
|
@ -350,13 +387,6 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
};
|
||||
}
|
||||
|
||||
if ( scriptletCache.resetTime < reng.modifyTime ) {
|
||||
scriptletCache.reset();
|
||||
}
|
||||
|
||||
let cacheDetails = scriptletCache.lookup(hostname);
|
||||
if ( cacheDetails === undefined ) {
|
||||
const fullCode = [];
|
||||
for ( const token of $exceptions ) {
|
||||
if ( $scriptlets.has(token) ) {
|
||||
$scriptlets.delete(token);
|
||||
|
@ -365,27 +395,30 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
}
|
||||
}
|
||||
for ( const token of $scriptlets ) {
|
||||
lookupScriptlet(token, $scriptletMap, $scriptletDependencyMap);
|
||||
lookupScriptlet(token, $mainWorldMap, $isolatedWorldMap);
|
||||
}
|
||||
for ( const token of $scriptlets ) {
|
||||
fullCode.push($scriptletMap.get(token));
|
||||
const mainWorldCode = [];
|
||||
for ( const js of $mainWorldMap.values() ) {
|
||||
mainWorldCode.push(js);
|
||||
}
|
||||
for ( const code of $scriptletDependencyMap.values() ) {
|
||||
fullCode.push(code);
|
||||
const isolatedWorldCode = [];
|
||||
for ( const js of $isolatedWorldMap.values() ) {
|
||||
isolatedWorldCode.push(js);
|
||||
}
|
||||
cacheDetails = {
|
||||
code: fullCode.join('\n\n'),
|
||||
mainWorld: mainWorldCode.join('\n\n'),
|
||||
isolatedWorld: isolatedWorldCode.join('\n\n'),
|
||||
filters: [
|
||||
...Array.from($scriptlets).map(s => `##+js(${s})`),
|
||||
...Array.from($exceptions).map(s => `#@#+js(${s})`),
|
||||
].join('\n'),
|
||||
};
|
||||
scriptletCache.add(hostname, cacheDetails);
|
||||
$scriptletMap.clear();
|
||||
$scriptletDependencyMap.clear();
|
||||
$mainWorldMap.clear();
|
||||
$isolatedWorldMap.clear();
|
||||
}
|
||||
|
||||
if ( cacheDetails.code === '' ) {
|
||||
if ( cacheDetails.mainWorld === '' && cacheDetails.isolatedWorld === '' ) {
|
||||
return { filters: cacheDetails.filters };
|
||||
}
|
||||
|
||||
|
@ -398,7 +431,8 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
scriptletGlobals.push([ 'canDebug', true ]);
|
||||
}
|
||||
|
||||
const out = [
|
||||
return {
|
||||
mainWorld: cacheDetails.mainWorld === '' ? '' : [
|
||||
'(function() {',
|
||||
'// >>>> start of private namespace',
|
||||
'',
|
||||
|
@ -407,13 +441,27 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
// For use by scriptlets to share local data among themselves
|
||||
`const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`,
|
||||
'',
|
||||
cacheDetails.code,
|
||||
cacheDetails.mainWorld,
|
||||
'',
|
||||
'// <<<< end of private namespace',
|
||||
'})();',
|
||||
];
|
||||
|
||||
return { scriptlets: out.join('\n'), filters: cacheDetails.filters };
|
||||
].join('\n'),
|
||||
isolatedWorld: cacheDetails.isolatedWorld === '' ? '' : [
|
||||
'function() {',
|
||||
'// >>>> start of private namespace',
|
||||
'',
|
||||
µb.hiddenSettings.debugScriptlets ? 'debugger;' : ';',
|
||||
'',
|
||||
// For use by scriptlets to share local data among themselves
|
||||
`const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`,
|
||||
'',
|
||||
cacheDetails.isolatedWorld,
|
||||
'',
|
||||
'// <<<< end of private namespace',
|
||||
'}',
|
||||
].join('\n'),
|
||||
filters: cacheDetails.filters,
|
||||
};
|
||||
};
|
||||
|
||||
scriptletFilteringEngine.injectNow = function(details) {
|
||||
|
@ -430,9 +478,9 @@ scriptletFilteringEngine.injectNow = function(details) {
|
|||
request.entity = entityFromDomain(request.domain);
|
||||
const scriptletDetails = this.retrieve(request);
|
||||
if ( scriptletDetails === undefined ) { return; }
|
||||
const { scriptlets = '', filters } = scriptletDetails;
|
||||
if ( scriptlets === '' ) { return scriptletDetails; }
|
||||
let code = contentscriptCode.assemble(request.hostname, scriptlets, filters);
|
||||
const { mainWorld = '', isolatedWorld = '', filters } = scriptletDetails;
|
||||
if ( mainWorld !== '' ) {
|
||||
let code = mainWorldInjector.assemble(request.hostname, mainWorld, filters);
|
||||
if ( µb.hiddenSettings.debugScriptletInjector ) {
|
||||
code = 'debugger;\n' + code;
|
||||
}
|
||||
|
@ -442,6 +490,19 @@ scriptletFilteringEngine.injectNow = function(details) {
|
|||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
});
|
||||
}
|
||||
if ( isolatedWorld !== '' ) {
|
||||
let code = isolatedWorldInjector.assemble(request.hostname, isolatedWorld);
|
||||
if ( µb.hiddenSettings.debugScriptletInjector ) {
|
||||
code = 'debugger;\n' + code;
|
||||
}
|
||||
vAPI.tabs.executeScript(details.tabId, {
|
||||
code,
|
||||
frameId: details.frameId,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
});
|
||||
}
|
||||
return scriptletDetails;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue