Improve trusted-replace-argument scriptlet

As discussed with filter list maintainers, added ability to
partially replace an argument using the `repl:` prefix. Updated
documentation:

---

@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.

---

Aditionally, more scriptlets moved into their own files.
This commit is contained in:
Raymond Hill 2024-11-27 15:47:17 -05:00
parent e43cb6771a
commit adced29b5b
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
16 changed files with 723 additions and 516 deletions

View file

@ -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';

View file

@ -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 = [];

View file

@ -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';

View file

@ -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';

View file

@ -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,
],
});

View file

@ -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',
});

View file

@ -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,
],
});

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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,
],
});

View file

@ -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,
],
});

View file

@ -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';

116
src/js/arglist-parser.js Normal file
View file

@ -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;
}
}
}

View file

@ -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 ) {

View file

@ -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