-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0aeb1d5
commit e45a707
Showing
2 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]; |