Skip to content

Commit

Permalink
draft trusted-set-constant
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislav-atr committed Dec 29, 2022
1 parent 0aeb1d5 commit e45a707
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ export * from './trusted-set-cookie';
export * from './trusted-set-cookie-reload';
export * from './trusted-replace-fetch-response';
export * from './trusted-set-local-storage-item';
export * from './trusted-set-constant';
334 changes: 334 additions & 0 deletions src/scriptlets/trusted-set-constant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
import {
hit,
logMessage,
noopArray,
noopObject,
noopCallbackFunc,
noopFunc,
trueFunc,
falseFunc,
throwFunc,
noopPromiseReject,
noopPromiseResolve,
getPropertyInChain,
setPropertyAccess,
toRegExp,
matchStackTrace,
nativeIsNaN,
isEmptyObject,
getNativeRegexpTest,
// following helpers should be imported and injected
// because they are used by helpers above
shouldAbortInlineOrInjectedScript,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet set-constant
*
* @description
* Creates a constant property and assigns it one of the values from the predefined list.
*
* > Actually, it's not a constant. Please note, that it can be rewritten with a value of a different type.
*
* > If empty object is present in chain it will be trapped until chain leftovers appear.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/wiki/Resources-Library#set-constantjs-
*
* Related ABP snippet:
* https://github.com/adblockplus/adblockpluscore/blob/adblockpluschrome-3.9.4/lib/content/snippets.js#L1361
*
* **Syntax**
* ```
* example.org#%#//scriptlet('set-constant', property, value[, stack])
* ```
*
* - `property` - required, path to a property (joined with `.` if needed). The property must be attached to `window`.
* - `value` - required. Possible values:
* - positive decimal integer `<= 32767`
* - one of the predefined constants:
* - `undefined`
* - `false`
* - `true`
* - `null`
* - `emptyObj` - empty object
* - `emptyArr` - empty array
* - `noopFunc` - function with empty body
* - `noopCallbackFunc` - function returning noopFunc
* - `trueFunc` - function returning true
* - `falseFunc` - function returning false
* - `throwFunc` - function throwing an error
* - `noopPromiseResolve` - function returning Promise object that is resolved with an empty response
* - `noopPromiseReject` - function returning Promise.reject()
* - `''` - empty string
* - `-1` - number value `-1`
* - `yes`
* - `no`
* - `stack` - optional, string or regular expression that must match the current function call stack trace;
* if regular expression is invalid it will be skipped
*
* **Examples**
* ```
* ! Any access to `window.first` will return `false`
* example.org#%#//scriptlet('set-constant', 'first', 'false')
*
* ✔ window.first === false
* ```
*
* ```
* ! Any call to `window.second()` will return `true`
* example.org#%#//scriptlet('set-constant', 'second', 'trueFunc')
*
* ✔ window.second() === true
* ✔ window.second.toString() === "function trueFunc() {return true;}"
* ```
*
* ```
* ! Any call to `document.third()` will return `true` if the method is related to `checking.js`
* example.org#%#//scriptlet('set-constant', 'document.third', 'trueFunc', 'checking.js')
*
* ✔ document.third() === true // if the condition described above is met
* ```
*/
/* eslint-enable max-len */
export function trustedSetConstant(source, property, value, infer = false, stack) {
if (!property
|| !matchStackTrace(stack, new Error().stack)) {
return;
}


const constantValue = infer ? inferValue(value) : value;

/**
* #%#//scriptlet('trusted-set-constant', 'prop', '12.34') => typeof window.prop === 'string'
*
* #%#//scriptlet('trusted-set-constant', 'prop', '12.34', true) => typeof window.prop === 'number'
*
*
*
* #%#//scriptlet('trusted-set-constant', 'prop', '{}') => typeof window.prop === 'string'
*
* #%#//scriptlet('trusted-set-constant', 'prop', '{}', true) => typeof window.prop === 'object'
*/

/**
* Infers value from string argument
* @param {string} value
* @returns {any}
*/
function inferValue(value) {
if (value === 'undefined') {
return undefined;
} else if (value === 'false') {
return false;
} else if (value === 'true') {
return true;
} else if (value === 'null') {
return null;
} else if (value === 'NaN') {
return NaN;
}

const numVal = value.indexOf('.') === -1
? parseInt(value, 10) : parseFloat(value, 10);
if (!isNaN(numVal)) {
return numVal;
}

try {
let parsableVal = JSON.parse(value);
if (typeof parsableVal === 'object') {
return parsableVal;
}
} catch { /* no */ }

throw new Error('wtf');
}

const constantValue2 = convertValue(value, type)
/**
* Converts value to a specified type
* @param {string} value
* @param {string} type
* @returns
*/
function convertValue(value, type) {
switch (type) {
case 'number':
return parseInt(value, 10);
case 'decimal':
return parseFloat(value, 10);
case 'object':
return JSON.parse(value);
case 'NaN':
return NaN;
case 'undefined':
return undefined;
case 'null':
return null;
case 'true':
return true;
case 'false':
return false;

default: {
throw new Error('wtf');
}
}
}

let canceled = false;
const mustCancel = (value) => {
if (canceled) {
return canceled;
}
canceled = value !== undefined
&& constantValue !== undefined
&& typeof value !== typeof constantValue
&& value !== null;
return canceled;
};

const trapProp = (base, prop, configurable, handler) => {
if (!handler.init(base[prop])) {
return false;
}

const origDescriptor = Object.getOwnPropertyDescriptor(base, prop);
let prevSetter;
// This is required to prevent scriptlets overwrite each over
if (origDescriptor instanceof Object) {
// This check is required to avoid defining non-configurable props
if (!origDescriptor.configurable) {
const message = `Property '${prop}' is not configurable`;
logMessage(source, message);
return false;
}

base[prop] = constantValue;
if (origDescriptor.set instanceof Function) {
prevSetter = origDescriptor.set;
}
}
Object.defineProperty(base, prop, {
configurable,
get() {
return handler.get();
},
set(a) {
if (prevSetter !== undefined) {
prevSetter(a);
}
handler.set(a);
},
});
return true;
};

const setChainPropAccess = (owner, property) => {
const chainInfo = getPropertyInChain(owner, property);
const { base } = chainInfo;
const { prop, chain } = chainInfo;

// Handler method init is used to keep track of factual value
// and apply mustCancel() check only on end prop
const inChainPropHandler = {
factValue: undefined,
init(a) {
this.factValue = a;
return true;
},
get() {
return this.factValue;
},
set(a) {
// Prevent breakage due to loop assignments like win.obj = win.obj
if (this.factValue === a) {
return;
}

this.factValue = a;
if (a instanceof Object) {
setChainPropAccess(a, chain);
}
},
};
const endPropHandler = {
init(a) {
if (mustCancel(a)) {
return false;
}
return true;
},
get() {
return constantValue;
},
set(a) {
if (!mustCancel(a)) {
return;
}
constantValue = a;
},
};

// End prop case
if (!chain) {
const isTrapped = trapProp(base, prop, false, endPropHandler);
if (isTrapped) {
hit(source);
}
return;
}

// Null prop in chain
if (base !== undefined && base[prop] === null) {
trapProp(base, prop, true, inChainPropHandler);
return;
}

// Empty object prop in chain
if ((base instanceof Object || typeof base === 'object') && isEmptyObject(base)) {
trapProp(base, prop, true, inChainPropHandler);
}

// Defined prop in chain
const propValue = owner[prop];
if (propValue instanceof Object || (typeof propValue === 'object' && propValue !== null)) {
setChainPropAccess(propValue, chain);
}

// Undefined prop in chain
trapProp(base, prop, true, inChainPropHandler);
};
setChainPropAccess(window, property);
}

trustedSetConstant.names = [
'trusted-set-constant',
// trusted scriptlets support no aliases
];
trustedSetConstant.injections = [
hit,
logMessage,
noopArray,
noopObject,
noopFunc,
noopCallbackFunc,
trueFunc,
falseFunc,
throwFunc,
noopPromiseReject,
noopPromiseResolve,
getPropertyInChain,
setPropertyAccess,
toRegExp,
matchStackTrace,
nativeIsNaN,
isEmptyObject,
getNativeRegexpTest,
// following helpers should be imported and injected
// because they are used by helpers above
shouldAbortInlineOrInjectedScript,
];

0 comments on commit e45a707

Please sign in to comment.