From f96716dfa453df480ad172a0a835590fcf302c38 Mon Sep 17 00:00:00 2001 From: Stanislav A Date: Mon, 24 Oct 2022 13:03:53 +0300 Subject: [PATCH] add trsuted-replace-fetch-response scriptlet --- .../trusted-replace-fetch-response.js | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/scriptlets/trusted-replace-fetch-response.js diff --git a/src/scriptlets/trusted-replace-fetch-response.js b/src/scriptlets/trusted-replace-fetch-response.js new file mode 100644 index 000000000..a400aa6e3 --- /dev/null +++ b/src/scriptlets/trusted-replace-fetch-response.js @@ -0,0 +1,172 @@ +import { + hit, + getFetchData, + objectToString, + parseMatchProps, + validateParsedData, + getMatchPropsData, + noopPromiseResolve, + getWildcardSymbol, + // following helpers should be imported and injected + // because they are used by helpers above + toRegExp, + isValidStrPattern, + escapeRegExp, + isEmptyObject, + getRequestData, + getObjectEntries, + getObjectFromEntries, +} from '../helpers/index'; + +/* eslint-disable max-len */ +/** + * @scriptlet trusted-replace-fetch-response + * + * @description // FIXME + * Prevents `fetch` calls if **all** given parameters match + * + * Related UBO scriptlet: + * https://github.com/gorhill/uBlock/wiki/Resources-Library#no-fetch-ifjs- + * + * **Syntax** + * ``` + * example.org#%#//scriptlet('prevent-fetch'[, propsToMatch[, responseBody]]) + * ``` + * + * - `propsToMatch` - optional, string of space-separated properties to match; possible props: + * - string or regular expression for matching the URL passed to fetch call; empty string, wildcard `*` or invalid regular expression will match all fetch calls + * - colon-separated pairs `name:value` where + * - `name` is [`init` option name](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters) + * - `value` is string or regular expression for matching the value of the option passed to fetch call; invalid regular expression will cause any value matching + * - responseBody - optional, string for defining response body value, defaults to `emptyObj`. Possible values: + * - `emptyObj` - empty object + * - `emptyArr` - empty array + * > Usage with no arguments will log fetch calls to browser console; + * which is useful for debugging but permitted for production filter lists. + * + * **Examples** + * 1. Log all fetch calls + * ``` + * example.org#%#//scriptlet('prevent-fetch') + * ``` + * + * 2. Prevent all fetch calls + * ``` + * example.org#%#//scriptlet('prevent-fetch', '*') + * OR + * example.org#%#//scriptlet('prevent-fetch', '') + * ``` + * + * 3. Prevent fetch call for specific url + * ``` + * example.org#%#//scriptlet('prevent-fetch', '/url\\.part/') + * ``` + * + * 4. Prevent fetch call for specific request method + * ``` + * example.org#%#//scriptlet('prevent-fetch', 'method:HEAD') + * ``` + * + * 5. Prevent fetch call for specific url and request method + * ``` + * example.org#%#//scriptlet('prevent-fetch', '/specified_url_part/ method:/HEAD|GET/') + * ``` + * + * 6. Prevent fetch call and specify response body value + * ``` + * ! Specify response body for fetch call to a specific url + * example.org#%#//scriptlet('prevent-fetch', '/specified_url_part/ method:/HEAD|GET/', 'emptyArr') + * + * ! Specify response body for all fetch calls + * example.org#%#//scriptlet('prevent-fetch', '', 'emptyArr') + * ``` + */ +/* eslint-enable max-len */ +export function trustedReplaceFetchResponse(source, pattern = '', replacement = '', propsToMatch = '') { + // 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 + if (typeof fetch === 'undefined' + || typeof Proxy === 'undefined' + || typeof Response === 'undefined') { + return; + } + + if (typeof pattern === 'undefined' || typeof replacement === 'undefined') { + return; + } + + // eslint-disable-next-line no-console + const log = console.log.bind(console); + const nativeFetch = window.fetch; + + let shouldPrevent = false; + let fetchData; + + const handlerWrapper = (target, thisArg, args) => { + fetchData = getFetchData(args); + + if (typeof propsToMatch === 'undefined') { + // log if no propsToMatch given + const logMessage = `log: fetch( ${objectToString(fetchData)} )`; + log(source, logMessage); + } else if (propsToMatch === '' || propsToMatch === getWildcardSymbol()) { + // prevent all fetch calls + shouldPrevent = true; + } else { + const parsedData = parseMatchProps(propsToMatch); + if (!validateParsedData(parsedData)) { + // eslint-disable-next-line no-console + console.log(`Invalid parameter: ${propsToMatch}`); + shouldPrevent = false; + } else { + const matchData = getMatchPropsData(parsedData); + // prevent only if all props match + shouldPrevent = Object.keys(matchData) + .every((matchKey) => { + const matchValue = matchData[matchKey]; + return Object.prototype.hasOwnProperty.call(fetchData, matchKey) + && matchValue.test(fetchData[matchKey]); + }); + } + } + + if (shouldPrevent) { + // REPLACE CONTENT HERE + + + hit(source); + } + + return Reflect.apply(target, thisArg, args); + }; + + const fetchHandler = { + apply: handlerWrapper, + }; + + fetch = new Proxy(fetch, fetchHandler); // eslint-disable-line no-global-assign +} + +trustedReplaceFetchResponse.names = [ + 'trusted-replace-fetch-response', + +]; + +trustedReplaceFetchResponse.injections = [ + hit, + getFetchData, + objectToString, + parseMatchProps, + validateParsedData, + getMatchPropsData, + noopPromiseResolve, + getWildcardSymbol, + toRegExp, + isValidStrPattern, + escapeRegExp, + isEmptyObject, + getRequestData, + getObjectEntries, + getObjectFromEntries, +];