diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6e51a72..d429d0d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ### Added - `noopThrow` and `noopCallbackFunc` prop values for `set-constant` scriptlet +### Changed + +- add decimal delay matching for `prevent-setInterval` and `prevent-setTimeout` [#247](https://github.com/AdguardTeam/Scriptlets/issues/247) + ### Fixed - `prevent-xhr` and `trusted-replace-xhr-response` closure bug on multiple requests [#261](https://github.com/AdguardTeam/Scriptlets/issues/261) @@ -41,30 +45,29 @@ - spread of args bug at `getXhrData` call for `trusted-replace-xhr-response` - request properties array not being served to `getRequestData` and `parseMatchProps` helpers - ## v1.7.3 ### Added - [Trusted scriptlets](./README.md#trusted-scriptlets) with extended capabilities: - - trusted-click-element [#23](https://github.com/AdguardTeam/Scriptlets/issues/23) - - trusted-replace-xhr-response [#202](https://github.com/AdguardTeam/Scriptlets/issues/202) - - trusted-replace-fetch-response - - trusted-set-local-storage-item - - trusted-set-cookie + - `trusted-click-element` [#23](https://github.com/AdguardTeam/Scriptlets/issues/23) + - `trusted-replace-xhr-response` [#202](https://github.com/AdguardTeam/Scriptlets/issues/202) + - `trusted-replace-fetch-response` + - `trusted-set-local-storage-item` + - `trusted-set-cookie` - Scriptlets: - - xml-prune [#249](https://github.com/AdguardTeam/Scriptlets/issues/249) + - `xml-prune` [#249](https://github.com/AdguardTeam/Scriptlets/issues/249) -### Improved +### Changed - Scriptlets: - - prevent-element-src-loading [#228](https://github.com/AdguardTeam/Scriptlets/issues/228) - - prevent-fetch [#216](https://github.com/AdguardTeam/Scriptlets/issues/216) - - abort-on-stack-trace [#201](https://github.com/AdguardTeam/Scriptlets/issues/201) - - abort-current-inline-script [#251](https://github.com/AdguardTeam/Scriptlets/issues/251) - - set-cookie & set-cookie-reload + - `prevent-element-src-loading` [#228](https://github.com/AdguardTeam/Scriptlets/issues/228) + - `prevent-fetch` [#216](https://github.com/AdguardTeam/Scriptlets/issues/216) + - `abort-on-stack-trace` [#201](https://github.com/AdguardTeam/Scriptlets/issues/201) + - `abort-current-inline-script` [#251](https://github.com/AdguardTeam/Scriptlets/issues/251) + - `set-cookie` & `set-cookie-reload` - Redirects: - - google-ima3 [#255](https://github.com/AdguardTeam/Scriptlets/issues/255) - - metrika-yandex-tag [#254](https://github.com/AdguardTeam/Scriptlets/issues/254) - - googlesyndication-adsbygoogle [#252](https://github.com/AdguardTeam/Scriptlets/issues/252) + - `google-ima3` [#255](https://github.com/AdguardTeam/Scriptlets/issues/255) + - `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254) + - `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252) diff --git a/src/helpers/prevent-utils.js b/src/helpers/prevent-utils.js index b75ff2957..e93270800 100644 --- a/src/helpers/prevent-utils.js +++ b/src/helpers/prevent-utils.js @@ -4,6 +4,7 @@ import { isValidMatchNumber, isValidMatchStr, } from './string-utils'; +import { nativeIsNaN } from './number-utils'; /** * Checks whether the passed arg is proper callback @@ -18,6 +19,18 @@ export const isValidCallback = (callback) => { || typeof callback === 'string'; }; +/** + * Parses delay argument of setTimeout / setInterval methods into + * rounded down number for number/string values or passes on for other types. + * Needed for prevent-setTimeout and prevent-setInterval + * @param {any} delay + * @returns {any} number as parsed delay or any input type if `delay` is not parsable + */ +export const parseRawDelay = (delay) => { + const parsedDelay = Math.floor(parseInt(delay, 10)); + return typeof parsedDelay === 'number' && !nativeIsNaN(parsedDelay) ? parsedDelay : delay; +}; + /** * Checks whether 'callback' and 'delay' are matching * by given parameters 'matchCallback' and 'matchDelay'. @@ -45,16 +58,20 @@ export const isPreventionNeeded = ({ const { isInvertedMatch, matchRegexp } = parseMatchArg(matchCallback); const { isInvertedDelayMatch, delayMatch } = parseDelayArg(matchDelay); + // Parse delay for decimal, string and non-number values + // https://github.com/AdguardTeam/Scriptlets/issues/247 + const parsedDelay = parseRawDelay(delay); + let shouldPrevent = false; // https://github.com/AdguardTeam/Scriptlets/issues/105 const callbackStr = String(callback); if (delayMatch === null) { shouldPrevent = matchRegexp.test(callbackStr) !== isInvertedMatch; } else if (!matchCallback) { - shouldPrevent = (delay === delayMatch) !== isInvertedDelayMatch; + shouldPrevent = (parsedDelay === delayMatch) !== isInvertedDelayMatch; } else { shouldPrevent = matchRegexp.test(callbackStr) !== isInvertedMatch - && (delay === delayMatch) !== isInvertedDelayMatch; + && (parsedDelay === delayMatch) !== isInvertedDelayMatch; } return shouldPrevent; }; diff --git a/src/scriptlets/prevent-setInterval.js b/src/scriptlets/prevent-setInterval.js index 8f9f1fa7c..d831700a9 100644 --- a/src/scriptlets/prevent-setInterval.js +++ b/src/scriptlets/prevent-setInterval.js @@ -15,6 +15,7 @@ import { escapeRegExp, nativeIsFinite, isValidMatchNumber, + parseRawDelay, } from '../helpers/index'; /* eslint-disable max-len */ @@ -44,6 +45,7 @@ import { * - `matchDelay` - optional, must be an integer. * If starts with `!`, scriptlet will not match the delay but all other will be defused. * If do not start with `!`, the delay passed to the `setInterval` call will be matched. + * Decimal delay values will be rounded down, e.g `10.95` will be matched by `matchDelay` with value `10`. * * > If `prevent-setInterval` log looks like `setInterval(undefined, 1000)`, * it means that no callback was passed to setInterval() and that's not scriptlet issue @@ -118,6 +120,21 @@ import { * window.value = "test -- executed"; * }, 500); * ``` + * + * 5. Prevents `setInterval` calls if the callback contains `value` and delay is a decimal. + * ``` + * example.org#%#//scriptlet('prevent-setInterval', 'value', '300') + * ``` + * + * For instance, the following calls will be prevented: + * ```javascript + * setInterval(function () { + * window.test = "value"; + * }, 300); + * setInterval(function () { + * window.test = "value"; + * }, 300 + Math.random()); + * ``` */ /* eslint-enable max-len */ export function preventSetInterval(source, matchCallback, matchDelay) { @@ -218,4 +235,5 @@ preventSetInterval.injections = [ escapeRegExp, nativeIsFinite, isValidMatchNumber, + parseRawDelay, ]; diff --git a/src/scriptlets/prevent-setTimeout.js b/src/scriptlets/prevent-setTimeout.js index 846d3b95b..ceb6ca562 100644 --- a/src/scriptlets/prevent-setTimeout.js +++ b/src/scriptlets/prevent-setTimeout.js @@ -15,6 +15,7 @@ import { isValidStrPattern, nativeIsFinite, isValidMatchNumber, + parseRawDelay, } from '../helpers/index'; /* eslint-disable max-len */ @@ -44,6 +45,7 @@ import { * - `matchDelay` - optional, must be an integer. * If starts with `!`, scriptlet will not match the delay but all other will be defused. * If do not start with `!`, the delay passed to the `setTimeout` call will be matched. + * Decimal delay values will be rounded down, e.g `10.95` will be matched by `matchDelay` with value `10`. * * > If `prevent-setTimeout` log looks like `setTimeout(undefined, 1000)`, * it means that no callback was passed to setTimeout() and that's not scriptlet issue @@ -118,6 +120,21 @@ import { * window.value = "test -- executed"; * }, 500); * ``` + * + * 5. Prevents `setTimeout` calls if the callback contains `value` and delay is a decimal. + * ``` + * example.org#%#//scriptlet('prevent-setTimeout', 'value', '300') + * ``` + * + * For instance, the following calls will be prevented: + * ```javascript + * setTimeout(function () { + * window.test = "value"; + * }, 300); + * setTimeout(function () { + * window.test = "value"; + * }, 300 + Math.random()); + * ``` */ /* eslint-enable max-len */ export function preventSetTimeout(source, matchCallback, matchDelay) { @@ -221,4 +238,5 @@ preventSetTimeout.injections = [ isValidStrPattern, nativeIsFinite, isValidMatchNumber, + parseRawDelay, ]; diff --git a/tests/helpers/prevent-utils.js b/tests/helpers/prevent-utils.js new file mode 100644 index 000000000..0155cb4d9 --- /dev/null +++ b/tests/helpers/prevent-utils.js @@ -0,0 +1,23 @@ +import { parseRawDelay } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'scriptlets-redirects helpers'; + +module(name); +test('Test parseRawDelay', (assert) => { + assert.strictEqual(parseRawDelay(0), 0, 'parsing number ok'); + assert.strictEqual(parseRawDelay(10), 10, 'parsing number ok'); + assert.strictEqual(parseRawDelay(10.123), 10, 'parsing number ok'); + + assert.strictEqual(parseRawDelay('0'), 0, 'parsing number in string ok'); + assert.strictEqual(parseRawDelay('10'), 10, 'parsing number in string ok'); + assert.strictEqual(parseRawDelay('10.123'), 10, 'parsing number in string ok'); + + assert.strictEqual(parseRawDelay('string'), 'string', 'parsing string ok'); + + assert.strictEqual(parseRawDelay(null), null, 'parsing other types ok'); + assert.strictEqual(parseRawDelay(undefined), undefined, 'parsing other types ok'); + assert.strictEqual(parseRawDelay(false), false, 'parsing other types ok'); + // as NaN !== NaN + assert.strictEqual(parseRawDelay(NaN).toString(), 'NaN', 'parsing other types ok'); +}); diff --git a/tests/scriptlets/prevent-setInterval.test.js b/tests/scriptlets/prevent-setInterval.test.js index c81364506..47b75559d 100644 --- a/tests/scriptlets/prevent-setInterval.test.js +++ b/tests/scriptlets/prevent-setInterval.test.js @@ -18,7 +18,7 @@ const beforeEach = () => { const afterEach = () => { window.setInterval = nativeSetInterval; testIntervals.forEach((i) => (clearInterval(i))); - clearGlobalProps('hit', '__debug', 'aaa', 'one', 'two', 'three', 'four'); + clearGlobalProps('hit', '__debug', 'aaa', 'one', 'two', 'three', 'four', 'five'); console.log = nativeConsole; // eslint-disable-line no-console }; @@ -67,7 +67,11 @@ test('no args -- logging', (assert) => { // We need to run our assertion after all timeouts setTimeout(() => { assert.strictEqual(window.hit, 'FIRED', 'hit fired'); - assert.strictEqual(loggedMessage, `prevent-setInterval: setInterval(${callback.toString()}, ${timeout})`, 'console.hit input ok'); + assert.strictEqual( + loggedMessage, + `prevent-setInterval: setInterval(${callback.toString()}, ${timeout})`, + 'console.hit input ok', + ); assert.strictEqual(window[agLogSetInterval], 'changed', 'property changed'); clearGlobalProps(agLogSetInterval); done(); @@ -368,3 +372,114 @@ test('prevent-setInterval: single square bracket in matchCallback', (assert) => const testInterval = setInterval(callback, 10); testIntervals.push(testInterval); }); + +test('match any callback + decimal delay', (assert) => { + const done = assert.async(); + window.one = 'old one'; + window.two = 'old two'; + window.three = 'old three'; + // We need to run our assertion after all timeouts + setTimeout(() => { + assert.equal(window.one, 'NEW ONE', 'property \'one\' is changed due to non-matched delay'); + assert.equal(window.two, 'old two', 'property \'two\' should NOT be changed'); + assert.equal(window.three, 'old three', 'property \'three\' should NOT be changed'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '10']; + runScriptlet(name, scriptletArgs); + + // only this one SHOULD NOT be prevented because of delay mismatch + const one = () => { window.one = 'NEW ONE'; }; + const intervalTest1 = setInterval(one, 25); + testIntervals.push(intervalTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const intervalTest2 = setInterval(second, 10.05); + testIntervals.push(intervalTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const intervalTest3 = setInterval(third, 10.95); + testIntervals.push(intervalTest3); +}); + +test('match any callback + non-number, decimal and string delays', (assert) => { + const done = assert.async(); + window.one = 'old one'; + window.two = 'old two'; + window.three = 'old three'; + window.four = 'old four'; + window.five = 'old five'; + // We need to run our assertion after all timeouts + setTimeout(() => { + assert.equal(window.one, 'old one', 'property \'one\' should NOT be changed'); + assert.equal(window.two, 'NEW TWO', 'property \'two\' should be changed'); + assert.equal(window.three, 'NEW THREE', 'property \'three\' should be changed'); + + assert.equal(window.four, 'old four', 'property \'four\' should NOT be changed'); + assert.equal(window.five, 'NEW FIVE', 'property \'five\' should be changed'); + + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '25']; + runScriptlet(name, scriptletArgs); + + // only this one SHOULD NOT be prevented because of delay mismatch + const one = () => { window.one = 'NEW ONE'; }; + const intervalTest1 = setInterval(one, 25.123); + testIntervals.push(intervalTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const intervalTest2 = setInterval(second, null); + testIntervals.push(intervalTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const intervalTest3 = setInterval(third, false); + testIntervals.push(intervalTest3); + + // test with string delays + const fourth = () => { window.four = 'NEW FOUR'; }; + const intervalTest4 = setInterval(fourth, '25.123'); + testIntervals.push(intervalTest4); + + const fifth = () => { window.five = 'NEW FIVE'; }; + const intervalTest5 = setInterval(fifth, '10'); + testIntervals.push(intervalTest5); +}); + +test('match any callback, falsy non-numbers delays dont collide with 0 ', (assert) => { + const done = assert.async(); + window.one = 'one'; + window.two = 'two'; + window.three = 'three'; + // We need to run our assertion after all timeouts + setTimeout(() => { + assert.equal(window.one, 'one', 'property \'one\' should NOT be changed'); + assert.equal(window.two, 'NEW TWO', 'property \'two\' should be changed'); + assert.equal(window.three, 'NEW THREE', 'property \'three\' should be changed'); + + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '0']; + runScriptlet(name, scriptletArgs); + + const first = () => { window.one = 'NEW ONE'; }; + const intervalTest1 = setInterval(first, 0); + testIntervals.push(intervalTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const intervalTest2 = setInterval(second, null); + testIntervals.push(intervalTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const intervalTest3 = setInterval(third, undefined); + testIntervals.push(intervalTest3); +}); diff --git a/tests/scriptlets/prevent-setTimeout.test.js b/tests/scriptlets/prevent-setTimeout.test.js index b1454f543..0010cfa89 100644 --- a/tests/scriptlets/prevent-setTimeout.test.js +++ b/tests/scriptlets/prevent-setTimeout.test.js @@ -18,7 +18,7 @@ const beforeEach = () => { const afterEach = () => { window.setTimeout = nativeSetTimeout; testTimeouts.forEach((t) => (clearTimeout(t))); - clearGlobalProps('hit', '__debug', 'one', 'two', 'three', 'four'); + clearGlobalProps('hit', '__debug', 'one', 'two', 'three', 'four', 'five'); console.log = nativeConsole; // eslint-disable-line no-console }; @@ -373,3 +373,114 @@ test('prevent-setTimeout: single square bracket in matchCallback', (assert) => { const timeoutTest = setTimeout(callback, 10); testTimeouts.push(timeoutTest); }); + +test('match any callback + decimal delay', (assert) => { + const done = assert.async(); + window.one = 'one'; + window.two = 'two'; + window.three = 'three'; + // We need to run our assertion after all timeouts + nativeSetTimeout(() => { + assert.equal(window.one, 'NEW ONE', 'property \'one\' is changed due to non-matched delay'); + assert.equal(window.two, 'two', 'property \'two\' should NOT be changed'); + assert.equal(window.three, 'three', 'property \'three\' should NOT be changed'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '10']; + runScriptlet(name, scriptletArgs); + + // only this one SHOULD NOT be prevented because of delay mismatch + const first = () => { window.one = 'NEW ONE'; }; + const timeoutTest1 = setTimeout(first, 30); + testTimeouts.push(timeoutTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const timeoutTest2 = setTimeout(second, 10.05); + testTimeouts.push(timeoutTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const timeoutTest3 = setTimeout(third, 10.95); + testTimeouts.push(timeoutTest3); +}); + +test('match any callback + non-number, decimal and string delays', (assert) => { + const done = assert.async(); + window.one = 'one'; + window.two = 'two'; + window.three = 'three'; + window.four = 'old four'; + window.five = 'old five'; + // We need to run our assertion after all timeouts + nativeSetTimeout(() => { + assert.equal(window.one, 'one', 'property \'one\' should NOT be changed'); + assert.equal(window.two, 'NEW TWO', 'property \'two\' should be changed'); + assert.equal(window.three, 'NEW THREE', 'property \'three\' should be changed'); + + assert.equal(window.four, 'old four', 'property \'four\' should NOT be changed'); + assert.equal(window.five, 'NEW FIVE', 'property \'five\' should be changed'); + + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '25']; + runScriptlet(name, scriptletArgs); + + // only this one SHOULD NOT be prevented because of delay mismatch + const first = () => { window.one = 'NEW ONE'; }; + const timeoutTest1 = setTimeout(first, 25.123); + testTimeouts.push(timeoutTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const timeoutTest2 = setTimeout(second, null); + testTimeouts.push(timeoutTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const timeoutTest3 = setTimeout(third, true); + testTimeouts.push(timeoutTest3); + + // test with string delays + const fourth = () => { window.four = 'NEW FOUR'; }; + const timeoutTest4 = setTimeout(fourth, '25.123'); + testTimeouts.push(timeoutTest4); + + const fifth = () => { window.five = 'NEW FIVE'; }; + const timeoutTest5 = setTimeout(fifth, '10'); + testTimeouts.push(timeoutTest5); +}); + +test('match any callback, falsy non-numbers delays dont collide with 0 ', (assert) => { + const done = assert.async(); + window.one = 'one'; + window.two = 'two'; + window.three = 'three'; + // We need to run our assertion after all timeouts + nativeSetTimeout(() => { + assert.equal(window.one, 'one', 'property \'one\' should NOT be changed'); + assert.equal(window.two, 'NEW TWO', 'property \'two\' should be changed'); + assert.equal(window.three, 'NEW THREE', 'property \'three\' should be changed'); + + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + done(); + }, 100); + + // run scriptlet code + const scriptletArgs = ['', '0']; + runScriptlet(name, scriptletArgs); + + const first = () => { window.one = 'NEW ONE'; }; + const timeoutTest1 = setTimeout(first, 0); + testTimeouts.push(timeoutTest1); + + const second = () => { window.two = 'NEW TWO'; }; + const timeoutTest2 = setTimeout(second, null); + testTimeouts.push(timeoutTest2); + + const third = () => { window.three = 'NEW THREE'; }; + const timeoutTest3 = setTimeout(third, undefined); + testTimeouts.push(timeoutTest3); +}); diff --git a/tests/scriptlets/prevent-xhr.test.js b/tests/scriptlets/prevent-xhr.test.js index 91dc1585e..18b149ed2 100644 --- a/tests/scriptlets/prevent-xhr.test.js +++ b/tests/scriptlets/prevent-xhr.test.js @@ -598,8 +598,7 @@ if (isSupported) { runScriptlet(name, MATCH_DATA); - const done1 = assert.async(); - const done2 = assert.async(); + const done = assert.async(2); const xhr1 = new XMLHttpRequest(); const xhr2 = new XMLHttpRequest(); @@ -611,7 +610,7 @@ if (isSupported) { assert.strictEqual(xhr1.readyState, 4, 'Response done'); assert.ok(xhr1.response, 'Response data exists'); assert.strictEqual(window.hit, undefined, 'hit should not fire'); - done1(); + done(); }; xhr2.onload = () => { @@ -619,7 +618,7 @@ if (isSupported) { assert.strictEqual(typeof xhr2.responseText, 'string', 'Response text mocked'); assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); clearGlobalProps('hit'); - done2(); + done(); }; xhr1.send();