diff --git a/assets/resources/attribute.js b/assets/resources/attribute.js index 7873183de16b4..b6ecdf7a3e597 100644 --- a/assets/resources/attribute.js +++ b/assets/resources/attribute.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { registerScriptlet } from './base.js'; diff --git a/assets/resources/base.js b/assets/resources/base.js index dc677b7cb794c..2c54418a777d8 100644 --- a/assets/resources/base.js +++ b/assets/resources/base.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ export const registeredScriptlets = []; diff --git a/assets/resources/cookie.js b/assets/resources/cookie.js index 1be419121f5b5..3fbeb1858d122 100644 --- a/assets/resources/cookie.js +++ b/assets/resources/cookie.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { registerScriptlet } from './base.js'; diff --git a/assets/resources/localstorage.js b/assets/resources/localstorage.js index 6ea13df73a49e..73e41546e135d 100644 --- a/assets/resources/localstorage.js +++ b/assets/resources/localstorage.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { getSafeCookieValuesFn } from './cookie.js'; diff --git a/assets/resources/parse-replace.js b/assets/resources/parse-replace.js new file mode 100644 index 0000000000000..da638735f7140 --- /dev/null +++ b/assets/resources/parse-replace.js @@ -0,0 +1,54 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { createArglistParser } from './shared.js'; +import { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +export function parseReplaceFn(s) { + if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; } + const parser = createArglistParser('/'); + parser.nextArg(s, 1); + let pattern = s.slice(parser.argBeg, parser.argEnd); + if ( parser.transform ) { + pattern = parser.normalizeArg(pattern); + } + if ( pattern === '' ) { return; } + parser.nextArg(s, parser.separatorEnd); + let replacement = s.slice(parser.argBeg, parser.argEnd); + if ( parser.separatorEnd === parser.separatorBeg ) { return; } + if ( parser.transform ) { + replacement = parser.normalizeArg(replacement); + } + const flags = s.slice(parser.separatorEnd); + try { + return { re: new RegExp(pattern, flags), replacement }; + } catch(_) { + } +} +registerScriptlet(parseReplaceFn, { + name: 'parse-replace.fn', + dependencies: [ + createArglistParser, + ], +}); diff --git a/assets/resources/proxy-apply.js b/assets/resources/proxy-apply.js new file mode 100644 index 0000000000000..09d357f9052b6 --- /dev/null +++ b/assets/resources/proxy-apply.js @@ -0,0 +1,109 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +export 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 ( proxyApplyFn.CtorContext === undefined ) { + proxyApplyFn.ctorContexts = []; + proxyApplyFn.CtorContext = class { + constructor(...args) { + this.init(...args); + } + init(callFn, callArgs) { + this.callFn = callFn; + this.callArgs = callArgs; + return this; + } + reflect() { + const r = Reflect.construct(this.callFn, this.callArgs); + this.callFn = this.callArgs = undefined; + proxyApplyFn.ctorContexts.push(this); + return r; + } + static factory(...args) { + return proxyApplyFn.ctorContexts.length !== 0 + ? proxyApplyFn.ctorContexts.pop().init(...args) + : new proxyApplyFn.CtorContext(...args); + } + }; + proxyApplyFn.applyContexts = []; + proxyApplyFn.ApplyContext = class { + constructor(...args) { + this.init(...args); + } + init(callFn, thisArg, callArgs) { + this.callFn = callFn; + this.thisArg = thisArg; + this.callArgs = callArgs; + return this; + } + reflect() { + const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs); + this.callFn = this.thisArg = this.callArgs = undefined; + proxyApplyFn.applyContexts.push(this); + return r; + } + static factory(...args) { + return proxyApplyFn.applyContexts.length !== 0 + ? proxyApplyFn.applyContexts.pop().init(...args) + : new proxyApplyFn.ApplyContext(...args); + } + }; + } + const fnStr = fn.toString(); + const toString = (function toString() { return fnStr; }).bind(null); + const proxyDetails = { + apply(target, thisArg, args) { + return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args)); + }, + get(target, prop) { + if ( prop === 'toString' ) { return toString; } + return Reflect.get(target, prop); + }, + }; + if ( fn.prototype?.constructor === fn ) { + proxyDetails.construct = function(target, args) { + return handler(proxyApplyFn.CtorContext.factory(target, args)); + }; + } + context[prop] = new Proxy(fn, proxyDetails); +} +registerScriptlet(proxyApplyFn, { + name: 'proxy-apply.fn', +}); diff --git a/assets/resources/replace-argument.js b/assets/resources/replace-argument.js new file mode 100644 index 0000000000000..150bf047b8a39 --- /dev/null +++ b/assets/resources/replace-argument.js @@ -0,0 +1,105 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { parseReplaceFn } from './parse-replace.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; +import { validateConstantFn } from './set-constant.js'; + +/** + * @scriptlet trusted-replace-argument.js + * + * @description + * Replace an argument passed to a method. Requires a trusted source. + * + * @param propChain + * The property chain to the function which argument must be replaced when + * called. + * + * @param argposRaw + * The zero-based position of the argument in the argument list. Use a negative + * number for a position relative to the last argument. + * + * @param argraw + * The replacement value, validated using the same heuristic as with the + * `set-constant.js` scriptlet. + * If the replacement value matches `json:...`, the value will be the + * json-parsed string after `json:`. + * If the replacement value matches `repl:/.../.../`, the target argument will + * be replaced according the regex-replacement directive following `repl:` + * + * @param [, condition, pattern] + * Optional. The replacement will occur only when pattern matches the target + * argument. + * + * */ + +export function trustedReplaceArgument( + propChain = '', + argposRaw = '', + argraw = '' +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw); + const argoffset = parseInt(argposRaw, 10) || 0; + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + const replacer = argraw.startsWith('repl:/') && + parseReplaceFn(argraw.slice(5)) || undefined; + const value = replacer === undefined && + validateConstantFn(true, argraw, extraArgs) || undefined; + const reCondition = extraArgs.condition + ? safe.patternToRegex(extraArgs.condition) + : /^/; + proxyApplyFn(propChain, function(context) { + const { callArgs } = context; + if ( argposRaw === '' ) { + safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`); + return context.reflect(); + } + const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset; + if ( argpos < 0 || argpos >= callArgs.length ) { + return context.reflect(); + } + const argBefore = callArgs[argpos]; + if ( safe.RegExp_test.call(reCondition, argBefore) === false ) { + return context.reflect(); + } + const argAfter = replacer && typeof argBefore === 'string' + ? argBefore.replace(replacer.re, replacer.replacement) + : value; + callArgs[argpos] = argAfter; + safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${argAfter}`); + return context.reflect(); + }); +} +registerScriptlet(trustedReplaceArgument, { + name: 'trusted-replace-argument.js', + requiresTrust: true, + dependencies: [ + parseReplaceFn, + proxyApplyFn, + safeSelf, + validateConstantFn, + ], +}); diff --git a/assets/resources/run-at.js b/assets/resources/run-at.js index 65e1be534dd5a..545324dcf1160 100644 --- a/assets/resources/run-at.js +++ b/assets/resources/run-at.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { registerScriptlet } from './base.js'; diff --git a/assets/resources/safe-self.js b/assets/resources/safe-self.js index bad9eece359ae..a1840e13b6464 100644 --- a/assets/resources/safe-self.js +++ b/assets/resources/safe-self.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { registerScriptlet } from './base.js'; diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 46c31b6dc4e13..d045352acc4d3 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -18,23 +18,20 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import './attribute.js'; -import './cookie.js'; -import './localstorage.js'; -import './run-at.js'; -import './safe-self.js'; +import './replace-argument.js'; import './spoof-css.js'; import { runAt, runAtHtmlElementFn } from './run-at.js'; import { getAllCookiesFn } from './cookie.js'; import { getAllLocalStorageFn } from './localstorage.js'; +import { proxyApplyFn } from './proxy-apply.js'; import { registeredScriptlets } from './base.js'; import { safeSelf } from './safe-self.js'; +import { validateConstantFn } from './set-constant.js'; // Externally added to the private namespace in which scriptlets execute. /* global scriptletGlobals */ @@ -289,228 +286,6 @@ function abortCurrentScriptCore( /******************************************************************************/ -builtinScriptlets.push({ - name: 'validate-constant.fn', - fn: validateConstantFn, - dependencies: [ - 'safe-self.fn', - ], -}); -function validateConstantFn(trusted, raw, extraArgs = {}) { - const safe = safeSelf(); - 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 ( raw === 'throwFunc' ) { - value = function(){ throw ''; }; - } else if ( /^-?\d+$/.test(raw) ) { - value = parseInt(raw); - if ( isNaN(raw) ) { return; } - if ( Math.abs(raw) > 0x7FFF ) { return; } - } else if ( trusted ) { - if ( raw.startsWith('json:') ) { - try { value = safe.JSON_parse(raw.slice(5)); } catch(ex) { return; } - } else 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; -} - -/******************************************************************************/ - -builtinScriptlets.push({ - name: 'set-constant.fn', - fn: setConstantFn, - dependencies: [ - 'run-at.fn', - 'safe-self.fn', - 'validate-constant.fn', - ], -}); -function setConstantFn( - trusted = false, - chain = '', - 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, rawValue) { - const trappedProp = (( ) => { - const pos = chain.lastIndexOf('.'); - if ( pos === -1 ) { return chain; } - return chain.slice(pos+1); - })(); - const cloakFunc = fn => { - safe.Object_defineProperty(fn, 'name', { value: trappedProp }); - return new Proxy(fn, { - defineProperty(target, prop) { - if ( prop !== 'toString' ) { - return Reflect.defineProperty(...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); - }, - }); - }; - if ( trappedProp === '' ) { return; } - const thisScript = document.currentScript; - let normalValue = validateConstantFn(trusted, rawValue, extraArgs); - if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) { - normalValue = cloakFunc(normalValue); - } - let aborted = false; - const mustAbort = function(v) { - if ( trusted ) { return false; } - if ( aborted ) { return true; } - aborted = - (v !== undefined && v !== null) && - (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] : normalValue) === false ) { return; } - const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); - let prevGetter, prevSetter; - if ( odesc instanceof safe.Object ) { - owner[prop] = normalValue; - if ( odesc.get instanceof Function ) { - prevGetter = odesc.get; - } - if ( odesc.set instanceof Function ) { - prevSetter = odesc.set; - } - } - try { - safe.Object_defineProperty(owner, prop, { - configurable, - get() { - if ( prevGetter !== undefined ) { - prevGetter(); - } - return handler.getter(); - }, - set(a) { - if ( prevSetter !== undefined ) { - prevSetter(a); - } - handler.setter(a); - } - }); - safe.uboLog(logPrefix, 'Trap installed'); - } catch(ex) { - safe.uboErr(logPrefix, 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() { - if ( document.currentScript === thisScript ) { - return this.v; - } - safe.uboLog(logPrefix, 'Property read'); - return normalValue; - }, - setter: function(a) { - if ( mustAbort(a) === false ) { return; } - normalValue = a; - } - }); - return; - } - const prop = chain.slice(0, pos); - const v = owner[prop]; - chain = chain.slice(pos + 1); - if ( v instanceof safe.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 safe.Object ) { - trapChain(a, chain); - } - } - }); - }; - trapChain(window, chain); - } - runAt(( ) => { - setConstant(chain, rawValue); - }, extraArgs.runAt); -} - -/******************************************************************************/ - builtinScriptlets.push({ name: 'replace-node-text.fn', fn: replaceNodeTextFn, @@ -1045,93 +820,6 @@ function replaceFetchResponseFn( /******************************************************************************/ -builtinScriptlets.push({ - name: 'proxy-apply.fn', - fn: proxyApplyFn, -}); -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 ( proxyApplyFn.CtorContext === undefined ) { - proxyApplyFn.ctorContexts = []; - proxyApplyFn.CtorContext = class { - constructor(...args) { - this.init(...args); - } - init(callFn, callArgs) { - this.callFn = callFn; - this.callArgs = callArgs; - return this; - } - reflect() { - const r = Reflect.construct(this.callFn, this.callArgs); - this.callFn = this.callArgs = undefined; - proxyApplyFn.ctorContexts.push(this); - return r; - } - static factory(...args) { - return proxyApplyFn.ctorContexts.length !== 0 - ? proxyApplyFn.ctorContexts.pop().init(...args) - : new proxyApplyFn.CtorContext(...args); - } - }; - proxyApplyFn.applyContexts = []; - proxyApplyFn.ApplyContext = class { - constructor(...args) { - this.init(...args); - } - init(callFn, thisArg, callArgs) { - this.callFn = callFn; - this.thisArg = thisArg; - this.callArgs = callArgs; - return this; - } - reflect() { - const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs); - this.callFn = this.thisArg = this.callArgs = undefined; - proxyApplyFn.applyContexts.push(this); - return r; - } - static factory(...args) { - return proxyApplyFn.applyContexts.length !== 0 - ? proxyApplyFn.applyContexts.pop().init(...args) - : new proxyApplyFn.ApplyContext(...args); - } - }; - } - const fnStr = fn.toString(); - const toString = (function toString() { return fnStr; }).bind(null); - const proxyDetails = { - apply(target, thisArg, args) { - return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args)); - }, - get(target, prop) { - if ( prop === 'toString' ) { return toString; } - return Reflect.get(target, prop); - }, - }; - if ( fn.prototype?.constructor === fn ) { - proxyDetails.construct = function(target, args) { - return handler(proxyApplyFn.CtorContext.factory(target, args)); - }; - } - context[prop] = new Proxy(fn, proxyDetails); -} - -/******************************************************************************/ - builtinScriptlets.push({ name: 'prevent-xhr.fn', fn: preventXhrFn, @@ -2205,24 +1893,6 @@ function noRequestAnimationFrameIf( /******************************************************************************/ -builtinScriptlets.push({ - name: 'set-constant.js', - aliases: [ - 'set.js', - ], - fn: setConstant, - dependencies: [ - 'set-constant.fn' - ], -}); -function setConstant( - ...args -) { - setConstantFn(false, ...args); -} - -/******************************************************************************/ - builtinScriptlets.push({ name: 'no-setInterval-if.js', aliases: [ @@ -3440,32 +3110,6 @@ function replaceNodeText( replaceNodeTextFn(nodeName, pattern, replacement, ...extraArgs); } -/******************************************************************************* - * - * trusted-set-constant.js - * - * Set specified property to any value. This is essentially the same as - * set-constant.js, but with no restriction as to which values can be used. - * - **/ - -builtinScriptlets.push({ - name: 'trusted-set-constant.js', - requiresTrust: true, - aliases: [ - 'trusted-set.js', - ], - fn: trustedSetConstant, - dependencies: [ - 'set-constant.fn' - ], -}); -function trustedSetConstant( - ...args -) { - setConstantFn(true, ...args); -} - /******************************************************************************* * * trusted-replace-fetch-response.js @@ -3880,50 +3524,6 @@ function trustedPruneOutboundObject( /******************************************************************************/ -builtinScriptlets.push({ - name: 'trusted-replace-argument.js', - requiresTrust: true, - fn: trustedReplaceArgument, - dependencies: [ - 'proxy-apply.fn', - 'safe-self.fn', - 'validate-constant.fn', - ], -}); -function trustedReplaceArgument( - propChain = '', - argposRaw = '', - argraw = '' -) { - if ( propChain === '' ) { return; } - const safe = safeSelf(); - const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw); - const argoffset = parseInt(argposRaw, 10) || 0; - const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); - const normalValue = validateConstantFn(true, argraw, extraArgs); - const reCondition = extraArgs.condition - ? safe.patternToRegex(extraArgs.condition) - : /^/; - proxyApplyFn(propChain, function(context) { - const { callArgs } = context; - if ( argposRaw === '' ) { - safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`); - return context.reflect(); - } - const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset; - if ( argpos >= 0 && argpos < callArgs.length ) { - const argBefore = callArgs[argpos]; - if ( safe.RegExp_test.call(reCondition, argBefore) ) { - callArgs[argpos] = normalValue; - safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${normalValue}`); - } - } - return context.reflect(); - }); -} - -/******************************************************************************/ - builtinScriptlets.push({ name: 'trusted-replace-outbound-text.js', requiresTrust: true, diff --git a/assets/resources/set-constant.js b/assets/resources/set-constant.js new file mode 100644 index 0000000000000..127f27bbbe910 --- /dev/null +++ b/assets/resources/set-constant.js @@ -0,0 +1,287 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function validateConstantFn(trusted, raw, extraArgs = {}) { + const safe = safeSelf(); + 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 ( raw === 'throwFunc' ) { + value = function(){ throw ''; }; + } else if ( /^-?\d+$/.test(raw) ) { + value = parseInt(raw); + if ( isNaN(raw) ) { return; } + if ( Math.abs(raw) > 0x7FFF ) { return; } + } else if ( trusted ) { + if ( raw.startsWith('json:') ) { + try { value = safe.JSON_parse(raw.slice(5)); } catch(ex) { return; } + } else 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; +} +registerScriptlet(validateConstantFn, { + name: 'validate-constant.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function setConstantFn( + trusted = false, + chain = '', + 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, rawValue) { + const trappedProp = (( ) => { + const pos = chain.lastIndexOf('.'); + if ( pos === -1 ) { return chain; } + return chain.slice(pos+1); + })(); + const cloakFunc = fn => { + safe.Object_defineProperty(fn, 'name', { value: trappedProp }); + return new Proxy(fn, { + defineProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.defineProperty(...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); + }, + }); + }; + if ( trappedProp === '' ) { return; } + const thisScript = document.currentScript; + let normalValue = validateConstantFn(trusted, rawValue, extraArgs); + if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) { + normalValue = cloakFunc(normalValue); + } + let aborted = false; + const mustAbort = function(v) { + if ( trusted ) { return false; } + if ( aborted ) { return true; } + aborted = + (v !== undefined && v !== null) && + (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] : normalValue) === false ) { return; } + const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); + let prevGetter, prevSetter; + if ( odesc instanceof safe.Object ) { + owner[prop] = normalValue; + if ( odesc.get instanceof Function ) { + prevGetter = odesc.get; + } + if ( odesc.set instanceof Function ) { + prevSetter = odesc.set; + } + } + try { + safe.Object_defineProperty(owner, prop, { + configurable, + get() { + if ( prevGetter !== undefined ) { + prevGetter(); + } + return handler.getter(); + }, + set(a) { + if ( prevSetter !== undefined ) { + prevSetter(a); + } + handler.setter(a); + } + }); + safe.uboLog(logPrefix, 'Trap installed'); + } catch(ex) { + safe.uboErr(logPrefix, 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() { + if ( document.currentScript === thisScript ) { + return this.v; + } + safe.uboLog(logPrefix, 'Property read'); + return normalValue; + }, + setter: function(a) { + if ( mustAbort(a) === false ) { return; } + normalValue = a; + } + }); + return; + } + const prop = chain.slice(0, pos); + const v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v instanceof safe.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 safe.Object ) { + trapChain(a, chain); + } + } + }); + }; + trapChain(window, chain); + } + runAt(( ) => { + setConstant(chain, rawValue); + }, extraArgs.runAt); +} +registerScriptlet(setConstantFn, { + name: 'set-constant.fn', + dependencies: [ + runAt, + safeSelf, + validateConstantFn, + ], +}); + +/******************************************************************************/ + +export function setConstant( + ...args +) { + setConstantFn(false, ...args); +} +registerScriptlet(setConstant, { + name: 'set-constant.js', + aliases: [ + 'set.js', + ], + dependencies: [ + setConstantFn, + ], +}); + +/******************************************************************************* + * + * trusted-set-constant.js + * + * Set specified property to any value. This is essentially the same as + * set-constant.js, but with no restriction as to which values can be used. + * + **/ + +export function trustedSetConstant( + ...args +) { + setConstantFn(true, ...args); +} +registerScriptlet(trustedSetConstant, { + name: 'trusted-set-constant.js', + requiresTrust: true, + aliases: [ + 'trusted-set.js', + ], + dependencies: [ + setConstantFn, + ], +}); diff --git a/assets/resources/shared.js b/assets/resources/shared.js new file mode 100644 index 0000000000000..89d8503e7e4d0 --- /dev/null +++ b/assets/resources/shared.js @@ -0,0 +1,44 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +// Code imported from main code base and exposed as injectable scriptlets +import { ArglistParser } from '../../js/arglist-parser.js'; + +import { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +registerScriptlet(ArglistParser, { + name: 'arglist-parser.fn', +}); + +/******************************************************************************/ + +export function createArglistParser(...args) { + return new ArglistParser(...args); +} +registerScriptlet(createArglistParser, { + name: 'create-arglist-parser.fn', + dependencies: [ + ArglistParser, + ], +}); diff --git a/assets/resources/spoof-css.js b/assets/resources/spoof-css.js index 32b51acf407c0..7cc7b9f95574c 100644 --- a/assets/resources/spoof-css.js +++ b/assets/resources/spoof-css.js @@ -18,8 +18,6 @@ Home: https://github.com/gorhill/uBlock - The scriptlets below are meant to be injected only into a - web page context. */ import { registerScriptlet } from './base.js'; diff --git a/src/js/arglist-parser.js b/src/js/arglist-parser.js new file mode 100644 index 0000000000000..d8200df5822a1 --- /dev/null +++ b/src/js/arglist-parser.js @@ -0,0 +1,116 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2020-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/******************************************************************************/ + +export class ArglistParser { + constructor(separatorChar = ',', mustQuote = false) { + this.separatorChar = this.actualSeparatorChar = separatorChar; + this.separatorCode = this.actualSeparatorCode = separatorChar.charCodeAt(0); + this.mustQuote = mustQuote; + this.quoteBeg = 0; this.quoteEnd = 0; + this.argBeg = 0; this.argEnd = 0; + this.separatorBeg = 0; this.separatorEnd = 0; + this.transform = false; + this.failed = false; + this.reWhitespaceStart = /^\s+/; + this.reWhitespaceEnd = /\s+$/; + this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/; + this.reTrailingEscapeChars = /\\+$/; + } + nextArg(pattern, beg = 0) { + const len = pattern.length; + this.quoteBeg = beg + this.leftWhitespaceCount(pattern.slice(beg)); + this.failed = false; + const qc = pattern.charCodeAt(this.quoteBeg); + if ( qc === 0x22 /* " */ || qc === 0x27 /* ' */ || qc === 0x60 /* ` */ ) { + this.indexOfNextArgSeparator(pattern, qc); + if ( this.argEnd !== len ) { + this.quoteEnd = this.argEnd + 1; + this.separatorBeg = this.separatorEnd = this.quoteEnd; + this.separatorEnd += this.leftWhitespaceCount(pattern.slice(this.quoteEnd)); + if ( this.separatorEnd === len ) { return this; } + if ( pattern.charCodeAt(this.separatorEnd) === this.separatorCode ) { + this.separatorEnd += 1; + return this; + } + } + } + this.indexOfNextArgSeparator(pattern, this.separatorCode); + this.separatorBeg = this.separatorEnd = this.argEnd; + if ( this.separatorBeg < len ) { + this.separatorEnd += 1; + } + this.argEnd -= this.rightWhitespaceCount(pattern.slice(0, this.separatorBeg)); + this.quoteEnd = this.argEnd; + if ( this.mustQuote ) { + this.failed = true; + } + return this; + } + normalizeArg(s, char = '') { + if ( char === '' ) { char = this.actualSeparatorChar; } + let out = ''; + let pos = 0; + while ( (pos = s.lastIndexOf(char)) !== -1 ) { + out = s.slice(pos) + out; + s = s.slice(0, pos); + const match = this.reTrailingEscapeChars.exec(s); + if ( match === null ) { continue; } + const tail = (match[0].length & 1) !== 0 + ? match[0].slice(0, -1) + : match[0]; + out = tail + out; + s = s.slice(0, -match[0].length); + } + if ( out === '' ) { return s; } + return s + out; + } + leftWhitespaceCount(s) { + const match = this.reWhitespaceStart.exec(s); + return match === null ? 0 : match[0].length; + } + rightWhitespaceCount(s) { + const match = this.reWhitespaceEnd.exec(s); + return match === null ? 0 : match[0].length; + } + indexOfNextArgSeparator(pattern, separatorCode) { + this.argBeg = this.argEnd = separatorCode !== this.separatorCode + ? this.quoteBeg + 1 + : this.quoteBeg; + this.transform = false; + if ( separatorCode !== this.actualSeparatorCode ) { + this.actualSeparatorCode = separatorCode; + this.actualSeparatorChar = String.fromCharCode(separatorCode); + } + while ( this.argEnd < pattern.length ) { + const pos = pattern.indexOf(this.actualSeparatorChar, this.argEnd); + if ( pos === -1 ) { + return (this.argEnd = pattern.length); + } + if ( this.reOddTrailingEscape.test(pattern.slice(0, pos)) === false ) { + return (this.argEnd = pos); + } + this.transform = true; + this.argEnd = pos + 1; + } + } +} diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index f49b0bfe7208d..2be165eb107ef 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -22,6 +22,7 @@ /******************************************************************************/ import * as cssTree from '../lib/csstree/css-tree.js'; +import { ArglistParser } from './arglist-parser.js'; import Regex from '../lib/regexanalyzer/regex.js'; /******************************************************************************* @@ -606,102 +607,6 @@ const exCharCodeAt = (s, i) => { /******************************************************************************/ -class ArgListParser { - constructor(separatorChar = ',', mustQuote = false) { - this.separatorChar = this.actualSeparatorChar = separatorChar; - this.separatorCode = this.actualSeparatorCode = separatorChar.charCodeAt(0); - this.mustQuote = mustQuote; - this.quoteBeg = 0; this.quoteEnd = 0; - this.argBeg = 0; this.argEnd = 0; - this.separatorBeg = 0; this.separatorEnd = 0; - this.transform = false; - this.failed = false; - this.reWhitespaceStart = /^\s+/; - this.reWhitespaceEnd = /\s+$/; - this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/; - this.reTrailingEscapeChars = /\\+$/; - } - nextArg(pattern, beg = 0) { - const len = pattern.length; - this.quoteBeg = beg + this.leftWhitespaceCount(pattern.slice(beg)); - this.failed = false; - const qc = pattern.charCodeAt(this.quoteBeg); - if ( qc === 0x22 /* " */ || qc === 0x27 /* ' */ || qc === 0x60 /* ` */ ) { - this.indexOfNextArgSeparator(pattern, qc); - if ( this.argEnd !== len ) { - this.quoteEnd = this.argEnd + 1; - this.separatorBeg = this.separatorEnd = this.quoteEnd; - this.separatorEnd += this.leftWhitespaceCount(pattern.slice(this.quoteEnd)); - if ( this.separatorEnd === len ) { return this; } - if ( pattern.charCodeAt(this.separatorEnd) === this.separatorCode ) { - this.separatorEnd += 1; - return this; - } - } - } - this.indexOfNextArgSeparator(pattern, this.separatorCode); - this.separatorBeg = this.separatorEnd = this.argEnd; - if ( this.separatorBeg < len ) { - this.separatorEnd += 1; - } - this.argEnd -= this.rightWhitespaceCount(pattern.slice(0, this.separatorBeg)); - this.quoteEnd = this.argEnd; - if ( this.mustQuote ) { - this.failed = true; - } - return this; - } - normalizeArg(s, char = '') { - if ( char === '' ) { char = this.actualSeparatorChar; } - let out = ''; - let pos = 0; - while ( (pos = s.lastIndexOf(char)) !== -1 ) { - out = s.slice(pos) + out; - s = s.slice(0, pos); - const match = this.reTrailingEscapeChars.exec(s); - if ( match === null ) { continue; } - const tail = (match[0].length & 1) !== 0 - ? match[0].slice(0, -1) - : match[0]; - out = tail + out; - s = s.slice(0, -match[0].length); - } - if ( out === '' ) { return s; } - return s + out; - } - leftWhitespaceCount(s) { - const match = this.reWhitespaceStart.exec(s); - return match === null ? 0 : match[0].length; - } - rightWhitespaceCount(s) { - const match = this.reWhitespaceEnd.exec(s); - return match === null ? 0 : match[0].length; - } - indexOfNextArgSeparator(pattern, separatorCode) { - this.argBeg = this.argEnd = separatorCode !== this.separatorCode - ? this.quoteBeg + 1 - : this.quoteBeg; - this.transform = false; - if ( separatorCode !== this.actualSeparatorCode ) { - this.actualSeparatorCode = separatorCode; - this.actualSeparatorChar = String.fromCharCode(separatorCode); - } - while ( this.argEnd < pattern.length ) { - const pos = pattern.indexOf(this.actualSeparatorChar, this.argEnd); - if ( pos === -1 ) { - return (this.argEnd = pattern.length); - } - if ( this.reOddTrailingEscape.test(pattern.slice(0, pos)) === false ) { - return (this.argEnd = pos); - } - this.transform = true; - this.argEnd = pos + 1; - } - } -} - -/******************************************************************************/ - class AstWalker { constructor(parser, from = 0) { this.parser = parser; @@ -904,8 +809,8 @@ export class AstFilterParser { this.reBadPP = /(?:^|[;,])\s*report-to\b/i; this.reNetOption = /^(~?)([134a-z_-]+)(=?)/; this.reNoopOption = /^_+$/; - this.netOptionValueParser = new ArgListParser(','); - this.scriptletArgListParser = new ArgListParser(','); + this.netOptionValueParser = new ArglistParser(','); + this.scriptletArgListParser = new ArglistParser(','); } finish() { @@ -3100,7 +3005,7 @@ export function parseHeaderValue(arg) { export function parseReplaceValue(s) { if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; } - const parser = new ArgListParser('/'); + const parser = new ArglistParser('/'); parser.nextArg(s, 1); let pattern = s.slice(parser.argBeg, parser.argEnd); if ( parser.transform ) { diff --git a/tools/make-nodejs.sh b/tools/make-nodejs.sh index 270456814b9b3..87e96ddade909 100755 --- a/tools/make-nodejs.sh +++ b/tools/make-nodejs.sh @@ -7,6 +7,7 @@ set -e DES=$1 mkdir -p $DES/js +cp src/js/arglist-parser.js $DES/js cp src/js/base64-custom.js $DES/js cp src/js/biditrie.js $DES/js cp src/js/dynamic-net-filtering.js $DES/js