diff --git a/src/helpers/string-utils.js b/src/helpers/string-utils.js index bccb08a4f..67fb51643 100644 --- a/src/helpers/string-utils.js +++ b/src/helpers/string-utils.js @@ -355,11 +355,11 @@ export function generateRandomResponse(customResponseText) { /** * Infers value from string argument + * Inferring goes from more specific to more ambiguous options * @param {string} value * @returns {any} */ export function inferValue(value) { - // describe waterfall here if (value === 'undefined') { return undefined; } if (value === 'false') { @@ -372,18 +372,23 @@ export function inferValue(value) { return NaN; } - const numVal = parseFloat(value, 10) + const numVal = parseFloat(value, 10); if (!nativeIsNaN(numVal)) { + if (Math.abs(numVal) > 0x7FFF) { + throw new Error('number values bigger than 32767 are not allowed'); + } return numVal; } + let errorMessage = `'${value}' value type can't be inferred`; try { const parsableVal = JSON.parse(value); if (parsableVal instanceof Object) { return parsableVal; } - } catch { /* no */ } + } catch (e) { + errorMessage += `: ${e}`; + } - const errorMessage = `'${value}' argument's value can't be inferred`; throw new TypeError(errorMessage); -} \ No newline at end of file +} diff --git a/src/scriptlets/trusted-set-constant.js b/src/scriptlets/trusted-set-constant.js index e9171ccd0..32aee9e4d 100644 --- a/src/scriptlets/trusted-set-constant.js +++ b/src/scriptlets/trusted-set-constant.js @@ -93,13 +93,19 @@ import { * ``` */ /* eslint-enable max-len */ -export function trustedSetConstant(source, property, value, infer = false, stack) { +export function trustedSetConstant(source, property, value, shouldInfer, stack) { if (!property || !matchStackTrace(stack, new Error().stack)) { return; } - const constantValue = infer ? inferValue(value, type) : value; + let constantValue; + try { + constantValue = shouldInfer === 'infer' ? inferValue(value) : value; + } catch (e) { + logMessage(source, e); + return; + } let canceled = false; const mustCancel = (value) => { diff --git a/tests/helpers/prevent-utils.js b/tests/helpers/prevent-utils.test.js similarity index 100% rename from tests/helpers/prevent-utils.js rename to tests/helpers/prevent-utils.test.js diff --git a/tests/helpers/string-utils.test.js b/tests/helpers/string-utils.test.js index da7a872eb..a91bd17c4 100644 --- a/tests/helpers/string-utils.test.js +++ b/tests/helpers/string-utils.test.js @@ -5,47 +5,47 @@ const name = 'scriptlets-redirects helpers'; module(name); -// test('Test toRegExp for valid inputs', (assert) => { -// const DEFAULT_VALUE = '.?'; -// const defaultRegexp = new RegExp(DEFAULT_VALUE); -// let inputStr; -// let expRegex; - -// inputStr = '/abc/'; -// expRegex = /abc/; -// assert.deepEqual(toRegExp(inputStr), expRegex); - -// inputStr = '/[a-z]{1,9}/'; -// expRegex = /[a-z]{1,9}/; -// assert.deepEqual(toRegExp(inputStr), expRegex); - -// inputStr = ''; -// assert.deepEqual(toRegExp(inputStr), defaultRegexp); -// }); - -// test('Test toRegExp for invalid inputs', (assert) => { -// let inputStr; - -// assert.throws(() => { -// inputStr = '/\\/'; -// toRegExp(inputStr); -// }); - -// assert.throws(() => { -// inputStr = '/[/'; -// toRegExp(inputStr); -// }); - -// assert.throws(() => { -// inputStr = '/*/'; -// toRegExp(inputStr); -// }); - -// assert.throws(() => { -// inputStr = '/[0-9]++/'; -// toRegExp(inputStr); -// }); -// }); +test('Test toRegExp for valid inputs', (assert) => { + const DEFAULT_VALUE = '.?'; + const defaultRegexp = new RegExp(DEFAULT_VALUE); + let inputStr; + let expRegex; + + inputStr = '/abc/'; + expRegex = /abc/; + assert.deepEqual(toRegExp(inputStr), expRegex); + + inputStr = '/[a-z]{1,9}/'; + expRegex = /[a-z]{1,9}/; + assert.deepEqual(toRegExp(inputStr), expRegex); + + inputStr = ''; + assert.deepEqual(toRegExp(inputStr), defaultRegexp); +}); + +test('Test toRegExp for invalid inputs', (assert) => { + let inputStr; + + assert.throws(() => { + inputStr = '/\\/'; + toRegExp(inputStr); + }); + + assert.throws(() => { + inputStr = '/[/'; + toRegExp(inputStr); + }); + + assert.throws(() => { + inputStr = '/*/'; + toRegExp(inputStr); + }); + + assert.throws(() => { + inputStr = '/[0-9]++/'; + toRegExp(inputStr); + }); +}); test('inferValue works as expected with valid args', (assert) => { // convert to number 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..91d5c4812 --- /dev/null +++ b/tests/scriptlets/trusted-set-constant.test.js @@ -0,0 +1,340 @@ +/* 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')(); + +const SHOULD_INFER_KEYWORD = 'infer'; + +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', SHOULD_INFER_KEYWORD); + assert.strictEqual(window[trueProp], true, '"true" is set as boolean'); + clearGlobalProps(trueProp); + + // setting constant to false + const falseProp = 'falseProp'; + runScriptletFromTag(falseProp, 'false', SHOULD_INFER_KEYWORD); + assert.strictEqual(window[falseProp], false, '"false" is set as boolean'); + clearGlobalProps(falseProp); + + // setting constant to undefined + const undefinedProp = 'undefinedProp'; + runScriptletFromTag(undefinedProp, 'undefined', SHOULD_INFER_KEYWORD); + assert.strictEqual(window[undefinedProp], undefined, '"undefined" is set as undefined'); + clearGlobalProps(undefinedProp); + + // setting constant to null + const nullProp1 = 'nullProp'; + runScriptletFromTag(nullProp1, 'null', SHOULD_INFER_KEYWORD); + assert.strictEqual(window[nullProp1], null, '"null" is set as null'); + clearGlobalProps(nullProp1); + + // setting constant to NaN + const nanProp1 = 'nanProp'; + runScriptletFromTag(nanProp1, 'NaN', SHOULD_INFER_KEYWORD); + assert.ok(nativeIsNaN(window[nanProp1]), '"NaN" is set as NaN'); + clearGlobalProps(nanProp1); + + // setting constant to number + const numberProp = 'numberProp'; + runScriptletFromTag(numberProp, '1234', SHOULD_INFER_KEYWORD); + assert.strictEqual(window[numberProp], 1234, '"1234" is set as number'); + clearGlobalProps(numberProp); + + // setting constant to -1 + const minusOneProp = 'minusOneProp'; + runScriptletFromTag(minusOneProp, '-12.34', SHOULD_INFER_KEYWORD); + 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"]', SHOULD_INFER_KEYWORD); + 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"}}', SHOULD_INFER_KEYWORD); + 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) { + 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, SHOULD_INFER_KEYWORD); + assert.strictEqual(window[illegalProp], undefined); + clearGlobalProps(illegalProp); + + // not setting constant to unknown value + runScriptletFromTag(illegalProp, '{|', SHOULD_INFER_KEYWORD); + 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', SHOULD_INFER_KEYWORD); + 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', SHOULD_INFER_KEYWORD); + 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', SHOULD_INFER_KEYWORD); + 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', SHOULD_INFER_KEYWORD); + 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', SHOULD_INFER_KEYWORD); + runScriptletFromTag('chained.property.bbb', '10', SHOULD_INFER_KEYWORD); + + 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', SHOULD_INFER_KEYWORD); + runScriptletFromTag('chained.property.bbb', '10', SHOULD_INFER_KEYWORD); + window.chained = { property: {} }; + + assert.strictEqual(window.chained.property.aaa, true); + assert.strictEqual(window.chained.property.bbb, 10); + + clearGlobalProps('chained'); + }); + + // FIXME + 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', SHOULD_INFER_KEYWORD, stackMatch); + assert.strictEqual(window[trueProp], true, 'stack match: trueProp - ok'); + clearGlobalProps(trueProp); + + const numProp = 'numProp'; + runScriptletFromTag(numProp, '123', SHOULD_INFER_KEYWORD, 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', SHOULD_INFER_KEYWORD); + // 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) { + assert.ok(input.includes('testProp'), 'non-configurable prop logged'); + }; + Object.defineProperty(window, 'testProp', { + value: 5, + configurable: false, + }); + runScriptletFromTag('window.testProp', '0', SHOULD_INFER_KEYWORD); + assert.strictEqual(window.testProp, 5, 'error avoided'); + clearGlobalProps('testProp'); + }); +}