diff --git a/scripts/build-tests.js b/scripts/build-tests.js index 760bcb998..d75871f96 100644 --- a/scripts/build-tests.js +++ b/scripts/build-tests.js @@ -81,10 +81,10 @@ const getTestConfigs = () => { const MULTIPLE_TEST_FILES_DIRS = [ 'scriptlets', 'redirects', + 'helpers', ]; const ONE_TEST_FILE_DIRS = [ 'lib-tests', - 'helpers', ]; const multipleFilesConfigs = MULTIPLE_TEST_FILES_DIRS diff --git a/src/helpers/noop.js b/src/helpers/noop.js index cdd9e5e6b..b4cca11ec 100644 --- a/src/helpers/noop.js +++ b/src/helpers/noop.js @@ -53,10 +53,11 @@ export const noopObject = () => ({}); export const noopPromiseReject = () => Promise.reject(); // eslint-disable-line compat/compat /** - * Returns Promise object that is resolved with a response - * @param {string} [responseBody='{}'] value of response body + * Returns Promise object that is resolved value of response body + * @param {string} [url=''] value of response url to set on response object + * @param {string} [response='default'] value of response type to set on response object */ -export const noopPromiseResolve = (responseBody = '{}') => { +export const noopPromiseResolve = (responseBody = '{}', responseUrl = '', responseType = 'default') => { if (typeof Response === 'undefined') { return; } @@ -65,6 +66,14 @@ export const noopPromiseResolve = (responseBody = '{}') => { status: 200, statusText: 'OK', }); + + // Mock response' url & type to avoid adb checks + // https://github.com/AdguardTeam/Scriptlets/issues/216 + Object.defineProperties(response, { + url: { value: responseUrl }, + type: { value: responseType }, + }); + // eslint-disable-next-line compat/compat, consistent-return return Promise.resolve(response); }; diff --git a/src/helpers/request-utils.js b/src/helpers/request-utils.js index b80370e3a..bc93ee3c7 100644 --- a/src/helpers/request-utils.js +++ b/src/helpers/request-utils.js @@ -86,18 +86,40 @@ export const getXhrData = (method, url, async, user, password) => { export const parseMatchProps = (propsToMatchStr) => { const PROPS_DIVIDER = ' '; const PAIRS_MARKER = ':'; + const LEGAL_MATCH_PROPS = [ + 'method', + 'url', + 'headers', + 'body', + 'mode', + 'credentials', + 'cache', + 'redirect', + 'referrer', + 'referrerPolicy', + 'integrity', + 'keepalive', + 'signal', + 'async', + ]; const propsObj = {}; const props = propsToMatchStr.split(PROPS_DIVIDER); props.forEach((prop) => { const dividerInd = prop.indexOf(PAIRS_MARKER); - if (dividerInd === -1) { - propsObj.url = prop; - } else { - const key = prop.slice(0, dividerInd); + + const key = prop.slice(0, dividerInd); + const hasLegalMatchProp = LEGAL_MATCH_PROPS.indexOf(key) !== -1; + + if (hasLegalMatchProp) { const value = prop.slice(dividerInd + 1); propsObj[key] = value; + } else { + // Escape multiple colons in prop + // i.e regex value and/or url with protocol specified, with or without 'url:' match prop + // https://github.com/AdguardTeam/Scriptlets/issues/216#issuecomment-1178591463 + propsObj.url = prop; } }); diff --git a/src/scriptlets/prevent-fetch.js b/src/scriptlets/prevent-fetch.js index 061322f02..ae8d9d7ab 100644 --- a/src/scriptlets/prevent-fetch.js +++ b/src/scriptlets/prevent-fetch.js @@ -30,7 +30,7 @@ import { * * **Syntax** * ``` - * example.org#%#//scriptlet('prevent-fetch'[, propsToMatch[, responseBody]]) + * example.org#%#//scriptlet('prevent-fetch'[, propsToMatch[, responseBody[, responseType]]]) * ``` * * - `propsToMatch` - optional, string of space-separated properties to match; possible props: @@ -41,8 +41,12 @@ import { * - responseBody - optional, string for defining response body value, defaults to `emptyObj`. Possible values: * - `emptyObj` - empty object * - `emptyArr` - empty array + * - responseType - optional, string for defining response type, defaults to `default`. Possible values: + * - default + * - opaque + * * > Usage with no arguments will log fetch calls to browser console; - * which is useful for debugging but permitted for production filter lists. + * which is useful for debugging but not permitted for production filter lists. * * **Examples** * 1. Log all fetch calls @@ -82,7 +86,7 @@ import { * ``` */ /* eslint-enable max-len */ -export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') { +export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType = 'default') { // do nothing if browser does not support fetch or Proxy (e.g. Internet Explorer) // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy @@ -101,6 +105,13 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') { return; } + // Skip disallowed response types + if (!(responseType === 'default' || responseType === 'opaque')) { + // eslint-disable-next-line no-console + console.log(`Invalid parameter: ${responseType}`); + return; + } + const handlerWrapper = (target, thisArg, args) => { let shouldPrevent = false; const fetchData = getFetchData(args); @@ -131,7 +142,7 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj') { if (shouldPrevent) { hit(source); - return noopPromiseResolve(strResponseBody); + return noopPromiseResolve(strResponseBody, fetchData.url, responseType); } return Reflect.apply(target, thisArg, args); diff --git a/tests/helpers/fetch-utils.test.js b/tests/helpers/fetch-utils.test.js new file mode 100644 index 000000000..03440fd39 --- /dev/null +++ b/tests/helpers/fetch-utils.test.js @@ -0,0 +1,87 @@ +import { parseMatchProps } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'scriptlets-redirects helpers'; + +module(name); + +const GET_METHOD = 'GET'; +const METHOD_PROP = 'method'; +const URL_PROP = 'url'; + +const URL1 = 'example.com'; +const URL2 = 'http://example.com'; +const URL3 = '/^https?://example.org/'; +const URL4 = '/^https?://example.org/section#user:45/comments/'; + +test('Test parseMatchProps with different url props, simple input', (assert) => { + assert.strictEqual(parseMatchProps(URL1).url, URL1, 'No url match prop, no protocol, not regexp'); + assert.strictEqual(parseMatchProps(`url:${URL1}`).url, URL1, 'url match prop, no protocol, not regexp'); + + assert.strictEqual(parseMatchProps(URL2).url, URL2, 'No url match prop, has protocol, not regexp'); + assert.strictEqual(parseMatchProps(`url:${URL2}`).url, URL2, 'url match prop, has protocol, not regexp'); + + assert.strictEqual(parseMatchProps(URL3).url, URL3, 'No url match prop, has protocol, regexp'); + assert.strictEqual(parseMatchProps(`url:${URL3}`).url, URL3, 'url match prop, has protocol, regexp'); + + assert.strictEqual(parseMatchProps(URL4).url, URL4, 'No url match prop, has protocol, regexp, extra colon in url'); + assert.strictEqual(parseMatchProps(`url:${URL4}`).url, URL4, 'url match prop, has protocol, extra colon in url'); +}); + +test('Test parseMatchProps with different url props, mixed input', (assert) => { + const INPUT1 = `${URL1} ${METHOD_PROP}:${GET_METHOD}`; + const expected1 = { + url: URL1, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT1), expected1, 'No url match prop, no protocol, not regexp'); + + const INPUT1_PREFIXED = `${URL_PROP}:${URL1} ${METHOD_PROP}:${GET_METHOD}`; + const expectedPrefixed1 = { + url: URL1, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT1_PREFIXED), expectedPrefixed1, 'Has url match prop, no protocol, not regexp'); + + const INPUT2 = `${URL2} ${METHOD_PROP}:${GET_METHOD}`; + const expected2 = { + url: URL2, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT2), expected2, 'No url match prop, has protocol, not regexp'); + + const INPUT2_PREFIXED = `${URL_PROP}:${URL2} ${METHOD_PROP}:${GET_METHOD}`; + const expectedPrefixed2 = { + url: URL2, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT2_PREFIXED), expectedPrefixed2, 'Has url match prop, has protocol, not regexp'); + + const INPUT3 = `${URL3} ${METHOD_PROP}:${GET_METHOD}`; + const expected3 = { + url: URL3, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT3), expected3, 'No url match prop, has protocol, regexp'); + + const INPUT3_PREFIXED = `${URL_PROP}:${URL3} ${METHOD_PROP}:${GET_METHOD}`; + const expectedPrefixed3 = { + url: URL3, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT3_PREFIXED), expectedPrefixed3, 'Has url match prop, has protocol, regexp'); + + const INPUT4 = `${URL4} ${METHOD_PROP}:${GET_METHOD}`; + const expected4 = { + url: URL4, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT4), expected4, 'No url match prop, has protocol, regexp, extra colon in url'); + + const INPUT4_PREFIXED = `${URL_PROP}:${URL4} ${METHOD_PROP}:${GET_METHOD}`; + const expectedPrefixed4 = { + url: URL4, + [METHOD_PROP]: GET_METHOD, + }; + assert.deepEqual(parseMatchProps(INPUT4_PREFIXED), expectedPrefixed4, 'Has url match prop, has protocol, regexp, extra colon in url'); +}); diff --git a/tests/helpers/index.test.js b/tests/helpers/index.test.js index bd992d1d5..b606a3cdf 100644 --- a/tests/helpers/index.test.js +++ b/tests/helpers/index.test.js @@ -1,110 +1,5 @@ -import { - toRegExp, - getNumberFromString, - noopPromiseResolve, - matchStackTrace, -} from '../../src/helpers'; - -const { test, module } = QUnit; -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 getNumberFromString for all data types inputs', (assert) => { - let inputValue; - - // Boolean - inputValue = true; - assert.strictEqual(getNumberFromString(inputValue), null); - - // null - inputValue = null; - assert.strictEqual(getNumberFromString(inputValue), null); - - // undefined - inputValue = undefined; - assert.strictEqual(getNumberFromString(inputValue), null); - - // undefined - inputValue = undefined; - assert.strictEqual(getNumberFromString(inputValue), null); - - // number - inputValue = 123; - assert.strictEqual(getNumberFromString(inputValue), 123); - - // valid string - inputValue = '123parsable'; - assert.strictEqual(getNumberFromString(inputValue), 123); - - // invalid string - inputValue = 'not parsable 123'; - assert.strictEqual(getNumberFromString(inputValue), null); - - // object - inputValue = { test: 'test' }; - assert.strictEqual(getNumberFromString(inputValue), null); - - // array - inputValue = ['test']; - assert.strictEqual(getNumberFromString(inputValue), null); -}); - -test('Test noopPromiseResolve for valid response.body values', async (assert) => { - const objResponse = await noopPromiseResolve('{}'); - const objBody = await objResponse.json(); - - const arrResponse = await noopPromiseResolve('[]'); - const arrBody = await arrResponse.json(); - - assert.ok(typeof objBody === 'object' && !objBody.length); - assert.ok(Array.isArray(arrBody) && !arrBody.length); -}); - -test('Test matchStackTrace for working with getNativeRegexpTest helper', async (assert) => { - const match = matchStackTrace('stack', new Error().stack); - - assert.ok(!match); -}); +import './get-number-from-string.test'; +import './match-stack-trace.test'; +import './noop-promise-resolve.test'; +import './parse-match-props.test'; +import './to-regexp.test'; diff --git a/tests/helpers/match-stack.test.js b/tests/helpers/match-stack.test.js new file mode 100644 index 000000000..ebd90405d --- /dev/null +++ b/tests/helpers/match-stack.test.js @@ -0,0 +1,12 @@ +import { matchStackTrace } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'scriptlets-redirects helpers'; + +module(name); + +test('Test matchStackTrace for working with getNativeRegexpTest helper', async (assert) => { + const match = matchStackTrace('stack', new Error().stack); + + assert.ok(!match); +}); diff --git a/tests/helpers/noop.test.js b/tests/helpers/noop.test.js new file mode 100644 index 000000000..510386fad --- /dev/null +++ b/tests/helpers/noop.test.js @@ -0,0 +1,24 @@ +import { noopPromiseResolve } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'scriptlets-redirects helpers'; + +module(name); + +test('Test noopPromiseResolve for valid response props', async (assert) => { + const TEST_URL = 'url'; + const TEST_TYPE = 'opaque'; + const objResponse = await noopPromiseResolve('{}'); + const objBody = await objResponse.json(); + + const arrResponse = await noopPromiseResolve('[]'); + const arrBody = await arrResponse.json(); + + const responseWithUrl = await noopPromiseResolve('{}', TEST_URL); + const responseWithType = await noopPromiseResolve('{}', '', TEST_TYPE); + + assert.ok(responseWithUrl.url === TEST_URL); + assert.ok(typeof objBody === 'object' && !objBody.length); + assert.ok(Array.isArray(arrBody) && !arrBody.length); + assert.strictEqual(responseWithType.type, TEST_TYPE); +}); diff --git a/tests/helpers/number-utils.test.js b/tests/helpers/number-utils.test.js new file mode 100644 index 000000000..eb6e6f242 --- /dev/null +++ b/tests/helpers/number-utils.test.js @@ -0,0 +1,86 @@ +import { getNumberFromString } from '../../src/helpers'; + +const { test, module } = QUnit; +const name = 'scriptlets-redirects helpers'; + +module(name); + +test('Test getNumberFromString for all data types inputs', (assert) => { + let inputValue; + + // Boolean + inputValue = true; + assert.strictEqual(getNumberFromString(inputValue), null); + + // null + inputValue = null; + assert.strictEqual(getNumberFromString(inputValue), null); + + // undefined + inputValue = undefined; + assert.strictEqual(getNumberFromString(inputValue), null); + + // undefined + inputValue = undefined; + assert.strictEqual(getNumberFromString(inputValue), null); + + // number + inputValue = 123; + assert.strictEqual(getNumberFromString(inputValue), 123); + + // valid string + inputValue = '123parsable'; + assert.strictEqual(getNumberFromString(inputValue), 123); + + // invalid string + inputValue = 'not parsable 123'; + assert.strictEqual(getNumberFromString(inputValue), null); + + // object + inputValue = { test: 'test' }; + assert.strictEqual(getNumberFromString(inputValue), null); + + // array + inputValue = ['test']; + assert.strictEqual(getNumberFromString(inputValue), null); +}); + +test('Test getNumberFromString for all data types inputs', (assert) => { + let inputValue; + + // Boolean + inputValue = true; + assert.strictEqual(getNumberFromString(inputValue), null); + + // null + inputValue = null; + assert.strictEqual(getNumberFromString(inputValue), null); + + // undefined + inputValue = undefined; + assert.strictEqual(getNumberFromString(inputValue), null); + + // undefined + inputValue = undefined; + assert.strictEqual(getNumberFromString(inputValue), null); + + // number + inputValue = 123; + assert.strictEqual(getNumberFromString(inputValue), 123); + + // valid string + inputValue = '123parsable'; + assert.strictEqual(getNumberFromString(inputValue), 123); + + // invalid string + inputValue = 'not parsable 123'; + assert.strictEqual(getNumberFromString(inputValue), null); + + // object + inputValue = { test: 'test' }; + assert.strictEqual(getNumberFromString(inputValue), null); + + // array + inputValue = ['test']; + assert.strictEqual(getNumberFromString(inputValue), null); +}); diff --git a/tests/helpers/string-utils.test.js b/tests/helpers/string-utils.test.js new file mode 100644 index 000000000..50114b75e --- /dev/null +++ b/tests/helpers/string-utils.test.js @@ -0,0 +1,48 @@ +import { toRegExp } from '../../src/helpers'; + +const { test, module } = QUnit; +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); + }); +}); diff --git a/tests/scriptlets/prevent-fetch.test.js b/tests/scriptlets/prevent-fetch.test.js index cc4935230..4233d71df 100644 --- a/tests/scriptlets/prevent-fetch.test.js +++ b/tests/scriptlets/prevent-fetch.test.js @@ -287,4 +287,48 @@ if (!isSupported) { assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); done(); }); + + test('simple fetch - valid response type', async (assert) => { + const OPAQUE_RESPONSE_TYPE = 'opaque'; + const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test01.json`; + const init = { + method: 'GET', + }; + + runScriptlet(name, ['*', '', OPAQUE_RESPONSE_TYPE]); + const done = assert.async(); + + const response = await fetch(INPUT_JSON_PATH, init); + + assert.strictEqual(response.type, OPAQUE_RESPONSE_TYPE, 'Response type is set'); + assert.strictEqual(window.hit, 'FIRED', 'hit function fired'); + done(); + }); + + test('simple fetch - invalid response type', async (assert) => { + const INVALID_RESPONSE_TYPE = 'invalid_type'; + const BASIC_RESPONSE_TYPE = 'basic'; + const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/test01.json`; + const init = { + method: 'GET', + }; + + const expectedJson = { + a1: 1, + b2: 'test', + c3: 3, + }; + + runScriptlet(name, ['*', '', INVALID_RESPONSE_TYPE]); + const done = assert.async(); + + const response = await fetch(INPUT_JSON_PATH, init); + const actualJson = await response.json(); + + assert.deepEqual(actualJson, expectedJson, 'Request is not modified'); + + assert.strictEqual(response.type, BASIC_RESPONSE_TYPE, 'Response type is not modified'); + assert.strictEqual(window.hit, undefined, 'hit function fired'); + done(); + }); }