1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-05 11:37:01 +02:00

Add scriptlet dependencies to reduce code duplication

This commit is contained in:
Raymond Hill 2023-03-26 09:13:17 -04:00
parent 439951824a
commit 236fb3ad59
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 546 additions and 605 deletions

View File

@ -26,11 +26,64 @@
export const builtinScriptlets = [];
/// abort-current-script.js
/*******************************************************************************
Helper functions
These are meant to be used as dependencies to injectable scriptlets.
*******************************************************************************/
builtinScriptlets.push({
name: 'pattern-to-regex.fn',
fn: patternToRegex,
});
function patternToRegex(pattern) {
if ( pattern === '' ) {
return /^/;
}
if ( pattern.startsWith('/') && pattern.endsWith('/') ) {
return new RegExp(pattern.slice(1, -1));
}
return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
}
/******************************************************************************/
builtinScriptlets.push({
name: 'get-exception-token.fn',
fn: getExceptionToken,
});
function getExceptionToken() {
const token =
String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const oe = self.onerror;
self.onerror = function(msg, ...args) {
if ( typeof msg === 'string' && msg.includes(token) ) { return true; }
if ( oe instanceof Function ) {
return oe.call(this, msg, ...args);
}
}.bind();
return token;
}
/*******************************************************************************
Injectable scriptlets
These are meant to be used in the MAIN (webpage) execution world.
*******************************************************************************/
builtinScriptlets.push({
name: 'abort-current-script.js',
aliases: [ 'acs.js', 'abort-current-inline-script.js', 'acis.js' ],
fn: abortCurrentScript,
dependencies: [
'pattern-to-regex.fn',
'get-exception-token.fn',
],
});
// Issues to mind before changing anything:
// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
@ -41,21 +94,8 @@ function abortCurrentScript(
) {
if ( typeof target !== 'string' ) { return; }
if ( target === '' ) { return; }
const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
const reNeedle = (( ) => {
if ( needle === '' ) { return /^/; }
if ( /^\/.+\/$/.test(needle) ) {
return new RegExp(needle.slice(1,-1));
}
return new RegExp(needle.replace(reRegexEscape, '\\$&'));
})();
const reContext = (( ) => {
if ( context === '' ) { return; }
if ( /^\/.+\/$/.test(context) ) {
return new RegExp(context.slice(1,-1));
}
return new RegExp(context.replace(reRegexEscape, '\\$&'));
})();
const reNeedle = patternToRegex(needle);
const reContext = patternToRegex(context);
const thisScript = document.currentScript;
const chain = target.split('.');
let owner = window;
@ -75,8 +115,7 @@ function abortCurrentScript(
value = owner[prop];
desc = undefined;
}
const magic = String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const exceptionToken = getExceptionToken();
const scriptTexts = new WeakMap();
const getScriptText = elem => {
let text = elem.textContent;
@ -103,11 +142,9 @@ function abortCurrentScript(
const e = document.currentScript;
if ( e instanceof HTMLScriptElement === false ) { return; }
if ( e === thisScript ) { return; }
if ( reContext !== undefined && reContext.test(e.src) === false ) {
return;
}
if ( reContext.test(e.src) === false ) { return; }
if ( reNeedle.test(getScriptText(e)) === false ) { return; }
throw new ReferenceError(magic);
throw new ReferenceError(exceptionToken);
};
Object.defineProperty(owner, prop, {
get: function() {
@ -125,33 +162,26 @@ function abortCurrentScript(
}
}
});
const oe = window.onerror;
window.onerror = function(msg) {
if ( typeof msg === 'string' && msg.includes(magic) ) {
return true;
}
if ( oe instanceof Function ) {
return oe.apply(this, arguments);
}
}.bind();
}
/******************************************************************************/
/// abort-on-property-read.js
builtinScriptlets.push({
name: 'abort-on-property-read.js',
aliases: [ 'aopr.js' ],
fn: abortOnPropertyRead,
dependencies: [
'get-exception-token.fn',
],
});
function abortOnPropertyRead(
chain = ''
) {
if ( typeof chain !== 'string' ) { return; }
if ( chain === '' ) { return; }
const magic = String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const exceptionToken = getExceptionToken();
const abort = function() {
throw new ReferenceError(magic);
throw new ReferenceError(exceptionToken);
};
const makeProxy = function(owner, chain) {
const pos = chain.indexOf('.');
@ -186,31 +216,24 @@ function abortOnPropertyRead(
};
const owner = window;
makeProxy(owner, chain);
const oe = window.onerror;
window.onerror = function(msg, src, line, col, error) {
if ( typeof msg === 'string' && msg.indexOf(magic) !== -1 ) {
return true;
}
if ( oe instanceof Function ) {
return oe(msg, src, line, col, error);
}
}.bind();
}
/******************************************************************************/
/// abort-on-property-write.js
builtinScriptlets.push({
name: 'abort-on-property-write.js',
aliases: [ 'aopw.js' ],
fn: abortOnPropertyWrite,
dependencies: [
'get-exception-token.fn',
],
});
function abortOnPropertyWrite(
prop = ''
) {
if ( typeof prop !== 'string' ) { return; }
if ( prop === '' ) { return; }
const magic = String.fromCharCode(Date.now() % 26 + 97) +
Math.floor(Math.random() * 982451653 + 982451653).toString(36);
const exceptionToken = getExceptionToken();
let owner = window;
for (;;) {
const pos = prop.indexOf('.');
@ -222,26 +245,21 @@ function abortOnPropertyWrite(
delete owner[prop];
Object.defineProperty(owner, prop, {
set: function() {
throw new ReferenceError(magic);
throw new ReferenceError(exceptionToken);
}
});
const oe = window.onerror;
window.onerror = function(msg, src, line, col, error) {
if ( typeof msg === 'string' && msg.indexOf(magic) !== -1 ) {
return true;
}
if ( oe instanceof Function ) {
return oe(msg, src, line, col, error);
}
}.bind();
}
/******************************************************************************/
/// abort-on-stack-trace.js
builtinScriptlets.push({
name: 'abort-on-stack-trace.js',
aliases: [ 'aost.js' ],
fn: abortOnStackTrace,
dependencies: [
'pattern-to-regex.fn',
'get-exception-token.fn',
],
});
// Status is currently experimental
function abortOnStackTrace(
@ -250,19 +268,8 @@ function abortOnStackTrace(
logLevel = ''
) {
if ( typeof chain !== 'string' ) { return; }
const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
if ( needle === '' ) {
needle = '^';
} else if ( /^\/.+\/$/.test(needle) ) {
needle = needle.slice(1,-1);
} else {
needle = needle.replace(reRegexEscape, '\\$&');
}
const reNeedle = new RegExp(needle);
const magic = String.fromCharCode(Math.random() * 26 + 97) +
Math.floor(
(0.25 + Math.random() * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
const reNeedle = patternToRegex(needle);
const exceptionToken = getExceptionToken();
const log = console.log.bind(console);
const ErrorCtor = self.Error;
const mustAbort = function(err) {
@ -274,7 +281,7 @@ function abortOnStackTrace(
// Normalize stack trace
const lines = [];
for ( let line of err.stack.split(/[\n\r]+/) ) {
if ( line.includes(magic) ) { continue; }
if ( line.includes(exceptionToken) ) { continue; }
line = line.trim();
let match = /(.*?@)?(\S+)(:\d+):\d+\)?$/.exec(line);
if ( match === null ) { continue; }
@ -310,14 +317,14 @@ function abortOnStackTrace(
let v = owner[chain];
Object.defineProperty(owner, chain, {
get: function() {
if ( mustAbort(new ErrorCtor(magic)) ) {
throw new ReferenceError(magic);
if ( mustAbort(new ErrorCtor(exceptionToken)) ) {
throw new ReferenceError(exceptionToken);
}
return v;
},
set: function(a) {
if ( mustAbort(new ErrorCtor(magic)) ) {
throw new ReferenceError(magic);
if ( mustAbort(new ErrorCtor(exceptionToken)) ) {
throw new ReferenceError(exceptionToken);
}
v = a;
},
@ -345,23 +352,17 @@ function abortOnStackTrace(
};
const owner = window;
makeProxy(owner, chain);
const oe = window.onerror;
window.onerror = function(msg, src, line, col, error) {
if ( typeof msg === 'string' && msg.indexOf(magic) !== -1 ) {
return true;
}
if ( oe instanceof Function ) {
return oe(msg, src, line, col, error);
}
}.bind();
}
/******************************************************************************/
/// addEventListener-defuser.js
builtinScriptlets.push({
name: 'addEventListener-defuser.js',
aliases: [ 'aeld.js' ],
fn: addEventListenerDefuser,
dependencies: [
'pattern-to-regex.fn',
],
});
// https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120
function addEventListenerDefuser(
@ -374,22 +375,8 @@ function addEventListenerDefuser(
let { type = '', pattern = '' } = details;
if ( typeof type !== 'string' ) { return; }
if ( typeof pattern !== 'string' ) { return; }
if ( type === '' ) {
type = '^';
} else if ( /^\/.+\/$/.test(type) ) {
type = type.slice(1,-1);
} else {
type = type.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const reType = new RegExp(type);
if ( pattern === '' ) {
pattern = '^';
} else if ( /^\/.+\/$/.test(pattern) ) {
pattern = pattern.slice(1,-1);
} else {
pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const rePattern = new RegExp(pattern);
const reType = patternToRegex(type);
const rePattern = patternToRegex(pattern);
const logfn = console.log.bind(console);
const proto = self.EventTarget.prototype;
proto.addEventListener = new Proxy(proto.addEventListener, {
@ -423,11 +410,14 @@ function addEventListenerDefuser(
});
}
/******************************************************************************/
/// json-prune.js
builtinScriptlets.push({
name: 'json-prune.js',
fn: jsonPrune,
dependencies: [
'pattern-to-regex.fn',
],
});
// When no "prune paths" argument is provided, the scriptlet is
// used for logging purpose and the "needle paths" argument is
@ -451,15 +441,7 @@ function jsonPrune(
: [];
} else {
log = console.log.bind(console);
let needle;
if ( rawNeedlePaths === '' ) {
needle = '.?';
} else if ( rawNeedlePaths.charAt(0) === '/' && rawNeedlePaths.slice(-1) === '/' ) {
needle = rawNeedlePaths.slice(1, -1);
} else {
needle = rawNeedlePaths.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
reLogNeedle = new RegExp(needle);
reLogNeedle = patternToRegex(rawNeedlePaths);
}
const findOwner = function(root, path, prune = false) {
let owner = root;
@ -534,12 +516,15 @@ function jsonPrune(
});
}
/******************************************************************************/
/// nano-setInterval-booster.js
builtinScriptlets.push({
name: 'nano-setInterval-booster.js',
aliases: [ 'nano-sib.js' ],
fn: nanoSetIntervalBooster,
dependencies: [
'pattern-to-regex.fn',
],
});
// Imported from:
// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L126
@ -559,14 +544,7 @@ function nanoSetIntervalBooster(
boostArg = ''
) {
if ( typeof needleArg !== 'string' ) { return; }
if ( needleArg === '' ) {
needleArg = '.?';
} else if ( needleArg.charAt(0) === '/' && needleArg.slice(-1) === '/' ) {
needleArg = needleArg.slice(1, -1);
} else {
needleArg = needleArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const reNeedle = new RegExp(needleArg);
const reNeedle = patternToRegex(needleArg);
let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
let boost = parseFloat(boostArg);
@ -587,12 +565,15 @@ function nanoSetIntervalBooster(
});
}
/******************************************************************************/
/// nano-setTimeout-booster.js
builtinScriptlets.push({
name: 'nano-setTimeout-booster.js',
aliases: [ 'nano-stb.js' ],
fn: nanoSetTimeoutBooster,
dependencies: [
'pattern-to-regex.fn',
],
});
// Imported from:
// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L82
@ -613,14 +594,7 @@ function nanoSetTimeoutBooster(
boostArg = ''
) {
if ( typeof needleArg !== 'string' ) { return; }
if ( needleArg === '' ) {
needleArg = '.?';
} else if ( needleArg.charAt(0) === '/' && needleArg.slice(-1) === '/' ) {
needleArg = needleArg.slice(1, -1);
} else {
needleArg = needleArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const reNeedle = new RegExp(needleArg);
const reNeedle = patternToRegex(needleArg);
let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
let boost = parseFloat(boostArg);
@ -641,39 +615,37 @@ function nanoSetTimeoutBooster(
});
}
/******************************************************************************/
/// noeval-if.js
builtinScriptlets.push({
name: 'noeval-if.js',
fn: noevalIf,
fn: noEvalIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noevalIf(
function noEvalIf(
needle = ''
) {
if ( typeof needle !== 'string' ) { return; }
if ( needle === '' ) {
needle = '.?';
} else if ( needle.slice(0,1) === '/' && needle.slice(-1) === '/' ) {
needle = needle.slice(1,-1);
} else {
needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
needle = new RegExp(needle);
window.eval = new Proxy(window.eval, { // jshint ignore: line
const reNeedle = patternToRegex(needle);
window.eval = new Proxy(window.eval, { // jshint ignore: line
apply: function(target, thisArg, args) {
const a = args[0];
if ( needle.test(a.toString()) === false ) {
return target.apply(thisArg, args);
}
if ( reNeedle.test(a.toString()) ) { return; }
return target.apply(thisArg, args);
}
});
}
/******************************************************************************/
/// no-fetch-if.js
builtinScriptlets.push({
name: 'no-fetch-if.js',
fn: noFetchIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noFetchIf(
arg1 = '',
@ -691,14 +663,7 @@ function noFetchIf(
key = 'url';
value = condition;
}
if ( value === '' ) {
value = '^';
} else if ( value.startsWith('/') && value.endsWith('/') ) {
value = value.slice(1, -1);
} else {
value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
needles.push({ key, re: new RegExp(value) });
needles.push({ key, re: patternToRegex(value) });
}
const log = needles.length === 0 ? console.log.bind(console) : undefined;
self.fetch = new Proxy(self.fetch, {
@ -746,8 +711,8 @@ function noFetchIf(
});
}
/******************************************************************************/
/// refresh-defuser.js
builtinScriptlets.push({
name: 'refresh-defuser.js',
fn: refreshDefuser,
@ -773,8 +738,8 @@ function refreshDefuser(
}
}
/******************************************************************************/
/// remove-attr.js
builtinScriptlets.push({
name: 'remove-attr.js',
aliases: [ 'ra.js' ],
@ -840,8 +805,8 @@ function removeAttr(
}
}
/******************************************************************************/
/// remove-class.js
builtinScriptlets.push({
name: 'remove-class.js',
aliases: [ 'rc.js' ],
@ -905,12 +870,15 @@ function removeClass(
}
}
/******************************************************************************/
/// no-requestAnimationFrame-if.js
builtinScriptlets.push({
name: 'no-requestAnimationFrame-if.js',
aliases: [ 'norafif.js' ],
fn: noRequestAnimationFrameIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noRequestAnimationFrameIf(
needle = ''
@ -918,13 +886,8 @@ function noRequestAnimationFrameIf(
if ( typeof needle !== 'string' ) { return; }
const needleNot = needle.charAt(0) === '!';
if ( needleNot ) { needle = needle.slice(1); }
if ( needle.startsWith('/') && needle.endsWith('/') ) {
needle = needle.slice(1, -1);
} else if ( needle !== '' ) {
needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const log = needleNot === false && needle === '' ? console.log : undefined;
const reNeedle = new RegExp(needle);
const reNeedle = patternToRegex(needle);
window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, {
apply: function(target, thisArg, args) {
const a = String(args[0]);
@ -942,8 +905,8 @@ function noRequestAnimationFrameIf(
});
}
/******************************************************************************/
/// set-constant.js
builtinScriptlets.push({
name: 'set-constant.js',
aliases: [ 'set.js' ],
@ -1108,12 +1071,15 @@ function setConstant(
trapChain(window, chain);
}
/******************************************************************************/
/// no-setInterval-if.js
builtinScriptlets.push({
name: 'no-setInterval-if.js',
aliases: [ 'nosiif.js' ],
fn: noSetIntervalIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noSetIntervalIf(
needle = '',
@ -1129,17 +1095,10 @@ function noSetIntervalIf(
if ( delayNot ) { delay = delay.slice(1); }
delay = parseInt(delay, 10);
}
if ( needle === '' ) {
needle = '';
} else if ( needle.startsWith('/') && needle.endsWith('/') ) {
needle = needle.slice(1,-1);
} else {
needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const log = needleNot === false && needle === '' && delay === undefined
? console.log
: undefined;
const reNeedle = new RegExp(needle);
const reNeedle = patternToRegex(needle);
window.setInterval = new Proxy(window.setInterval, {
apply: function(target, thisArg, args) {
const a = String(args[0]);
@ -1163,12 +1122,15 @@ function noSetIntervalIf(
});
}
/******************************************************************************/
/// no-setTimeout-if.js
builtinScriptlets.push({
name: 'no-setTimeout-if.js',
aliases: [ 'nostif.js', 'setTimeout-defuser.js' ],
fn: noSetTimeoutIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noSetTimeoutIf(
needle = '',
@ -1184,17 +1146,10 @@ function noSetTimeoutIf(
if ( delayNot ) { delay = delay.slice(1); }
delay = parseInt(delay, 10);
}
if ( needle === '' ) {
needle = '';
} else if ( needle.startsWith('/') && needle.endsWith('/') ) {
needle = needle.slice(1,-1);
} else {
needle = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const log = needleNot === false && needle === '' && delay === undefined
? console.log
: undefined;
const reNeedle = new RegExp(needle);
const reNeedle = patternToRegex(needle);
window.setTimeout = new Proxy(window.setTimeout, {
apply: function(target, thisArg, args) {
const a = String(args[0]);
@ -1218,27 +1173,20 @@ function noSetTimeoutIf(
});
}
/******************************************************************************/
/// webrtc-if.js
builtinScriptlets.push({
name: 'webrtc-if.js',
fn: webrtcIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function webrtcIf(
good = ''
) {
if ( typeof good !== 'string' ) { return; }
if ( good.startsWith('/') && good.endsWith('/') ) {
good = good.slice(1, -1);
} else {
good = good.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
let reGood;
try {
reGood = new RegExp(good);
} catch(ex) {
return;
}
const reGood = patternToRegex(good);
const rtcName = window.RTCPeerConnection
? 'RTCPeerConnection'
: (window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : '');
@ -1292,11 +1240,14 @@ function webrtcIf(
});
}
/******************************************************************************/
/// no-xhr-if.js
builtinScriptlets.push({
name: 'no-xhr-if.js',
fn: noXhrIf,
dependencies: [
'pattern-to-regex.fn',
],
});
function noXhrIf(
arg1 = ''
@ -1315,14 +1266,7 @@ function noXhrIf(
key = 'url';
value = condition;
}
if ( value === '' ) {
value = '^';
} else if ( value.startsWith('/') && value.endsWith('/') ) {
value = value.slice(1, -1);
} else {
value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
needles.push({ key, re: new RegExp(value) });
needles.push({ key, re: patternToRegex(value) });
}
const log = needles.length === 0 ? console.log.bind(console) : undefined;
self.XMLHttpRequest = class extends self.XMLHttpRequest {
@ -1369,11 +1313,14 @@ function noXhrIf(
};
}
/******************************************************************************/
/// window-close-if.js
builtinScriptlets.push({
name: 'window-close-if.js',
fn: windowCloseIf,
dependencies: [
'pattern-to-regex.fn',
],
});
// https://github.com/uBlockOrigin/uAssets/issues/10323#issuecomment-992312847
// https://github.com/AdguardTeam/Scriptlets/issues/158
@ -1382,19 +1329,14 @@ function windowCloseIf(
arg1 = ''
) {
if ( typeof arg1 !== 'string' ) { return; }
let reStr;
let subject = '';
if ( arg1 === '' ) {
reStr = '^';
} else if ( /^\/.*\/$/.test(arg1) ) {
reStr = arg1.slice(1, -1);
if ( /^\/.*\/$/.test(arg1) ) {
subject = window.location.href;
} else {
reStr = arg1.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} else if ( arg1 !== '' ) {
subject = `${window.location.pathname}${window.location.search}`;
}
try {
const re = new RegExp(reStr);
const re = patternToRegex(arg1);
if ( re.test(subject) ) {
window.close();
}
@ -1403,8 +1345,8 @@ function windowCloseIf(
}
}
/******************************************************************************/
/// window.name-defuser.js
builtinScriptlets.push({
name: 'window.name-defuser.js',
fn: windowNameDefuser,
@ -1416,8 +1358,8 @@ function windowNameDefuser() {
}
}
/******************************************************************************/
/// overlay-buster.js
builtinScriptlets.push({
name: 'overlay-buster.js',
fn: overlayBuster,
@ -1476,8 +1418,8 @@ function overlayBuster() {
}
}
/******************************************************************************/
/// alert-buster.js
builtinScriptlets.push({
name: 'alert-buster.js',
fn: alertBuster,
@ -1491,8 +1433,8 @@ function alertBuster() {
});
}
/******************************************************************************/
/// nowebrtc.js
builtinScriptlets.push({
name: 'nowebrtc.js',
fn: noWebrtc,
@ -1530,8 +1472,8 @@ function noWebrtc() {
}
}
/******************************************************************************/
/// golem.de.js
builtinScriptlets.push({
name: 'golem.de.js',
fn: golemDe,
@ -1555,8 +1497,8 @@ function golemDe() {
}.bind(window);
}
/******************************************************************************/
/// adfly-defuser.js
builtinScriptlets.push({
name: 'adfly-defuser.js',
fn: adflyDefuser,
@ -1623,8 +1565,8 @@ function adflyDefuser() {
}
}
/******************************************************************************/
/// disable-newtab-links.js
builtinScriptlets.push({
name: 'disable-newtab-links.js',
fn: disableNewtabLinks,
@ -1644,23 +1586,21 @@ function disableNewtabLinks() {
});
}
/******************************************************************************/
/// cookie-remover.js
builtinScriptlets.push({
name: 'cookie-remover.js',
fn: cookieRemover,
dependencies: [
'pattern-to-regex.fn',
],
});
// https://github.com/NanoAdblocker/NanoFilters/issues/149
function cookieRemover(
needle = ''
) {
if ( typeof needle !== 'string' ) { return; }
let reName = /./;
if ( /^\/.+\/$/.test(needle) ) {
reName = new RegExp(needle.slice(1,-1));
} else if ( needle !== '' ) {
reName = new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
}
const reName = patternToRegex(needle);
const removeCookie = function() {
document.cookie.split(';').forEach(cookieStr => {
let pos = cookieStr.indexOf('=');
@ -1700,11 +1640,14 @@ function cookieRemover(
window.addEventListener('beforeunload', removeCookie);
}
/******************************************************************************/
/// xml-prune.js
builtinScriptlets.push({
name: 'xml-prune.js',
fn: xmlPrune,
dependencies: [
'pattern-to-regex.fn',
],
});
function xmlPrune(
selector = '',
@ -1713,14 +1656,7 @@ function xmlPrune(
) {
if ( typeof selector !== 'string' ) { return; }
if ( selector === '' ) { return; }
let reUrl;
if ( urlPattern === '' ) {
reUrl = /^/;
} else if ( /^\/.*\/$/.test(urlPattern) ) {
reUrl = new RegExp(urlPattern.slice(1, -1));
} else {
reUrl = new RegExp(urlPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
}
const reUrl = patternToRegex(urlPattern);
const pruner = text => {
if ( (/^\s*</.test(text) && />\s*$/.test(text)) === false ) {
return text;
@ -1767,8 +1703,8 @@ function xmlPrune(
});
}
/******************************************************************************/
/// m3u-prune.js
builtinScriptlets.push({
name: 'm3u-prune.js',
fn: m3uPrune,
@ -1889,8 +1825,8 @@ function m3uPrune(
});
}
/******************************************************************************/
/// href-sanitizer.js
builtinScriptlets.push({
name: 'href-sanitizer.js',
fn: hrefSanitizer,
@ -1980,8 +1916,8 @@ function hrefSanitizer(
}
}
/******************************************************************************/
/// call-nothrow.js
builtinScriptlets.push({
name: 'call-nothrow.js',
fn: callNothrow,
@ -2014,3 +1950,4 @@ function callNothrow(
});
}
/******************************************************************************/

View File

@ -34,6 +34,7 @@ import {
const extToMimeMap = new Map([
[ 'css', 'text/css' ],
[ 'fn', 'fn/javascript' ], // invented mime type for internal use
[ 'gif', 'image/gif' ],
[ 'html', 'text/html' ],
[ 'js', 'text/javascript' ],
@ -55,11 +56,14 @@ const typeToMimeMap = new Map([
const validMimes = new Set(extToMimeMap.values());
const mimeFromName = function(name) {
const mimeFromName = name => {
const match = /\.([^.]+)$/.exec(name);
if ( match !== null ) {
return extToMimeMap.get(match[1]);
}
if ( match === null ) { return ''; }
return extToMimeMap.get(match[1]);
};
const removeTopCommentBlock = text => {
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
};
// vAPI.warSecret() is optional, it could be absent in some environments,
@ -70,15 +74,19 @@ const warSecret = typeof vAPI === 'object' && vAPI !== null
? vAPI.warSecret
: ( ) => '';
const RESOURCES_SELFIE_VERSION = 7;
const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources';
/******************************************************************************/
/******************************************************************************/
const RedirectEntry = class {
class RedirectEntry {
constructor() {
this.mime = '';
this.data = '';
this.warURL = undefined;
this.params = undefined;
this.dependencies = [];
}
// Prevent redirection to web accessible resources when the request is
@ -116,7 +124,7 @@ const RedirectEntry = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/701
if ( this.data === '' ) {
const mime = typeToMimeMap.get(fctxt.type);
if ( mime === undefined ) { return; }
if ( mime === '' ) { return; }
return `data:${mime},`;
}
if ( this.data.startsWith('data:') === false ) {
@ -141,10 +149,11 @@ const RedirectEntry = class {
return this.data;
}
static fromContent(mime, content) {
static fromContent(mime, content, dependencies = []) {
const r = new RedirectEntry();
r.mime = mime;
r.data = content;
r.dependencies.push(...dependencies);
return r;
}
@ -154,324 +163,296 @@ const RedirectEntry = class {
r.data = selfie.data;
r.warURL = selfie.warURL;
r.params = selfie.params;
r.dependencies = selfie.dependencies || [];
return r;
}
};
}
/******************************************************************************/
/******************************************************************************/
const RedirectEngine = function() {
this.aliases = new Map();
this.resources = new Map();
this.reset();
this.modifyTime = Date.now();
this.resourceNameRegister = '';
};
/******************************************************************************/
RedirectEngine.prototype.reset = function() {
};
/******************************************************************************/
RedirectEngine.prototype.freeze = function() {
};
/******************************************************************************/
RedirectEngine.prototype.tokenToURL = function(
fctxt,
token,
asDataURI = false
) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
this.resourceNameRegister = token;
return entry.toURL(fctxt, asDataURI);
};
/******************************************************************************/
RedirectEngine.prototype.tokenToDNR = function(token) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
if ( entry.warURL === undefined ) { return; }
return entry.warURL;
};
/******************************************************************************/
RedirectEngine.prototype.hasToken = function(token) {
if ( token === 'none' ) { return true; }
const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */;
if ( asDataURI ) {
token = token.slice(1);
class RedirectEngine {
constructor() {
this.aliases = new Map();
this.resources = new Map();
this.reset();
this.modifyTime = Date.now();
this.resourceNameRegister = '';
}
return this.resources.get(this.aliases.get(token) || token) !== undefined;
};
/******************************************************************************/
RedirectEngine.prototype.toSelfie = async function() {
};
/******************************************************************************/
RedirectEngine.prototype.fromSelfie = async function() {
return true;
};
/******************************************************************************/
RedirectEngine.prototype.resourceContentFromName = function(name, mime) {
const entry = this.resources.get(this.aliases.get(name) || name);
if ( entry === undefined ) { return; }
if ( mime === undefined || entry.mime.startsWith(mime) ) {
return entry.toContent();
reset() {
}
};
/******************************************************************************/
freeze() {
}
// https://github.com/uBlockOrigin/uAssets/commit/deefe875551197d655f79cb540e62dfc17c95f42
// Consider 'none' a reserved keyword, to be used to disable redirection.
// https://github.com/uBlockOrigin/uBlock-issues/issues/1419
// Append newlines to raw text to ensure processing of trailing resource.
tokenToURL(
fctxt,
token,
asDataURI = false
) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
this.resourceNameRegister = token;
return entry.toURL(fctxt, asDataURI);
}
RedirectEngine.prototype.resourcesFromString = function(text) {
const lineIter = new LineIterator(
removeTopCommentBlock(text) + '\n\n'
);
const reNonEmptyLine = /\S/;
let fields, encoded, details;
tokenToDNR(token) {
const entry = this.resources.get(this.aliases.get(token) || token);
if ( entry === undefined ) { return; }
if ( entry.warURL === undefined ) { return; }
return entry.warURL;
}
while ( lineIter.eot() === false ) {
const line = lineIter.next();
if ( line.startsWith('#') ) { continue; }
if ( line.startsWith('// ') ) { continue; }
hasToken(token) {
if ( token === 'none' ) { return true; }
const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */;
if ( asDataURI ) {
token = token.slice(1);
}
return this.resources.get(this.aliases.get(token) || token) !== undefined;
}
async toSelfie() {
}
async fromSelfie() {
return true;
}
contentFromName(name, mime = '') {
const entry = this.resources.get(this.aliases.get(name) || name);
if ( entry === undefined ) { return; }
if ( entry.mime.startsWith(mime) === false ) { return; }
return {
js: entry.toContent(),
dependencies: entry.dependencies.slice(),
};
}
// https://github.com/uBlockOrigin/uAssets/commit/deefe8755511
// Consider 'none' a reserved keyword, to be used to disable redirection.
// https://github.com/uBlockOrigin/uBlock-issues/issues/1419
// Append newlines to raw text to ensure processing of trailing resource.
resourcesFromString(text) {
const lineIter = new LineIterator(
removeTopCommentBlock(text) + '\n\n'
);
const reNonEmptyLine = /\S/;
let fields, encoded, details;
while ( lineIter.eot() === false ) {
const line = lineIter.next();
if ( line.startsWith('#') ) { continue; }
if ( line.startsWith('// ') ) { continue; }
if ( fields === undefined ) {
if ( line === '' ) { continue; }
// Modern parser
if ( line.startsWith('/// ') ) {
const name = line.slice(4).trim();
fields = [ name, mimeFromName(name) ];
continue;
}
// Legacy parser
const head = line.trim().split(/\s+/);
if ( head.length !== 2 ) { continue; }
if ( head[0] === 'none' ) { continue; }
let pos = head[1].indexOf(';');
if ( pos === -1 ) { pos = head[1].length; }
if ( validMimes.has(head[1].slice(0, pos)) === false ) {
continue;
}
encoded = head[1].indexOf(';') !== -1;
fields = head;
continue;
}
if ( fields === undefined ) {
if ( line === '' ) { continue; }
// Modern parser
if ( line.startsWith('/// ') ) {
const name = line.slice(4).trim();
fields = [ name, mimeFromName(name) ];
if ( details === undefined ) {
details = [];
}
const [ prop, value ] = line.slice(4).trim().split(/\s+/);
if ( value !== undefined ) {
details.push({ prop, value });
}
continue;
}
// Legacy parser
const head = line.trim().split(/\s+/);
if ( head.length !== 2 ) { continue; }
if ( head[0] === 'none' ) { continue; }
let pos = head[1].indexOf(';');
if ( pos === -1 ) { pos = head[1].length; }
if ( validMimes.has(head[1].slice(0, pos)) === false ) {
if ( reNonEmptyLine.test(line) ) {
fields.push(encoded ? line.trim() : line);
continue;
}
encoded = head[1].indexOf(';') !== -1;
fields = head;
continue;
}
if ( line.startsWith('/// ') ) {
if ( details === undefined ) {
details = [];
}
const [ prop, value ] = line.slice(4).trim().split(/\s+/);
if ( value !== undefined ) {
details.push({ prop, value });
}
continue;
}
if ( reNonEmptyLine.test(line) ) {
fields.push(encoded ? line.trim() : line);
continue;
}
// No more data, add the resource.
const name = this.aliases.get(fields[0]) || fields[0];
const mime = fields[1];
const content = orphanizeString(
fields.slice(2).join(encoded ? '' : '\n')
);
this.resources.set(
name,
RedirectEntry.fromContent(mime, content)
);
if ( Array.isArray(details) ) {
for ( const { prop, value } of details ) {
if ( prop !== 'alias' ) { continue; }
this.aliases.set(value, name);
}
}
fields = undefined;
details = undefined;
}
this.modifyTime = Date.now();
};
const removeTopCommentBlock = function(text) {
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
};
/******************************************************************************/
RedirectEngine.prototype.loadBuiltinResources = function(fetcher) {
this.resources = new Map();
this.aliases = new Map();
const fetches = [
import('/assets/resources/scriptlets.js').then(module => {
for ( const scriptlet of module.builtinScriptlets ) {
const { name, aliases, fn } = scriptlet;
const entry = RedirectEntry.fromContent(
mimeFromName(name),
fn.toString()
);
this.resources.set(name, entry);
if ( Array.isArray(aliases) === false ) { continue; }
for ( const alias of aliases ) {
this.aliases.set(alias, name);
// No more data, add the resource.
const name = this.aliases.get(fields[0]) || fields[0];
const mime = fields[1];
const content = orphanizeString(
fields.slice(2).join(encoded ? '' : '\n')
);
this.resources.set(name, RedirectEntry.fromContent(mime, content));
if ( Array.isArray(details) ) {
for ( const { prop, value } of details ) {
if ( prop !== 'alias' ) { continue; }
this.aliases.set(value, name);
}
}
this.modifyTime = Date.now();
}),
];
const store = (name, data = undefined) => {
const details = redirectableResources.get(name);
const entry = RedirectEntry.fromSelfie({
mime: mimeFromName(name),
data,
warURL: `/web_accessible_resources/${name}`,
params: details.params,
});
this.resources.set(name, entry);
if ( details.alias === undefined ) { return; }
if ( Array.isArray(details.alias) ) {
for ( const alias of details.alias ) {
this.aliases.set(alias, name);
fields = undefined;
details = undefined;
}
this.modifyTime = Date.now();
}
loadBuiltinResources(fetcher) {
this.resources = new Map();
this.aliases = new Map();
const fetches = [
import('/assets/resources/scriptlets.js').then(module => {
for ( const scriptlet of module.builtinScriptlets ) {
const { name, aliases, fn } = scriptlet;
const entry = RedirectEntry.fromContent(
mimeFromName(name),
fn.toString(),
scriptlet.dependencies,
);
this.resources.set(name, entry);
if ( Array.isArray(aliases) === false ) { continue; }
for ( const alias of aliases ) {
this.aliases.set(alias, name);
}
}
this.modifyTime = Date.now();
}),
];
const store = (name, data = undefined) => {
const details = redirectableResources.get(name);
const entry = RedirectEntry.fromSelfie({
mime: mimeFromName(name),
data,
warURL: `/web_accessible_resources/${name}`,
params: details.params,
});
this.resources.set(name, entry);
if ( details.alias === undefined ) { return; }
if ( Array.isArray(details.alias) ) {
for ( const alias of details.alias ) {
this.aliases.set(alias, name);
}
} else {
this.aliases.set(details.alias, name);
}
} else {
this.aliases.set(details.alias, name);
}
};
};
const processBlob = (name, blob) => {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = ( ) => {
store(name, reader.result);
resolve();
};
reader.onabort = reader.onerror = ( ) => {
resolve();
};
reader.readAsDataURL(blob);
const processBlob = (name, blob) => {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = ( ) => {
store(name, reader.result);
resolve();
};
reader.onabort = reader.onerror = ( ) => {
resolve();
};
reader.readAsDataURL(blob);
});
};
const processText = (name, text) => {
store(name, removeTopCommentBlock(text));
};
const process = result => {
const match = /^\/web_accessible_resources\/([^?]+)/.exec(result.url);
if ( match === null ) { return; }
const name = match[1];
return result.content instanceof Blob
? processBlob(name, result.content)
: processText(name, result.content);
};
for ( const [ name, details ] of redirectableResources ) {
if ( typeof details.data !== 'string' ) {
store(name);
continue;
}
fetches.push(
fetcher(`/web_accessible_resources/${name}`, {
responseType: details.data
}).then(
result => process(result)
)
);
}
return Promise.all(fetches);
}
getResourceDetails() {
const out = new Map([
[ 'none', { canInject: false, canRedirect: true, aliasOf: '' } ],
]);
for ( const [ name, entry ] of this.resources ) {
out.set(name, {
canInject: typeof entry.data === 'string',
canRedirect: entry.warURL !== undefined,
aliasOf: '',
extensionPath: entry.warURL,
});
}
for ( const [ alias, name ] of this.aliases ) {
const original = out.get(name);
if ( original === undefined ) { continue; }
const aliased = Object.assign({}, original);
aliased.aliasOf = name;
out.set(alias, aliased);
}
return Array.from(out).sort((a, b) => {
return a[0].localeCompare(b[0]);
});
};
}
const processText = (name, text) => {
store(name, removeTopCommentBlock(text));
};
const process = result => {
const match = /^\/web_accessible_resources\/([^?]+)/.exec(result.url);
if ( match === null ) { return; }
const name = match[1];
return result.content instanceof Blob
? processBlob(name, result.content)
: processText(name, result.content);
};
for ( const [ name, details ] of redirectableResources ) {
if ( typeof details.data !== 'string' ) {
store(name);
continue;
}
fetches.push(
fetcher(`/web_accessible_resources/${name}`, {
responseType: details.data
}).then(
result => process(result)
)
selfieFromResources(storage) {
storage.put(
RESOURCES_SELFIE_NAME,
JSON.stringify({
version: RESOURCES_SELFIE_VERSION,
aliases: Array.from(this.aliases),
resources: Array.from(this.resources),
})
);
}
return Promise.all(fetches);
};
/******************************************************************************/
RedirectEngine.prototype.getResourceDetails = function() {
const out = new Map([
[ 'none', { canInject: false, canRedirect: true, aliasOf: '' } ],
]);
for ( const [ name, entry ] of this.resources ) {
out.set(name, {
canInject: typeof entry.data === 'string',
canRedirect: entry.warURL !== undefined,
aliasOf: '',
extensionPath: entry.warURL,
});
async resourcesFromSelfie(storage) {
const result = await storage.get(RESOURCES_SELFIE_NAME);
let selfie;
try {
selfie = JSON.parse(result.content);
} catch(ex) {
}
if (
selfie instanceof Object === false ||
selfie.version !== RESOURCES_SELFIE_VERSION ||
Array.isArray(selfie.resources) === false
) {
return false;
}
this.aliases = new Map(selfie.aliases);
this.resources = new Map();
for ( const [ token, entry ] of selfie.resources ) {
this.resources.set(token, RedirectEntry.fromSelfie(entry));
}
return true;
}
for ( const [ alias, name ] of this.aliases ) {
const original = out.get(name);
if ( original === undefined ) { continue; }
const aliased = Object.assign({}, original);
aliased.aliasOf = name;
out.set(alias, aliased);
invalidateResourcesSelfie(storage) {
storage.remove(RESOURCES_SELFIE_NAME);
}
return Array.from(out).sort((a, b) => {
return a[0].localeCompare(b[0]);
});
};
/******************************************************************************/
const RESOURCES_SELFIE_VERSION = 7;
const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources';
RedirectEngine.prototype.selfieFromResources = function(storage) {
storage.put(
RESOURCES_SELFIE_NAME,
JSON.stringify({
version: RESOURCES_SELFIE_VERSION,
aliases: Array.from(this.aliases),
resources: Array.from(this.resources),
})
);
};
RedirectEngine.prototype.resourcesFromSelfie = async function(storage) {
const result = await storage.get(RESOURCES_SELFIE_NAME);
let selfie;
try {
selfie = JSON.parse(result.content);
} catch(ex) {
}
if (
selfie instanceof Object === false ||
selfie.version !== RESOURCES_SELFIE_VERSION ||
Array.isArray(selfie.resources) === false
) {
return false;
}
this.aliases = new Map(selfie.aliases);
this.resources = new Map();
for ( const [ token, entry ] of selfie.resources ) {
this.resources.set(token, RedirectEntry.fromSelfie(entry));
}
return true;
};
RedirectEngine.prototype.invalidateResourcesSelfie = function(storage) {
storage.remove(RESOURCES_SELFIE_NAME);
};
}
/******************************************************************************/

View File

@ -141,38 +141,40 @@ const normalizeRawFilter = function(parser) {
return `+js(${args.join(', ')})`;
};
const lookupScriptlet = function(rawToken, reng, toInject) {
if ( toInject.has(rawToken) ) { return; }
if ( scriptletCache.resetTime < reng.modifyTime ) {
scriptletCache.reset();
const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) {
if ( scriptletMap.has(rawToken) ) { return; }
const pos = rawToken.indexOf(',');
let token, args = '';
if ( pos === -1 ) {
token = rawToken;
} else {
token = rawToken.slice(0, pos).trim();
args = rawToken.slice(pos + 1).trim();
}
let content = scriptletCache.lookup(rawToken);
if ( content === undefined ) {
const pos = rawToken.indexOf(',');
let token, args = '';
if ( pos === -1 ) {
token = rawToken;
} else {
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 {
token = `${token}.js`;
}
content = reng.resourceContentFromName(token, 'text/javascript');
if ( !content ) { return; }
content = patchScriptlet(content, args);
content =
'try {\n' +
content + '\n' +
'} catch ( e ) { }';
scriptletCache.add(rawToken, content);
// TODO: The alias lookup can be removed once scriptlet resources
// with obsolete name are converted to their new name.
if ( redirectEngine.aliases.has(token) ) {
token = redirectEngine.aliases.get(token);
} else {
token = `${token}.js`;
}
toInject.set(rawToken, content);
const details = redirectEngine.contentFromName(token, 'text/javascript');
if ( details === undefined ) { return; }
const content = patchScriptlet(details.js, args);
const dependencies = details.dependencies || [];
while ( dependencies.length !== 0 ) {
const token = dependencies.shift();
if ( dependencyMap.has(token) ) { continue; }
const details = redirectEngine.contentFromName(token, 'fn/javascript');
if ( details === undefined ) { continue; }
dependencyMap.set(token, details.js);
if ( Array.isArray(details.dependencies) === false ) { continue; }
dependencies.push(...details.dependencies);
}
scriptletMap.set(
rawToken,
[ 'try {', content, '} catch (e) {', '}' ].join('\n')
);
};
// Fill-in scriptlet argument placeholders.
@ -183,31 +185,31 @@ const patchScriptlet = function(content, args) {
if ( args.startsWith('{') && args.endsWith('}') ) {
return content.replace('{{args}}', args);
}
if ( args === '' ) {
return content.replace('{{args}}', '');
}
const arglist = [];
if ( args !== '' ) {
let s = args;
let len = s.length;
let beg = 0, pos = 0;
let i = 1;
while ( beg < len ) {
pos = s.indexOf(',', pos);
// Escaped comma? If so, skip.
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
s = s.slice(0, pos - 1) + s.slice(pos);
len -= 1;
continue;
}
if ( pos === -1 ) { pos = len; }
arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
beg = pos = pos + 1;
i++;
let s = args;
let len = s.length;
let beg = 0, pos = 0;
let i = 1;
while ( beg < len ) {
pos = s.indexOf(',', pos);
// Escaped comma? If so, skip.
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
s = s.slice(0, pos - 1) + s.slice(pos);
len -= 1;
continue;
}
if ( pos === -1 ) { pos = len; }
arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
beg = pos = pos + 1;
i++;
}
for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]);
}
content = content.replace('{{args}}', arglist.map(a => `'${a}'`).join(', '));
return content;
return content.replace('{{args}}', arglist.map(a => `'${a}'`).join(', '));
};
const logOne = function(tabId, url, filter) {
@ -225,6 +227,7 @@ const logOne = function(tabId, url, filter) {
scriptletFilteringEngine.reset = function() {
scriptletDB.clear();
duplicates.clear();
scriptletCache.reset();
acceptedCount = 0;
discardedCount = 0;
};
@ -232,6 +235,7 @@ scriptletFilteringEngine.reset = function() {
scriptletFilteringEngine.freeze = function() {
duplicates.clear();
scriptletDB.collectGarbage();
scriptletCache.reset();
};
scriptletFilteringEngine.compile = function(parser, writer) {
@ -292,7 +296,8 @@ scriptletFilteringEngine.fromCompiledContent = function(reader) {
const $scriptlets = new Set();
const $exceptions = new Set();
const $scriptletToCodeMap = new Map();
const $scriptletMap = new Map();
const $scriptletDependencyMap = new Map();
scriptletFilteringEngine.retrieve = function(request, options = {}) {
if ( scriptletDB.size === 0 ) { return; }
@ -328,40 +333,58 @@ scriptletFilteringEngine.retrieve = function(request, options = {}) {
return;
}
$scriptletToCodeMap.clear();
for ( const token of $scriptlets ) {
lookupScriptlet(token, redirectEngine, $scriptletToCodeMap);
if ( scriptletCache.resetTime < redirectEngine.modifyTime ) {
scriptletCache.reset();
}
if ( $scriptletToCodeMap.size === 0 ) { return; }
// Return an array of scriptlets, and log results if needed.
const out = [];
for ( const [ token, code ] of $scriptletToCodeMap ) {
const isException = $exceptions.has(token);
if ( isException === false ) {
out.push(code);
let cacheDetails = scriptletCache.lookup(hostname);
if ( cacheDetails === undefined ) {
const fullCode = [];
for ( const token of $scriptlets ) {
if ( $exceptions.has(token) ) { continue; }
lookupScriptlet(token, $scriptletMap, $scriptletDependencyMap);
}
if ( mustLog === false ) { continue; }
if ( isException ) {
logOne(request.tabId, request.url, `#@#+js(${token})`);
} else {
options.logEntries.push({
token: `##+js(${token})`,
tabId: request.tabId,
url: request.url,
});
for ( const token of $scriptlets ) {
const isException = $exceptions.has(token);
if ( isException === false ) {
fullCode.push($scriptletMap.get(token));
}
}
for ( const code of $scriptletDependencyMap.values() ) {
fullCode.push(code);
}
cacheDetails = {
code: fullCode.join('\n'),
tokens: Array.from($scriptlets),
exceptions: Array.from($exceptions),
};
scriptletCache.add(hostname, cacheDetails);
$scriptletMap.clear();
$scriptletDependencyMap.clear();
}
if ( mustLog ) {
for ( const token of cacheDetails.tokens ) {
if ( cacheDetails.exceptions.includes(token) ) {
logOne(request.tabId, request.url, `#@#+js(${token})`);
} else {
options.logEntries.push({
token: `##+js(${token})`,
tabId: request.tabId,
url: request.url,
});
}
}
}
if ( out.length === 0 ) { return; }
if ( cacheDetails.code === '' ) { return; }
const out = [ cacheDetails.code ];
if ( µb.hiddenSettings.debugScriptlets ) {
out.unshift('debugger;');
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/156
// Provide a private Map() object available for use by all
// scriptlets.
out.unshift(
'(function() {',
'// >>>> start of private namespace',