From 236fb3ad59ecf3e01066004fbe86d17bf2d00808 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 26 Mar 2023 09:13:17 -0400 Subject: [PATCH] Add scriptlet dependencies to reduce code duplication --- assets/resources/scriptlets.js | 415 ++++++++++-------------- src/js/redirect-engine.js | 567 ++++++++++++++++----------------- src/js/scriptlet-filtering.js | 169 +++++----- 3 files changed, 546 insertions(+), 605 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 4934a2b6f..2210f06cb 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -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*\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( }); } +/******************************************************************************/ diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 6105ec57b..dee269734 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -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); -}; +} /******************************************************************************/ diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 4ffe9ebaa..1c4cb33fd 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -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',