mirror of
https://github.com/gorhill/uBlock.git
synced 2024-10-06 09:37:12 +02:00
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:
parent
abeadf18eb
commit
aa7f77aaad
@ -1175,16 +1175,15 @@ vAPI.messaging = {
|
|||||||
const shortSecrets = [];
|
const shortSecrets = [];
|
||||||
let lastShortSecretTime = 0;
|
let lastShortSecretTime = 0;
|
||||||
|
|
||||||
// Long secrets are meant to be used multiple times, but for at most a few
|
// Long secrets are valid until revoked or uBO restarts. The realm is one
|
||||||
// minutes. The realm is one value out of 36^18 = over 10^28 values.
|
// value out of 36^18 = over 10^28 values.
|
||||||
const longSecrets = [ '', '' ];
|
const longSecrets = new Set();
|
||||||
let lastLongSecretTimeSlice = 0;
|
|
||||||
|
|
||||||
const guard = details => {
|
const guard = details => {
|
||||||
const match = reSecret.exec(details.url);
|
const match = reSecret.exec(details.url);
|
||||||
if ( match === null ) { return { cancel: true }; }
|
if ( match === null ) { return { cancel: true }; }
|
||||||
const secret = match[1];
|
const secret = match[1];
|
||||||
if ( longSecrets.includes(secret) ) { return; }
|
if ( longSecrets.has(secret) ) { return; }
|
||||||
const pos = shortSecrets.indexOf(secret);
|
const pos = shortSecrets.indexOf(secret);
|
||||||
if ( pos === -1 ) { return { cancel: true }; }
|
if ( pos === -1 ) { return { cancel: true }; }
|
||||||
shortSecrets.splice(pos, 1);
|
shortSecrets.splice(pos, 1);
|
||||||
@ -1212,14 +1211,13 @@ vAPI.messaging = {
|
|||||||
shortSecrets.push(secret);
|
shortSecrets.push(secret);
|
||||||
return secret;
|
return secret;
|
||||||
},
|
},
|
||||||
long: ( ) => {
|
long: previous => {
|
||||||
const timeSlice = Date.now() >>> 19; // Changes every ~9 minutes
|
if ( previous !== undefined ) {
|
||||||
if ( timeSlice !== lastLongSecretTimeSlice ) {
|
longSecrets.delete(previous);
|
||||||
longSecrets[1] = longSecrets[0];
|
|
||||||
longSecrets[0] = `${generateSecret()}${generateSecret()}${generateSecret()}`;
|
|
||||||
lastLongSecretTimeSlice = timeSlice;
|
|
||||||
}
|
}
|
||||||
return longSecrets[0];
|
const secret = `${generateSecret()}${generateSecret()}${generateSecret()}`;
|
||||||
|
longSecrets.add(secret);
|
||||||
|
return secret;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1320,8 +1320,6 @@ vAPI.DOMFilterer = class {
|
|||||||
vAPI.userStylesheet.apply();
|
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' ) {
|
if ( scriptletDetails && typeof self.uBO_scriptletsInjected !== 'string' ) {
|
||||||
self.uBO_scriptletsInjected = scriptletDetails.filters;
|
self.uBO_scriptletsInjected = scriptletDetails.filters;
|
||||||
if ( scriptletDetails.mainWorld ) {
|
if ( scriptletDetails.mainWorld ) {
|
||||||
|
@ -23,10 +23,10 @@
|
|||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
import './utils.js';
|
|
||||||
import logger from './logger.js';
|
import logger from './logger.js';
|
||||||
import µb from './background.js';
|
import µb from './background.js';
|
||||||
|
|
||||||
|
import { MRUCache } from './mrucache.js';
|
||||||
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
|
import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js';
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
@ -244,13 +244,13 @@ const FilterContainer = function() {
|
|||||||
canonical: 'highGenericHideSimple',
|
canonical: 'highGenericHideSimple',
|
||||||
dict: new Set(),
|
dict: new Set(),
|
||||||
str: '',
|
str: '',
|
||||||
mru: new µb.MRUCache(16)
|
mru: new MRUCache(16)
|
||||||
};
|
};
|
||||||
this.highlyGeneric.complex = {
|
this.highlyGeneric.complex = {
|
||||||
canonical: 'highGenericHideComplex',
|
canonical: 'highGenericHideComplex',
|
||||||
dict: new Set(),
|
dict: new Set(),
|
||||||
str: '',
|
str: '',
|
||||||
mru: new µb.MRUCache(16)
|
mru: new MRUCache(16)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Short-lived: content is valid only during one function call. These
|
// Short-lived: content is valid only during one function call. These
|
||||||
|
@ -718,12 +718,17 @@ const retrieveContentScriptParameters = async function(sender, request) {
|
|||||||
if ( logger.enabled || request.needScriptlets ) {
|
if ( logger.enabled || request.needScriptlets ) {
|
||||||
const scriptletDetails = scriptletFilteringEngine.injectNow(request);
|
const scriptletDetails = scriptletFilteringEngine.injectNow(request);
|
||||||
if ( scriptletDetails !== undefined ) {
|
if ( scriptletDetails !== undefined ) {
|
||||||
if ( logger.enabled ) {
|
if ( logger.enabled && typeof scriptletDetails.filters === 'string' ) {
|
||||||
scriptletFilteringEngine.logFilters(
|
const fctxt = µb.filteringContext
|
||||||
tabId,
|
.duplicate()
|
||||||
request.url,
|
.fromTabId(tabId)
|
||||||
scriptletDetails.filters
|
.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 ) {
|
if ( request.needScriptlets ) {
|
||||||
response.scriptletDetails = scriptletDetails;
|
response.scriptletDetails = scriptletDetails;
|
||||||
|
58
src/js/mrucache.js
Normal file
58
src/js/mrucache.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
300
src/js/scriptlet-filtering-core.js
Normal file
300
src/js/scriptlet-filtering-core.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
@ -29,7 +29,8 @@ import µb from './background.js';
|
|||||||
import { onBroadcast } from './broadcast.js';
|
import { onBroadcast } from './broadcast.js';
|
||||||
import { redirectEngine as reng } from './redirect-engine.js';
|
import { redirectEngine as reng } from './redirect-engine.js';
|
||||||
import { sessionFirewall } from './filtering-engines.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 {
|
import {
|
||||||
domainFromHostname,
|
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 {
|
const contentScriptRegisterer = new (class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.hostnameToDetails = new Map();
|
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 mainWorldInjector = (( ) => {
|
||||||
const parts = [
|
const parts = [
|
||||||
@ -221,279 +177,85 @@ 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) {
|
export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine {
|
||||||
if ( typeof filters !== 'string' ) { return; }
|
constructor() {
|
||||||
const fctxt = µb.filteringContext
|
super();
|
||||||
.duplicate()
|
this.warOrigin = vAPI.getURL('/web_accessible_resources');
|
||||||
.fromTabId(tabId)
|
this.warSecret = undefined;
|
||||||
.setRealm('extended')
|
this.scriptletCache = new MRUCache(32);
|
||||||
.setType('scriptlet')
|
this.isDevBuild = undefined;
|
||||||
.setURL(url)
|
onBroadcast(msg => {
|
||||||
.setDocOriginFromURL(url);
|
if ( msg.what !== 'hiddenSettingsChanged' ) { return; }
|
||||||
for ( const filter of filters.split('\n') ) {
|
this.scriptletCache.reset();
|
||||||
fctxt.setFilter({ source: 'extended', raw: filter }).toLogger();
|
this.isDevBuild = undefined;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
scriptletFilteringEngine.reset = function() {
|
reset() {
|
||||||
scriptletDB.clear();
|
super.reset();
|
||||||
duplicates.clear();
|
this.warSecret = vAPI.warSecret.long(this.warSecret);
|
||||||
|
this.scriptletCache.reset();
|
||||||
contentScriptRegisterer.reset();
|
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
|
freeze() {
|
||||||
// Ignore instances of exception filter with negated hostnames,
|
super.freeze();
|
||||||
// because there is no way to create an exception to an exception.
|
this.warSecret = vAPI.warSecret.long(this.warSecret);
|
||||||
|
this.scriptletCache.reset();
|
||||||
for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
|
contentScriptRegisterer.reset();
|
||||||
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) {
|
retrieve(request) {
|
||||||
reader.select('SCRIPTLET_FILTERS');
|
const { hostname } = request;
|
||||||
|
|
||||||
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
|
// https://github.com/gorhill/uBlock/issues/2835
|
||||||
// Do not inject scriptlets if the site is under an `allow` rule.
|
// Do not inject scriptlets if the site is under an `allow` rule.
|
||||||
if (
|
if ( µb.userSettings.advancedUserEnabled ) {
|
||||||
µb.userSettings.advancedUserEnabled &&
|
if ( sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2 ) {
|
||||||
sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( scriptletCache.resetTime < reng.modifyTime ) {
|
|
||||||
scriptletCache.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cacheDetails = scriptletCache.lookup(hostname);
|
if ( this.scriptletCache.resetTime < reng.modifyTime ) {
|
||||||
if ( cacheDetails === undefined ) {
|
this.warSecret = vAPI.warSecret.long(this.warSecret);
|
||||||
$scriptlets.clear();
|
this.scriptletCache.reset();
|
||||||
$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 ) {
|
let scriptletDetails = this.scriptletCache.lookup(hostname);
|
||||||
if ( $scriptlets.has(token) ) {
|
if ( scriptletDetails !== undefined ) {
|
||||||
$scriptlets.delete(token);
|
return scriptletDetails || undefined;
|
||||||
} else {
|
|
||||||
$exceptions.delete(token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.isDevBuild === undefined ) {
|
||||||
|
this.isDevBuild = vAPI.webextFlavor.soup.has('devbuild') ||
|
||||||
|
µb.hiddenSettings.filterAuthorMode;
|
||||||
}
|
}
|
||||||
for ( const token of $scriptlets ) {
|
|
||||||
lookupScriptlet(token, $mainWorldMap, $isolatedWorldMap);
|
if ( this.warSecret === undefined ) {
|
||||||
|
this.warSecret = vAPI.warSecret.long();
|
||||||
}
|
}
|
||||||
const mainWorldCode = [];
|
|
||||||
for ( const js of $mainWorldMap.values() ) {
|
const options = {
|
||||||
mainWorldCode.push(js);
|
scriptletGlobals: [
|
||||||
}
|
[ 'warOrigin', this.warOrigin ],
|
||||||
const isolatedWorldCode = [];
|
[ 'warSecret', this.warSecret ],
|
||||||
for ( const js of $isolatedWorldMap.values() ) {
|
],
|
||||||
isolatedWorldCode.push(js);
|
debug: this.isDevBuild,
|
||||||
}
|
debugScriptlets: µb.hiddenSettings.debugScriptlets,
|
||||||
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();
|
scriptletDetails = super.retrieve(request, options);
|
||||||
$isolatedWorldMap.clear();
|
|
||||||
|
this.scriptletCache.add(hostname, scriptletDetails || null);
|
||||||
|
|
||||||
|
return scriptletDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( cacheDetails.mainWorld === '' && cacheDetails.isolatedWorld === '' ) {
|
injectNow(details) {
|
||||||
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; }
|
if ( typeof details.frameId !== 'number' ) { return; }
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
tabId: details.tabId,
|
tabId: details.tabId,
|
||||||
frameId: details.frameId,
|
frameId: details.frameId,
|
||||||
@ -502,13 +264,16 @@ scriptletFilteringEngine.injectNow = function(details) {
|
|||||||
domain: undefined,
|
domain: undefined,
|
||||||
entity: undefined
|
entity: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
request.domain = domainFromHostname(request.hostname);
|
request.domain = domainFromHostname(request.hostname);
|
||||||
request.entity = entityFromDomain(request.domain);
|
request.entity = entityFromDomain(request.domain);
|
||||||
|
|
||||||
const scriptletDetails = this.retrieve(request);
|
const scriptletDetails = this.retrieve(request);
|
||||||
if ( scriptletDetails === undefined ) {
|
if ( scriptletDetails === undefined ) {
|
||||||
contentScriptRegisterer.unregister(request.hostname);
|
contentScriptRegisterer.unregister(request.hostname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentScript = [];
|
const contentScript = [];
|
||||||
if ( µb.hiddenSettings.debugScriptletInjector ) {
|
if ( µb.hiddenSettings.debugScriptletInjector ) {
|
||||||
contentScript.push('debugger');
|
contentScript.push('debugger');
|
||||||
@ -520,7 +285,9 @@ scriptletFilteringEngine.injectNow = function(details) {
|
|||||||
if ( isolatedWorld !== '' ) {
|
if ( isolatedWorld !== '' ) {
|
||||||
contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld));
|
contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld));
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = contentScript.join('\n\n');
|
const code = contentScript.join('\n\n');
|
||||||
|
|
||||||
const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code);
|
const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code);
|
||||||
if ( isAlreadyInjected !== true ) {
|
if ( isAlreadyInjected !== true ) {
|
||||||
vAPI.tabs.executeScript(details.tabId, {
|
vAPI.tabs.executeScript(details.tabId, {
|
||||||
@ -530,22 +297,15 @@ scriptletFilteringEngine.injectNow = function(details) {
|
|||||||
runAt: 'document_start',
|
runAt: 'document_start',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return scriptletDetails;
|
return scriptletDetails;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
scriptletFilteringEngine.toSelfie = function() {
|
|
||||||
return scriptletDB.toSelfie();
|
|
||||||
};
|
|
||||||
|
|
||||||
scriptletFilteringEngine.fromSelfie = function(selfie) {
|
|
||||||
if ( selfie instanceof Object === false ) { return false; }
|
|
||||||
if ( selfie.version !== VERSION ) { return false; }
|
|
||||||
scriptletDB.fromSelfie(selfie);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
const scriptletFilteringEngine = new ScriptletFilteringEngineEx();
|
||||||
|
|
||||||
export default scriptletFilteringEngine;
|
export default scriptletFilteringEngine;
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -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
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||||
|
|
||||||
µb.escapeRegex = function(s) {
|
µb.escapeRegex = function(s) {
|
||||||
|
Loading…
Reference in New Issue
Block a user