Skip to content

Commit

Permalink
merge release/v1.7
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislav-atr committed Oct 31, 2022
2 parents 7379f0b + 0bc5d92 commit 6612144
Show file tree
Hide file tree
Showing 27 changed files with 936 additions and 150 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
20 changes: 17 additions & 3 deletions src/helpers/get-descriptor-addon.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { randomId } from './random-id';
/**
* Prevent infinite loops when trapping props that could be used by scriptlet's own helpers
* Example: window.RegExp, that is used by matchStackTrace > toRegExp
*
* https://github.com/AdguardTeam/Scriptlets/issues/251
* https://github.com/AdguardTeam/Scriptlets/issues/226
* https://github.com/AdguardTeam/Scriptlets/issues/232
*
Expand All @@ -12,9 +14,21 @@ export function getDescriptorAddon() {
isAbortingSuspended: false,
isolateCallback(cb, ...args) {
this.isAbortingSuspended = true;
const result = cb(...args);
this.isAbortingSuspended = false;
return result;
// try...catch is required in case if there are more than one inline scripts
// which should be aborted.
// so after the first successful abortion, `cb(...args);` will throw error,
// and we should not stop on that and continue to abort other scripts
try {
const result = cb(...args);
this.isAbortingSuspended = false;
return result;
} catch {
this.isAbortingSuspended = false;
const rid = randomId();
// It's necessary to throw error
// otherwise script will be not aborted
throw new ReferenceError(rid);
}
},
};
}
3 changes: 2 additions & 1 deletion src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export * from './array-utils';
export * from './cookie-utils';
export * from './number-utils';
export * from './adjust-set-utils';
export * from './fetch-utils';
export * from './request-utils';
export * from './object-utils';
export * from './prevent-window-open-utils';
export * from './add-event-listener-utils';
Expand All @@ -26,3 +26,4 @@ export * from './regexp-utils';
export * from './random-response';
export * from './get-descriptor-addon';
export * from './parse-flags';
export * from './match-request-props';
35 changes: 35 additions & 0 deletions src/helpers/match-request-props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
getMatchPropsData,
validateParsedData,
parseMatchProps,
} from './request-utils';

/**
* Checks if given propsToMatch string matches with given request data
* This is used by prevent-xhr, prevent-fetch, trusted-replace-xhr-response
* and trusted-replace-fetch-response scriptlets
* @param {string} propsToMatch
* @param {Object} requestData object with standard properties of fetch/xhr like url, method etc
* @returns {boolean}
*/
export const matchRequestProps = (propsToMatch, requestData) => {
let isMatched;

const parsedData = parseMatchProps(propsToMatch);
if (!validateParsedData(parsedData)) {
// eslint-disable-next-line no-console
console.log(`Invalid parameter: ${propsToMatch}`);
isMatched = false;
} else {
const matchData = getMatchPropsData(parsedData);
// prevent only if all props match
isMatched = Object.keys(matchData)
.every((matchKey) => {
const matchValue = matchData[matchKey];
return Object.prototype.hasOwnProperty.call(requestData, matchKey)
&& matchValue.test(requestData[matchKey]);
});
}

return isMatched;
};
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);
};
49 changes: 45 additions & 4 deletions src/helpers/fetch-utils.js → src/helpers/request-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ export const getFetchData = (args) => {
return fetchPropsObj;
};

/**
* Collect xhr.open arguments to object
* @param {string} method
* @param {string} url
* @param {string} async
* @param {string} user
* @param {string} password
* @returns {Object}
*/
export const getXhrData = (method, url, async, user, password) => {
return {
method,
url,
async,
user,
password,
};
};

/**
* Parse propsToMatch input string into object;
* used for prevent-fetch and prevent-xhr
Expand All @@ -67,18 +86,40 @@ export const getFetchData = (args) => {
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
4 changes: 3 additions & 1 deletion src/redirects/googlesyndication-adsbygoogle.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export function GoogleSyndicationAdsByGoogle(source) {
for (const key of Object.keys(arg)) {
if (typeof arg[key] === 'function') {
try {
arg[key].call();
// https://github.com/AdguardTeam/Scriptlets/issues/252
// argument "{}" is needed to fix issue with undefined argument
arg[key].call(this, {});
} catch {
/* empty */
}
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
2 changes: 1 addition & 1 deletion src/scriptlets/prevent-xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
* - value — range on numbers, for example `100-300`, limited to 500000 characters
*
* > Usage with no arguments will log XMLHttpRequest objects to browser console;
* which is useful for debugging but permitted for production filter lists.
* which is useful for debugging but not allowed for production filter lists.
*
* **Examples**
* 1. Log all XMLHttpRequests
Expand Down
2 changes: 1 addition & 1 deletion src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ export * from './close-window';
export * from './prevent-refresh';
export * from './prevent-element-src-loading';
export * from './no-topics';
export * from './trusted-replace-xhr-response';
export * from './xml-prune';
export * from './trusted-set-cookie';

16 changes: 11 additions & 5 deletions src/scriptlets/set-cookie-reload.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,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 @@ -26,20 +27,25 @@ 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) {
if (isCookieSetWithValue(document.cookie, name, value)) {
export function setCookieReload(source, name, value, path = '/') {
if (isCookieSetWithValue(name, value)) {
return;
}

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

if (cookieData) {
document.cookie = cookieData;
Expand Down
Loading

0 comments on commit 6612144

Please sign in to comment.