From eb2d6d1374301c449b2a358adcfc22d4b4af7bbe Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 19 May 2023 15:35:38 -0400 Subject: [PATCH] Bring scriptlets up to date --- .../no-addeventlistener-if.entity.js | 84 ++++- .../mv3/scriptlets/no-addeventlistener-if.js | 84 ++++- platform/mv3/scriptlets/no-setinterval-if.js | 8 +- .../mv3/scriptlets/no-settimeout-if.entity.js | 8 +- platform/mv3/scriptlets/no-settimeout-if.js | 8 +- .../mv3/scriptlets/set-constant.entity.js | 351 +++++++++++------- platform/mv3/scriptlets/set-constant.js | 351 +++++++++++------- 7 files changed, 599 insertions(+), 295 deletions(-) diff --git a/platform/mv3/scriptlets/no-addeventlistener-if.entity.js b/platform/mv3/scriptlets/no-addeventlistener-if.entity.js index e9f0f2e6e..dac815011 100644 --- a/platform/mv3/scriptlets/no-addeventlistener-if.entity.js +++ b/platform/mv3/scriptlets/no-addeventlistener-if.entity.js @@ -56,15 +56,70 @@ const regexpFromArg = arg => { /******************************************************************************/ +// Dependencies + +const scriptletGlobals = new Map(); + +function safeSelf() { + if ( scriptletGlobals.has('safeSelf') ) { + return scriptletGlobals.get('safeSelf'); + } + const safe = { + 'RegExp': self.RegExp, + 'RegExp_test': self.RegExp.prototype.test, + 'RegExp_exec': self.RegExp.prototype.exec, + 'addEventListener': self.EventTarget.prototype.addEventListener, + 'removeEventListener': self.EventTarget.prototype.removeEventListener, + }; + scriptletGlobals.set('safeSelf', safe); + return safe; +} + +function runAt(fn, when) { + const intFromReadyState = state => { + const targets = { + 'loading': 1, + 'interactive': 2, 'end': 2, '2': 2, + 'complete': 3, 'idle': 3, '3': 3, + }; + const tokens = Array.isArray(state) ? state : [ state ]; + for ( const token of tokens ) { + const prop = `${token}`; + if ( targets.hasOwnProperty(prop) === false ) { continue; } + return targets[prop]; + } + return 0; + }; + const runAt = intFromReadyState(when); + if ( intFromReadyState(document.readyState) >= runAt ) { + fn(); return; + } + const onStateChange = ( ) => { + if ( intFromReadyState(document.readyState) < runAt ) { return; } + fn(); + safe.removeEventListener.apply(document, args); + }; + const safe = safeSelf(); + const args = [ 'readystatechange', onStateChange, { capture: true } ]; + safe.addEventListener.apply(document, args); +} + +/******************************************************************************/ + const scriptlet = ( - needle1 = '', - needle2 = '' + arg1 = '', + arg2 = '' ) => { - const reNeedle1 = regexpFromArg(needle1); - const reNeedle2 = regexpFromArg(needle2); - self.EventTarget.prototype.addEventListener = new Proxy( - self.EventTarget.prototype.addEventListener, - { + const details = typeof arg1 !== 'object' + ? { type: arg1, pattern: arg2 } + : arg1; + const { type = '', pattern = '' } = details; + if ( typeof type !== 'string' ) { return; } + if ( typeof pattern !== 'string' ) { return; } + const reType = regexpFromArg(type); + const rePattern = regexpFromArg(pattern); + const trapEddEventListeners = ( ) => { + const eventListenerHandler = { apply: function(target, thisArg, args) { let type, handler; try { @@ -73,14 +128,21 @@ const scriptlet = ( } catch(ex) { } if ( - reNeedle1.test(type) === false || - reNeedle2.test(handler) === false + reType.test(type) === false || + rePattern.test(handler) === false ) { return target.apply(thisArg, args); } } - } - ); + }; + self.EventTarget.prototype.addEventListener = new Proxy( + self.EventTarget.prototype.addEventListener, + eventListenerHandler + ); + }; + runAt(( ) => { + trapEddEventListeners(); + }, details.runAt); }; /******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-addeventlistener-if.js b/platform/mv3/scriptlets/no-addeventlistener-if.js index e8da01169..7ec69df79 100644 --- a/platform/mv3/scriptlets/no-addeventlistener-if.js +++ b/platform/mv3/scriptlets/no-addeventlistener-if.js @@ -57,15 +57,70 @@ const regexpFromArg = arg => { /******************************************************************************/ +// Dependencies + +const scriptletGlobals = new Map(); + +function safeSelf() { + if ( scriptletGlobals.has('safeSelf') ) { + return scriptletGlobals.get('safeSelf'); + } + const safe = { + 'RegExp': self.RegExp, + 'RegExp_test': self.RegExp.prototype.test, + 'RegExp_exec': self.RegExp.prototype.exec, + 'addEventListener': self.EventTarget.prototype.addEventListener, + 'removeEventListener': self.EventTarget.prototype.removeEventListener, + }; + scriptletGlobals.set('safeSelf', safe); + return safe; +} + +function runAt(fn, when) { + const intFromReadyState = state => { + const targets = { + 'loading': 1, + 'interactive': 2, 'end': 2, '2': 2, + 'complete': 3, 'idle': 3, '3': 3, + }; + const tokens = Array.isArray(state) ? state : [ state ]; + for ( const token of tokens ) { + const prop = `${token}`; + if ( targets.hasOwnProperty(prop) === false ) { continue; } + return targets[prop]; + } + return 0; + }; + const runAt = intFromReadyState(when); + if ( intFromReadyState(document.readyState) >= runAt ) { + fn(); return; + } + const onStateChange = ( ) => { + if ( intFromReadyState(document.readyState) < runAt ) { return; } + fn(); + safe.removeEventListener.apply(document, args); + }; + const safe = safeSelf(); + const args = [ 'readystatechange', onStateChange, { capture: true } ]; + safe.addEventListener.apply(document, args); +} + +/******************************************************************************/ + const scriptlet = ( - needle1 = '', - needle2 = '' + arg1 = '', + arg2 = '' ) => { - const reNeedle1 = regexpFromArg(needle1); - const reNeedle2 = regexpFromArg(needle2); - self.EventTarget.prototype.addEventListener = new Proxy( - self.EventTarget.prototype.addEventListener, - { + const details = typeof arg1 !== 'object' + ? { type: arg1, pattern: arg2 } + : arg1; + const { type = '', pattern = '' } = details; + if ( typeof type !== 'string' ) { return; } + if ( typeof pattern !== 'string' ) { return; } + const reType = regexpFromArg(type); + const rePattern = regexpFromArg(pattern); + const trapEddEventListeners = ( ) => { + const eventListenerHandler = { apply: function(target, thisArg, args) { let type, handler; try { @@ -74,14 +129,21 @@ const scriptlet = ( } catch(ex) { } if ( - reNeedle1.test(type) === false || - reNeedle2.test(handler) === false + reType.test(type) === false || + rePattern.test(handler) === false ) { return target.apply(thisArg, args); } } - } - ); + }; + self.EventTarget.prototype.addEventListener = new Proxy( + self.EventTarget.prototype.addEventListener, + eventListenerHandler + ); + }; + runAt(( ) => { + trapEddEventListeners(); + }, details.runAt); }; /******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-setinterval-if.js b/platform/mv3/scriptlets/no-setinterval-if.js index 5792ecc3f..77f4cab24 100644 --- a/platform/mv3/scriptlets/no-setinterval-if.js +++ b/platform/mv3/scriptlets/no-setinterval-if.js @@ -83,7 +83,13 @@ const scriptlet = ( args[0] = function(){}; } return target.apply(thisArg, args); - } + }, + get(target, prop, receiver) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop, receiver); + }, }); }; diff --git a/platform/mv3/scriptlets/no-settimeout-if.entity.js b/platform/mv3/scriptlets/no-settimeout-if.entity.js index 536591111..1f5f07031 100644 --- a/platform/mv3/scriptlets/no-settimeout-if.entity.js +++ b/platform/mv3/scriptlets/no-settimeout-if.entity.js @@ -83,7 +83,13 @@ const scriptlet = ( args[0] = function(){}; } return target.apply(thisArg, args); - } + }, + get(target, prop, receiver) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop, receiver); + }, }); }; diff --git a/platform/mv3/scriptlets/no-settimeout-if.js b/platform/mv3/scriptlets/no-settimeout-if.js index 03f93d119..c2d80b6a3 100644 --- a/platform/mv3/scriptlets/no-settimeout-if.js +++ b/platform/mv3/scriptlets/no-settimeout-if.js @@ -83,7 +83,13 @@ const scriptlet = ( args[0] = function(){}; } return target.apply(thisArg, args); - } + }, + get(target, prop, receiver) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop, receiver); + }, }); }; diff --git a/platform/mv3/scriptlets/set-constant.entity.js b/platform/mv3/scriptlets/set-constant.entity.js index d758ef7f7..dacb05bbe 100644 --- a/platform/mv3/scriptlets/set-constant.entity.js +++ b/platform/mv3/scriptlets/set-constant.entity.js @@ -47,159 +47,240 @@ const entitiesMap = new Map(self.$entitiesMap$); /******************************************************************************/ -const scriptlet = ( - chain = '', - cValue = '' -) => { - if ( chain === '' ) { return; } - const trappedProp = (( ) => { - const pos = chain.lastIndexOf('.'); - if ( pos === -1 ) { return chain; } - return chain.slice(pos+1); - })(); - if ( trappedProp === '' ) { return; } - 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; +// Dependencies + +const scriptletGlobals = new Map(); + +function safeSelf() { + if ( scriptletGlobals.has('safeSelf') ) { + return scriptletGlobals.get('safeSelf'); } - 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; + const safe = { + 'RegExp': self.RegExp, + 'RegExp_test': self.RegExp.prototype.test, + 'RegExp_exec': self.RegExp.prototype.exec, + 'addEventListener': self.EventTarget.prototype.addEventListener, + 'removeEventListener': self.EventTarget.prototype.removeEventListener, }; - // 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; - } + scriptletGlobals.set('safeSelf', safe); + return safe; +} + +function runAt(fn, when) { + const intFromReadyState = state => { + const targets = { + 'loading': 1, + 'interactive': 2, 'end': 2, '2': 2, + 'complete': 3, 'idle': 3, '3': 3, + }; + const tokens = Array.isArray(state) ? state : [ state ]; + for ( const token of tokens ) { + const prop = `${token}`; + if ( targets.hasOwnProperty(prop) === false ) { continue; } + return targets[prop]; } - try { - objectDefineProperty(owner, prop, { - configurable, - get() { - if ( prevGetter !== undefined ) { - prevGetter(); + return 0; + }; + const runAt = intFromReadyState(when); + if ( intFromReadyState(document.readyState) >= runAt ) { + fn(); return; + } + const onStateChange = ( ) => { + if ( intFromReadyState(document.readyState) < runAt ) { return; } + fn(); + safe.removeEventListener.apply(document, args); + }; + const safe = safeSelf(); + const args = [ 'readystatechange', onStateChange, { capture: true } ]; + safe.addEventListener.apply(document, args); +} + +/******************************************************************************/ + +const scriptlet = ( + arg1 = '', + arg2 = '', + arg3 = '' +) => { + const details = typeof arg1 !== 'object' + ? { prop: arg1, value: arg2 } + : arg1; + if ( arg3 !== '' ) { + if ( /^\d$/.test(arg3) ) { + details.options = [ arg3 ]; + } else { + details.options = Array.from(arguments).slice(2); + } + } + const { prop: chain = '', value: cValue = '' } = details; + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } + const options = details.options || []; + 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 = parseInt(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, { + if ( options.includes('asFunction') ) { + cValue = ( ) => cValue; + } else if ( options.includes('asCallback') ) { + cValue = ( ) => (( ) => cValue); + } else if ( options.includes('asResolved') ) { + cValue = Promise.resolve(cValue); + } else if ( options.includes('asRejected') ) { + cValue = Promise.reject(cValue); + } + 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 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); + }; + trapChain(window, chain); + } + runAt(( ) => { + setConstant(chain, cValue); + }, options); }; /******************************************************************************/ diff --git a/platform/mv3/scriptlets/set-constant.js b/platform/mv3/scriptlets/set-constant.js index dc714c844..6652cbee9 100644 --- a/platform/mv3/scriptlets/set-constant.js +++ b/platform/mv3/scriptlets/set-constant.js @@ -47,159 +47,240 @@ const hostnamesMap = new Map(self.$hostnamesMap$); /******************************************************************************/ -const scriptlet = ( - chain = '', - cValue = '' -) => { - if ( chain === '' ) { return; } - const trappedProp = (( ) => { - const pos = chain.lastIndexOf('.'); - if ( pos === -1 ) { return chain; } - return chain.slice(pos+1); - })(); - if ( trappedProp === '' ) { return; } - 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; +// Dependencies + +const scriptletGlobals = new Map(); + +function safeSelf() { + if ( scriptletGlobals.has('safeSelf') ) { + return scriptletGlobals.get('safeSelf'); } - 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; + const safe = { + 'RegExp': self.RegExp, + 'RegExp_test': self.RegExp.prototype.test, + 'RegExp_exec': self.RegExp.prototype.exec, + 'addEventListener': self.EventTarget.prototype.addEventListener, + 'removeEventListener': self.EventTarget.prototype.removeEventListener, }; - // 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; - } + scriptletGlobals.set('safeSelf', safe); + return safe; +} + +function runAt(fn, when) { + const intFromReadyState = state => { + const targets = { + 'loading': 1, + 'interactive': 2, 'end': 2, '2': 2, + 'complete': 3, 'idle': 3, '3': 3, + }; + const tokens = Array.isArray(state) ? state : [ state ]; + for ( const token of tokens ) { + const prop = `${token}`; + if ( targets.hasOwnProperty(prop) === false ) { continue; } + return targets[prop]; } - try { - objectDefineProperty(owner, prop, { - configurable, - get() { - if ( prevGetter !== undefined ) { - prevGetter(); + return 0; + }; + const runAt = intFromReadyState(when); + if ( intFromReadyState(document.readyState) >= runAt ) { + fn(); return; + } + const onStateChange = ( ) => { + if ( intFromReadyState(document.readyState) < runAt ) { return; } + fn(); + safe.removeEventListener.apply(document, args); + }; + const safe = safeSelf(); + const args = [ 'readystatechange', onStateChange, { capture: true } ]; + safe.addEventListener.apply(document, args); +} + +/******************************************************************************/ + +const scriptlet = ( + arg1 = '', + arg2 = '', + arg3 = '' +) => { + const details = typeof arg1 !== 'object' + ? { prop: arg1, value: arg2 } + : arg1; + if ( arg3 !== '' ) { + if ( /^\d$/.test(arg3) ) { + details.options = [ arg3 ]; + } else { + details.options = Array.from(arguments).slice(2); + } + } + const { prop: chain = '', value: cValue = '' } = details; + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } + const options = details.options || []; + 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 = parseInt(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, { + if ( options.includes('asFunction') ) { + cValue = ( ) => cValue; + } else if ( options.includes('asCallback') ) { + cValue = ( ) => (( ) => cValue); + } else if ( options.includes('asResolved') ) { + cValue = Promise.resolve(cValue); + } else if ( options.includes('asRejected') ) { + cValue = Promise.reject(cValue); + } + 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 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); + }; + trapChain(window, chain); + } + runAt(( ) => { + setConstant(chain, cValue); + }, options); }; /******************************************************************************/