From 0f789e19a74a823c1c2855709961bbd88c697fa1 Mon Sep 17 00:00:00 2001 From: Stanislav Atroschenko Date: Fri, 13 Jan 2023 11:48:02 +0300 Subject: [PATCH] AG-9480 add trusted-set-constant scriptlet #137 Merge in ADGUARD-FILTERS/scriptlets from feature/AG-9480 to release/v1.8 Squashed commit of the following: commit e69dccdaba283b37e85a1540c7541baf0fa060e0 Author: Stanislav A Date: Thu Jan 12 14:49:58 2023 +0300 improve inferValue description commit 3dcf6d4840c3fdba5ea2ff05470cf6f8f25ad7c8 Author: Stanislav A Date: Thu Jan 12 12:59:05 2023 +0300 add 32767 to const and tweak failing test commit 39f1c2dc101f880b216141af4efa462a1c3fc3fa Author: Stanislav A Date: Thu Jan 12 12:23:50 2023 +0300 add testcases with string value commit bb1960e020517004d1cf665dd62621f4ad0ed5ac Author: Stanislav A Date: Wed Jan 11 21:40:48 2023 +0300 fix and improve description commit 68b112c5716c693db763ca7dd2653f13d1d31772 Author: Stanislav A Date: Wed Jan 11 19:37:30 2023 +0300 improve funcs descriptions and warning commit d62083387e10fddcd035cc632b5a24d550198014 Author: Stanislav A Date: Wed Jan 11 19:17:50 2023 +0300 improve value type inferred and remove shouldInfer argument commit 824e900af3499633d646ecadfe5ed8220696fa29 Author: Stanislav A Date: Wed Jan 11 17:39:17 2023 +0300 jsdoc set-constant helpers commit 71a5451cc2e1f1c7f1d5a6c15216fa1f9601284e Author: Stanislav A Date: Wed Jan 11 14:58:30 2023 +0300 fix typos commit b9bb8fe901e20e21f3d18cebee2118f51dfeccc1 Author: Stanislav A Date: Wed Jan 11 14:49:07 2023 +0300 swap 0x7FFF to 32767 everywhere commit 4fa3b37ee132d347dd51ce9172157beab4a42d7c Merge: 327216a7 96e65b4b Author: Stanislav A Date: Tue Jan 10 14:43:35 2023 +0300 Merge branch 'release/v1.8' into feature/AG-9480 commit 327216a7ee833bc0fbe41feb8da6a27b20db2536 Author: Stanislav A Date: Tue Jan 10 14:32:47 2023 +0300 update changelog commit 0af2569db8b5fad688b07e5d1547ebc399961c65 Author: Stanislav A Date: Tue Jan 10 12:55:01 2023 +0300 fix tests commit 726e63dc9cdd7b30f100782e4caafa230ee2de36 Author: Stanislav A Date: Mon Jan 9 21:16:26 2023 +0300 update description commit 105ea07b51b613ea91048390ce60ce897d234e7c Author: Stanislav A Date: Mon Jan 9 19:30:05 2023 +0300 fix helper and add scriptlet tests commit bcef888acf5bcec082ae8ddd3e1aa1791569fc14 Author: Stanislav A Date: Fri Dec 30 18:49:45 2022 +0300 draft inferValue helper commit e45a707220ea1319a525830757796ee7f769e30a Author: Stanislav A Date: Thu Dec 29 16:24:04 2022 +0300 draft trusted-set-constant --- CHANGELOG.md | 1 + src/helpers/storage-utils.js | 2 +- src/helpers/string-utils.js | 49 +++ src/scriptlets/scriptlets-list.js | 1 + src/scriptlets/set-attr.js | 2 +- src/scriptlets/set-constant.js | 25 +- src/scriptlets/trusted-set-constant.js | 274 +++++++++++++ tests/helpers/string-utils.test.js | 61 ++- tests/scriptlets/index.test.js | 1 + tests/scriptlets/trusted-set-constant.test.js | 369 ++++++++++++++++++ tests/scriptlets/trusted-set-cookie.test.js | 2 +- 11 files changed, 782 insertions(+), 5 deletions(-) create mode 100644 src/scriptlets/trusted-set-constant.js create mode 100644 tests/scriptlets/trusted-set-constant.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee0cd960..3dc2afc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased 1.8.x ### Added +- new `trusted-set-constant` scriptlet [#137](https://github.com/AdguardTeam/Scriptlets/issues/137) - `throwFunc` and `noopCallbackFunc` prop values for `set-constant` scriptlet ### Changed diff --git a/src/helpers/storage-utils.js b/src/helpers/storage-utils.js index acdb3e361..e01b1b37d 100644 --- a/src/helpers/storage-utils.js +++ b/src/helpers/storage-utils.js @@ -50,7 +50,7 @@ export const getLimitedStorageItemValue = (value) => { if (nativeIsNaN(validValue)) { throw new Error('Invalid value'); } - if (Math.abs(validValue) > 0x7FFF) { + if (Math.abs(validValue) > 32767) { throw new Error('Invalid value'); } } else if (value === 'yes') { diff --git a/src/helpers/string-utils.js b/src/helpers/string-utils.js index 2763bd091..f37dedb0b 100644 --- a/src/helpers/string-utils.js +++ b/src/helpers/string-utils.js @@ -372,3 +372,52 @@ export function generateRandomResponse(customResponseText) { customResponse = getRandomStrByLength(length); return customResponse; } + +/** + * Infers value from string argument + * Inferring goes from more specific to more ambiguous options + * Arrays, objects and strings are parsed via JSON.parse + * + * @param {string} value arbitrary string + * @returns {any} converted value + * @throws an error on unexpected input + */ +export function inferValue(value) { + if (value === 'undefined') { + return undefined; + } if (value === 'false') { + return false; + } if (value === 'true') { + return true; + } if (value === 'null') { + return null; + } if (value === 'NaN') { + return NaN; + } + + // Number class constructor works 2 times faster than JSON.parse + // and wont interpret mixed inputs like '123asd' as parseFloat would + const MAX_ALLOWED_NUM = 32767; + const numVal = Number(value); + if (!nativeIsNaN(numVal)) { + if (Math.abs(numVal) > MAX_ALLOWED_NUM) { + throw new Error('number values bigger than 32767 are not allowed'); + } + return numVal; + } + + let errorMessage = `'${value}' value type can't be inferred`; + try { + // Parse strings, arrays and objects represented as JSON strings + // '[1,2,3,"string"]' > [1, 2, 3, 'string'] + // '"arbitrary string"' > 'arbitrary string' + const parsableVal = JSON.parse(value); + if (parsableVal instanceof Object || typeof parsableVal === 'string') { + return parsableVal; + } + } catch (e) { + errorMessage += `: ${e}`; + } + + throw new TypeError(errorMessage); +} diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js index e718d182e..c2b5e5a60 100644 --- a/src/scriptlets/scriptlets-list.js +++ b/src/scriptlets/scriptlets-list.js @@ -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'; diff --git a/src/scriptlets/set-attr.js b/src/scriptlets/set-attr.js index d7d8af7e4..3dc7c5a52 100644 --- a/src/scriptlets/set-attr.js +++ b/src/scriptlets/set-attr.js @@ -60,7 +60,7 @@ export function setAttr(source, selector, attr, value = '') { if (value.length !== 0 && (nativeIsNaN(parseInt(value, 10)) || parseInt(value, 10) < 0 - || parseInt(value, 10) > 0x7FFF)) { + || parseInt(value, 10) > 32767)) { return; } diff --git a/src/scriptlets/set-constant.js b/src/scriptlets/set-constant.js index 31771a472..ca7e15f95 100644 --- a/src/scriptlets/set-constant.js +++ b/src/scriptlets/set-constant.js @@ -132,7 +132,7 @@ export function setConstant(source, property, value, stack) { if (nativeIsNaN(constantValue)) { return; } - if (Math.abs(constantValue) > 0x7FFF) { + if (Math.abs(constantValue) > 32767) { return; } } else if (value === '-1') { @@ -159,6 +159,18 @@ export function setConstant(source, property, value, stack) { return canceled; }; + /** + * Safely sets property on a given object + * + * IMPORTANT! this duplicates corresponding func in trusted-set-constant scriptlet as + * reorganizing this to common helpers will most definitely complicate debugging + * + * @param {Object} base arbitrary reachable object + * @param {string} prop property name + * @param {boolean} configurable if set property should be configurable + * @param {Object} handler custom property descriptor object + * @returns {boolean} true if prop was trapped successfully + */ const trapProp = (base, prop, configurable, handler) => { if (!handler.init(base[prop])) { return false; @@ -195,6 +207,17 @@ export function setConstant(source, property, value, stack) { return true; }; + /** + * Traverses given chain to set constant value to its end prop + * Chains that yet include non-object values (e.g null) are valid and will be + * traversed when appropriate chain member is set by an external script + * + * IMPORTANT! this duplicates corresponding func in trusted-set-constant scriptlet as + * reorganizing this to common helpers will most definitely complicate debugging + * + * @param {Object} owner object that owns chain + * @param {string} property chain of owner properties + */ const setChainPropAccess = (owner, property) => { const chainInfo = getPropertyInChain(owner, property); const { base } = chainInfo; diff --git a/src/scriptlets/trusted-set-constant.js b/src/scriptlets/trusted-set-constant.js new file mode 100644 index 000000000..5d8c612ce --- /dev/null +++ b/src/scriptlets/trusted-set-constant.js @@ -0,0 +1,274 @@ +import { + hit, + inferValue, + 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 trusted-set-constant + * @description + * Creates a constant property and assigns it a specified value. + * + * > 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. + * + * > Use [set-constant](./about-scriptlets.md#set-constant) to set predefined values and functions. + * + * **Syntax** + * ``` + * example.org#%#//scriptlet('trusted-set-constant', property, value[, stack]) + * ``` + * + * - `property` - required, path to a property (joined with `.` if needed). The property must be attached to `window`. + * - `value` - required, an arbitrary value to be set; value type is being inferred from the argument, e.g '500' will be set as number; + * to set string type value wrap argument into another pair of quotes: `'"500"'`; + * - `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** + * 1. Set property values of different types + * ``` + * ! Set string value wrapping argument into another pair of quotes + * example.org#%#//scriptlet('trusted-set-constant', 'click_r', '"null"') + * + * ✔ window.click_r === 'null' + * ✔ typeof window.click_r === 'string' + * + * ! Set inferred null value + * example.org#%#//scriptlet('trusted-set-constant', 'click_r', 'null') + * + * ✔ window.click_r === null + * ✔ typeof window.click_r === 'object' + * + * ! Set number type value + * example.org#%#//scriptlet('trusted-set-constant', 'click_r', '48') + * + * ✔ window.click_r === 48 + * ✔ typeof window.click_r === 'number' + * + * ! Set array or object as property value, argument should be a JSON string + * example.org#%#//scriptlet('trusted-set-constant', 'click_r', '[1,"string"]') + * example.org#%#//scriptlet('trusted-set-constant', 'click_r', '{"aaa":123,"bbb":{"ccc":"string"}}') + * ``` + * + * 2. Use script stack matching to set value + * ``` + * ! `document.first` will return `1` if the method is related to `checking.js` + * example.org#%#//scriptlet('trusted-set-constant', 'document.first', '1', 'checking.js') + * + * ✔ document.first === 1 // if the condition described above is met + * ``` + */ +/* eslint-enable max-len */ +export function trustedSetConstant(source, property, value, stack) { + if (!property + || !matchStackTrace(stack, new Error().stack)) { + return; + } + + let constantValue; + try { + constantValue = inferValue(value); + } catch (e) { + logMessage(source, e); + return; + } + + let canceled = false; + const mustCancel = (value) => { + if (canceled) { + return canceled; + } + canceled = value !== undefined + && constantValue !== undefined + && typeof value !== typeof constantValue + && value !== null; + return canceled; + }; + + /** + * Safely sets property on a given object + * + * IMPORTANT! this duplicates corresponding func in set-constant scriptlet as + * reorganizing this to common helpers will most definitely complicate debugging + * + * @param {Object} base arbitrary reachable object + * @param {string} prop property name + * @param {boolean} configurable if set property should be configurable + * @param {Object} handler custom property descriptor object + * @returns {boolean} true if prop was trapped successfully + */ + 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; + }; + + /** + * Traverses given chain to set constant value to its end prop + * Chains that yet include non-object values (e.g null) are valid and will be + * traversed when appropriate chain member is set by an external script + * + * IMPORTANT! this duplicates corresponding func in set-constant scriptlet as + * reorganizing this to common helpers will most definitely complicate debugging + * + * @param {Object} owner object that owns chain + * @param {string} property chain of owner properties + */ + 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, + inferValue, + 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, +]; diff --git a/tests/helpers/string-utils.test.js b/tests/helpers/string-utils.test.js index 50114b75e..a91bd17c4 100644 --- a/tests/helpers/string-utils.test.js +++ b/tests/helpers/string-utils.test.js @@ -1,4 +1,4 @@ -import { toRegExp } from '../../src/helpers'; +import { toRegExp, inferValue } from '../../src/helpers'; const { test, module } = QUnit; const name = 'scriptlets-redirects helpers'; @@ -46,3 +46,62 @@ test('Test toRegExp for invalid inputs', (assert) => { toRegExp(inputStr); }); }); + +test('inferValue works as expected with valid args', (assert) => { + // convert to number + let rawString = '1234'; + let expected = 1234; + let result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to number ok'); + + // convert to float + rawString = '-12.34'; + expected = -12.34; + result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to float ok'); + + // convert to boolean + rawString = 'false'; + expected = false; + result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to false ok'); + + rawString = 'true'; + expected = true; + result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to true ok'); + + // convert to undefined + rawString = 'undefined'; + expected = undefined; + result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to undefined ok'); + + // convert to null + rawString = 'null'; + expected = null; + result = inferValue(rawString); + assert.strictEqual(result, expected, 'value to null ok'); + + // convert to NaN + rawString = 'NaN'; + result = inferValue(rawString); + assert.ok(Number.isNaN(result), 'value to NaN ok'); + + // convert to object + rawString = '{"aaa":123,"bbb":{"ccc":"string"}}'; + expected = { + aaa: 123, + bbb: { + ccc: 'string', + }, + }; + result = inferValue(rawString); + assert.deepEqual(result, expected, 'value to object ok'); + + // convert to array + rawString = '[1,2,"string"]'; + expected = [1, 2, 'string']; + result = inferValue(rawString); + assert.deepEqual(result, expected, 'value to array ok'); +}); diff --git a/tests/scriptlets/index.test.js b/tests/scriptlets/index.test.js index 5f77855fe..aa6410e30 100644 --- a/tests/scriptlets/index.test.js +++ b/tests/scriptlets/index.test.js @@ -49,3 +49,4 @@ import './trusted-click-element.test'; import './trusted-set-cookie.test'; import './trusted-replace-fetch-response.test'; import './trusted-set-local-storage-item.test'; +import './trusted-set-constant.test'; diff --git a/tests/scriptlets/trusted-set-constant.test.js b/tests/scriptlets/trusted-set-constant.test.js new file mode 100644 index 000000000..d4ddebf8d --- /dev/null +++ b/tests/scriptlets/trusted-set-constant.test.js @@ -0,0 +1,369 @@ +/* eslint-disable no-underscore-dangle, no-console */ +import { clearGlobalProps } from '../helpers'; +import { nativeIsNaN } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'trusted-set-constant'; + +const nativeConsole = console.log; + +const afterEach = () => { + console.log = nativeConsole; + clearGlobalProps('hit', '__debug', 'counter'); +}; + +module(name, { afterEach }); + +const runScriptletFromTag = (...args) => { + const params = { + name, + args, + verbose: true, + }; + const script = document.createElement('script'); + script.textContent = window.scriptlets.invoke(params); + document.body.append(script); +}; + +const addSetPropTag = (property, value) => { + const script = document.createElement('script'); + script.textContent = `window['${property}'] = ${value};`; + document.body.append(script); +}; + +/** + * document.body.append does not work in Edge 15 + * https://caniuse.com/mdn-api_element_append + */ +const isSupported = (() => typeof document.body.append !== 'undefined')(); + +if (!isSupported) { + test('unsupported', (assert) => { + assert.ok(true, 'Browser does not support it'); + }); +} else { + test('Infer and set values correctly', (assert) => { + // setting constant to true + const trueProp = 'trueProp'; + runScriptletFromTag(trueProp, 'true'); + assert.strictEqual(window[trueProp], true, '"true" is set as boolean'); + clearGlobalProps(trueProp); + + // setting constant to false + const falseProp = 'falseProp'; + runScriptletFromTag(falseProp, 'false'); + assert.strictEqual(window[falseProp], false, '"false" is set as boolean'); + clearGlobalProps(falseProp); + + // setting constant to undefined + const undefinedProp = 'undefinedProp'; + runScriptletFromTag(undefinedProp, 'undefined'); + assert.strictEqual(window[undefinedProp], undefined, '"undefined" is set as undefined'); + clearGlobalProps(undefinedProp); + + // setting constant to null + const nullProp1 = 'nullProp'; + runScriptletFromTag(nullProp1, 'null'); + assert.strictEqual(window[nullProp1], null, '"null" is set as null'); + clearGlobalProps(nullProp1); + + // setting constant to string + const stringProp1 = 'stringProp1'; + runScriptletFromTag(stringProp1, '"123arbitrary string"'); + assert.strictEqual(window[stringProp1], '123arbitrary string', 'arbitrary type value is set'); + clearGlobalProps(stringProp1); + + const stringProp2 = 'stringProp2'; + runScriptletFromTag(stringProp2, '"true"'); + assert.strictEqual(window[stringProp2], 'true', '"true" is set as string'); + clearGlobalProps(stringProp2); + + const stringProp3 = 'stringProp3'; + runScriptletFromTag(stringProp3, '"NaN"'); + assert.strictEqual(window[stringProp3], 'NaN', '"NaN" is set as string'); + clearGlobalProps(stringProp3); + + const stringProp4 = 'stringProp4'; + runScriptletFromTag(stringProp4, '"undefined"'); + assert.strictEqual(window[stringProp4], 'undefined', '"undefined" is set as string'); + clearGlobalProps(stringProp4); + + const stringProp5 = 'stringProp5'; + runScriptletFromTag(stringProp5, '"null"'); + assert.strictEqual(window[stringProp5], 'null', '"null" is set as string'); + clearGlobalProps(stringProp5); + + // setting constant to NaN + const nanProp1 = 'nanProp'; + runScriptletFromTag(nanProp1, 'NaN'); + assert.ok(nativeIsNaN(window[nanProp1]), '"NaN" is set as NaN'); + clearGlobalProps(nanProp1); + + // setting constant to number + const numberProp = 'numberProp'; + runScriptletFromTag(numberProp, '1234'); + assert.strictEqual(window[numberProp], 1234, '"1234" is set as number'); + clearGlobalProps(numberProp); + + // setting constant to a negative number + const minusOneProp = 'minusOneProp'; + runScriptletFromTag(minusOneProp, '-12.34'); + assert.strictEqual(window[minusOneProp], -12.34, '"-12.34" is set as number'); + clearGlobalProps(minusOneProp); + + // setting constant to array + const arrayProp = 'arrayProp'; + runScriptletFromTag(arrayProp, '[1,2,3,"string"]'); + assert.deepEqual(window[arrayProp], [1, 2, 3, 'string'], '"[1,2,3,"string"]" is set as array'); + clearGlobalProps(arrayProp); + + // setting constant to array + const objectProp = 'objectProp'; + const expected = { + aaa: 123, + bbb: { + ccc: 'string', + }, + }; + runScriptletFromTag(objectProp, '{"aaa":123,"bbb":{"ccc":"string"}}'); + assert.deepEqual(window[objectProp], expected, '"{"aaa":123,"bbb":{"ccc":"string"}}" is set as object'); + clearGlobalProps(objectProp); + }); + + test('illegal values are handled', (assert) => { + assert.expect(4); + const illegalProp = 'illegalProp'; + + console.log = function log(input) { + if (typeof input !== 'string') { + return; + } + const messageLogged = input.includes('number values bigger than 32767 are not allowed') + || input.includes('value type can\'t be inferred'); + + assert.ok(messageLogged, 'appropriate message is logged'); + }; + + // not setting constant to illegalNumber + runScriptletFromTag(illegalProp, 32768); + assert.strictEqual(window[illegalProp], undefined); + clearGlobalProps(illegalProp); + + // not setting constant to unknown value + runScriptletFromTag(illegalProp, '{|'); + assert.strictEqual(window[illegalProp], undefined); + clearGlobalProps(illegalProp); + }); + + test('keep other keys after setting a value', (assert) => { + window.testObj = { + testChain: null, + }; + runScriptletFromTag('testObj.testChain.testProp', 'true'); + window.testObj.testChain = { + testProp: false, + otherProp: 'someValue', + testMethod: () => { }, + }; + assert.strictEqual(window.testObj.testChain.testProp, true, 'target prop set'); + assert.strictEqual(window.testObj.testChain.otherProp, 'someValue', 'sibling value property is kept'); + assert.strictEqual(typeof window.testObj.testChain.testMethod, 'function', 'sibling function property is kept'); + clearGlobalProps('testObj'); + }); + + test('set value on null prop', (assert) => { + // end prop is null + window.nullProp2 = null; + runScriptletFromTag('nullProp2', '15'); + assert.strictEqual(window.nullProp2, 15, 'null end prop changed'); + clearGlobalProps('nullProp2'); + }); + + test('set value through chain with empty object', (assert) => { + window.emptyObj = {}; + runScriptletFromTag('emptyObj.a.prop', '"true"'); + window.emptyObj.a = {}; + assert.strictEqual(window.emptyObj.a.prop, 'true', 'target prop set'); + clearGlobalProps('emptyObj'); + }); + + test('set value through chain with null', (assert) => { + // null prop in chain + window.nullChain = { + nullProp: null, + }; + runScriptletFromTag('nullChain.nullProp.endProp', 'true'); + window.nullChain.nullProp = { + endProp: false, + }; + assert.strictEqual(window.nullChain.nullProp.endProp, true, 'chain with null trapped'); + clearGlobalProps('nullChain'); + }); + + test('sets values to the chained properties', (assert) => { + window.chained = { property: {} }; + runScriptletFromTag('chained.property.aaa', 'true'); + assert.strictEqual(window.chained.property.aaa, true); + clearGlobalProps('chained'); + }); + + test('sets values on the same chain (defined)', (assert) => { + window.chained = { property: {} }; + runScriptletFromTag('chained.property.aaa', 'true'); + runScriptletFromTag('chained.property.bbb', '10'); + + assert.strictEqual(window.chained.property.aaa, true); + assert.strictEqual(window.chained.property.bbb, 10); + + clearGlobalProps('chained'); + }); + + test('sets values on the same chain (undefined)', (assert) => { + runScriptletFromTag('chained.property.aaa', 'true'); + runScriptletFromTag('chained.property.bbb', '10'); + window.chained = { property: {} }; + + assert.strictEqual(window.chained.property.aaa, true); + assert.strictEqual(window.chained.property.bbb, 10); + + clearGlobalProps('chained'); + }); + + test('values with same types are not overwritten, values with different types are overwritten', (assert) => { + const property = 'customProperty'; + const firstValue = 10; + const anotherValue = 100; + const anotherTypeValue = true; + + runScriptletFromTag(property, firstValue); + assert.strictEqual(window[property], firstValue); + + addSetPropTag(property, anotherValue); + assert.strictEqual(window[property], firstValue, 'values with same types are not overwritten'); + + addSetPropTag(property, anotherTypeValue); + assert.strictEqual(window[property], anotherTypeValue, 'values with different types are overwritten'); + + clearGlobalProps(property); + }); + + test('sets values correctly + stack match', (assert) => { + const stackMatch = 'trusted-set-constant'; + const trueProp = 'trueProp02'; + runScriptletFromTag(trueProp, 'true', stackMatch); + assert.strictEqual(window[trueProp], true, 'stack match: trueProp - ok'); + clearGlobalProps(trueProp); + + const numProp = 'numProp'; + runScriptletFromTag(numProp, '123', stackMatch); + assert.strictEqual(window[numProp], 123, 'stack match: numProp - ok'); + clearGlobalProps(numProp); + }); + + test('sets values correctly + no stack match', (assert) => { + window.chained = { property: {} }; + const stackNoMatch = 'no_match.js'; + + runScriptletFromTag('chained.property.aaa', 'true', stackNoMatch); + + assert.strictEqual(window.chained.property.aaa, undefined); + clearGlobalProps('chained'); + + const property = 'customProp'; + const firstValue = 10; + + runScriptletFromTag(property, firstValue, stackNoMatch); + + assert.strictEqual(window[property], undefined); + clearGlobalProps(property); + }); + + test('trusted-set-constant: does not work - invalid regexp pattern for stack arg', (assert) => { + const stackArg = '/\\/'; + + const property = 'customProp'; + const value = '10'; + + runScriptletFromTag(property, value, stackArg); + + assert.strictEqual(window[property], undefined, 'property should not be set'); + clearGlobalProps(property); + }); + + test('no value setting if chain is not relevant', (assert) => { + window.chain = { property: {} }; + runScriptletFromTag('noprop.property.aaa', 'true'); + assert.deepEqual(window.chain.property, {}, 'predefined obj was not changed'); + assert.strictEqual(window.noprop, undefined, '"noprop" was not set'); + clearGlobalProps('chain'); + }); + + test('no value setting if some property in chain is undefined while loading', (assert) => { + const testObj = { prop: undefined }; + window.chain = testObj; + runScriptletFromTag('chain.prop.aaa', 'true'); + assert.deepEqual(window.chain, testObj, 'predefined obj was not changed'); + clearGlobalProps('chain'); + }); + + test('no value setting if first property in chain is null', (assert) => { + window.chain = null; + runScriptletFromTag('chain.property.aaa', 'true'); + assert.strictEqual(window.chain, null, 'predefined obj was not changed'); + clearGlobalProps('chain'); + }); + + // for now the scriptlet does not set the chained property if one of chain prop is null. + // that might happen, for example, while loading the page. + // after the needed property is loaded, the scriptlet does not check it and do not set the value + // https://github.com/AdguardTeam/Scriptlets/issues/128 + test('set value after timeout if it was null earlier', (assert) => { + window.chain = null; + runScriptletFromTag('chain.property.aaa', 'true'); + assert.strictEqual(window.chain, null, 'predefined obj was not changed'); + + const done = assert.async(); + + setTimeout(() => { + window.chain = { property: {} }; + }, 50); + + setTimeout(() => { + assert.strictEqual(window.chain.property.aaa, undefined, 'chained prop was NOT set after delay'); + done(); + }, 100); + + clearGlobalProps('chain'); + }); + + test('set value after loop reassignment', (assert) => { + window.loopObj = { + chainProp: { + aaa: true, + }, + }; + runScriptletFromTag('loopObj.chainProp.bbb', '1'); + // eslint-disable-next-line no-self-assign + window.loopObj = window.loopObj; + window.loopObj.chainProp.bbb = 0; + assert.strictEqual(window.loopObj.chainProp.bbb, 1, 'value set after loop reassignment'); + clearGlobalProps('loopObj'); + }); + + test('trying to set non-configurable silently exits', (assert) => { + assert.expect(2); + console.log = function log(input) { + if (typeof input !== 'string') { + return; + } + assert.ok(input.includes('testProp'), 'non-configurable prop logged'); + }; + Object.defineProperty(window, 'testProp', { + value: 5, + configurable: false, + }); + runScriptletFromTag('window.testProp', '0'); + assert.strictEqual(window.testProp, 5, 'error avoided'); + clearGlobalProps('testProp'); + }); +} diff --git a/tests/scriptlets/trusted-set-cookie.test.js b/tests/scriptlets/trusted-set-cookie.test.js index 1800e6d44..3a92e2bd4 100644 --- a/tests/scriptlets/trusted-set-cookie.test.js +++ b/tests/scriptlets/trusted-set-cookie.test.js @@ -59,7 +59,7 @@ test('Set cookie with current time value', (assert) => { // Some time will pass between calling scriptlet // and qunit running assertion - const tolerance = 100; + const tolerance = 125; const cookieValue = parseCookieString(document.cookie)[cName]; const currentTime = new Date().getTime(); const timeDiff = currentTime - cookieValue;