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

View File

@ -34,6 +34,7 @@ import {
const extToMimeMap = new Map([ const extToMimeMap = new Map([
[ 'css', 'text/css' ], [ 'css', 'text/css' ],
[ 'fn', 'fn/javascript' ], // invented mime type for internal use
[ 'gif', 'image/gif' ], [ 'gif', 'image/gif' ],
[ 'html', 'text/html' ], [ 'html', 'text/html' ],
[ 'js', 'text/javascript' ], [ 'js', 'text/javascript' ],
@ -55,11 +56,14 @@ const typeToMimeMap = new Map([
const validMimes = new Set(extToMimeMap.values()); const validMimes = new Set(extToMimeMap.values());
const mimeFromName = function(name) { const mimeFromName = name => {
const match = /\.([^.]+)$/.exec(name); const match = /\.([^.]+)$/.exec(name);
if ( match !== null ) { if ( match === null ) { return ''; }
return extToMimeMap.get(match[1]); 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, // vAPI.warSecret() is optional, it could be absent in some environments,
@ -70,15 +74,19 @@ const warSecret = typeof vAPI === 'object' && vAPI !== null
? vAPI.warSecret ? vAPI.warSecret
: ( ) => ''; : ( ) => '';
const RESOURCES_SELFIE_VERSION = 7;
const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources';
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
const RedirectEntry = class { class RedirectEntry {
constructor() { constructor() {
this.mime = ''; this.mime = '';
this.data = ''; this.data = '';
this.warURL = undefined; this.warURL = undefined;
this.params = undefined; this.params = undefined;
this.dependencies = [];
} }
// Prevent redirection to web accessible resources when the request is // 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 // https://github.com/uBlockOrigin/uBlock-issues/issues/701
if ( this.data === '' ) { if ( this.data === '' ) {
const mime = typeToMimeMap.get(fctxt.type); const mime = typeToMimeMap.get(fctxt.type);
if ( mime === undefined ) { return; } if ( mime === '' ) { return; }
return `data:${mime},`; return `data:${mime},`;
} }
if ( this.data.startsWith('data:') === false ) { if ( this.data.startsWith('data:') === false ) {
@ -141,10 +149,11 @@ const RedirectEntry = class {
return this.data; return this.data;
} }
static fromContent(mime, content) { static fromContent(mime, content, dependencies = []) {
const r = new RedirectEntry(); const r = new RedirectEntry();
r.mime = mime; r.mime = mime;
r.data = content; r.data = content;
r.dependencies.push(...dependencies);
return r; return r;
} }
@ -154,324 +163,296 @@ const RedirectEntry = class {
r.data = selfie.data; r.data = selfie.data;
r.warURL = selfie.warURL; r.warURL = selfie.warURL;
r.params = selfie.params; r.params = selfie.params;
r.dependencies = selfie.dependencies || [];
return r; return r;
} }
}; }
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
const RedirectEngine = function() { class RedirectEngine {
this.aliases = new Map(); constructor() {
this.resources = new Map(); this.aliases = new Map();
this.reset(); this.resources = new Map();
this.modifyTime = Date.now(); this.reset();
this.resourceNameRegister = ''; 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);
} }
return this.resources.get(this.aliases.get(token) || token) !== undefined;
};
/******************************************************************************/ reset() {
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();
} }
};
/******************************************************************************/ freeze() {
}
// https://github.com/uBlockOrigin/uAssets/commit/deefe875551197d655f79cb540e62dfc17c95f42 tokenToURL(
// Consider 'none' a reserved keyword, to be used to disable redirection. fctxt,
// https://github.com/uBlockOrigin/uBlock-issues/issues/1419 token,
// Append newlines to raw text to ensure processing of trailing resource. 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) { tokenToDNR(token) {
const lineIter = new LineIterator( const entry = this.resources.get(this.aliases.get(token) || token);
removeTopCommentBlock(text) + '\n\n' if ( entry === undefined ) { return; }
); if ( entry.warURL === undefined ) { return; }
const reNonEmptyLine = /\S/; return entry.warURL;
let fields, encoded, details; }
while ( lineIter.eot() === false ) { hasToken(token) {
const line = lineIter.next(); if ( token === 'none' ) { return true; }
if ( line.startsWith('#') ) { continue; } const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */;
if ( line.startsWith('// ') ) { continue; } 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('/// ') ) { if ( line.startsWith('/// ') ) {
const name = line.slice(4).trim(); if ( details === undefined ) {
fields = [ name, mimeFromName(name) ]; details = [];
}
const [ prop, value ] = line.slice(4).trim().split(/\s+/);
if ( value !== undefined ) {
details.push({ prop, value });
}
continue; continue;
} }
// Legacy parser
const head = line.trim().split(/\s+/); if ( reNonEmptyLine.test(line) ) {
if ( head.length !== 2 ) { continue; } fields.push(encoded ? line.trim() : line);
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; continue;
} }
encoded = head[1].indexOf(';') !== -1;
fields = head;
continue;
}
if ( line.startsWith('/// ') ) { // No more data, add the resource.
if ( details === undefined ) { const name = this.aliases.get(fields[0]) || fields[0];
details = []; const mime = fields[1];
} const content = orphanizeString(
const [ prop, value ] = line.slice(4).trim().split(/\s+/); fields.slice(2).join(encoded ? '' : '\n')
if ( value !== undefined ) { );
details.push({ prop, value }); this.resources.set(name, RedirectEntry.fromContent(mime, content));
} if ( Array.isArray(details) ) {
continue; for ( const { prop, value } of details ) {
} if ( prop !== 'alias' ) { continue; }
this.aliases.set(value, name);
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);
} }
} }
this.modifyTime = Date.now();
}),
];
const store = (name, data = undefined) => { fields = undefined;
const details = redirectableResources.get(name); details = undefined;
const entry = RedirectEntry.fromSelfie({ }
mime: mimeFromName(name),
data, this.modifyTime = Date.now();
warURL: `/web_accessible_resources/${name}`, }
params: details.params,
}); loadBuiltinResources(fetcher) {
this.resources.set(name, entry); this.resources = new Map();
if ( details.alias === undefined ) { return; } this.aliases = new Map();
if ( Array.isArray(details.alias) ) {
for ( const alias of details.alias ) { const fetches = [
this.aliases.set(alias, name); 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) => { const processBlob = (name, blob) => {
return new Promise(resolve => { return new Promise(resolve => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = ( ) => { reader.onload = ( ) => {
store(name, reader.result); store(name, reader.result);
resolve(); resolve();
}; };
reader.onabort = reader.onerror = ( ) => { reader.onabort = reader.onerror = ( ) => {
resolve(); resolve();
}; };
reader.readAsDataURL(blob); 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) => { selfieFromResources(storage) {
store(name, removeTopCommentBlock(text)); storage.put(
}; RESOURCES_SELFIE_NAME,
JSON.stringify({
const process = result => { version: RESOURCES_SELFIE_VERSION,
const match = /^\/web_accessible_resources\/([^?]+)/.exec(result.url); aliases: Array.from(this.aliases),
if ( match === null ) { return; } resources: Array.from(this.resources),
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); async resourcesFromSelfie(storage) {
}; const result = await storage.get(RESOURCES_SELFIE_NAME);
let selfie;
/******************************************************************************/ try {
selfie = JSON.parse(result.content);
RedirectEngine.prototype.getResourceDetails = function() { } catch(ex) {
const out = new Map([ }
[ 'none', { canInject: false, canRedirect: true, aliasOf: '' } ], if (
]); selfie instanceof Object === false ||
for ( const [ name, entry ] of this.resources ) { selfie.version !== RESOURCES_SELFIE_VERSION ||
out.set(name, { Array.isArray(selfie.resources) === false
canInject: typeof entry.data === 'string', ) {
canRedirect: entry.warURL !== undefined, return false;
aliasOf: '', }
extensionPath: entry.warURL, 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); invalidateResourcesSelfie(storage) {
if ( original === undefined ) { continue; } storage.remove(RESOURCES_SELFIE_NAME);
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 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(', ')})`; return `+js(${args.join(', ')})`;
}; };
const lookupScriptlet = function(rawToken, reng, toInject) { const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) {
if ( toInject.has(rawToken) ) { return; } if ( scriptletMap.has(rawToken) ) { return; }
if ( scriptletCache.resetTime < reng.modifyTime ) { const pos = rawToken.indexOf(',');
scriptletCache.reset(); 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); // TODO: The alias lookup can be removed once scriptlet resources
if ( content === undefined ) { // with obsolete name are converted to their new name.
const pos = rawToken.indexOf(','); if ( redirectEngine.aliases.has(token) ) {
let token, args = ''; token = redirectEngine.aliases.get(token);
if ( pos === -1 ) { } else {
token = rawToken; token = `${token}.js`;
} 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);
} }
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. // Fill-in scriptlet argument placeholders.
@ -183,31 +185,31 @@ const patchScriptlet = function(content, args) {
if ( args.startsWith('{') && args.endsWith('}') ) { if ( args.startsWith('{') && args.endsWith('}') ) {
return content.replace('{{args}}', args); return content.replace('{{args}}', args);
} }
if ( args === '' ) {
return content.replace('{{args}}', '');
}
const arglist = []; const arglist = [];
if ( args !== '' ) { let s = args;
let s = args; let len = s.length;
let len = s.length; let beg = 0, pos = 0;
let beg = 0, pos = 0; let i = 1;
let i = 1; while ( beg < len ) {
while ( beg < len ) { pos = s.indexOf(',', pos);
pos = s.indexOf(',', pos); // Escaped comma? If so, skip.
// Escaped comma? If so, skip. if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) { s = s.slice(0, pos - 1) + s.slice(pos);
s = s.slice(0, pos - 1) + s.slice(pos); len -= 1;
len -= 1; continue;
continue;
}
if ( pos === -1 ) { pos = len; }
arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
beg = pos = pos + 1;
i++;
} }
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++ ) { for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]); content = content.replace(`{{${i+1}}}`, arglist[i]);
} }
content = content.replace('{{args}}', arglist.map(a => `'${a}'`).join(', ')); return content.replace('{{args}}', arglist.map(a => `'${a}'`).join(', '));
return content;
}; };
const logOne = function(tabId, url, filter) { const logOne = function(tabId, url, filter) {
@ -225,6 +227,7 @@ const logOne = function(tabId, url, filter) {
scriptletFilteringEngine.reset = function() { scriptletFilteringEngine.reset = function() {
scriptletDB.clear(); scriptletDB.clear();
duplicates.clear(); duplicates.clear();
scriptletCache.reset();
acceptedCount = 0; acceptedCount = 0;
discardedCount = 0; discardedCount = 0;
}; };
@ -232,6 +235,7 @@ scriptletFilteringEngine.reset = function() {
scriptletFilteringEngine.freeze = function() { scriptletFilteringEngine.freeze = function() {
duplicates.clear(); duplicates.clear();
scriptletDB.collectGarbage(); scriptletDB.collectGarbage();
scriptletCache.reset();
}; };
scriptletFilteringEngine.compile = function(parser, writer) { scriptletFilteringEngine.compile = function(parser, writer) {
@ -292,7 +296,8 @@ scriptletFilteringEngine.fromCompiledContent = function(reader) {
const $scriptlets = new Set(); const $scriptlets = new Set();
const $exceptions = new Set(); const $exceptions = new Set();
const $scriptletToCodeMap = new Map(); const $scriptletMap = new Map();
const $scriptletDependencyMap = new Map();
scriptletFilteringEngine.retrieve = function(request, options = {}) { scriptletFilteringEngine.retrieve = function(request, options = {}) {
if ( scriptletDB.size === 0 ) { return; } if ( scriptletDB.size === 0 ) { return; }
@ -328,40 +333,58 @@ scriptletFilteringEngine.retrieve = function(request, options = {}) {
return; return;
} }
$scriptletToCodeMap.clear(); if ( scriptletCache.resetTime < redirectEngine.modifyTime ) {
for ( const token of $scriptlets ) { scriptletCache.reset();
lookupScriptlet(token, redirectEngine, $scriptletToCodeMap);
} }
if ( $scriptletToCodeMap.size === 0 ) { return; }
// Return an array of scriptlets, and log results if needed. let cacheDetails = scriptletCache.lookup(hostname);
const out = []; if ( cacheDetails === undefined ) {
for ( const [ token, code ] of $scriptletToCodeMap ) { const fullCode = [];
const isException = $exceptions.has(token); for ( const token of $scriptlets ) {
if ( isException === false ) { if ( $exceptions.has(token) ) { continue; }
out.push(code); lookupScriptlet(token, $scriptletMap, $scriptletDependencyMap);
} }
if ( mustLog === false ) { continue; } for ( const token of $scriptlets ) {
if ( isException ) { const isException = $exceptions.has(token);
logOne(request.tabId, request.url, `#@#+js(${token})`); if ( isException === false ) {
} else { fullCode.push($scriptletMap.get(token));
options.logEntries.push({ }
token: `##+js(${token})`, }
tabId: request.tabId, for ( const code of $scriptletDependencyMap.values() ) {
url: request.url, 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 ) { if ( µb.hiddenSettings.debugScriptlets ) {
out.unshift('debugger;'); out.unshift('debugger;');
} }
// https://github.com/uBlockOrigin/uBlock-issues/issues/156
// Provide a private Map() object available for use by all
// scriptlets.
out.unshift( out.unshift(
'(function() {', '(function() {',
'// >>>> start of private namespace', '// >>>> start of private namespace',