diff options
Diffstat (limited to 'assets/resources/scriptlets.js')
-rw-r--r-- | assets/resources/scriptlets.js | 1126 |
1 files changed, 727 insertions, 399 deletions
diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 6e2654f..bbef272 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -22,11 +22,11 @@ web page context. */ +/* eslint no-prototype-builtins: 0 */ + // Externally added to the private namespace in which scriptlets execute. /* global scriptletGlobals */ -'use strict'; - export const builtinScriptlets = []; /******************************************************************************* @@ -42,8 +42,8 @@ builtinScriptlets.push({ fn: safeSelf, }); function safeSelf() { - if ( scriptletGlobals.has('safeSelf') ) { - return scriptletGlobals.get('safeSelf'); + if ( scriptletGlobals.safeSelf ) { + return scriptletGlobals.safeSelf; } const self = globalThis; const safe = { @@ -55,7 +55,10 @@ function safeSelf() { 'Math_max': Math.max, 'Math_min': Math.min, 'Math_random': Math.random, + 'Object': Object, 'Object_defineProperty': Object.defineProperty.bind(Object), + 'Object_fromEntries': Object.fromEntries.bind(Object), + 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object), 'RegExp': self.RegExp, 'RegExp_test': self.RegExp.prototype.test, 'RegExp_exec': self.RegExp.prototype.exec, @@ -70,11 +73,25 @@ function safeSelf() { 'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args), 'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args), 'log': console.log.bind(console), + // Properties + logLevel: 0, + // Methods + makeLogPrefix(...args) { + return this.sendToLogger && `[${args.join(' \u205D ')}]` || ''; + }, uboLog(...args) { - if ( scriptletGlobals.has('canDebug') === false ) { return; } - if ( args.length === 0 ) { return; } - if ( `${args[0]}` === '' ) { return; } - this.log('[uBO]', ...args); + if ( this.sendToLogger === undefined ) { return; } + if ( args === undefined || args[0] === '' ) { return; } + return this.sendToLogger('info', ...args); + + }, + uboErr(...args) { + if ( this.sendToLogger === undefined ) { return; } + if ( args === undefined || args[0] === '' ) { return; } + return this.sendToLogger('error', ...args); + }, + escapeRegexChars(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, initPattern(pattern, options = {}) { if ( pattern === '' ) { @@ -96,8 +113,7 @@ function safeSelf() { } if ( options.flags !== undefined ) { return { - re: new this.RegExp(pattern.replace( - /[.*+?^${}()|[\]\\]/g, '\\$&'), + re: new this.RegExp(this.escapeRegexChars(pattern), options.flags ), expect, @@ -116,7 +132,7 @@ function safeSelf() { if ( pattern === '' ) { return /^/; } const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); if ( match === null ) { - const reStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const reStr = this.escapeRegexChars(pattern); return new RegExp(verbatim ? `^${reStr}$` : reStr, flags); } try { @@ -137,10 +153,42 @@ function safeSelf() { } return out; }, []); - return Object.fromEntries(entries); + return this.Object_fromEntries(entries); }, }; - scriptletGlobals.set('safeSelf', safe); + scriptletGlobals.safeSelf = safe; + if ( scriptletGlobals.bcSecret === undefined ) { return safe; } + // This is executed only when the logger is opened + const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret); + let bcBuffer = []; + safe.logLevel = scriptletGlobals.logLevel || 1; + safe.sendToLogger = (type, ...args) => { + if ( args.length === 0 ) { return; } + const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`; + if ( bcBuffer === undefined ) { + return bc.postMessage({ what: 'messageToLogger', type, text }); + } + bcBuffer.push({ type, text }); + }; + bc.onmessage = ev => { + const msg = ev.data; + switch ( msg ) { + case 'iamready!': + if ( bcBuffer === undefined ) { break; } + bcBuffer.forEach(({ type, text }) => + bc.postMessage({ what: 'messageToLogger', type, text }) + ); + bcBuffer = undefined; + break; + case 'setScriptletLogLevelToOne': + safe.logLevel = 1; + break; + case 'setScriptletLogLevelToTwo': + safe.logLevel = 2; + break; + } + }; + bc.postMessage('areyouready?'); return safe; } @@ -176,18 +224,7 @@ builtinScriptlets.push({ }); function shouldDebug(details) { if ( details instanceof Object === false ) { return false; } - return scriptletGlobals.has('canDebug') && details.debug; -} - -/******************************************************************************/ - -builtinScriptlets.push({ - name: 'should-log.fn', - fn: shouldLog, -}); -function shouldLog(details) { - if ( details instanceof Object === false ) { return false; } - return scriptletGlobals.has('canDebug') && details.log; + return scriptletGlobals.canDebug && details.debug; } /******************************************************************************/ @@ -292,12 +329,12 @@ function generateContentFn(directive) { return Promise.resolve(randomize(len | 0)); } } - if ( directive.startsWith('war:') && scriptletGlobals.has('warOrigin') ) { + if ( directive.startsWith('war:') && scriptletGlobals.warOrigin ) { return new Promise(resolve => { - const warOrigin = scriptletGlobals.get('warOrigin'); + const warOrigin = scriptletGlobals.warOrigin; const warName = directive.slice(4); const fullpath = [ warOrigin, '/', warName ]; - const warSecret = scriptletGlobals.get('warSecret'); + const warSecret = scriptletGlobals.warSecret; if ( warSecret !== undefined ) { fullpath.push('?secret=', warSecret); } @@ -322,7 +359,6 @@ builtinScriptlets.push({ 'get-exception-token.fn', 'safe-self.fn', 'should-debug.fn', - 'should-log.fn', ], }); // Issues to mind before changing anything: @@ -335,6 +371,7 @@ function abortCurrentScriptCore( if ( typeof target !== 'string' ) { return; } if ( target === '' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('abort-current-script', target, needle, context); const reNeedle = safe.patternToRegex(needle); const reContext = safe.patternToRegex(context); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); @@ -358,7 +395,6 @@ function abortCurrentScriptCore( value = owner[prop]; desc = undefined; } - const log = shouldLog(extraArgs); const debug = shouldDebug(extraArgs); const exceptionToken = getExceptionToken(); const scriptTexts = new WeakMap(); @@ -388,20 +424,29 @@ function abortCurrentScriptCore( if ( e instanceof HTMLScriptElement === false ) { return; } if ( e === thisScript ) { return; } if ( context !== '' && reContext.test(e.src) === false ) { - if ( debug === 'nomatch' || debug === 'all' ) { debugger; } // jshint ignore: line + // eslint-disable-next-line no-debugger + if ( debug === 'nomatch' || debug === 'all' ) { debugger; } return; } - if ( log && e.src !== '' ) { safe.uboLog(`matched src: ${e.src}`); } + if ( safe.logLevel > 1 && context !== '' ) { + safe.uboLog(logPrefix, `Matched src\n${e.src}`); + } const scriptText = getScriptText(e); if ( reNeedle.test(scriptText) === false ) { - if ( debug === 'nomatch' || debug === 'all' ) { debugger; } // jshint ignore: line + // eslint-disable-next-line no-debugger + if ( debug === 'nomatch' || debug === 'all' ) { debugger; } return; } - if ( log ) { safe.uboLog(`matched script text: ${scriptText}`); } - if ( debug === 'match' || debug === 'all' ) { debugger; } // jshint ignore: line + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched text\n${scriptText}`); + } + // eslint-disable-next-line no-debugger + if ( debug === 'match' || debug === 'all' ) { debugger; } + safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); }; - if ( debug === 'install' ) { debugger; } // jshint ignore: line + // eslint-disable-next-line no-debugger + if ( debug === 'install' ) { debugger; } try { Object.defineProperty(owner, prop, { get: function() { @@ -420,40 +465,97 @@ function abortCurrentScriptCore( } }); } catch(ex) { - if ( log ) { safe.uboLog(ex); } + safe.uboErr(logPrefix, `Error: ${ex}`); } } /******************************************************************************/ builtinScriptlets.push({ - name: 'set-constant-core.fn', - fn: setConstantCore, + name: 'validate-constant.fn', + fn: validateConstantFn, dependencies: [ - 'run-at.fn', 'safe-self.fn', ], }); +function validateConstantFn(trusted, raw) { + const safe = safeSelf(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + let value; + if ( raw === 'undefined' ) { + value = undefined; + } else if ( raw === 'false' ) { + value = false; + } else if ( raw === 'true' ) { + value = true; + } else if ( raw === 'null' ) { + value = null; + } else if ( raw === "''" || raw === '' ) { + value = ''; + } else if ( raw === '[]' || raw === 'emptyArr' ) { + value = []; + } else if ( raw === '{}' || raw === 'emptyObj' ) { + value = {}; + } else if ( raw === 'noopFunc' ) { + value = function(){}; + } else if ( raw === 'trueFunc' ) { + value = function(){ return true; }; + } else if ( raw === 'falseFunc' ) { + value = function(){ return false; }; + } else if ( /^-?\d+$/.test(raw) ) { + value = parseInt(raw); + if ( isNaN(raw) ) { return; } + if ( Math.abs(raw) > 0x7FFF ) { return; } + } else if ( trusted ) { + if ( raw.startsWith('{') && raw.endsWith('}') ) { + try { value = safe.JSON_parse(raw).value; } catch(ex) { return; } + } + } else { + return; + } + if ( extraArgs.as !== undefined ) { + if ( extraArgs.as === 'function' ) { + return ( ) => value; + } else if ( extraArgs.as === 'callback' ) { + return ( ) => (( ) => value); + } else if ( extraArgs.as === 'resolved' ) { + return Promise.resolve(value); + } else if ( extraArgs.as === 'rejected' ) { + return Promise.reject(value); + } + } + return value; +} + +/******************************************************************************/ -function setConstantCore( +builtinScriptlets.push({ + name: 'set-constant.fn', + fn: setConstantFn, + dependencies: [ + 'run-at.fn', + 'safe-self.fn', + 'validate-constant.fn', + ], +}); +function setConstantFn( trusted = false, chain = '', - cValue = '' + rawValue = '' ) { if ( chain === '' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - function setConstant(chain, cValue) { + function setConstant(chain, rawValue) { const trappedProp = (( ) => { const pos = chain.lastIndexOf('.'); if ( pos === -1 ) { return chain; } return chain.slice(pos+1); })(); - if ( trappedProp === '' ) { return; } - const thisScript = document.currentScript; const cloakFunc = fn => { safe.Object_defineProperty(fn, 'name', { value: trappedProp }); - const proxy = new Proxy(fn, { + return new Proxy(fn, { defineProperty(target, prop) { if ( prop !== 'toString' ) { return Reflect.defineProperty(...arguments); @@ -475,50 +577,12 @@ function setConstantCore( return Reflect.get(...arguments); }, }); - return proxy; }; - if ( cValue === 'undefined' ) { - cValue = undefined; - } else if ( cValue === 'false' ) { - cValue = false; - } else if ( cValue === 'true' ) { - cValue = true; - } else if ( cValue === 'null' ) { - cValue = null; - } else if ( cValue === "''" || cValue === '' ) { - cValue = ''; - } else if ( cValue === '[]' || cValue === 'emptyArr' ) { - cValue = []; - } else if ( cValue === '{}' || cValue === 'emptyObj' ) { - cValue = {}; - } else if ( cValue === 'noopFunc' ) { - cValue = cloakFunc(function(){}); - } else if ( cValue === 'trueFunc' ) { - cValue = cloakFunc(function(){ return true; }); - } else if ( cValue === 'falseFunc' ) { - cValue = cloakFunc(function(){ return false; }); - } else if ( /^-?\d+$/.test(cValue) ) { - cValue = parseInt(cValue); - if ( isNaN(cValue) ) { return; } - if ( Math.abs(cValue) > 0x7FFF ) { return; } - } else if ( trusted ) { - if ( cValue.startsWith('{') && cValue.endsWith('}') ) { - try { cValue = safe.JSON_parse(cValue).value; } catch(ex) { return; } - } - } else { - return; - } - if ( extraArgs.as !== undefined ) { - const value = cValue; - if ( extraArgs.as === 'function' ) { - cValue = ( ) => value; - } else if ( extraArgs.as === 'callback' ) { - cValue = ( ) => (( ) => value); - } else if ( extraArgs.as === 'resolved' ) { - cValue = Promise.resolve(value); - } else if ( extraArgs.as === 'rejected' ) { - cValue = Promise.reject(value); - } + if ( trappedProp === '' ) { return; } + const thisScript = document.currentScript; + let normalValue = validateConstantFn(trusted, rawValue); + if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) { + normalValue = cloakFunc(normalValue); } let aborted = false; const mustAbort = function(v) { @@ -526,18 +590,21 @@ function setConstantCore( if ( aborted ) { return true; } aborted = (v !== undefined && v !== null) && - (cValue !== undefined && cValue !== null) && - (typeof v !== typeof cValue); + (normalValue !== undefined && normalValue !== null) && + (typeof v !== typeof normalValue); + if ( aborted ) { + safe.uboLog(logPrefix, `Aborted because value set to ${v}`); + } return aborted; }; // https://github.com/uBlockOrigin/uBlock-issues/issues/156 // Support multiple trappers for the same property. const trapProp = function(owner, prop, configurable, handler) { - if ( handler.init(configurable ? owner[prop] : cValue) === false ) { return; } - const odesc = Object.getOwnPropertyDescriptor(owner, prop); + if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; } + const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); let prevGetter, prevSetter; - if ( odesc instanceof Object ) { - owner[prop] = cValue; + if ( odesc instanceof safe.Object ) { + owner[prop] = normalValue; if ( odesc.get instanceof Function ) { prevGetter = odesc.get; } @@ -552,7 +619,7 @@ function setConstantCore( if ( prevGetter !== undefined ) { prevGetter(); } - return handler.getter(); // cValue + return handler.getter(); }, set(a) { if ( prevSetter !== undefined ) { @@ -561,7 +628,9 @@ function setConstantCore( handler.setter(a); } }); + safe.uboLog(logPrefix, 'Trap installed'); } catch(ex) { + safe.uboErr(logPrefix, ex); } }; const trapChain = function(owner, chain) { @@ -575,13 +644,15 @@ function setConstantCore( return true; }, getter: function() { - return document.currentScript === thisScript - ? this.v - : cValue; + if ( document.currentScript === thisScript ) { + return this.v; + } + safe.uboLog(logPrefix, 'Property read'); + return normalValue; }, setter: function(a) { if ( mustAbort(a) === false ) { return; } - cValue = a; + normalValue = a; } }); return; @@ -589,7 +660,7 @@ function setConstantCore( const prop = chain.slice(0, pos); const v = owner[prop]; chain = chain.slice(pos + 1); - if ( v instanceof Object || typeof v === 'object' && v !== null ) { + if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) { trapChain(v, chain); return; } @@ -604,7 +675,7 @@ function setConstantCore( }, setter: function(a) { this.v = a; - if ( a instanceof Object ) { + if ( a instanceof safe.Object ) { trapChain(a, chain); } } @@ -613,7 +684,7 @@ function setConstantCore( trapChain(window, chain); } runAt(( ) => { - setConstant(chain, cValue); + setConstant(chain, rawValue); }, extraArgs.runAt); } @@ -633,18 +704,18 @@ function replaceNodeTextFn( replacement = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('replace-node-text.fn', ...Array.from(arguments)); const reNodeName = safe.patternToRegex(nodeName, 'i', true); const rePattern = safe.patternToRegex(pattern, 'gms'); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const shouldLog = scriptletGlobals.has('canDebug') && extraArgs.log || 0; const reCondition = safe.patternToRegex(extraArgs.condition || '', 'ms'); const stop = (takeRecord = true) => { if ( takeRecord ) { handleMutations(observer.takeRecords()); } observer.disconnect(); - if ( shouldLog !== 0 ) { - safe.uboLog(`replace-node-text-core.fn: quitting "${pattern}" => "${replacement}"`); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, 'Quitting'); } }; let sedCount = extraArgs.sedCount || 0; @@ -659,10 +730,10 @@ function replaceNodeTextFn( ? before.replace(rePattern, replacement) : replacement; node.textContent = after; - if ( shouldLog !== 0 ) { - safe.uboLog('replace-node-text.fn before:\n', before); - safe.uboLog('replace-node-text.fn after:\n', after); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Text before:\n${before.trim()}`); } + safe.uboLog(logPrefix, `Text after:\n${after.trim()}`); return sedCount === 0 || (sedCount -= 1) !== 0; }; const handleMutations = mutations => { @@ -690,9 +761,7 @@ function replaceNodeTextFn( if ( handleNode(node) ) { continue; } stop(); break; } - if ( shouldLog !== 0 ) { - safe.uboLog(`replace-node-text-core.fn ${count} nodes present before installing mutation observer`); - } + safe.uboLog(logPrefix, `${count} nodes present before installing mutation observer`); } if ( extraArgs.stay ) { return; } runAt(( ) => { @@ -713,8 +782,6 @@ builtinScriptlets.push({ dependencies: [ 'matches-stack-trace.fn', 'object-find-owner.fn', - 'safe-self.fn', - 'should-log.fn', ], }); // When no "prune paths" argument is provided, the scriptlet is @@ -731,15 +798,12 @@ function objectPruneFn( extraArgs = {} ) { if ( typeof rawPrunePaths !== 'string' ) { return; } - const safe = safeSelf(); const prunePaths = rawPrunePaths !== '' ? rawPrunePaths.split(/ +/) : []; const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' ? rawNeedlePaths.split(/ +/) : []; - const logLevel = shouldLog({ log: rawPrunePaths === '' || extraArgs.log }); - const reLogNeedle = safe.patternToRegex(logLevel === true ? rawNeedlePaths : ''); if ( stackNeedleDetails.matchAll !== true ) { if ( matchesStackTrace(stackNeedleDetails, extraArgs.logstack) === false ) { return; @@ -754,14 +818,6 @@ function objectPruneFn( } return true; }; - objectPruneFn.logJson = (json, msg, reNeedle) => { - if ( reNeedle.test(json) === false ) { return; } - safeSelf().uboLog(`objectPrune()`, msg, location.hostname, json); - }; - } - const jsonBefore = logLevel ? safe.JSON_stringify(obj, null, 2) : ''; - if ( logLevel === true || logLevel === 'all' ) { - objectPruneFn.logJson(jsonBefore, `prune:"${rawPrunePaths}" log:"${logLevel}"`, reLogNeedle); } if ( prunePaths.length === 0 ) { return; } let outcome = 'nomatch'; @@ -772,9 +828,6 @@ function objectPruneFn( } } } - if ( logLevel === outcome ) { - objectPruneFn.logJson(jsonBefore, `prune:"${rawPrunePaths}" log:"${logLevel}"`, reLogNeedle); - } if ( outcome === 'match' ) { return obj; } } @@ -812,14 +865,33 @@ function objectFindOwnerFn( return modified; } const prop = chain.slice(0, pos); + const next = chain.slice(pos + 1); + let found = false; + if ( prop === '[-]' && Array.isArray(owner) ) { + let i = owner.length; + while ( i-- ) { + if ( objectFindOwnerFn(owner[i], next) === false ) { continue; } + owner.splice(i, 1); + found = true; + } + return found; + } + if ( prop === '{-}' && owner instanceof Object ) { + for ( const key of Object.keys(owner) ) { + if ( objectFindOwnerFn(owner[key], next) === false ) { continue; } + delete owner[key]; + found = true; + } + return found; + } if ( prop === '[]' && Array.isArray(owner) || + prop === '{}' && owner instanceof Object || prop === '*' && owner instanceof Object ) { - const next = chain.slice(pos + 1); - let found = false; for ( const key of Object.keys(owner) ) { - found = objectFindOwnerFn(owner[key], next, prune) || found; + if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; } + found = true; } return found; } @@ -827,7 +899,57 @@ function objectFindOwnerFn( owner = owner[prop]; chain = chain.slice(pos + 1); } - return true; +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'get-all-cookies.fn', + fn: getAllCookiesFn, +}); +function getAllCookiesFn() { + return document.cookie.split(/\s*;\s*/).map(s => { + const pos = s.indexOf('='); + if ( pos === 0 ) { return; } + if ( pos === -1 ) { return `${s.trim()}=`; } + const key = s.slice(0, pos).trim(); + const value = s.slice(pos+1).trim(); + return { key, value }; + }).filter(s => s !== undefined); +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'get-all-local-storage.fn', + fn: getAllLocalStorageFn, +}); +function getAllLocalStorageFn(which = 'localStorage') { + const storage = self[which]; + const out = []; + for ( let i = 0; i < storage.length; i++ ) { + const key = storage.key(i); + const value = storage.getItem(key); + return { key, value }; + } + return out; +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'get-cookie.fn', + fn: getCookieFn, +}); +function getCookieFn( + name = '' +) { + for ( const s of document.cookie.split(/\s*;\s*/) ) { + const pos = s.indexOf('='); + if ( pos === -1 ) { continue; } + if ( s.slice(0, pos) !== name ) { continue; } + return s.slice(pos+1).trim(); + } } /******************************************************************************/ @@ -835,6 +957,9 @@ function objectFindOwnerFn( builtinScriptlets.push({ name: 'set-cookie.fn', fn: setCookieFn, + dependencies: [ + 'get-cookie.fn', + ], }); function setCookieFn( trusted = false, @@ -844,16 +969,17 @@ function setCookieFn( path = '', options = {}, ) { - const getCookieValue = name => { - for ( const s of document.cookie.split(/\s*;\s*/) ) { - const pos = s.indexOf('='); - if ( pos === -1 ) { continue; } - if ( s.slice(0, pos) !== name ) { continue; } - return s.slice(pos+1); - } - }; + // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 + // https://github.com/uBlockOrigin/uBlock-issues/issues/2777 + if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) { + name = encodeURIComponent(name); + } + // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 + if ( /[^!#-+\--:<-[\]-~]/.test(value) ) { + value = encodeURIComponent(value); + } - const cookieBefore = getCookieValue(name); + const cookieBefore = getCookieFn(name); if ( cookieBefore !== undefined && options.dontOverwrite ) { return; } if ( cookieBefore === value && options.reload ) { return; } @@ -881,9 +1007,12 @@ function setCookieFn( } catch(_) { } - if ( options.reload && getCookieValue(name) === value ) { + const done = getCookieFn(name) === value; + if ( done && options.reload ) { window.location.reload(); } + + return done; } /******************************************************************************/ @@ -1070,7 +1199,7 @@ function matchObjectProperties(propNeedles, ...objs) { let value = haystack[prop]; if ( value === undefined ) { continue; } if ( typeof value !== 'string' ) { - try { value = JSON.stringify(value); } + try { value = safe.JSON_stringify(value); } catch(ex) { } if ( typeof value !== 'string' ) { continue; } } @@ -1090,7 +1219,6 @@ builtinScriptlets.push({ 'object-prune.fn', 'parse-properties-to-match.fn', 'safe-self.fn', - 'should-log.fn', ], }); function jsonPruneFetchResponseFn( @@ -1098,23 +1226,22 @@ function jsonPruneFetchResponseFn( rawNeedlePaths = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); - const logLevel = shouldLog({ log: rawPrunePaths === '' || extraArgs.log, }); - const log = logLevel ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url'); const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); + const logall = rawPrunePaths === ''; const applyHandler = function(target, thisArg, args) { const fetchPromise = Reflect.apply(target, thisArg, args); - if ( logLevel === true ) { - log('json-prune-fetch-response:', JSON.stringify(Array.from(args)).slice(1,-1)); - } - if ( rawPrunePaths === '' ) { return fetchPromise; } - let outcome = 'match'; + let outcome = logall ? 'nomatch' : 'match'; if ( propNeedles.size !== 0 ) { const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; if ( objs[0] instanceof Request ) { - try { objs[0] = safe.Request_clone.call(objs[0]); } - catch(ex) { log(ex); } + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } } if ( args[1] instanceof Object ) { objs.push(args[1]); @@ -1122,19 +1249,19 @@ function jsonPruneFetchResponseFn( if ( matchObjectProperties(propNeedles, ...objs) === false ) { outcome = 'nomatch'; } - if ( outcome === logLevel || logLevel === 'all' ) { - log( - `json-prune-fetch-response (${outcome})`, - `\n\tfetchPropsToMatch: ${JSON.stringify(Array.from(propNeedles)).slice(1,-1)}`, - '\n\tprops:', ...objs, - ); - } } - if ( outcome === 'nomatch' ) { return fetchPromise; } + if ( logall === false && outcome === 'nomatch' ) { return fetchPromise; } + if ( safe.logLevel > 1 && outcome !== 'nomatch' && propNeedles.size !== 0 ) { + safe.uboLog(logPrefix, `Matched optional "propsToMatch"\n${extraArgs.propsToMatch}`); + } return fetchPromise.then(responseBefore => { const response = responseBefore.clone(); return response.json().then(objBefore => { if ( typeof objBefore !== 'object' ) { return responseBefore; } + if ( logall ) { + safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); + return responseBefore; + } const objAfter = objectPruneFn( objBefore, rawPrunePaths, @@ -1143,6 +1270,7 @@ function jsonPruneFetchResponseFn( extraArgs ); if ( typeof objAfter !== 'object' ) { return responseBefore; } + safe.uboLog(logPrefix, 'Pruned'); const responseAfter = Response.json(objAfter, { status: responseBefore.status, statusText: responseBefore.statusText, @@ -1156,11 +1284,11 @@ function jsonPruneFetchResponseFn( }); return responseAfter; }).catch(reason => { - log('json-prune-fetch-response:', reason); + safe.uboErr(logPrefix, 'Error:', reason); return responseBefore; }); }).catch(reason => { - log('json-prune-fetch-response:', reason); + safe.uboErr(logPrefix, 'Error:', reason); return fetchPromise; }); }; @@ -1178,7 +1306,6 @@ builtinScriptlets.push({ 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', - 'should-log.fn', ], }); function replaceFetchResponseFn( @@ -1189,27 +1316,24 @@ function replaceFetchResponseFn( ) { if ( trusted !== true ) { return; } const safe = safeSelf(); - const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); - const logLevel = shouldLog({ - log: pattern === '' || extraArgs.log, - }); - const log = logLevel ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); + const logPrefix = safe.makeLogPrefix('replace-fetch-response', pattern, replacement, propsToMatch); if ( pattern === '*' ) { pattern = '.*'; } const rePattern = safe.patternToRegex(pattern); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { - if ( logLevel === true ) { - log('replace-fetch-response:', JSON.stringify(Array.from(args)).slice(1,-1)); - } const fetchPromise = Reflect.apply(target, thisArg, args); if ( pattern === '' ) { return fetchPromise; } let outcome = 'match'; if ( propNeedles.size !== 0 ) { const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; if ( objs[0] instanceof Request ) { - try { objs[0] = safe.Request_clone.call(objs[0]); } - catch(ex) { log(ex); } + try { + objs[0] = safe.Request_clone.call(objs[0]); + } + catch(ex) { + safe.uboErr(logPrefix, ex); + } } if ( args[1] instanceof Object ) { objs.push(args[1]); @@ -1217,28 +1341,18 @@ function replaceFetchResponseFn( if ( matchObjectProperties(propNeedles, ...objs) === false ) { outcome = 'nomatch'; } - if ( outcome === logLevel || logLevel === 'all' ) { - log( - `replace-fetch-response (${outcome})`, - `\n\tpropsToMatch: ${JSON.stringify(Array.from(propNeedles)).slice(1,-1)}`, - '\n\tprops:', ...args, - ); - } } if ( outcome === 'nomatch' ) { return fetchPromise; } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched "propsToMatch"\n${propsToMatch}`); + } return fetchPromise.then(responseBefore => { const response = responseBefore.clone(); return response.text().then(textBefore => { const textAfter = textBefore.replace(rePattern, replacement); const outcome = textAfter !== textBefore ? 'match' : 'nomatch'; - if ( outcome === logLevel || logLevel === 'all' ) { - log( - `replace-fetch-response (${outcome})`, - `\n\tpattern: ${pattern}`, - `\n\treplacement: ${replacement}`, - ); - } if ( outcome === 'nomatch' ) { return responseBefore; } + safe.uboLog(logPrefix, 'Replaced'); const responseAfter = new Response(textAfter, { status: responseBefore.status, statusText: responseBefore.statusText, @@ -1252,17 +1366,48 @@ function replaceFetchResponseFn( }); return responseAfter; }).catch(reason => { - log('replace-fetch-response:', reason); + safe.uboErr(logPrefix, reason); return responseBefore; }); }).catch(reason => { - log('replace-fetch-response:', reason); + safe.uboErr(logPrefix, reason); return fetchPromise; }); } }); } +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'proxy-apply.fn', + fn: proxyApplyFn, + dependencies: [ + 'safe-self.fn', + ], +}); +function proxyApplyFn( + target = '', + handler = '' +) { + let context = globalThis; + let prop = target; + for (;;) { + const pos = prop.indexOf('.'); + if ( pos === -1 ) { break; } + context = context[prop.slice(0, pos)]; + if ( context instanceof Object === false ) { return; } + prop = prop.slice(pos+1); + } + const fn = context[prop]; + if ( typeof fn !== 'function' ) { return; } + if ( fn.prototype && fn.prototype.constructor === fn ) { + context[prop] = new Proxy(fn, { construct: handler }); + return (...args) => { return Reflect.construct(...args); }; + } + context[prop] = new Proxy(fn, { apply: handler }); + return (...args) => { return Reflect.apply(...args); }; +} /******************************************************************************* @@ -1303,6 +1448,7 @@ builtinScriptlets.push({ fn: abortOnPropertyRead, dependencies: [ 'get-exception-token.fn', + 'safe-self.fn', ], }); function abortOnPropertyRead( @@ -1310,8 +1456,11 @@ function abortOnPropertyRead( ) { if ( typeof chain !== 'string' ) { return; } if ( chain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('abort-on-property-read', chain); const exceptionToken = getExceptionToken(); const abort = function() { + safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); }; const makeProxy = function(owner, chain) { @@ -1359,6 +1508,7 @@ builtinScriptlets.push({ fn: abortOnPropertyWrite, dependencies: [ 'get-exception-token.fn', + 'safe-self.fn', ], }); function abortOnPropertyWrite( @@ -1366,6 +1516,8 @@ function abortOnPropertyWrite( ) { if ( typeof prop !== 'string' ) { return; } if ( prop === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('abort-on-property-write', prop); const exceptionToken = getExceptionToken(); let owner = window; for (;;) { @@ -1378,6 +1530,7 @@ function abortOnPropertyWrite( delete owner[prop]; Object.defineProperty(owner, prop, { set: function() { + safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); } }); @@ -1462,7 +1615,6 @@ builtinScriptlets.push({ 'run-at.fn', 'safe-self.fn', 'should-debug.fn', - 'should-log.fn', ], }); // https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120 @@ -1472,32 +1624,68 @@ function addEventListenerDefuser( ) { const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern); const reType = safe.patternToRegex(type, undefined, true); const rePattern = safe.patternToRegex(pattern); - const log = shouldLog(extraArgs); const debug = shouldDebug(extraArgs); + const targetSelector = extraArgs.elements || undefined; + const elementMatches = elem => { + if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; } + const elems = Array.from(document.querySelectorAll(targetSelector)); + return elems.includes(elem); + }; + const elementDetails = elem => { + if ( elem instanceof Window ) { return 'window'; } + if ( elem instanceof Document ) { return 'document'; } + if ( elem instanceof Element === false ) { return '?'; } + const parts = []; + if ( elem.id !== '' ) { parts.push(`#${CSS.escape(elem.id)}`); } + for ( let i = 0; i < elem.classList.length; i++ ) { + parts.push(`.${CSS.escape(elem.classList.item(i))}`); + } + for ( let i = 0; i < elem.attributes.length; i++ ) { + const attr = elem.attributes.item(i); + if ( attr.name === 'id' ) { continue; } + if ( attr.name === 'class' ) { continue; } + parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`); + } + return parts.join(''); + }; + const shouldPrevent = (thisArg, type, handler) => { + const matchesType = safe.RegExp_test.call(reType, type); + const matchesHandler = safe.RegExp_test.call(rePattern, handler); + const matchesEither = matchesType || matchesHandler; + const matchesBoth = matchesType && matchesHandler; + if ( debug === 1 && matchesBoth || debug === 2 && matchesEither ) { + debugger; // eslint-disable-line no-debugger + } + if ( matchesBoth && targetSelector !== undefined ) { + if ( elementMatches(thisArg) === false ) { return false; } + } + return matchesBoth; + }; const trapEddEventListeners = ( ) => { const eventListenerHandler = { apply: function(target, thisArg, args) { - let type, handler; + let t, h; try { - type = String(args[0]); - handler = args[1] instanceof Function - ? String(safe.Function_toString(args[1])) - : String(args[1]); + t = String(args[0]); + if ( typeof args[1] === 'function' ) { + h = String(safe.Function_toString(args[1])); + } else if ( typeof args[1] === 'object' && args[1] !== null ) { + if ( typeof args[1].handleEvent === 'function' ) { + h = String(safe.Function_toString(args[1].handleEvent)); + } + } else { + h = String(args[1]); + } } catch(ex) { } - const matchesType = safe.RegExp_test.call(reType, type); - const matchesHandler = safe.RegExp_test.call(rePattern, handler); - const matchesEither = matchesType || matchesHandler; - const matchesBoth = matchesType && matchesHandler; - if ( log === 1 && matchesBoth || log === 2 && matchesEither || log === 3 ) { - safe.uboLog(`addEventListener('${type}', ${handler})`); + if ( type === '' && pattern === '' ) { + safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`); + } else if ( shouldPrevent(thisArg, t, h) ) { + return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`); } - if ( debug === 1 && matchesBoth || debug === 2 && matchesEither ) { - debugger; // jshint ignore:line - } - if ( matchesBoth ) { return; } return Reflect.apply(target, thisArg, args); }, get(target, prop, receiver) { @@ -1533,19 +1721,28 @@ function jsonPrune( stackNeedle = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle); const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true }); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); JSON.parse = new Proxy(JSON.parse, { apply: function(target, thisArg, args) { const objBefore = Reflect.apply(target, thisArg, args); + if ( rawPrunePaths === '' ) { + safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); + } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedleDetails, extraArgs - ); - return objAfter || objBefore; + ); + if ( objAfter === undefined ) { return objBefore; } + safe.uboLog(logPrefix, 'Pruned'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`); + } + return objAfter; }, }); } @@ -1579,7 +1776,6 @@ builtinScriptlets.push({ 'object-prune.fn', 'parse-properties-to-match.fn', 'safe-self.fn', - 'should-log.fn', ], }); function jsonPruneXhrResponse( @@ -1587,10 +1783,9 @@ function jsonPruneXhrResponse( rawNeedlePaths = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths); const xhrInstances = new WeakMap(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); - const logLevel = shouldLog({ log: rawPrunePaths === '' || extraArgs.log, }); - const log = logLevel ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url'); const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); self.XMLHttpRequest = class extends self.XMLHttpRequest { @@ -1602,10 +1797,10 @@ function jsonPruneXhrResponse( outcome = 'nomatch'; } } - if ( outcome === logLevel || outcome === 'all' ) { - log(`xhr.open(${method}, ${url}, ${args.join(', ')})`); - } if ( outcome === 'match' ) { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`); + } xhrInstances.set(this, xhrDetails); } return super.open(method, url, ...args); @@ -1630,8 +1825,10 @@ function jsonPruneXhrResponse( if ( typeof innerResponse === 'object' ) { objBefore = innerResponse; } else if ( typeof innerResponse === 'string' ) { - try { objBefore = safe.JSON_parse(innerResponse); } - catch(ex) { } + try { + objBefore = safe.JSON_parse(innerResponse); + } catch(ex) { + } } if ( typeof objBefore !== 'object' ) { return (xhrDetails.response = innerResponse); @@ -1648,6 +1845,7 @@ function jsonPruneXhrResponse( outerResponse = typeof innerResponse === 'string' ? safe.JSON_stringify(objAfter) : objAfter; + safe.uboLog(logPrefix, 'Pruned'); } else { outerResponse = innerResponse; } @@ -1826,9 +2024,9 @@ function noEvalIf( /******************************************************************************/ builtinScriptlets.push({ - name: 'no-fetch-if.js', + name: 'prevent-fetch.js', aliases: [ - 'prevent-fetch.js', + 'no-fetch-if.js', ], fn: noFetchIf, dependencies: [ @@ -1842,6 +2040,7 @@ function noFetchIf( ) { if ( typeof propsToMatch !== 'string' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-fetch', propsToMatch, responseBody); const needles = []; for ( const condition of propsToMatch.split(/\s+/) ) { if ( condition === '' ) { continue; } @@ -1856,7 +2055,6 @@ function noFetchIf( } needles.push({ key, re: safe.patternToRegex(value) }); } - const log = needles.length === 0 ? console.log.bind(console) : undefined; self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { const details = args[0] instanceof self.Request @@ -1868,17 +2066,16 @@ function noFetchIf( for ( const prop in details ) { let v = details[prop]; if ( typeof v !== 'string' ) { - try { v = JSON.stringify(v); } + try { v = safe.JSON_stringify(v); } catch(ex) { } } if ( typeof v !== 'string' ) { continue; } props.set(prop, v); } - if ( log !== undefined ) { - const out = Array.from(props) - .map(a => `${a[0]}:${a[1]}`) - .join(' '); - log(`uBO: fetch(${out})`); + if ( propsToMatch === '' && responseBody === '' ) { + const out = Array.from(props).map(a => `${a[0]}:${a[1]}`); + safe.uboLog(logPrefix, `Called: ${out.join('\n')}`); + return Reflect.apply(target, thisArg, args); } proceed = needles.length === 0; for ( const { key, re } of needles ) { @@ -1902,21 +2099,23 @@ function noFetchIf( responseType = desURL.origin !== document.location.origin ? 'cors' : 'basic'; - } catch(_) { + } catch(ex) { + safe.uboErr(logPrefix, `Error: ${ex}`); } } return generateContentFn(responseBody).then(text => { + safe.uboLog(logPrefix, `Prevented with response "${text}"`); const response = new Response(text, { statusText: 'OK', headers: { 'Content-Length': text.length, } }); - Object.defineProperty(response, 'url', { + safe.Object_defineProperty(response, 'url', { value: details.url }); if ( responseType !== '' ) { - Object.defineProperty(response, 'type', { + safe.Object_defineProperty(response, 'type', { value: responseType }); } @@ -1937,6 +2136,7 @@ builtinScriptlets.push({ world: 'ISOLATED', dependencies: [ 'run-at.fn', + 'safe-self.fn', ], }); // https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/ @@ -1944,9 +2144,12 @@ function preventRefresh( arg1 = '' ) { if ( typeof arg1 !== 'string' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-refresh', arg1); const defuse = ( ) => { const meta = document.querySelector('meta[http-equiv="refresh" i][content]'); if ( meta === null ) { return; } + safe.uboLog(logPrefix, `Prevented "${meta.textContent}"`); const s = arg1 === '' ? meta.getAttribute('content') : arg1; @@ -2146,13 +2349,13 @@ builtinScriptlets.push({ ], fn: setConstant, dependencies: [ - 'set-constant-core.fn' + 'set-constant.fn' ], }); function setConstant( ...args ) { - setConstantCore(false, ...args); + setConstantFn(false, ...args); } /******************************************************************************/ @@ -2175,6 +2378,7 @@ function noSetIntervalIf( ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-setInterval', needle, delay); const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } if ( delay === '' ) { delay = undefined; } @@ -2184,9 +2388,6 @@ function noSetIntervalIf( if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } - const log = needleNot === false && needle === '' && delay === undefined - ? console.log - : undefined; const reNeedle = safe.patternToRegex(needle); self.setInterval = new Proxy(self.setInterval, { apply: function(target, thisArg, args) { @@ -2194,19 +2395,20 @@ function noSetIntervalIf( ? String(safe.Function_toString(args[0])) : String(args[0]); const b = args[1]; - if ( log !== undefined ) { - log('uBO: setInterval("%s", %s)', a, b); - } else { - let defuse; - if ( needle !== '' ) { - defuse = reNeedle.test(a) !== needleNot; - } - if ( defuse !== false && delay !== undefined ) { - defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; - } - if ( defuse ) { - args[0] = function(){}; - } + if ( needle === '' && delay === undefined ) { + safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); + return Reflect.apply(target, thisArg, args); + } + let defuse; + if ( needle !== '' ) { + defuse = reNeedle.test(a) !== needleNot; + } + if ( defuse !== false && delay !== undefined ) { + defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; + } + if ( defuse ) { + args[0] = function(){}; + safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); } return Reflect.apply(target, thisArg, args); }, @@ -2239,6 +2441,7 @@ function noSetTimeoutIf( ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needle, delay); const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } if ( delay === '' ) { delay = undefined; } @@ -2248,9 +2451,6 @@ function noSetTimeoutIf( if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } - const log = needleNot === false && needle === '' && delay === undefined - ? console.log - : undefined; const reNeedle = safe.patternToRegex(needle); self.setTimeout = new Proxy(self.setTimeout, { apply: function(target, thisArg, args) { @@ -2258,19 +2458,20 @@ function noSetTimeoutIf( ? String(safe.Function_toString(args[0])) : String(args[0]); const b = args[1]; - if ( log !== undefined ) { - log('uBO: setTimeout("%s", %s)', a, b); - } else { - let defuse; - if ( needle !== '' ) { - defuse = reNeedle.test(a) !== needleNot; - } - if ( defuse !== false && delay !== undefined ) { - defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; - } - if ( defuse ) { - args[0] = function(){}; - } + if ( needle === '' && delay === undefined ) { + safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); + return Reflect.apply(target, thisArg, args); + } + let defuse; + if ( needle !== '' ) { + defuse = reNeedle.test(a) !== needleNot; + } + if ( defuse !== false && delay !== undefined ) { + defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; + } + if ( defuse ) { + args[0] = function(){}; + safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); } return Reflect.apply(target, thisArg, args); }, @@ -2371,10 +2572,11 @@ function noXhrIf( directive = '' ) { if ( typeof propsToMatch !== 'string' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-xhr', propsToMatch, directive); const xhrInstances = new WeakMap(); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); - const log = propNeedles.size === 0 ? console.log.bind(console) : undefined; - const warOrigin = scriptletGlobals.get('warOrigin'); + const warOrigin = scriptletGlobals.warOrigin; const headers = { 'date': '', 'content-type': '', @@ -2382,15 +2584,15 @@ function noXhrIf( }; self.XMLHttpRequest = class extends self.XMLHttpRequest { open(method, url, ...args) { - if ( log !== undefined ) { - log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`); - return super.open(method, url, ...args); - } xhrInstances.delete(this); if ( warOrigin !== undefined && url.startsWith(warOrigin) ) { return super.open(method, url, ...args); } const haystack = { method, url }; + if ( propsToMatch === '' && directive === '' ) { + safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`); + return super.open(method, url, ...args); + } if ( matchObjectProperties(propNeedles, haystack) ) { xhrInstances.set(this, haystack); } @@ -2468,6 +2670,7 @@ function noXhrIf( details.xhr.dispatchEvent(new Event('readystatechange')); details.xhr.dispatchEvent(new Event('load')); details.xhr.dispatchEvent(new Event('loadend')); + safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`); }); } getResponseHeader(headerName) { @@ -2507,7 +2710,6 @@ builtinScriptlets.push({ fn: noWindowOpenIf, dependencies: [ 'safe-self.fn', - 'should-log.fn', ], }); function noWindowOpenIf( @@ -2516,6 +2718,7 @@ function noWindowOpenIf( decoy = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('no-window-open-if', pattern, delay, decoy); const targetMatchResult = pattern.startsWith('!') === false; if ( targetMatchResult === false ) { pattern = pattern.slice(1); @@ -2525,8 +2728,6 @@ function noWindowOpenIf( if ( isNaN(autoRemoveAfter) ) { autoRemoveAfter = -1; } - const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const logLevel = shouldLog(extraArgs); const createDecoy = function(tag, urlProp, url) { const decoyElem = document.createElement(tag); decoyElem[urlProp] = url; @@ -2541,12 +2742,13 @@ function noWindowOpenIf( window.open = new Proxy(window.open, { apply: function(target, thisArg, args) { const haystack = args.join(' '); - if ( logLevel ) { - safe.uboLog('window.open:', haystack); - } if ( rePattern.test(haystack) !== targetMatchResult ) { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Allowed (${args.join(', ')})`); + } return Reflect.apply(target, thisArg, args); } + safe.uboLog(logPrefix, `Prevented (${args.join(', ')})`); if ( autoRemoveAfter < 0 ) { return null; } const decoyElem = decoy === 'obj' ? createDecoy('object', 'data', ...args) @@ -2568,14 +2770,14 @@ function noWindowOpenIf( }, }); } - if ( logLevel ) { + if ( safe.logLevel !== 0 ) { popup = new Proxy(popup, { get: function(target, prop) { - safe.uboLog('window.open / get', prop, '===', target[prop]); + safe.uboLog(logPrefix, 'window.open / get', prop, '===', target[prop]); return Reflect.get(...arguments); }, set: function(target, prop, value) { - safe.uboLog('window.open / set', prop, '=', value); + safe.uboLog(logPrefix, 'window.open / set', prop, '=', value); return Reflect.set(...arguments); }, }); @@ -2758,31 +2960,6 @@ function noWebrtc() { /******************************************************************************/ 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); - let haystack; - try { - haystack = b.toString(); - } catch(ex) { - } - if ( - typeof haystack === 'string' && - /^\s*function\s*\(\)\s*\{\s*window\.clearTimeout\(r\)\s*\}\s*$/.test(haystack) - ) { - b(); - } - }.bind(window); -} - -/******************************************************************************/ - -builtinScriptlets.push({ name: 'disable-newtab-links.js', fn: disableNewtabLinks, }); @@ -2884,7 +3061,6 @@ builtinScriptlets.push({ fn: xmlPrune, dependencies: [ 'safe-self.fn', - 'should-log.fn', ], }); function xmlPrune( @@ -2895,9 +3071,9 @@ function xmlPrune( if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('xml-prune', selector, selectorCheck, urlPattern); const reUrl = safe.patternToRegex(urlPattern); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const log = shouldLog(extraArgs) ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); const queryAll = (xmlDoc, selector) => { const isXpath = /^xpath\(.+\)$/.test(selector); if ( isXpath === false ) { @@ -2924,21 +3100,21 @@ function xmlPrune( } if ( extraArgs.logdoc ) { const serializer = new XMLSerializer(); - log(`xmlPrune: document is\n\t${serializer.serializeToString(xmlDoc)}`); + safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`); } const items = queryAll(xmlDoc, selector); if ( items.length === 0 ) { return xmlDoc; } - log(`xmlPrune: removing ${items.length} items`); + safe.uboLog(logPrefix, `Removing ${items.length} items`); for ( const item of items ) { if ( item.nodeType === 1 ) { item.remove(); } else if ( item.nodeType === 2 ) { item.ownerElement.removeAttribute(item.nodeName); } - log(`xmlPrune: ${item.constructor.name}.${item.nodeName} removed`); + safe.uboLog(logPrefix, `${item.constructor.name}.${item.nodeName} removed`); } } catch(ex) { - log(ex); + safe.uboErr(logPrefix, `Error: ${ex}`); } return xmlDoc; }; @@ -3030,7 +3206,6 @@ builtinScriptlets.push({ fn: m3uPrune, dependencies: [ 'safe-self.fn', - 'should-log.fn', ], }); // https://en.wikipedia.org/wiki/M3U @@ -3040,9 +3215,8 @@ function m3uPrune( ) { if ( typeof m3uPattern !== 'string' ) { return; } const safe = safeSelf(); - const options = safe.getExtraArgs(Array.from(arguments), 2); - const logLevel = shouldLog(options); - const uboLog = logLevel ? ((...args) => safe.uboLog(...args)) : (( ) => { }); + const logPrefix = safe.makeLogPrefix('m3u-prune', m3uPattern, urlPattern); + const toLog = []; const regexFromArg = arg => { if ( arg === '' ) { return /^/; } const match = /^\/(.+)\/([gms]*)$/.exec(arg); @@ -3061,22 +3235,22 @@ function m3uPrune( if ( lines[i].startsWith('#EXT-X-CUE:TYPE="SpliceOut"') === false ) { return false; } - uboLog('m3u-prune: discarding', `\n\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; if ( lines[i].startsWith('#EXT-X-ASSET:CAID') ) { - uboLog(`\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-SCTE35:') ) { - uboLog(`\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-CUE-IN') ) { - uboLog(`\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-SCTE35:') ) { - uboLog(`\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } return true; @@ -3084,10 +3258,10 @@ function m3uPrune( const pruneInfBlock = (lines, i) => { if ( lines[i].startsWith('#EXTINF') === false ) { return false; } if ( reM3u.test(lines[i+1]) === false ) { return false; } - uboLog('m3u-prune: discarding', `\n\t${lines[i]}, \n\t${lines[i+1]}`); + toLog.push('Discarding', `\t${lines[i]}, \t${lines[i+1]}`); lines[i] = lines[i+1] = undefined; i += 2; if ( lines[i].startsWith('#EXT-X-DISCONTINUITY') ) { - uboLog(`\t${lines[i]}`); + toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } return true; @@ -3124,9 +3298,7 @@ function m3uPrune( } text = before.trim() + '\n' + after.trim(); reM3u.lastIndex = before.length + 1; - uboLog('m3u-prune: discarding\n', - discard.split(/\n+/).map(s => `\t${s}`).join('\n') - ); + toLog.push('Discarding', ...discard.split(/\n+/).map(s => `\t${s}`)); if ( reM3u.global === false ) { break; } } return text; @@ -3151,13 +3323,18 @@ function m3uPrune( return Reflect.apply(target, thisArg, args); } return realFetch(...args).then(realResponse => - realResponse.text().then(text => - new Response(pruner(text), { + realResponse.text().then(text => { + const response = new Response(pruner(text), { status: realResponse.status, statusText: realResponse.statusText, headers: realResponse.headers, - }) - ) + }); + if ( toLog.length !== 0 ) { + toLog.unshift(logPrefix); + safe.uboLog(toLog.join('\n')); + } + return response; + }) ); } }); @@ -3175,6 +3352,10 @@ function m3uPrune( if ( textout === textin ) { return; } Object.defineProperty(thisArg, 'response', { value: textout }); Object.defineProperty(thisArg, 'responseText', { value: textout }); + if ( toLog.length !== 0 ) { + toLog.unshift(logPrefix); + safe.uboLog(toLog.join('\n')); + } }); return Reflect.apply(target, thisArg, args); } @@ -3220,6 +3401,7 @@ builtinScriptlets.push({ world: 'ISOLATED', dependencies: [ 'run-at.fn', + 'safe-self.fn', ], }); function hrefSanitizer( @@ -3228,6 +3410,8 @@ function hrefSanitizer( ) { if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source); if ( source === '' ) { source = 'text'; } const sanitizeCopycats = (href, text) => { let elems = []; @@ -3239,6 +3423,7 @@ function hrefSanitizer( for ( const elem of elems ) { elem.setAttribute('href', text); } + return elems.length; }; const validateURL = text => { if ( text === '' ) { return ''; } @@ -3266,7 +3451,7 @@ function hrefSanitizer( return elem.textContent .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters - ; + ; } return ''; }; @@ -3287,7 +3472,8 @@ function hrefSanitizer( if ( hrefAfter === '' ) { continue; } if ( hrefAfter === href ) { continue; } elem.setAttribute('href', hrefAfter); - sanitizeCopycats(href, hrefAfter); + const count = sanitizeCopycats(href, hrefAfter); + safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`); } return true; }; @@ -3305,7 +3491,7 @@ function hrefSanitizer( if ( shouldSanitize ) { break; } } if ( shouldSanitize === false ) { return; } - timer = self.requestAnimationFrame(( ) => { + timer = self.requestIdleCallback(( ) => { timer = undefined; sanitize(); }); @@ -3402,21 +3588,28 @@ function spoofCSS( propToValueMap.set(toCamelCase(args[i+0]), args[i+1]); } const safe = safeSelf(); - const canDebug = scriptletGlobals.has('canDebug'); + const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args); + const canDebug = scriptletGlobals.canDebug; const shouldDebug = canDebug && propToValueMap.get('debug') || 0; - const shouldLog = canDebug && propToValueMap.has('log') || 0; + const instanceProperties = [ 'cssText', 'length', 'parentRule' ]; const spoofStyle = (prop, real) => { const normalProp = toCamelCase(prop); const shouldSpoof = propToValueMap.has(normalProp); const value = shouldSpoof ? propToValueMap.get(normalProp) : real; - if ( shouldLog === 2 || shouldSpoof && shouldLog === 1 ) { - safe.uboLog(prop, value); + if ( shouldSpoof ) { + safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`); } return value; }; + const cloackFunc = (fn, thisArg, name) => { + const trap = fn.bind(thisArg); + Object.defineProperty(trap, 'name', { value: name }); + return trap; + }; self.getComputedStyle = new Proxy(self.getComputedStyle, { apply: function(target, thisArg, args) { - if ( shouldDebug !== 0 ) { debugger; } // jshint ignore: line + // eslint-disable-next-line no-debugger + if ( shouldDebug !== 0 ) { debugger; } const style = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(args[0]) === false ) { return style; } @@ -3424,11 +3617,14 @@ function spoofCSS( get(target, prop, receiver) { if ( typeof target[prop] === 'function' ) { if ( prop === 'getPropertyValue' ) { - return (function(prop) { + return cloackFunc(function(prop) { return spoofStyle(prop, target[prop]); - }).bind(target); + }, target, 'getPropertyValue'); } - return target[prop].bind(target); + return cloackFunc(target[prop], target, prop); + } + if ( instanceProperties.includes(prop) ) { + return Reflect.get(target, prop); } return spoofStyle(prop, Reflect.get(target, prop, receiver)); }, @@ -3455,7 +3651,8 @@ function spoofCSS( }); Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, { apply: function(target, thisArg, args) { - if ( shouldDebug !== 0 ) { debugger; } // jshint ignore: line + // eslint-disable-next-line no-debugger + if ( shouldDebug !== 0 ) { debugger; } const rect = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(thisArg) === false ) { return rect; } @@ -3524,7 +3721,8 @@ function setCookie( path = '' ) { if ( name === '' ) { return; } - name = encodeURIComponent(name); + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const validValues = [ 'accept', 'reject', @@ -3545,17 +3743,21 @@ function setCookie( if ( validValues.includes(unquoted) === false ) { if ( /^\d+$/.test(unquoted) === false ) { return; } const n = parseInt(value, 10); - if ( n > 15 ) { return; } + if ( n > 32767 ) { return; } } - setCookieFn( + const done = setCookieFn( false, name, value, '', path, - safeSelf().getExtraArgs(Array.from(arguments), 3) + safe.getExtraArgs(Array.from(arguments), 3) ); + + if ( done ) { + safe.uboLog(logPrefix, 'Done'); + } } // For compatibility with AdGuard @@ -3641,6 +3843,7 @@ builtinScriptlets.push({ world: 'ISOLATED', dependencies: [ 'run-at.fn', + 'safe-self.fn', ], }); function setAttr( @@ -3648,9 +3851,11 @@ function setAttr( attr = '', value = '' ) { - if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } + if ( attr === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-attr', attr, value); const validValues = [ '', 'false', 'true' ]; let copyFrom = ''; @@ -3685,7 +3890,11 @@ function setAttr( const before = elem.getAttribute(attr); const after = extractValue(elem); if ( after === before ) { continue; } + if ( after !== '' && /^on/i.test(attr) ) { + if ( attr.toLowerCase() in elem ) { continue; } + } elem.setAttribute(attr, after); + safe.uboLog(logPrefix, `${attr}="${after}"`); } return true; }; @@ -3806,6 +4015,58 @@ function multiup() { document.addEventListener('click', handler, { capture: true }); } +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'remove-cache-storage-item.js', + fn: removeCacheStorageItem, + world: 'ISOLATED', + dependencies: [ + 'safe-self.fn', + ], +}); +function removeCacheStorageItem( + cacheNamePattern = '', + requestPattern = '' +) { + if ( cacheNamePattern === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('remove-cache-storage-item', cacheNamePattern, requestPattern); + const cacheStorage = self.caches; + if ( cacheStorage instanceof Object === false ) { return; } + const reCache = safe.patternToRegex(cacheNamePattern, undefined, true); + const reRequest = safe.patternToRegex(requestPattern, undefined, true); + cacheStorage.keys().then(cacheNames => { + for ( const cacheName of cacheNames ) { + if ( reCache.test(cacheName) === false ) { continue; } + if ( requestPattern === '' ) { + cacheStorage.delete(cacheName).then(result => { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Deleting ${cacheName}`); + } + if ( result !== true ) { return; } + safe.uboLog(logPrefix, `Deleted ${cacheName}: ${result}`); + }); + continue; + } + cacheStorage.open(cacheName).then(cache => { + cache.keys().then(requests => { + for ( const request of requests ) { + if ( reRequest.test(request.url) === false ) { continue; } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Deleting ${cacheName}/${request.url}`); + } + cache.delete(request).then(result => { + if ( result !== true ) { return; } + safe.uboLog(logPrefix, `Deleted ${cacheName}/${request.url}: ${result}`); + }); + } + }); + }); + } + }); +} + /******************************************************************************* * @@ -3883,13 +4144,13 @@ builtinScriptlets.push({ ], fn: trustedSetConstant, dependencies: [ - 'set-constant-core.fn' + 'set-constant.fn' ], }); function trustedSetConstant( ...args ) { - setConstantCore(true, ...args); + setConstantFn(true, ...args); } /******************************************************************************* @@ -3921,6 +4182,8 @@ function trustedSetCookie( ) { if ( name === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const time = new Date(); if ( value === '$now$' ) { @@ -3942,7 +4205,7 @@ function trustedSetCookie( expires = time.toUTCString(); } - setCookieFn( + const done = setCookieFn( true, name, value, @@ -3950,6 +4213,10 @@ function trustedSetCookie( path, safeSelf().getExtraArgs(Array.from(arguments), 4) ); + + if ( done ) { + safe.uboLog(logPrefix, 'Done'); + } } // For compatibility with AdGuard @@ -4018,6 +4285,9 @@ function trustedSetSessionStorageItem(key = '', value = '') { builtinScriptlets.push({ name: 'trusted-replace-fetch-response.js', requiresTrust: true, + aliases: [ + 'trusted-rpfr.js', + ], fn: trustedReplaceFetchResponse, dependencies: [ 'replace-fetch-response.fn', @@ -4037,7 +4307,6 @@ builtinScriptlets.push({ 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', - 'should-log.fn', ], }); function trustedReplaceXhrResponse( @@ -4046,12 +4315,8 @@ function trustedReplaceXhrResponse( propsToMatch = '' ) { const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-replace-xhr-response', pattern, replacement, propsToMatch); const xhrInstances = new WeakMap(); - const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const logLevel = shouldLog({ - log: pattern === '' && 'all' || extraArgs.log, - }); - const log = logLevel ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); if ( pattern === '*' ) { pattern = '.*'; } const rePattern = safe.patternToRegex(pattern); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); @@ -4065,10 +4330,10 @@ function trustedReplaceXhrResponse( outcome = 'nomatch'; } } - if ( outcome === logLevel || outcome === 'all' ) { - log(`xhr.open(${method}, ${url}, ${args.join(', ')})`); - } if ( outcome === 'match' ) { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched "propsToMatch"`); + } xhrInstances.set(outerXhr, xhrDetails); } return super.open(method, url, ...args); @@ -4094,13 +4359,8 @@ function trustedReplaceXhrResponse( } const textBefore = innerResponse; const textAfter = textBefore.replace(rePattern, replacement); - const outcome = textAfter !== textBefore ? 'match' : 'nomatch'; - if ( outcome === logLevel || logLevel === 'all' ) { - log( - `trusted-replace-xhr-response (${outcome})`, - `\n\tpattern: ${pattern}`, - `\n\treplacement: ${replacement}`, - ); + if ( textAfter !== textBefore ) { + safe.uboLog(logPrefix, 'Match'); } return (xhrDetails.response = textAfter); } @@ -4129,22 +4389,63 @@ builtinScriptlets.push({ fn: trustedClickElement, world: 'ISOLATED', dependencies: [ + 'get-all-cookies.fn', + 'get-all-local-storage.fn', 'run-at-html-element.fn', 'safe-self.fn', ], }); function trustedClickElement( selectors = '', - extraMatch = '', // not yet supported + extraMatch = '', delay = '' ) { - if ( extraMatch !== '' ) { return; } - const safe = safeSelf(); - const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const uboLog = extraArgs.log !== undefined - ? ((...args) => { safe.uboLog(...args); }) - : (( ) => { }); + const logPrefix = safe.makeLogPrefix('trusted-click-element', selectors, extraMatch, delay); + + if ( extraMatch !== '' ) { + const assertions = extraMatch.split(',').map(s => { + const pos1 = s.indexOf(':'); + const s1 = pos1 !== -1 ? s.slice(0, pos1) : s; + const not = s1.startsWith('!'); + const type = not ? s1.slice(1) : s1; + const s2 = pos1 !== -1 ? s.slice(pos1+1).trim() : ''; + if ( s2 === '' ) { return; } + const out = { not, type }; + const match = /^\/(.+)\/(i?)$/.exec(s2); + if ( match !== null ) { + out.re = new RegExp(match[1], match[2] || undefined); + return out; + } + const pos2 = s2.indexOf('='); + const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2; + const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : ''; + out.re = new RegExp(`^${this.escapeRegexChars(key)}=${this.escapeRegexChars(value)}`); + return out; + }).filter(details => details !== undefined); + const allCookies = assertions.some(o => o.type === 'cookie') + ? getAllCookiesFn() + : []; + const allStorageItems = assertions.some(o => o.type === 'localStorage') + ? getAllLocalStorageFn() + : []; + const hasNeedle = (haystack, needle) => { + for ( const { key, value } of haystack ) { + if ( needle.test(`${key}=${value}`) ) { return true; } + } + return false; + }; + for ( const { not, type, re } of assertions ) { + switch ( type ) { + case 'cookie': + if ( hasNeedle(allCookies, re) === not ) { return; } + break; + case 'localStorage': + if ( hasNeedle(allStorageItems, re) === not ) { return; } + break; + } + } + } const querySelectorEx = (selector, context = document) => { const pos = selector.indexOf(' >>> '); @@ -4181,12 +4482,12 @@ function trustedClickElement( const next = notFound => { if ( selectorList.length === 0 ) { - uboLog(`trusted-click-element: Completed`); + safe.uboLog(logPrefix, 'Completed'); return terminate(); } const tnow = Date.now(); if ( tnow >= tbye ) { - uboLog(`trusted-click-element: Timed out`); + safe.uboLog(logPrefix, 'Timed out'); return terminate(); } if ( notFound ) { observe(); } @@ -4195,7 +4496,7 @@ function trustedClickElement( next.timer = undefined; process(); }, delay); - uboLog(`trusted-click-element: Waiting for ${selectorList[0]}...`); + safe.uboLog(logPrefix, `Waiting for ${selectorList[0]}...`); }; next.stop = ( ) => { if ( next.timer === undefined ) { return; } @@ -4239,7 +4540,7 @@ function trustedClickElement( selectorList.unshift(selector); return next(true); } - uboLog(`trusted-click-element: Clicked ${selector}`); + safe.uboLog(logPrefix, `Clicked ${selector}`); elem.click(); tnext += clickDelay; next(); @@ -4336,40 +4637,67 @@ builtinScriptlets.push({ fn: trustedPruneOutboundObject, dependencies: [ 'object-prune.fn', + 'proxy-apply.fn', 'safe-self.fn', ], }); function trustedPruneOutboundObject( - entryPoint = '', + propChain = '', rawPrunePaths = '', rawNeedlePaths = '' ) { - if ( entryPoint === '' ) { return; } - let context = globalThis; - let prop = entryPoint; - for (;;) { - const pos = prop.indexOf('.'); - if ( pos === -1 ) { break; } - context = context[prop.slice(0, pos)]; - if ( context instanceof Object === false ) { return; } - prop = prop.slice(pos+1); - } - if ( typeof context[prop] !== 'function' ) { return; } + if ( propChain === '' ) { return; } const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - context[prop] = new Proxy(context[prop], { - apply: function(target, thisArg, args) { - const objBefore = Reflect.apply(target, thisArg, args); - if ( objBefore instanceof Object === false ) { return objBefore; } - const objAfter = objectPruneFn( - objBefore, - rawPrunePaths, - rawNeedlePaths, - { matchAll: true }, - extraArgs - ); - return objAfter || objBefore; - }, + const reflector = proxyApplyFn(propChain, function(...args) { + const objBefore = reflector(...args); + if ( objBefore instanceof Object === false ) { return objBefore; } + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + { matchAll: true }, + extraArgs + ); + return objAfter || objBefore; + }); +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'trusted-replace-argument.js', + requiresTrust: true, + fn: trustedReplaceArgument, + dependencies: [ + 'proxy-apply.fn', + 'safe-self.fn', + 'validate-constant.fn', + ], +}); +function trustedReplaceArgument( + propChain = '', + argpos = '', + argraw = '' +) { + if ( propChain === '' ) { return; } + if ( argpos === '' ) { return; } + if ( argraw === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argpos, argraw); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + const normalValue = validateConstantFn(true, argraw); + const reCondition = extraArgs.condition + ? safe.patternToRegex(extraArgs.condition) + : /^/; + const reflector = proxyApplyFn(propChain, function(...args) { + const arglist = args[args.length-1]; + if ( Array.isArray(arglist) === false ) { return reflector(...args); } + const argBefore = arglist[argpos]; + if ( reCondition.test(argBefore) === false ) { return reflector(...args); } + arglist[argpos] = normalValue; + safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${normalValue}`); + return reflector(...args); }); } |