Split scriptlet filtering engine into lo- and hi-level classes

The idea is to remove as many dependencies as possible for
low-level ScriptletFilteringEngine in order to make it easier
to reuse the module outside uBO itself.

The high-level derived class takes care of caching and
injection of scriptlets into documents, which requires
more knowledge about the environment in which scriptlets
are to be used.

Also improve scriptlet cache usage to minimize overhead of
retrieving scriptlets.
This commit is contained in:
Raymond Hill 2023-12-21 10:48:01 -05:00
parent abeadf18eb
commit aa7f77aaad
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
8 changed files with 505 additions and 428 deletions

View File

@ -1175,16 +1175,15 @@ vAPI.messaging = {
const shortSecrets = [];
let lastShortSecretTime = 0;
// Long secrets are meant to be used multiple times, but for at most a few
// minutes. The realm is one value out of 36^18 = over 10^28 values.
const longSecrets = [ '', '' ];
let lastLongSecretTimeSlice = 0;
// Long secrets are valid until revoked or uBO restarts. The realm is one
// value out of 36^18 = over 10^28 values.
const longSecrets = new Set();
const guard = details => {
const match = reSecret.exec(details.url);
if ( match === null ) { return { cancel: true }; }
const secret = match[1];
if ( longSecrets.includes(secret) ) { return; }
if ( longSecrets.has(secret) ) { return; }
const pos = shortSecrets.indexOf(secret);
if ( pos === -1 ) { return { cancel: true }; }
shortSecrets.splice(pos, 1);
@ -1212,14 +1211,13 @@ vAPI.messaging = {
shortSecrets.push(secret);
return secret;
},
long: ( ) => {
const timeSlice = Date.now() >>> 19; // Changes every ~9 minutes
if ( timeSlice !== lastLongSecretTimeSlice ) {
longSecrets[1] = longSecrets[0];
longSecrets[0] = `${generateSecret()}${generateSecret()}${generateSecret()}`;
lastLongSecretTimeSlice = timeSlice;
long: previous => {
if ( previous !== undefined ) {
longSecrets.delete(previous);
}
return longSecrets[0];
const secret = `${generateSecret()}${generateSecret()}${generateSecret()}`;
longSecrets.add(secret);
return secret;
},
};
}

View File

@ -1320,8 +1320,6 @@ vAPI.DOMFilterer = class {
vAPI.userStylesheet.apply();
}
// Library of resources is located at:
// 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.mainWorld ) {

View File

@ -23,10 +23,10 @@
/******************************************************************************/
import './utils.js';
import logger from './logger.js';
import µb from './background.js';
import { MRUCache } from './mrucache.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
/******************************************************************************/
@ -244,13 +244,13 @@ const FilterContainer = function() {
canonical: 'highGenericHideSimple',
dict: new Set(),
str: '',
mru: new µb.MRUCache(16)
mru: new MRUCache(16)
};
this.highlyGeneric.complex = {
canonical: 'highGenericHideComplex',
dict: new Set(),
str: '',
mru: new µb.MRUCache(16)
mru: new MRUCache(16)
};
// Short-lived: content is valid only during one function call. These

View File

@ -718,12 +718,17 @@ const retrieveContentScriptParameters = async function(sender, request) {
if ( logger.enabled || request.needScriptlets ) {
const scriptletDetails = scriptletFilteringEngine.injectNow(request);
if ( scriptletDetails !== undefined ) {
if ( logger.enabled ) {
scriptletFilteringEngine.logFilters(
tabId,
request.url,
scriptletDetails.filters
);
if ( logger.enabled && typeof scriptletDetails.filters === 'string' ) {
const fctxt = µb.filteringContext
.duplicate()
.fromTabId(tabId)
.setRealm('extended')
.setType('scriptlet')
.setURL(request.url)
.setDocOriginFromURL(request.url);
for ( const raw of scriptletDetails.filters.split('\n') ) {
fctxt.setFilter({ source: 'extended', raw }).toLogger();
}
}
if ( request.needScriptlets ) {
response.scriptletDetails = scriptletDetails;

58
src/js/mrucache.js Normal file
View File

@ -0,0 +1,58 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
export class MRUCache {
constructor(maxSize) {
this.maxSize = maxSize;
this.array = [];
this.map = new Map();
this.resetTime = Date.now();
}
add(key, value) {
const found = this.map.has(key);
this.map.set(key, value);
if ( found ) { return; }
if ( this.array.length === this.maxSize ) {
this.map.delete(this.array.pop());
}
this.array.unshift(key);
}
remove(key) {
if ( this.map.delete(key) === false ) { return; }
this.array.splice(this.array.indexOf(key), 1);
}
lookup(key) {
const value = this.map.get(key);
if ( value === undefined ) { return; }
if ( this.array[0] === key ) { return value; }
const i = this.array.indexOf(key);
this.array.copyWithin(1, 0, i);
this.array[0] = key;
return value;
}
reset() {
this.array = [];
this.map.clear();
this.resetTime = Date.now();
}
}

View File

@ -0,0 +1,300 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2017-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
'use strict';
/******************************************************************************/
import { redirectEngine as reng } from './redirect-engine.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
/******************************************************************************/
// Increment when internal representation changes
const VERSION = 1;
const $scriptlets = new Set();
const $exceptions = new Set();
const $mainWorldMap = new Map();
const $isolatedWorldMap = new Map();
/******************************************************************************/
const normalizeRawFilter = (parser, sourceIsTrusted = false) => {
const args = parser.getScriptletArgs();
if ( args.length !== 0 ) {
let token = `${args[0]}.js`;
if ( reng.aliases.has(token) ) {
token = reng.aliases.get(token);
}
if ( parser.isException() !== true ) {
if ( sourceIsTrusted !== true ) {
if ( reng.tokenRequiresTrust(token) ) { return; }
}
}
args[0] = token.slice(0, -3);
}
return JSON.stringify(args);
};
const lookupScriptlet = (rawToken, mainMap, isolatedMap, debug = false) => {
if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; }
const args = JSON.parse(rawToken);
const token = `${args[0]}.js`;
const details = reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { return; }
const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap;
const content = patchScriptlet(details.js, args.slice(1));
const dependencies = details.dependencies || [];
while ( dependencies.length !== 0 ) {
const token = dependencies.shift();
if ( targetWorldMap.has(token) ) { continue; }
const details = reng.contentFromName(token, 'fn/javascript') ||
reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { continue; }
targetWorldMap.set(token, details.js);
if ( Array.isArray(details.dependencies) === false ) { continue; }
dependencies.push(...details.dependencies);
}
targetWorldMap.set(rawToken, [
'try {',
'// >>>> scriptlet start',
content,
'// <<<< scriptlet end',
'} catch (e) {',
debug ? 'console.error(e);' : '',
'}',
].join('\n'));
};
// Fill-in scriptlet argument placeholders.
const patchScriptlet = (content, arglist) => {
if ( content.startsWith('function') && content.endsWith('}') ) {
content = `(${content})({{args}});`;
}
for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]);
}
return content.replace('{{args}}',
JSON.stringify(arglist).slice(1,-1).replace(/\$/g, '$$$')
);
};
const decompile = json => {
const args = JSON.parse(json).map(s => s.replace(/,/g, '\\,'));
if ( args.length === 0 ) { return '+js()'; }
return `+js(${args.join(', ')})`;
};
/******************************************************************************/
export class ScriptletFilteringEngine {
constructor() {
this.acceptedCount = 0;
this.discardedCount = 0;
this.scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION);
this.duplicates = new Set();
}
getFilterCount() {
return this.scriptletDB.size;
}
reset() {
this.scriptletDB.clear();
this.duplicates.clear();
this.acceptedCount = 0;
this.discardedCount = 0;
}
freeze() {
this.duplicates.clear();
this.scriptletDB.collectGarbage();
}
// parser: instance of AstFilterParser from static-filtering-parser.js
// writer: instance of CompiledListWriter from static-filtering-io.js
compile(parser, writer) {
writer.select('SCRIPTLET_FILTERS');
// Only exception filters are allowed to be global.
const isException = parser.isException();
const normalized = normalizeRawFilter(parser, writer.properties.get('trustedSource'));
// Can fail if there is a mismatch with trust requirement
if ( normalized === undefined ) { return; }
// Tokenless is meaningful only for exception filters.
if ( normalized === '[]' && isException === false ) { return; }
if ( parser.hasOptions() === false ) {
if ( isException ) {
writer.push([ 32, '', 1, normalized ]);
}
return;
}
// https://github.com/gorhill/uBlock/issues/3375
// Ignore instances of exception filter with negated hostnames,
// because there is no way to create an exception to an exception.
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
let kind = 0;
if ( isException ) {
if ( not ) { continue; }
kind |= 1;
} else if ( not ) {
kind |= 1;
}
writer.push([ 32, hn, kind, normalized ]);
}
}
// writer: instance of CompiledListReader from static-filtering-io.js
fromCompiledContent(reader) {
reader.select('SCRIPTLET_FILTERS');
while ( reader.next() ) {
this.acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( this.duplicates.has(fingerprint) ) {
this.discardedCount += 1;
continue;
}
this.duplicates.add(fingerprint);
const args = reader.args();
if ( args.length < 4 ) { continue; }
this.scriptletDB.store(args[1], args[2], args[3]);
}
}
toSelfie() {
return this.scriptletDB.toSelfie();
}
fromSelfie(selfie) {
if ( selfie instanceof Object === false ) { return false; }
if ( selfie.version !== VERSION ) { return false; }
this.scriptletDB.fromSelfie(selfie);
return true;
}
retrieve(request, options = {}) {
if ( this.scriptletDB.size === 0 ) { return; }
$scriptlets.clear();
$exceptions.clear();
const { hostname } = request;
this.scriptletDB.retrieve(hostname, [ $scriptlets, $exceptions ]);
const entity = request.entity !== ''
? `${hostname.slice(0, -request.domain.length)}${request.entity}`
: '*';
this.scriptletDB.retrieve(entity, [ $scriptlets, $exceptions ], 1);
if ( $scriptlets.size === 0 ) { return; }
// Wholly disable scriptlet injection?
if ( $exceptions.has('[]') ) {
return { filters: '#@#+js()' };
}
for ( const token of $exceptions ) {
if ( $scriptlets.has(token) ) {
$scriptlets.delete(token);
} else {
$exceptions.delete(token);
}
}
for ( const token of $scriptlets ) {
lookupScriptlet(token, $mainWorldMap, $isolatedWorldMap, options.debug);
}
const mainWorldCode = [];
for ( const js of $mainWorldMap.values() ) {
mainWorldCode.push(js);
}
const isolatedWorldCode = [];
for ( const js of $isolatedWorldMap.values() ) {
isolatedWorldCode.push(js);
}
const scriptletDetails = {
mainWorld: mainWorldCode.join('\n\n'),
isolatedWorld: isolatedWorldCode.join('\n\n'),
filters: [
...Array.from($scriptlets).map(s => `##${decompile(s)}`),
...Array.from($exceptions).map(s => `#@#${decompile(s)}`),
].join('\n'),
};
$mainWorldMap.clear();
$isolatedWorldMap.clear();
if ( scriptletDetails.mainWorld === '' ) {
if ( scriptletDetails.isolatedWorld === '' ) {
return { filters: scriptletDetails.filters };
}
}
const scriptletGlobals = options.scriptletGlobals || [];
if ( options.debug ) {
scriptletGlobals.push([ 'canDebug', true ]);
}
return {
mainWorld: scriptletDetails.mainWorld === '' ? '' : [
'(function() {',
'// >>>> start of private namespace',
'',
options.debugScriptlets ? 'debugger;' : ';',
'',
// For use by scriptlets to share local data among themselves
`const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`,
'',
scriptletDetails.mainWorld,
'',
'// <<<< end of private namespace',
'})();',
].join('\n'),
isolatedWorld: scriptletDetails.isolatedWorld === '' ? '' : [
'function() {',
'// >>>> start of private namespace',
'',
options.debugScriptlets ? 'debugger;' : ';',
'',
// For use by scriptlets to share local data among themselves
`const scriptletGlobals = new Map(${JSON.stringify(scriptletGlobals, null, 2)});`,
'',
scriptletDetails.isolatedWorld,
'',
'// <<<< end of private namespace',
'}',
].join('\n'),
filters: scriptletDetails.filters,
};
}
}
/******************************************************************************/

View File

@ -29,7 +29,8 @@ import µb from './background.js';
import { onBroadcast } from './broadcast.js';
import { redirectEngine as reng } from './redirect-engine.js';
import { sessionFirewall } from './filtering-engines.js';
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
import { MRUCache } from './mrucache.js';
import { ScriptletFilteringEngine } from './scriptlet-filtering-core.js';
import {
domainFromHostname,
@ -39,31 +40,6 @@ import {
/******************************************************************************/
// Increment when internal representation changes
const VERSION = 1;
const duplicates = new Set();
const scriptletCache = new µb.MRUCache(32);
const scriptletDB = new StaticExtFilteringHostnameDB(1, VERSION);
let acceptedCount = 0;
let discardedCount = 0;
let isDevBuild;
const scriptletFilteringEngine = {
get acceptedCount() {
return acceptedCount;
},
get discardedCount() {
return discardedCount;
},
getFilterCount() {
return scriptletDB.size;
},
};
const contentScriptRegisterer = new (class {
constructor() {
this.hostnameToDetails = new Map();
@ -132,27 +108,7 @@ const contentScriptRegisterer = new (class {
}
})();
// Purpose of `contentscriptCode` below is too programmatically inject
// content script code which only purpose is to inject scriptlets. This
// essentially does the same as what uBO's declarative content script does,
// except that this allows to inject the scriptlets earlier than it is
// possible through the declarative content script.
//
// Declaratively:
// 1. Browser injects generic content script =>
// 2. Content script queries scriptlets =>
// 3. Main process sends scriptlets =>
// 4. Content script injects scriptlets
//
// Programmatically:
// 1. uBO injects specific scriptlets-aware content script =>
// 2. Content script injects scriptlets
//
// However currently this programmatic injection works well only on
// Chromium-based browsers, it does not work properly with Firefox. More
// investigations is needed to find out why this fails with Firefox.
// Consequently, the programmatic-injection code path is taken only with
// Chromium-based browsers.
/******************************************************************************/
const mainWorldInjector = (( ) => {
const parts = [
@ -221,331 +177,135 @@ const isolatedWorldInjector = (( ) => {
};
})();
const normalizeRawFilter = function(parser, sourceIsTrusted = false) {
const args = parser.getScriptletArgs();
if ( args.length !== 0 ) {
let token = `${args[0]}.js`;
if ( reng.aliases.has(token) ) {
token = reng.aliases.get(token);
}
if ( parser.isException() !== true ) {
if ( sourceIsTrusted !== true ) {
if ( reng.tokenRequiresTrust(token) ) { return; }
}
}
args[0] = token.slice(0, -3);
}
return JSON.stringify(args);
};
const lookupScriptlet = function(rawToken, mainMap, isolatedMap) {
if ( mainMap.has(rawToken) || isolatedMap.has(rawToken) ) { return; }
const args = JSON.parse(rawToken);
const token = `${args[0]}.js`;
const details = reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { return; }
const targetWorldMap = details.world !== 'ISOLATED' ? mainMap : isolatedMap;
const content = patchScriptlet(details.js, args.slice(1));
const dependencies = details.dependencies || [];
while ( dependencies.length !== 0 ) {
const token = dependencies.shift();
if ( targetWorldMap.has(token) ) { continue; }
const details = reng.contentFromName(token, 'fn/javascript') ||
reng.contentFromName(token, 'text/javascript');
if ( details === undefined ) { continue; }
targetWorldMap.set(token, details.js);
if ( Array.isArray(details.dependencies) === false ) { continue; }
dependencies.push(...details.dependencies);
}
targetWorldMap.set(rawToken, [
'try {',
'// >>>> scriptlet start',
content,
'// <<<< scriptlet end',
'} catch (e) {',
isDevBuild ? 'console.error(e);' : '',
'}',
].join('\n'));
};
// Fill-in scriptlet argument placeholders.
const patchScriptlet = function(content, arglist) {
if ( content.startsWith('function') && content.endsWith('}') ) {
content = `(${content})({{args}});`;
}
for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]);
}
return content.replace('{{args}}',
JSON.stringify(arglist).slice(1,-1).replace(/\$/g, '$$$')
);
};
const decompile = function(json) {
const args = JSON.parse(json).map(s => s.replace(/,/g, '\\,'));
if ( args.length === 0 ) { return '+js()'; }
return `+js(${args.join(', ')})`;
};
/******************************************************************************/
scriptletFilteringEngine.logFilters = function(tabId, url, filters) {
if ( typeof filters !== 'string' ) { return; }
const fctxt = µb.filteringContext
.duplicate()
.fromTabId(tabId)
.setRealm('extended')
.setType('scriptlet')
.setURL(url)
.setDocOriginFromURL(url);
for ( const filter of filters.split('\n') ) {
fctxt.setFilter({ source: 'extended', raw: filter }).toLogger();
}
};
scriptletFilteringEngine.reset = function() {
scriptletDB.clear();
duplicates.clear();
contentScriptRegisterer.reset();
scriptletCache.reset();
acceptedCount = 0;
discardedCount = 0;
};
scriptletFilteringEngine.freeze = function() {
duplicates.clear();
scriptletDB.collectGarbage();
scriptletCache.reset();
};
scriptletFilteringEngine.compile = function(parser, writer) {
writer.select('SCRIPTLET_FILTERS');
// Only exception filters are allowed to be global.
const isException = parser.isException();
const normalized = normalizeRawFilter(parser, writer.properties.get('trustedSource'));
// Can fail if there is a mismatch with trust requirement
if ( normalized === undefined ) { return; }
// Tokenless is meaningful only for exception filters.
if ( normalized === '[]' && isException === false ) { return; }
if ( parser.hasOptions() === false ) {
if ( isException ) {
writer.push([ 32, '', 1, normalized ]);
}
return;
}
// https://github.com/gorhill/uBlock/issues/3375
// Ignore instances of exception filter with negated hostnames,
// because there is no way to create an exception to an exception.
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
if ( bad ) { continue; }
let kind = 0;
if ( isException ) {
if ( not ) { continue; }
kind |= 1;
} else if ( not ) {
kind |= 1;
}
writer.push([ 32, hn, kind, normalized ]);
}
};
scriptletFilteringEngine.fromCompiledContent = function(reader) {
reader.select('SCRIPTLET_FILTERS');
while ( reader.next() ) {
acceptedCount += 1;
const fingerprint = reader.fingerprint();
if ( duplicates.has(fingerprint) ) {
discardedCount += 1;
continue;
}
duplicates.add(fingerprint);
const args = reader.args();
if ( args.length < 4 ) { continue; }
scriptletDB.store(args[1], args[2], args[3]);
}
};
const $scriptlets = new Set();
const $exceptions = new Set();
const $mainWorldMap = new Map();
const $isolatedWorldMap = new Map();
scriptletFilteringEngine.retrieve = function(request) {
if ( scriptletDB.size === 0 ) { return; }
const hostname = request.hostname;
// https://github.com/gorhill/uBlock/issues/2835
// Do not inject scriptlets if the site is under an `allow` rule.
if (
µb.userSettings.advancedUserEnabled &&
sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2
) {
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 { filters: '#@#+js()' };
}
for ( const token of $exceptions ) {
if ( $scriptlets.has(token) ) {
$scriptlets.delete(token);
} else {
$exceptions.delete(token);
}
}
for ( const token of $scriptlets ) {
lookupScriptlet(token, $mainWorldMap, $isolatedWorldMap);
}
const mainWorldCode = [];
for ( const js of $mainWorldMap.values() ) {
mainWorldCode.push(js);
}
const isolatedWorldCode = [];
for ( const js of $isolatedWorldMap.values() ) {
isolatedWorldCode.push(js);
}
cacheDetails = {
mainWorld: mainWorldCode.join('\n\n'),
isolatedWorld: isolatedWorldCode.join('\n\n'),
filters: [
...Array.from($scriptlets).map(s => `##${decompile(s)}`),
...Array.from($exceptions).map(s => `#@#${decompile(s)}`),
].join('\n'),
};
scriptletCache.add(hostname, cacheDetails);
$mainWorldMap.clear();
$isolatedWorldMap.clear();
}
if ( cacheDetails.mainWorld === '' && cacheDetails.isolatedWorld === '' ) {
return { filters: cacheDetails.filters };
}
const scriptletGlobals = [
[ 'warOrigin', vAPI.getURL('/web_accessible_resources') ],
[ 'warSecret', vAPI.warSecret.long() ],
];
if ( isDevBuild === undefined ) {
isDevBuild = vAPI.webextFlavor.soup.has('devbuild');
}
if ( isDevBuild || µb.hiddenSettings.filterAuthorMode ) {
scriptletGlobals.push([ 'canDebug', true ]);
}
return {
mainWorld: cacheDetails.mainWorld === '' ? '' : [
'(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.mainWorld,
'',
'// <<<< end of private namespace',
'})();',
].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) {
if ( typeof details.frameId !== 'number' ) { return; }
const request = {
tabId: details.tabId,
frameId: details.frameId,
url: details.url,
hostname: hostnameFromURI(details.url),
domain: undefined,
entity: undefined
};
request.domain = domainFromHostname(request.hostname);
request.entity = entityFromDomain(request.domain);
const scriptletDetails = this.retrieve(request);
if ( scriptletDetails === undefined ) {
contentScriptRegisterer.unregister(request.hostname);
return;
}
const contentScript = [];
if ( µb.hiddenSettings.debugScriptletInjector ) {
contentScript.push('debugger');
}
const { mainWorld = '', isolatedWorld = '', filters } = scriptletDetails;
if ( mainWorld !== '' ) {
contentScript.push(mainWorldInjector.assemble(request.hostname, mainWorld, filters));
}
if ( isolatedWorld !== '' ) {
contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld));
}
const code = contentScript.join('\n\n');
const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code);
if ( isAlreadyInjected !== true ) {
vAPI.tabs.executeScript(details.tabId, {
code,
frameId: details.frameId,
matchAboutBlank: true,
runAt: 'document_start',
export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine {
constructor() {
super();
this.warOrigin = vAPI.getURL('/web_accessible_resources');
this.warSecret = undefined;
this.scriptletCache = new MRUCache(32);
this.isDevBuild = undefined;
onBroadcast(msg => {
if ( msg.what !== 'hiddenSettingsChanged' ) { return; }
this.scriptletCache.reset();
this.isDevBuild = undefined;
});
}
return scriptletDetails;
};
scriptletFilteringEngine.toSelfie = function() {
return scriptletDB.toSelfie();
};
reset() {
super.reset();
this.warSecret = vAPI.warSecret.long(this.warSecret);
this.scriptletCache.reset();
contentScriptRegisterer.reset();
}
scriptletFilteringEngine.fromSelfie = function(selfie) {
if ( selfie instanceof Object === false ) { return false; }
if ( selfie.version !== VERSION ) { return false; }
scriptletDB.fromSelfie(selfie);
return true;
};
freeze() {
super.freeze();
this.warSecret = vAPI.warSecret.long(this.warSecret);
this.scriptletCache.reset();
contentScriptRegisterer.reset();
}
retrieve(request) {
const { hostname } = request;
// https://github.com/gorhill/uBlock/issues/2835
// Do not inject scriptlets if the site is under an `allow` rule.
if ( µb.userSettings.advancedUserEnabled ) {
if ( sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2 ) {
return;
}
}
if ( this.scriptletCache.resetTime < reng.modifyTime ) {
this.warSecret = vAPI.warSecret.long(this.warSecret);
this.scriptletCache.reset();
}
let scriptletDetails = this.scriptletCache.lookup(hostname);
if ( scriptletDetails !== undefined ) {
return scriptletDetails || undefined;
}
if ( this.isDevBuild === undefined ) {
this.isDevBuild = vAPI.webextFlavor.soup.has('devbuild') ||
µb.hiddenSettings.filterAuthorMode;
}
if ( this.warSecret === undefined ) {
this.warSecret = vAPI.warSecret.long();
}
const options = {
scriptletGlobals: [
[ 'warOrigin', this.warOrigin ],
[ 'warSecret', this.warSecret ],
],
debug: this.isDevBuild,
debugScriptlets: µb.hiddenSettings.debugScriptlets,
};
scriptletDetails = super.retrieve(request, options);
this.scriptletCache.add(hostname, scriptletDetails || null);
return scriptletDetails;
}
injectNow(details) {
if ( typeof details.frameId !== 'number' ) { return; }
const request = {
tabId: details.tabId,
frameId: details.frameId,
url: details.url,
hostname: hostnameFromURI(details.url),
domain: undefined,
entity: undefined
};
request.domain = domainFromHostname(request.hostname);
request.entity = entityFromDomain(request.domain);
const scriptletDetails = this.retrieve(request);
if ( scriptletDetails === undefined ) {
contentScriptRegisterer.unregister(request.hostname);
return;
}
const contentScript = [];
if ( µb.hiddenSettings.debugScriptletInjector ) {
contentScript.push('debugger');
}
const { mainWorld = '', isolatedWorld = '', filters } = scriptletDetails;
if ( mainWorld !== '' ) {
contentScript.push(mainWorldInjector.assemble(request.hostname, mainWorld, filters));
}
if ( isolatedWorld !== '' ) {
contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld));
}
const code = contentScript.join('\n\n');
const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code);
if ( isAlreadyInjected !== true ) {
vAPI.tabs.executeScript(details.tabId, {
code,
frameId: details.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
}
return scriptletDetails;
}
}
/******************************************************************************/
const scriptletFilteringEngine = new ScriptletFilteringEngineEx();
export default scriptletFilteringEngine;
/******************************************************************************/

View File

@ -84,48 +84,6 @@ import µb from './background.js';
/******************************************************************************/
µb.MRUCache = class {
constructor(size) {
this.size = size;
this.array = [];
this.map = new Map();
this.resetTime = Date.now();
}
add(key, value) {
const found = this.map.has(key);
this.map.set(key, value);
if ( !found ) {
if ( this.array.length === this.size ) {
this.map.delete(this.array.pop());
}
this.array.unshift(key);
}
}
remove(key) {
if ( this.map.has(key) ) {
this.array.splice(this.array.indexOf(key), 1);
}
}
lookup(key) {
const value = this.map.get(key);
if ( value !== undefined && this.array[0] !== key ) {
let i = this.array.indexOf(key);
do {
this.array[i] = this.array[i-1];
} while ( --i );
this.array[0] = key;
}
return value;
}
reset() {
this.array = [];
this.map.clear();
this.resetTime = Date.now();
}
};
/******************************************************************************/
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
µb.escapeRegex = function(s) {