Skip to content

Commit

Permalink
Merge branch 'release/v1.7' into feature/AG-17043
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislav-atr committed Oct 31, 2022
2 parents e1ac8b5 + 0bc5d92 commit edafb05
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 138 deletions.
2 changes: 1 addition & 1 deletion scripts/build-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions src/helpers/cookie-utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { nativeIsNaN } from './number-utils';

/**
* Prepares cookie string if given parameters are ok
* @param {string} name cookie name to set
* @param {string} value cookie value to set
* @param {string} path cookie path to set, 'none' for no path
* @returns {string|null} cookie string if ok OR null if not
*/
export const prepareCookie = (name, value) => {
export const prepareCookie = (name, value, path) => {
if (!name || !value) {
return null;
}

const log = console.log.bind(console); // eslint-disable-line no-console

let valueToSet;
if (value === 'true') {
valueToSet = 'true';
Expand All @@ -34,16 +38,27 @@ export const prepareCookie = (name, value) => {
} else if (/^\d+$/.test(value)) {
valueToSet = parseFloat(value);
if (nativeIsNaN(valueToSet)) {
log(`Invalid cookie value: '${value}'`);
return null;
}
if (Math.abs(valueToSet) < 0 || Math.abs(valueToSet) > 15) {
log(`Invalid cookie value: '${value}'`);
return null;
}
} else {
return null;
}

const pathToSet = 'path=/;';
let pathToSet;
if (path === '/') {
pathToSet = 'path=/';
} else if (path === 'none') {
pathToSet = '';
} else {
log(`Invalid cookie path: '${path}'`);
return null;
}

// eslint-disable-next-line max-len
const cookieData = `${encodeURIComponent(name)}=${encodeURIComponent(valueToSet)}; ${pathToSet}`;

Expand Down
15 changes: 12 additions & 3 deletions src/helpers/noop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
};
30 changes: 26 additions & 4 deletions src/helpers/request-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});

Expand Down
19 changes: 15 additions & 4 deletions src/scriptlets/prevent-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 10 additions & 4 deletions src/scriptlets/set-cookie-reload.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
* @scriptlet set-cookie-reload
*
* @description
* Sets a cookie with the specified name and value, and then reloads the current page.
* Sets a cookie with the specified name and value, and path,
* and reloads the current page after the cookie setting.
* If reloading option is not needed, use [set-cookie](#set-cookie) scriptlet.
*
* **Syntax**
* ```
* example.org#%#//scriptlet('set-cookie-reload', name, value)
* example.org#%#//scriptlet('set-cookie-reload', name, value[, path])
* ```
*
* - `name` - required, cookie name to be set
Expand All @@ -25,15 +26,20 @@ import {
* - `yes` / `Yes` / `Y`
* - `no`
* - `ok` / `OK`
* - `path` - optional, cookie path, defaults to `/`; possible values:
* - `/` — root path
* - `none` — to set no path at all
*
* **Examples**
* ```
* example.org#%#//scriptlet('set-cookie-reload', 'checking', 'ok')
*
* example.org#%#//scriptlet('set-cookie-reload', 'gdpr-settings-cookie', '1')
*
* example.org#%#//scriptlet('set-cookie-reload', 'cookie-set', 'true', 'none')
* ```
*/
export function setCookieReload(source, name, value) {
export function setCookieReload(source, name, value, path = '/') {
const isCookieSetWithValue = (name, value) => {
return document.cookie.split(';')
.some((cookieStr) => {
Expand All @@ -52,7 +58,7 @@ export function setCookieReload(source, name, value) {
return;
}

const cookieData = prepareCookie(name, value);
const cookieData = prepareCookie(name, value, path);

if (cookieData) {
document.cookie = cookieData;
Expand Down
15 changes: 10 additions & 5 deletions src/scriptlets/set-cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { hit, nativeIsNaN, prepareCookie } from '../helpers/index';
* @scriptlet set-cookie
*
* @description
* Sets a cookie with the specified name and value. Cookie path defaults to root.
* Sets a cookie with the specified name, value, and path.
*
* **Syntax**
* ```
* example.org#%#//scriptlet('set-cookie', name, value)
* example.org#%#//scriptlet('set-cookie', name, value[, path])
* ```
*
* - `name` - required, cookie name to be set
Expand All @@ -21,17 +21,22 @@ import { hit, nativeIsNaN, prepareCookie } from '../helpers/index';
* - `yes` / `Yes` / `Y`
* - `no`
* - `ok` / `OK`
* - `path` - optional, cookie path, defaults to `/`; possible values:
* - `/` — root path
* - `none` — to set no path at all
*
* **Examples**
* ```
* example.org#%#//scriptlet('set-cookie', 'ReadlyCookieConsent', '1')
* example.org#%#//scriptlet('set-cookie', 'CookieConsent', '1')
*
* example.org#%#//scriptlet('set-cookie', 'gdpr-settings-cookie', 'true')
*
* example.org#%#//scriptlet('set-cookie', 'cookie_consent', 'ok', 'none')
* ```
*/
/* eslint-enable max-len */
export function setCookie(source, name, value) {
const cookieData = prepareCookie(name, value);
export function setCookie(source, name, value, path = '/') {
const cookieData = prepareCookie(name, value, path);

if (cookieData) {
hit(source);
Expand Down
87 changes: 87 additions & 0 deletions tests/helpers/fetch-utils.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
Loading

0 comments on commit edafb05

Please sign in to comment.