From e1500ee88d2524da0c93e85b8855d0671a3c6cdb Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 11 Apr 2023 21:45:40 -0400 Subject: [PATCH] Add ability to defer set-constant execution A new optional parameter has been added to `set-constant` scriptlet: `runAt`, default to `0`. ..##+js(set, document.body.oncontextmenu, null, 2) When the `runAt` parameter is present, uBO will take it into account to possibly defer execution of `set-constant`: - `runAt` not present: execute immediately - `runAt` = 1: execute immediately - `runAt` = 2: execute when document state is "interactive" - `runAt` = 3: execute when document state is `"complete" --- assets/resources/scriptlets.js | 297 ++++++++++++++++++--------------- 1 file changed, 160 insertions(+), 137 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 88e119c25..eea4f6448 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -982,162 +982,185 @@ builtinScriptlets.push({ fn: setConstant, }); function setConstant( - chain = '', - cValue = '' + arg1 = '', + arg2 = '', + arg3 = 0 ) { + const details = typeof arg1 !== 'object' + ? { prop: arg1, value: arg2, runAt: parseInt(arg3, 10) || 0 } + : arg1; + const { prop: chain = '', value: cValue = '' } = details; if ( typeof chain !== 'string' ) { return; } if ( chain === '' ) { return; } - const trappedProp = (( ) => { - const pos = chain.lastIndexOf('.'); - if ( pos === -1 ) { return chain; } - return chain.slice(pos+1); - })(); - if ( trappedProp === '' ) { return; } - const thisScript = document.currentScript; - const objectDefineProperty = Object.defineProperty.bind(Object); - const cloakFunc = fn => { - objectDefineProperty(fn, 'name', { value: trappedProp }); - const proxy = new Proxy(fn, { - defineProperty(target, prop) { - if ( prop !== 'toString' ) { - return Reflect.deleteProperty(...arguments); - } - return true; - }, - deleteProperty(target, prop) { - if ( prop !== 'toString' ) { - return Reflect.deleteProperty(...arguments); - } - return true; - }, - get(target, prop) { - if ( prop === 'toString' ) { - return function() { - return `function ${trappedProp}() { [native code] }`; - }.bind(null); - } - 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 = ''; - } else if ( cValue === '[]' ) { - cValue = []; - } else if ( cValue === '{}' ) { - 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 = parseFloat(cValue); - if ( isNaN(cValue) ) { return; } - if ( Math.abs(cValue) > 0x7FFF ) { return; } - } else { - return; - } - let aborted = false; - const mustAbort = function(v) { - if ( aborted ) { return true; } - aborted = - (v !== undefined && v !== null) && - (cValue !== undefined && cValue !== null) && - (typeof v !== typeof cValue); - 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(owner[prop]) === false ) { return; } - const odesc = Object.getOwnPropertyDescriptor(owner, prop); - let prevGetter, prevSetter; - if ( odesc instanceof Object ) { - owner[prop] = cValue; - if ( odesc.get instanceof Function ) { - prevGetter = odesc.get; - } - if ( odesc.set instanceof Function ) { - prevSetter = odesc.set; - } - } - try { - objectDefineProperty(owner, prop, { - configurable, - get() { - if ( prevGetter !== undefined ) { - prevGetter(); + function setConstant(chain, cValue) { + const trappedProp = (( ) => { + const pos = chain.lastIndexOf('.'); + if ( pos === -1 ) { return chain; } + return chain.slice(pos+1); + })(); + if ( trappedProp === '' ) { return; } + const thisScript = document.currentScript; + const objectDefineProperty = Object.defineProperty.bind(Object); + const cloakFunc = fn => { + objectDefineProperty(fn, 'name', { value: trappedProp }); + const proxy = new Proxy(fn, { + defineProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.deleteProperty(...arguments); } - return handler.getter(); // cValue + return true; }, - set(a) { - if ( prevSetter !== undefined ) { - prevSetter(a); + deleteProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.deleteProperty(...arguments); } - handler.setter(a); - } + return true; + }, + get(target, prop) { + if ( prop === 'toString' ) { + return function() { + return `function ${trappedProp}() { [native code] }`; + }.bind(null); + } + return Reflect.get(...arguments); + }, }); - } catch(ex) { + 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 = ''; + } else if ( cValue === '[]' ) { + cValue = []; + } else if ( cValue === '{}' ) { + 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 = parseFloat(cValue); + if ( isNaN(cValue) ) { return; } + if ( Math.abs(cValue) > 0x7FFF ) { return; } + } else { + return; } - }; - const trapChain = function(owner, chain) { - const pos = chain.indexOf('.'); - if ( pos === -1 ) { - trapProp(owner, chain, false, { + let aborted = false; + const mustAbort = function(v) { + if ( aborted ) { return true; } + aborted = + (v !== undefined && v !== null) && + (cValue !== undefined && cValue !== null) && + (typeof v !== typeof cValue); + 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); + let prevGetter, prevSetter; + if ( odesc instanceof Object ) { + owner[prop] = cValue; + if ( odesc.get instanceof Function ) { + prevGetter = odesc.get; + } + if ( odesc.set instanceof Function ) { + prevSetter = odesc.set; + } + } + try { + objectDefineProperty(owner, prop, { + configurable, + get() { + if ( prevGetter !== undefined ) { + prevGetter(); + } + return handler.getter(); // cValue + }, + set(a) { + if ( prevSetter !== undefined ) { + prevSetter(a); + } + handler.setter(a); + } + }); + } catch(ex) { + } + }; + const trapChain = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + trapProp(owner, chain, false, { + v: undefined, + init: function(v) { + if ( mustAbort(v) ) { return false; } + this.v = v; + return true; + }, + getter: function() { + return document.currentScript === thisScript + ? this.v + : cValue; + }, + setter: function(a) { + if ( mustAbort(a) === false ) { return; } + cValue = a; + } + }); + return; + } + const prop = chain.slice(0, pos); + const v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v instanceof Object || typeof v === 'object' && v !== null ) { + trapChain(v, chain); + return; + } + trapProp(owner, prop, true, { v: undefined, init: function(v) { - if ( mustAbort(v) ) { return false; } this.v = v; return true; }, getter: function() { - return document.currentScript === thisScript - ? this.v - : cValue; + return this.v; }, setter: function(a) { - if ( mustAbort(a) === false ) { return; } - cValue = a; + this.v = a; + if ( a instanceof Object ) { + trapChain(a, chain); + } } }); - return; - } - const prop = chain.slice(0, pos); - const v = owner[prop]; - chain = chain.slice(pos + 1); - if ( v instanceof Object || typeof v === 'object' && v !== null ) { - trapChain(v, chain); - return; - } - trapProp(owner, prop, true, { - v: undefined, - init: function(v) { - this.v = v; - return true; - }, - getter: function() { - return this.v; - }, - setter: function(a) { - this.v = a; - if ( a instanceof Object ) { - trapChain(a, chain); - } - } - }); + }; + trapChain(window, chain); + } + const runAt = details.runAt; + if ( runAt === 0 ) { + setConstant(chain, cValue); return; + } + const docReadyState = ( ) => { + return ({ loading: 1, interactive: 2, complete: 3, })[document.readyState] || 0; }; - trapChain(window, chain); + if ( docReadyState() >= runAt ) { + setConstant(chain, cValue); return; + } + const onReadyStateChange = ( ) => { + if ( docReadyState() < runAt ) { return; } + setConstant(chain, cValue); + document.removeEventListener('readystatechange', onReadyStateChange); + }; + document.addEventListener('readystatechange', onReadyStateChange); } /******************************************************************************/