From 18a84d2819d49444fc31c5350677ecc5b2ec73c6 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 24 Mar 2023 14:05:18 -0400 Subject: [PATCH] Refactor scriptlets injection code Builtin scriptlets are no longer parsed as text-based resources, they are imported as JS functions, and `toString()` is used to obtain text-based representation of a scriptlet. Scriptlet parameters are now passed as function call arguments rather than by replacing text-based occurrences of `{{i}}`. The arguments are always string values (see below for exception). Support for argument as Object has been added. This opens the door to have scriptlets using named arguments rather than positional arguments, and hence easier to extend functionality of existing scriptlets. Example: example.com##+js(scriplet, { "prop": "adblock", "value": false, "log": true }) Compatibility with user-provided scriptlets has been preserved. User-provided scriptlets can benefit some of the changes: Use the form `function(..){..}` instead of `(function(..){..})();` in order to received scriptlet arguments as part of function call -- instead of using `{{i}}`. If using the form `function(..){..}`, you can choose to receive an Object as argument -- just be sure that your scriptlet's parameter is valid JSON notation. --- .jshintrc | 2 +- assets/resources/scriptlets.js | 614 ++++++++++++++++++------------ src/js/redirect-engine.js | 23 +- src/js/scriptlet-filtering.js | 55 +-- src/js/static-filtering-parser.js | 152 +++++--- 5 files changed, 531 insertions(+), 315 deletions(-) diff --git a/.jshintrc b/.jshintrc index 895ad5a56..46c0d2e57 100644 --- a/.jshintrc +++ b/.jshintrc @@ -2,7 +2,7 @@ "browser": true, "devel": true, "eqeqeq": true, - "esversion": 9, + "esversion": 11, "globals": { "chrome": false, // global variable in Chromium, Chrome, Opera "globalThis": false, diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 9bcbd4308..4911d1dce 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -22,37 +22,35 @@ web page context. */ -// The lines below are skipped by the resource parser. Purpose is clean -// jshinting. -(function() { -// >>>> start of private namespace 'use strict'; - - - +export const builtinScriptlets = []; /// abort-current-script.js -/// alias acs.js -/// alias abort-current-inline-script.js -/// alias acis.js +builtinScriptlets.push({ + name: 'abort-current-script.js', + aliases: [ 'acs.js', 'abort-current-inline-script.js', 'acis.js' ], + fn: abortCurrentScript, +}); // Issues to mind before changing anything: // https://github.com/uBlockOrigin/uBlock-issues/issues/2154 -(function() { - const target = '{{1}}'; - if ( target === '' || target === '{{1}}' ) { return; } +function abortCurrentScript( + target = '', + needle = '', + context = '' +) { + if ( typeof target !== 'string' ) { return; } + if ( target === '' ) { return; } const reRegexEscape = /[.*+?^${}()|[\]\\]/g; - const needle = '{{2}}'; const reNeedle = (( ) => { - if ( needle === '' || needle === '{{2}}' ) { return /^/; } + if ( needle === '' ) { return /^/; } if ( /^\/.+\/$/.test(needle) ) { return new RegExp(needle.slice(1,-1)); } return new RegExp(needle.replace(reRegexEscape, '\\$&')); })(); - const context = '{{3}}'; const reContext = (( ) => { - if ( context === '' || context === '{{3}}' ) { return; } + if ( context === '' ) { return; } if ( /^\/.+\/$/.test(context) ) { return new RegExp(context.slice(1,-1)); } @@ -136,12 +134,20 @@ return oe.apply(this, arguments); } }.bind(); -})(); +} /// abort-on-property-read.js -/// alias aopr.js -(function() { +builtinScriptlets.push({ + name: 'abort-on-property-read.js', + aliases: [ 'aopr.js' ], + fn: abortOnPropertyRead, +}); +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 abort = function() { @@ -179,7 +185,6 @@ }); }; const owner = window; - let chain = '{{1}}'; makeProxy(owner, chain); const oe = window.onerror; window.onerror = function(msg, src, line, col, error) { @@ -190,15 +195,22 @@ return oe(msg, src, line, col, error); } }.bind(); -})(); +} /// abort-on-property-write.js -/// alias aopw.js -(function() { +builtinScriptlets.push({ + name: 'abort-on-property-write.js', + aliases: [ 'aopw.js' ], + fn: abortOnPropertyWrite, +}); +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); - let prop = '{{1}}'; let owner = window; for (;;) { const pos = prop.indexOf('.'); @@ -222,18 +234,24 @@ return oe(msg, src, line, col, error); } }.bind(); -})(); +} /// abort-on-stack-trace.js -/// alias aost.js +builtinScriptlets.push({ + name: 'abort-on-stack-trace.js', + aliases: [ 'aost.js' ], + fn: abortOnStackTrace, +}); // Status is currently experimental -(function() { - let chain = '{{1}}'; - let needle = '{{2}}'; - let logLevel = '{{3}}'; +function abortOnStackTrace( + chain = '', + needle = '', + logLevel = '' +) { + if ( typeof chain !== 'string' ) { return; } const reRegexEscape = /[.*+?^${}()|[\]\\]/g; - if ( needle === '' || needle === '{{2}}' ) { + if ( needle === '' ) { needle = '^'; } else if ( /^\/.+\/$/.test(needle) ) { needle = needle.slice(1,-1); @@ -336,15 +354,22 @@ return oe(msg, src, line, col, error); } }.bind(); -})(); +} /// addEventListener-defuser.js -/// alias aeld.js +builtinScriptlets.push({ + name: 'addEventListener-defuser.js', + aliases: [ 'aeld.js' ], + fn: addEventListenerDefuser, +}); // https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120 -(function() { - let needle1 = '{{1}}'; - if ( needle1 === '' || needle1 === '{{1}}' ) { +function addEventListenerDefuser( + needle1 = '', + needle2 = '' +) { + if ( typeof needle1 !== 'string' ) { return; } + if ( needle1 === '' ) { needle1 = '.?'; } else if ( /^\/.+\/$/.test(needle1) ) { needle1 = needle1.slice(1,-1); @@ -352,8 +377,7 @@ needle1 = needle1.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } needle1 = new RegExp(needle1); - let needle2 = '{{2}}'; - if ( needle2 === '' || needle2 === '{{2}}' ) { + if ( needle2 === '' ) { needle2 = '.?'; } else if ( /^\/.+\/$/.test(needle2) ) { needle2 = needle2.slice(1,-1); @@ -380,13 +404,17 @@ } } ); -})(); +} /// addEventListener-logger.js -/// alias aell.js +builtinScriptlets.push({ + name: 'addEventListener-logger.js', + aliases: [ 'aell.js' ], + fn: addEventListenerLogger, +}); // https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120 -(function() { +function addEventListenerLogger() { const log = console.log.bind(console); self.EventTarget.prototype.addEventListener = new Proxy( self.EventTarget.prototype.addEventListener, @@ -403,35 +431,38 @@ } } ); -})(); +} /// json-prune.js -// +builtinScriptlets.push({ + name: 'json-prune.js', + fn: jsonPrune, +}); // When no "prune paths" argument is provided, the scriptlet is // used for logging purpose and the "needle paths" argument is // used to filter logging output. // // https://github.com/uBlockOrigin/uBlock-issues/issues/1545 // - Add support for "remove everything if needle matches" case -// -(function() { - const rawPrunePaths = '{{1}}'; - const rawNeedlePaths = '{{2}}'; - const prunePaths = rawPrunePaths !== '{{1}}' && rawPrunePaths !== '' +function jsonPrune( + rawPrunePaths = '', + rawNeedlePaths = '' +) { + if ( typeof rawPrunePaths !== 'string' ) { return; } + const prunePaths = rawPrunePaths !== '' ? rawPrunePaths.split(/ +/) : []; let needlePaths; let log, reLogNeedle; if ( prunePaths.length !== 0 ) { - needlePaths = prunePaths.length !== 0 && - rawNeedlePaths !== '{{2}}' && rawNeedlePaths !== '' + needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' ? rawNeedlePaths.split(/ +/) : []; } else { log = console.log.bind(console); let needle; - if ( rawNeedlePaths === '' || rawNeedlePaths === '{{2}}' ) { + if ( rawNeedlePaths === '' ) { needle = '.?'; } else if ( rawNeedlePaths.charAt(0) === '/' && rawNeedlePaths.slice(-1) === '/' ) { needle = rawNeedlePaths.slice(1, -1); @@ -511,9 +542,15 @@ return Reflect.apply(...arguments).then(o => pruner(o)); }, }); -})(); +} +/// nano-setInterval-booster.js +builtinScriptlets.push({ + name: 'nano-setInterval-booster.js', + aliases: [ 'nano-sib.js' ], + fn: nanoSetIntervalBooster, +}); // Imported from: // https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L126 // @@ -526,15 +563,12 @@ // boostRatio - The delay multiplier when there is a match, 0.5 speeds up by // 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up // 20 times. Speed up and down both cap at 50 times. -/// nano-setInterval-booster.js -/// alias nano-sib.js -(function() { - let needleArg = '{{1}}'; - if ( needleArg === '{{1}}' ) { needleArg = ''; } - let delayArg = '{{2}}'; - if ( delayArg === '{{2}}' ) { delayArg = ''; } - let boostArg = '{{3}}'; - if ( boostArg === '{{3}}' ) { boostArg = ''; } +function nanoSetIntervalBooster( + needleArg = '', + delayArg = '', + boostArg = '' +) { + if ( typeof needleArg !== 'string' ) { return; } if ( needleArg === '' ) { needleArg = '.?'; } else if ( needleArg.charAt(0) === '/' && needleArg.slice(-1) === '/' ) { @@ -561,9 +595,15 @@ return target.apply(thisArg, args); } }); -})(); +} +/// nano-setTimeout-booster.js +builtinScriptlets.push({ + name: 'nano-setTimeout-booster.js', + aliases: [ 'nano-stb.js' ], + fn: nanoSetTimeoutBooster, +}); // Imported from: // https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L82 // @@ -577,15 +617,12 @@ // boostRatio - The delay multiplier when there is a match, 0.5 speeds up by // 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up // 20 times. Speed up and down both cap at 50 times. -/// nano-setTimeout-booster.js -/// alias nano-stb.js -(function() { - let needleArg = '{{1}}'; - if ( needleArg === '{{1}}' ) { needleArg = ''; } - let delayArg = '{{2}}'; - if ( delayArg === '{{2}}' ) { delayArg = ''; } - let boostArg = '{{3}}'; - if ( boostArg === '{{3}}' ) { boostArg = ''; } +function nanoSetTimeoutBooster( + needleArg = '', + delayArg = '', + boostArg = '' +) { + if ( typeof needleArg !== 'string' ) { return; } if ( needleArg === '' ) { needleArg = '.?'; } else if ( needleArg.charAt(0) === '/' && needleArg.slice(-1) === '/' ) { @@ -612,13 +649,19 @@ return target.apply(thisArg, args); } }); -})(); +} /// noeval-if.js -(function() { - let needle = '{{1}}'; - if ( needle === '' || needle === '{{1}}' ) { +builtinScriptlets.push({ + name: 'noeval-if.js', + fn: 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); @@ -634,13 +677,18 @@ } } }); -})(); +} /// no-fetch-if.js -(function() { - let arg1 = '{{1}}'; - if ( arg1 === '{{1}}' ) { arg1 = ''; } +builtinScriptlets.push({ + name: 'no-fetch-if.js', + fn: noFetchIf, +}); +function noFetchIf( + arg1 = '', +) { + if ( typeof arg1 !== 'string' ) { return; } const needles = []; for ( const condition of arg1.split(/\s+/) ) { if ( condition === '' ) { continue; } @@ -706,35 +754,23 @@ : Promise.resolve(new Response()); } }); -})(); - - -/// no-floc.js -// https://github.com/uBlockOrigin/uBlock-issues/issues/1553 -(function() { - if ( Document instanceof Object === false ) { return; } - if ( Document.prototype.interestCohort instanceof Function === false ) { - return; - } - Document.prototype.interestCohort = new Proxy( - Document.prototype.interestCohort, - { - apply: function() { - return Promise.reject(); - } - } - ); -})(); +} /// refresh-defuser.js +builtinScriptlets.push({ + name: 'refresh-defuser.js', + fn: refreshDefuser, +}); // https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/ -(function() { - const arg1 = '{{1}}'; +function refreshDefuser( + arg1 = '' +) { + if ( typeof arg1 !== 'string' ) { return; } const defuse = ( ) => { const meta = document.querySelector('meta[http-equiv="refresh" i][content]'); if ( meta === null ) { return; } - const s = arg1 === '' || arg1 === '{{1}}' + const s = arg1 === '' ? meta.getAttribute('content') : arg1; const ms = Math.max(parseFloat(s) || 0, 0) * 1000; @@ -745,20 +781,26 @@ } else { defuse(); } -})(); +} /// remove-attr.js -/// alias ra.js -(function() { - const token = '{{1}}'; - if ( token === '' || token === '{{1}}' ) { return; } +builtinScriptlets.push({ + name: 'remove-attr.js', + aliases: [ 'ra.js' ], + fn: removeAttr, +}); +function removeAttr( + token = '', + selector = '', + behavior = '' +) { + if ( typeof token !== 'string' ) { return; } + if ( token === '' ) { return; } const tokens = token.split(/\s*\|\s*/); - let selector = '{{2}}'; - if ( selector === '' || selector === '{{2}}' ) { + if ( selector === '' ) { selector = `[${tokens.join('],[')}]`; } - let behavior = '{{3}}'; let timer; const rmattr = ( ) => { timer = undefined; @@ -806,20 +848,26 @@ } else { self.addEventListener('DOMContentLoaded', start, { once: true }); } -})(); +} /// remove-class.js -/// alias rc.js -(function() { - const token = '{{1}}'; - if ( token === '' || token === '{{1}}' ) { return; } +builtinScriptlets.push({ + name: 'remove-class.js', + aliases: [ 'rc.js' ], + fn: removeClass, +}); +function removeClass( + token = '', + selector = '', + behavior = '' +) { + if ( typeof token !== 'string' ) { return; } + if ( token === '' ) { return; } const tokens = token.split(/\s*\|\s*/); - let selector = '{{2}}'; - if ( selector === '' || selector === '{{2}}' ) { + if ( selector === '' ) { selector = '.' + tokens.map(a => CSS.escape(a)).join(',.'); } - let behavior = '{{3}}'; let timer; const rmclass = function() { timer = undefined; @@ -865,14 +913,19 @@ } else { start(); } -})(); +} /// no-requestAnimationFrame-if.js -/// alias norafif.js -(function() { - let needle = '{{1}}'; - if ( needle === '{{1}}' ) { needle = ''; } +builtinScriptlets.push({ + name: 'no-requestAnimationFrame-if.js', + aliases: [ 'norafif.js' ], + fn: noRequestAnimationFrameIf, +}); +function noRequestAnimationFrameIf( + needle = '' +) { + if ( typeof needle !== 'string' ) { return; } const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } if ( needle.startsWith('/') && needle.endsWith('/') ) { @@ -897,15 +950,21 @@ return target.apply(thisArg, args); } }); -})(); +} /// set-constant.js -/// alias set.js -(function() { - const chain = '{{1}}'; - if ( chain === '{{1}}' || chain === '' ) { return; } - let cValue = '{{2}}'; +builtinScriptlets.push({ + name: 'set-constant.js', + aliases: [ 'set.js' ], + fn: setConstant, +}); +function setConstant( + chain = '', + cValue = '' +) { + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } const trappedProp = (( ) => { const pos = chain.lastIndexOf('.'); if ( pos === -1 ) { return chain; } @@ -1057,24 +1116,30 @@ }); }; trapChain(window, chain); -})(); +} /// no-setInterval-if.js -/// alias nosiif.js -(function() { - let needle = '{{1}}'; +builtinScriptlets.push({ + name: 'no-setInterval-if.js', + aliases: [ 'nosiif.js' ], + fn: noSetIntervalIf, +}); +function noSetIntervalIf( + needle = '', + delay = '' +) { + if ( typeof needle !== 'string' ) { return; } const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } - let delay = '{{2}}'; - if ( delay === '{{2}}' ) { delay = undefined; } + if ( delay === '' ) { delay = undefined; } let delayNot = false; if ( delay !== undefined ) { delayNot = delay.charAt(0) === '!'; if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } - if ( needle === '' || needle === '{{1}}' ) { + if ( needle === '' ) { needle = ''; } else if ( needle.startsWith('/') && needle.endsWith('/') ) { needle = needle.slice(1,-1); @@ -1106,25 +1171,30 @@ return target.apply(thisArg, args); } }); -})(); +} /// no-setTimeout-if.js -/// alias nostif.js -/// alias setTimeout-defuser.js -(function() { - let needle = '{{1}}'; +builtinScriptlets.push({ + name: 'no-setTimeout-if.js', + aliases: [ 'nostif.js', 'setTimeout-defuser.js' ], + fn: noSetTimeoutIf, +}); +function noSetTimeoutIf( + needle = '', + delay = '' +) { + if ( typeof needle !== 'string' ) { return; } const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } - let delay = '{{2}}'; - if ( delay === '{{2}}' ) { delay = undefined; } + if ( delay === '' ) { delay = undefined; } let delayNot = false; if ( delay !== undefined ) { delayNot = delay.charAt(0) === '!'; if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } - if ( needle === '' || needle === '{{1}}' ) { + if ( needle === '' ) { needle = ''; } else if ( needle.startsWith('/') && needle.endsWith('/') ) { needle = needle.slice(1,-1); @@ -1156,12 +1226,18 @@ return target.apply(thisArg, args); } }); -})(); +} /// webrtc-if.js -(function() { - let good = '{{1}}'; +builtinScriptlets.push({ + name: 'webrtc-if.js', + fn: webrtcIf, +}); +function webrtcIf( + good = '' +) { + if ( typeof good !== 'string' ) { return; } if ( good.startsWith('/') && good.endsWith('/') ) { good = good.slice(1, -1); } else { @@ -1224,14 +1300,19 @@ return Reflect.construct(target, args); } }); -})(); +} /// no-xhr-if.js -(function() { +builtinScriptlets.push({ + name: 'no-xhr-if.js', + fn: noXhrIf, +}); +function noXhrIf( + arg1 = '' +) { + if ( typeof arg1 !== 'string' ) { return; } const xhrInstances = new WeakMap(); - let arg1 = '{{1}}'; - if ( arg1 === '{{1}}' ) { arg1 = ''; } const needles = []; for ( const condition of arg1.split(/\s+/) ) { if ( condition === '' ) { continue; } @@ -1296,18 +1377,24 @@ this.dispatchEvent(new Event('loadend')); } }; -})(); +} +/// window-close-if.js +builtinScriptlets.push({ + name: 'window-close-if.js', + fn: windowCloseIf, +}); // https://github.com/uBlockOrigin/uAssets/issues/10323#issuecomment-992312847 // https://github.com/AdguardTeam/Scriptlets/issues/158 // https://github.com/uBlockOrigin/uBlock-issues/discussions/2270 -/// window-close-if.js -(function() { - const arg1 = '{{1}}'; +function windowCloseIf( + arg1 = '' +) { + if ( typeof arg1 !== 'string' ) { return; } let reStr; let subject = ''; - if ( arg1 === '{{1}}' || arg1 === '' ) { + if ( arg1 === '' ) { reStr = '^'; } else if ( /^\/.*\/$/.test(arg1) ) { reStr = arg1.slice(1, -1); @@ -1324,26 +1411,32 @@ } catch(ex) { console.log(ex); } -})(); +} -// https://github.com/gorhill/uBlock/issues/1228 /// window.name-defuser.js -(function() { +builtinScriptlets.push({ + name: 'window.name-defuser.js', + fn: windowNameDefuser, +}); +// https://github.com/gorhill/uBlock/issues/1228 +function windowNameDefuser() { if ( window === window.top ) { window.name = ''; } -})(); +} +/// overlay-buster.js +builtinScriptlets.push({ + name: 'overlay-buster.js', + fn: overlayBuster, +}); // Experimental: Generic nuisance overlay buster. // if this works well and proves to be useful, this may end up // as a stock tool in uBO's popup panel. -/// overlay-buster.js -(function() { - if ( window !== window.top ) { - return; - } +function overlayBuster() { + if ( window !== window.top ) { return; } var tstart; var ttl = 30000; var delay = 0; @@ -1391,23 +1484,31 @@ } else { domReady(); } -})(); +} -// https://github.com/uBlockOrigin/uAssets/issues/8 /// alert-buster.js -(function() { +builtinScriptlets.push({ + name: 'alert-buster.js', + fn: alertBuster, +}); +// https://github.com/uBlockOrigin/uAssets/issues/8 +function alertBuster() { window.alert = new Proxy(window.alert, { apply: function(a) { console.info(a); }, }); -})(); +} -// https://github.com/uBlockOrigin/uAssets/issues/58 /// gpt-defuser.js -(function() { +builtinScriptlets.push({ + name: 'gpt-defuser.js', + fn: gptDefuser, +}); +// https://github.com/uBlockOrigin/uAssets/issues/58 +function gptDefuser() { const noopfn = function() { }; let props = '_resetGPT resetGPT resetAndLoadGPTRecovery _resetAndLoadGPTRecovery setupGPT setupGPTuo'; @@ -1423,12 +1524,16 @@ }); } } -})(); +} -// Prevent web pages from using RTCPeerConnection(), and report attempts in console. /// nowebrtc.js -(function() { +builtinScriptlets.push({ + name: 'nowebrtc.js', + fn: noWebrtc, +}); +// Prevent web pages from using RTCPeerConnection(), and report attempts in console. +function noWebrtc() { var rtcName = window.RTCPeerConnection ? 'RTCPeerConnection' : ( window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : '' ); @@ -1458,12 +1563,16 @@ }; }.bind(null); } -})(); +} -// https://github.com/uBlockOrigin/uAssets/issues/88 /// golem.de.js -(function() { +builtinScriptlets.push({ + name: 'golem.de.js', + fn: golemDe, +}); +// https://github.com/uBlockOrigin/uAssets/issues/88 +function golemDe() { const rael = window.addEventListener; window.addEventListener = function(a, b) { rael(...arguments); @@ -1479,13 +1588,17 @@ b(); } }.bind(window); -})(); +} +/// upmanager-defuser.js +builtinScriptlets.push({ + name: 'upmanager-defuser.js', + fn: upmanagerDefuser, +}); // https://forums.lanik.us/viewtopic.php?f=64&t=32278 // https://www.reddit.com/r/chrome/comments/58eix6/ublock_origin_not_working_on_certain_sites/ -/// upmanager-defuser.js -(function() { +function upmanagerDefuser() { var onerror = window.onerror; window.onerror = function(msg, source, lineno, colno, error) { if ( typeof msg === 'string' && msg.indexOf('upManager') !== -1 ) { @@ -1496,24 +1609,32 @@ } }; Object.defineProperty(window, 'upManager', { value: function() {} }); -})(); +} // https://github.com/uBlockOrigin/uAssets/issues/110 /// smartadserver.com.js -(function() { +builtinScriptlets.push({ + name: 'smartadserver.com.js', + fn: smartadserverCom, +}); +function smartadserverCom() { Object.defineProperties(window, { SmartAdObject: { value: function(){} }, SmartAdServerAjax: { value: function(){} }, smartAd: { value: { LoadAds: function() {}, Register: function() {} } } }); -})(); +} +/// adfly-defuser.js +builtinScriptlets.push({ + name: 'adfly-defuser.js', + fn: adflyDefuser, +}); // https://github.com/reek/anti-adblock-killer/issues/3774#issuecomment-348536138 // https://github.com/uBlockOrigin/uAssets/issues/883 -/// adfly-defuser.js -(function() { +function adflyDefuser() { // Based on AdsBypasser // License: // https://github.com/adsbypasser/adsbypasser/blob/master/LICENSE @@ -1571,12 +1692,16 @@ } catch (err) { window.console.error("Failed to set up Adfly bypasser!"); } -})(); +} -// https://github.com/uBlockOrigin/uAssets/issues/913 /// disable-newtab-links.js -(function() { +builtinScriptlets.push({ + name: 'disable-newtab-links.js', + fn: disableNewtabLinks, +}); +// https://github.com/uBlockOrigin/uAssets/issues/913 +function disableNewtabLinks() { document.addEventListener('click', function(ev) { var target = ev.target; while ( target !== null ) { @@ -1588,11 +1713,15 @@ target = target.parentNode; } }); -})(); +} /// damoh-defuser.js -(function() { +builtinScriptlets.push({ + name: 'damoh-defuser.js', + fn: damohDefuser, +}); +function damohDefuser() { const handled = new WeakSet(); let asyncTimer; const cleanVideo = function() { @@ -1615,16 +1744,20 @@ }; const observer = new MutationObserver(cleanVideoAsync); observer.observe(document, { childList: true, subtree: true }); -})(); +} /// twitch-videoad.js +builtinScriptlets.push({ + name: 'twitch-videoad.js', + fn: twitchVideoad, +}); // https://github.com/uBlockOrigin/uAssets/issues/5184 // https://github.com/pixeltris/TwitchAdSolutions/commit/6be4c5313035 // https://github.com/pixeltris/TwitchAdSolutions/commit/3d2883ea9e3a // https://github.com/pixeltris/TwitchAdSolutions/commit/7233b5fd2284 // https://github.com/pixeltris/TwitchAdSolutions/commit/aad8946dab2b -(function() { +function twitchVideoad() { if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; } window.fetch = new Proxy(window.fetch, { apply: function(target, thisArg, args) { @@ -1649,17 +1782,23 @@ return Reflect.apply(target, thisArg, args); } }); -})(); +} -// https://github.com/NanoAdblocker/NanoFilters/issues/149 /// cookie-remover.js -(function() { - const needle = '{{1}}'; +builtinScriptlets.push({ + name: 'cookie-remover.js', + fn: cookieRemover, +}); +// 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 !== '' && needle !== '{{1}}' ) { + } else if ( needle !== '' ) { reName = new RegExp(needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); } const removeCookie = function() { @@ -1699,24 +1838,21 @@ }; removeCookie(); window.addEventListener('beforeunload', removeCookie); -})(); +} /// xml-prune.js -(function() { - let selector = '{{1}}'; - if ( selector === '{{1}}' ) { - selector = ''; - } +builtinScriptlets.push({ + name: 'xml-prune.js', + fn: xmlPrune, +}); +function xmlPrune( + selector = '', + selectorCheck = '', + urlPattern = '' +) { + if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } - let selectorCheck = '{{2}}'; - if ( selectorCheck === '{{2}}' ) { - selectorCheck = ''; - } - let urlPattern = '{{3}}'; - if ( urlPattern === '{{3}}' ) { - urlPattern = ''; - } let reUrl; if ( urlPattern === '' ) { reUrl = /^/; @@ -1769,20 +1905,20 @@ ); } }); -})(); +} /// m3u-prune.js +builtinScriptlets.push({ + name: 'm3u-prune.js', + fn: m3uPrune, +}); // https://en.wikipedia.org/wiki/M3U -(function() { - let m3uPattern = '{{1}}'; - if ( m3uPattern === '{{1}}' ) { - m3uPattern = ''; - } - let urlPattern = '{{2}}'; - if ( urlPattern === '{{2}}' ) { - urlPattern = ''; - } +function m3uPrune( + m3uPattern = '', + urlPattern = '' +) { + if ( typeof m3uPattern !== 'string' ) { return; } const regexFromArg = arg => { if ( arg === '' ) { return /^/; } const match = /^\/(.+)\/([gms]*)$/.exec(arg); @@ -1891,16 +2027,20 @@ return Reflect.apply(target, thisArg, args); } }); -})(); +} /// href-sanitizer.js -(function() { - let selector = '{{1}}'; - if ( selector === '{{1}}' ) { selector = ''; } +builtinScriptlets.push({ + name: 'href-sanitizer.js', + fn: hrefSanitizer, +}); +function hrefSanitizer( + selector = '', + source = '' +) { + if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } - let source = '{{2}}'; - if ( source === '{{2}}' ) { source = ''; } if ( source === '' ) { source = 'text'; } const sanitizeCopycats = (href, text) => { let elems = []; @@ -1978,13 +2118,19 @@ } else { start(); } -})(); +} /// call-nothrow.js -(function() { - const chain = '{{1}}'; - if ( chain === '' || chain === '{{1}}' ) { return; } +builtinScriptlets.push({ + name: 'call-nothrow.js', + fn: callNothrow, +}); +function callNothrow( + chain = '' +) { + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } const parts = chain.split('.'); let owner = window, prop; for (;;) { @@ -2006,9 +2152,5 @@ return r; }, }); -})(); +} - -// These lines below are skipped by the resource parser. -// <<<< end of private namespace -})(); diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index e18ee7521..6105ec57b 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -325,13 +325,20 @@ RedirectEngine.prototype.loadBuiltinResources = function(fetcher) { this.aliases = new Map(); const fetches = [ - fetcher( - '/assets/resources/scriptlets.js' - ).then(result => { - const content = result.content; - if ( typeof content !== 'string' ) { return; } - if ( content.length === 0 ) { return; } - this.resourcesFromString(content); + 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(); }), ]; @@ -426,7 +433,7 @@ RedirectEngine.prototype.getResourceDetails = function() { /******************************************************************************/ -const RESOURCES_SELFIE_VERSION = 6; +const RESOURCES_SELFIE_VERSION = 7; const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources'; RedirectEngine.prototype.selfieFromResources = function(storage) { diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 4ef2d50f3..4ffe9ebaa 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -149,7 +149,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) { let content = scriptletCache.lookup(rawToken); if ( content === undefined ) { const pos = rawToken.indexOf(','); - let token, args; + let token, args = ''; if ( pos === -1 ) { token = rawToken; } else { @@ -165,10 +165,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) { } content = reng.resourceContentFromName(token, 'text/javascript'); if ( !content ) { return; } - if ( args ) { - content = patchScriptlet(content, args); - if ( !content ) { return; } - } + content = patchScriptlet(content, args); content = 'try {\n' + content + '\n' + @@ -180,26 +177,36 @@ const lookupScriptlet = function(rawToken, reng, toInject) { // Fill-in scriptlet argument placeholders. const patchScriptlet = function(content, 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; } - content = content.replace( - `{{${i}}}`, - s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&') - ); - beg = pos = pos + 1; - i++; + if ( content.startsWith('function') ) { + content = `(${content})({{args}});`; } + if ( args.startsWith('{') && args.endsWith('}') ) { + return content.replace('{{args}}', 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++; + } + } + 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; }; diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 8844d544c..911fa5912 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -118,6 +118,7 @@ export const NODE_TYPE_EXT_PATTERN_HTML = iota++; export const NODE_TYPE_EXT_PATTERN_RESPONSEHEADER = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN = iota++; +export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG = iota++; export const NODE_TYPE_NET_RAW = iota++; export const NODE_TYPE_NET_EXCEPTION = iota++; @@ -276,6 +277,7 @@ export const nodeNameFromNodeType = new Map([ [ NODE_TYPE_EXT_PATTERN_RESPONSEHEADER, 'extPatternResponseheader' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET, 'extPatternScriptlet' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, 'extPatternScriptletToken' ], + [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, 'extPatternScriptletArgs' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, 'extPatternScriptletArg' ], [ NODE_TYPE_NET_RAW, 'netRaw' ], [ NODE_TYPE_NET_EXCEPTION, 'netException' ], @@ -748,6 +750,7 @@ export class AstFilterParser { this.reHostnamePatternPart = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+/; this.reHostnameLabel = /[^.]+/g; this.reResponseheaderPattern = /^\^responseheader\(.*\)$/; + this.rePatternScriptletJsonArgs = /^\{.*\}$/; // TODO: mind maxTokenLength this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/; this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/; @@ -2118,53 +2121,59 @@ export class AstFilterParser { let prev = head, next = 0; const s = this.getNodeString(parent); const argsEnd = s.length; - let argCount = 0; - let argBeg = 0, argEnd = 0, argBodyBeg = 0, argBodyEnd = 0; - let rawArg = ''; - while ( argBeg < argsEnd ) { - argEnd = this.indexOfNextScriptletArgSeparator(s, argBeg); - rawArg = s.slice(argBeg, argEnd); - argBodyBeg = argBeg + this.leftWhitespaceCount(rawArg); - if ( argBodyBeg !== argBodyEnd ) { - next = this.allocTypedNode( - NODE_TYPE_EXT_DECORATION, - parentBeg + argBodyEnd, - parentBeg + argBodyBeg - ); - prev = this.linkRight(prev, next); - } - argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg); - if ( argCount === 0 ) { - rawArg = s.slice(argBodyBeg, argBodyEnd); - const tokenEnd = rawArg.endsWith('.js') - ? argBodyEnd - 3 - : argBodyEnd; - next = this.allocTypedNode( - NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, - parentBeg + argBodyBeg, - parentBeg + tokenEnd - ); - prev = this.linkRight(prev, next); - if ( tokenEnd !== argBodyEnd ) { - next = this.allocTypedNode( - NODE_TYPE_IGNORE, - parentBeg + argBodyEnd - 3, - parentBeg + argBodyEnd - ); - prev = this.linkRight(prev, next); - } - } else { - next = this.allocTypedNode( - NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, - parentBeg + argBodyBeg, - parentBeg + argBodyEnd - ); - prev = this.linkRight(prev, next); - } - argBeg = argEnd + 1; - argCount += 1; + // token + let argEnd = this.indexOfNextScriptletArgSeparator(s, 0); + let rawArg = s.slice(0, argEnd); + let argBodyBeg = this.leftWhitespaceCount(rawArg); + if ( argBodyBeg !== 0 ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_DECORATION, + parentBeg, + parentBeg + argBodyBeg + ); + prev = this.linkRight(prev, next); } - if ( argsEnd !== argBodyEnd ) { + let argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg); + rawArg = s.slice(argBodyBeg, argBodyEnd); + const tokenEnd = rawArg.endsWith('.js') + ? argBodyEnd - 3 + : argBodyEnd; + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, + parentBeg + argBodyBeg, + parentBeg + tokenEnd + ); + prev = this.linkRight(prev, next); + // ignore pointless `.js` + if ( tokenEnd !== argBodyEnd ) { + next = this.allocTypedNode( + NODE_TYPE_IGNORE, + parentBeg + argBodyEnd - 3, + parentBeg + argBodyEnd + ); + prev = this.linkRight(prev, next); + } + // all args + argBodyBeg = argEnd + 1; + const rawArgs = s.slice(argBodyBeg, argsEnd); + argBodyBeg += this.leftWhitespaceCount(rawArgs); + next = this.allocTypedNode( + NODE_TYPE_EXT_DECORATION, + parentBeg + argBodyEnd, + parentBeg + argBodyBeg + ); + prev = this.linkRight(prev, next); + argBodyEnd = argsEnd - this.rightWhitespaceCount(rawArgs); + if ( argBodyBeg !== argBodyEnd ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, + parentBeg + argBodyBeg, + parentBeg + argBodyEnd + ); + this.linkDown(next, this.parseExtPatternScriptletArglist(next)); + prev = this.linkRight(prev, next); + } + if ( argBodyEnd !== argsEnd ) { next = this.allocTypedNode( NODE_TYPE_EXT_DECORATION, parentBeg + argBodyEnd, @@ -2175,6 +2184,57 @@ export class AstFilterParser { return this.throwHeadNode(head); } + parseExtPatternScriptletArglist(parent) { + const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; + const parentEnd = this.nodes[parent+NODE_END_INDEX]; + if ( parentEnd === parentBeg ) { return 0; } + const s = this.getNodeString(parent); + let next = 0, prev = 0; + // json-based arg? + const match = this.rePatternScriptletJsonArgs.exec(s); + if ( match !== null ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, + parentBeg, + parentEnd + ); + try { + void JSON.parse(s); + } catch(ex) { + this.addNodeFlags(next, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + } + return next; + } + // positional args + const argsEnd = s.length; + let argBodyBeg = 0, argBodyEnd = 0, argEnd = 0; + let t = ''; + while ( argBodyBeg < argsEnd ) { + argEnd = this.indexOfNextScriptletArgSeparator(s, argBodyBeg); + t = s.slice(argBodyBeg, argEnd); + argBodyEnd = argEnd - this.rightWhitespaceCount(t); + next = this.allocTypedNode( + NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, + parentBeg + argBodyBeg, + parentBeg + argBodyEnd + ); + prev = this.linkRight(prev, next); + if ( argEnd === argsEnd ) { break; } + t = s.slice(argEnd + 1); + argBodyBeg = argEnd + 1 + this.leftWhitespaceCount(t); + if ( argBodyEnd !== argBodyBeg ) { + next = this.allocTypedNode( + NODE_TYPE_EXT_DECORATION, + parentBeg + argBodyEnd, + parentBeg + argBodyBeg + ); + prev = this.linkRight(prev, next); + } + } + return next; + } + indexOfNextScriptletArgSeparator(pattern, beg = 0) { const patternEnd = pattern.length; if ( beg >= patternEnd ) { return patternEnd; }