diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7d07315..4953241d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,16 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
+- improved `prevent-fetch` — if `responseType` is not specified,
+ original response type is returned instead of `default` [#297](https://github.com/AdguardTeam/Scriptlets/issues/291)
+
### Fixed
-- issue with reloading website if `$now$`/`$currentDate$` value is used in `trusted-set-cookie-reload` scriptlet [#291](https://github.com/AdguardTeam/Scriptlets/issues/291)
+- website reloading if `$now$`/`$currentDate$` value is used
+ in `trusted-set-cookie-reload` scriptlet [#291](https://github.com/AdguardTeam/Scriptlets/issues/291)
## [v1.9.7] - 2023-03-14
### Added
-- ability for `trusted-click-element` scriptlet to click element if `cookie`/`localStorage` item doesn't exist [#298](https://github.com/AdguardTeam/Scriptlets/issues/298)
-- static delay between multiple clicks in `trusted-click-element` [#284](https://github.com/AdguardTeam/Scriptlets/issues/284)
+- ability for `trusted-click-element` scriptlet to click element
+ if `cookie`/`localStorage` item doesn't exist [#298](https://github.com/AdguardTeam/Scriptlets/issues/298)
+- static delay between multiple clicks in `trusted-click-element`
+ [#284](https://github.com/AdguardTeam/Scriptlets/issues/284)
### Changed
@@ -23,7 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- issue with `MutationObserver.disconnect()` in `trusted-click-element` [#284](https://github.com/AdguardTeam/Scriptlets/issues/284)
+- issue with `MutationObserver.disconnect()`
+ in `trusted-click-element` [#284](https://github.com/AdguardTeam/Scriptlets/issues/284)
## [v1.9.1] - 2023-03-07
@@ -36,13 +45,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
-- decreased the minimal value for the `boost` parameter to `0.001` for `adjust-setTimeout` and `adjust-setInterval` [#262](https://github.com/AdguardTeam/Scriptlets/issues/262)
+- decreased the minimal value for the `boost` parameter to `0.001`
+ for `adjust-setTimeout` and `adjust-setInterval` [#262](https://github.com/AdguardTeam/Scriptlets/issues/262)
### Fixed
-- `prevent-element-src-loading` throwing error if `thisArg` is `undefined` [#270](https://github.com/AdguardTeam/Scriptlets/issues/270)
+- `prevent-element-src-loading` throwing error
+ if `thisArg` is `undefined` [#270](https://github.com/AdguardTeam/Scriptlets/issues/270)
- logging `null` in `json-prune` [#282](https://github.com/AdguardTeam/Scriptlets/issues/282)
-- `xml-prune` does not prune a request if `new Request()` is used and issue with throwing error while logging some requests [#289](https://github.com/AdguardTeam/Scriptlets/issues/289)
+- `xml-prune`: no pruning a request if `new Request()` is used,
+ throwing an error while logging some requests [#289](https://github.com/AdguardTeam/Scriptlets/issues/289)
- improve performance of the `isValidScriptletName()` method
@@ -53,17 +65,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- new `trusted-set-constant` scriptlet [#137](https://github.com/AdguardTeam/Scriptlets/issues/137)
- new `inject-css-in-shadow-dom` scriptlet [#267](https://github.com/AdguardTeam/Scriptlets/issues/267)
- `throwFunc` and `noopCallbackFunc` prop values for `set-constant` scriptlet
-- `recreateIframeForSlot` method mock to `googletagservices-gpt` redirect [#259](https://github.com/AdguardTeam/Scriptlets/issues/259)
+- `recreateIframeForSlot` method mock
+ to `googletagservices-gpt` redirect [#259](https://github.com/AdguardTeam/Scriptlets/issues/259)
### Changed
-- add decimal delay matching for `prevent-setInterval` and `prevent-setTimeout` [#247](https://github.com/AdguardTeam/Scriptlets/issues/247)
+- add decimal delay matching for `prevent-setInterval` and `prevent-setTimeout`
+ [#247](https://github.com/AdguardTeam/Scriptlets/issues/247)
- debug logging to include rule text when available
- `getScriptletFunction` calls to throw error on unknown scriptlet names
### Fixed
-- `prevent-xhr` and `trusted-replace-xhr-response` closure bug on multiple requests [#261](https://github.com/AdguardTeam/Scriptlets/issues/261)
+- `prevent-xhr` and `trusted-replace-xhr-response` closure bug on multiple requests
+ [#261](https://github.com/AdguardTeam/Scriptlets/issues/261)
- missing `googletagmanager-gtm` in compatibility table
@@ -83,7 +98,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-- `prevent-addEventListener` and `log-addEventListener` loosing context when encountering already bound `.addEventListener`
+- `prevent-addEventListener` and `log-addEventListener` loosing context
+ when encountering already bound `.addEventListener`
- `google-ima3` conversion
@@ -91,14 +107,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
-* `set-constant` ADG→UBO conversion for [`emptyArr` and `emptyObj`](https://github.com/uBlockOrigin/uBlock-issues/issues/2411)
+- `set-constant` ADG→UBO conversion
+ for [`emptyArr` and `emptyObj`](https://github.com/uBlockOrigin/uBlock-issues/issues/2411)
## [v1.7.13] - 2022-12-13
### Fixed
-* `isEmptyObject` helper not counting `prototype` as an object property
+- `isEmptyObject` helper not counting `prototype` as an object property
## [v1.7.10] - 2022-12-07
@@ -110,7 +127,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `set-cookie-reload` infinite page reloading [#265](https://github.com/AdguardTeam/Scriptlets/issues/265)
-- breakage of `prevent-element-src-loading` due to `window` getting into `apply` wrapper [#264](https://github.com/AdguardTeam/Scriptlets/issues/264)
+- breakage of `prevent-element-src-loading` due to `window` getting into `apply` wrapper
+ [#264](https://github.com/AdguardTeam/Scriptlets/issues/264)
- spread of args bug at `getXhrData` call for `trusted-replace-xhr-response`
- request properties array not being served to `getRequestData` and `parseMatchProps` helpers
diff --git a/src/helpers/index.js b/src/helpers/index.js
index 58c75865..91885fd1 100644
--- a/src/helpers/index.js
+++ b/src/helpers/index.js
@@ -14,6 +14,7 @@ export * from './open-shadow-dom-utils';
export * from './prevent-utils';
export * from './prevent-window-open-utils';
export * from './regexp-utils';
+export * from './response-utils';
export * from './request-utils';
export * from './storage-utils';
export * from './string-utils';
diff --git a/src/helpers/response-utils.js b/src/helpers/response-utils.js
new file mode 100644
index 00000000..82bd92ef
--- /dev/null
+++ b/src/helpers/response-utils.js
@@ -0,0 +1,40 @@
+/**
+ * Modifies original response with the given replacement data.
+ *
+ * @param {Response} origResponse Original response.
+ * @param {Object} replacement Replacement data for response with possible keys:
+ * - `body`: optional, string, default to '{}';
+ * - `type`: optional, string, original response type is used if not specified.
+ *
+ * @returns {Response} Modified response.
+ */
+export const modifyResponse = (
+ origResponse,
+ replacement = {
+ body: '{}',
+ },
+) => {
+ const headers = {};
+ origResponse?.headers?.forEach((value, key) => {
+ headers[key] = value;
+ });
+
+ const modifiedResponse = new Response(replacement.body, {
+ status: origResponse.status,
+ statusText: origResponse.statusText,
+ headers,
+ });
+
+ // Mock response url and type to avoid adblocker detection
+ // https://github.com/AdguardTeam/Scriptlets/issues/216
+ Object.defineProperties(modifiedResponse, {
+ url: {
+ value: origResponse.url,
+ },
+ type: {
+ value: replacement.type || origResponse.type,
+ },
+ });
+
+ return modifiedResponse;
+};
diff --git a/src/helpers/string-utils.js b/src/helpers/string-utils.js
index a03361a3..0d44e3b8 100644
--- a/src/helpers/string-utils.js
+++ b/src/helpers/string-utils.js
@@ -90,6 +90,8 @@ export const getBeforeRegExp = (str, rx) => {
/**
* Checks whether the string starts with the substring
*
+ * @deprecated use String.prototype.startsWith() instead. AG-18883
+ *
* @param {string} str full string
* @param {string} prefix substring
* @returns {boolean} if string start with the substring
@@ -103,6 +105,8 @@ export const startsWith = (str, prefix) => {
/**
* Checks whether the string ends with the substring
*
+ * @deprecated use String.prototype.endsWith() instead. AG-18883
+ *
* @param {string} str full string
* @param {string} ending substring
* @returns {boolean} string ends with the substring
diff --git a/src/scriptlets/prevent-fetch.js b/src/scriptlets/prevent-fetch.js
index 1c5e2501..b9bf5404 100644
--- a/src/scriptlets/prevent-fetch.js
+++ b/src/scriptlets/prevent-fetch.js
@@ -2,9 +2,9 @@ import {
hit,
getFetchData,
objectToString,
- noopPromiseResolve,
matchRequestProps,
logMessage,
+ modifyResponse,
// following helpers should be imported and injected
// because they are used by helpers above
toRegExp,
@@ -24,7 +24,7 @@ import {
/**
* @scriptlet prevent-fetch
* @description
- * Prevents `fetch` calls if **all** given parameters match
+ * Prevents `fetch` calls if **all** given parameters match.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/wiki/Resources-Library#no-fetch-ifjs-
@@ -35,14 +35,18 @@ import {
* ```
*
* - `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
+ * - 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:
+ * - `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
- * - `responseType` — optional, string for defining response type, defaults to `default`. Possible values:
+ * - `responseType` — optional, string for defining response type,
+ * original response type is used if not specified. Possible values:
* - `default`
* - `opaque`
*
@@ -92,7 +96,7 @@ import {
* ```
*/
/* eslint-enable max-len */
-export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType = 'default') {
+export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', responseType) {
// 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
@@ -108,16 +112,27 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', re
} else if (responseBody === 'emptyArr') {
strResponseBody = '[]';
} else {
+ logMessage(source, `Invalid responseBody parameter: '${responseBody}'`);
return;
}
- // Skip disallowed response types
- if (!(responseType === 'default' || responseType === 'opaque')) {
- logMessage(source, `Invalid parameter: ${responseType}`);
+ const isResponseTypeSpecified = typeof responseType !== 'undefined';
+ const isResponseTypeSupported = (responseType) => {
+ const SUPPORTED_TYPES = [
+ 'default',
+ 'opaque',
+ ];
+ return SUPPORTED_TYPES.includes(responseType);
+ };
+ // Skip disallowed response types,
+ // specified responseType has limited list of possible values
+ if (isResponseTypeSpecified
+ && !isResponseTypeSupported(responseType)) {
+ logMessage(source, `Invalid responseType parameter: '${responseType}'`);
return;
}
- const handlerWrapper = (target, thisArg, args) => {
+ const handlerWrapper = async (target, thisArg, args) => {
let shouldPrevent = false;
const fetchData = getFetchData(args);
if (typeof propsToMatch === 'undefined') {
@@ -130,7 +145,14 @@ export function preventFetch(source, propsToMatch, responseBody = 'emptyObj', re
if (shouldPrevent) {
hit(source);
- return noopPromiseResolve(strResponseBody, fetchData.url, responseType);
+ const origResponse = await Reflect.apply(target, thisArg, args);
+ return modifyResponse(
+ origResponse,
+ {
+ body: strResponseBody,
+ type: responseType,
+ },
+ );
}
return Reflect.apply(target, thisArg, args);
@@ -155,9 +177,9 @@ preventFetch.injections = [
hit,
getFetchData,
objectToString,
- noopPromiseResolve,
matchRequestProps,
logMessage,
+ modifyResponse,
toRegExp,
isValidStrPattern,
escapeRegExp,
diff --git a/tests/scriptlets/prevent-fetch.test.js b/tests/scriptlets/prevent-fetch.test.js
index 3e48b78a..2e71b482 100644
--- a/tests/scriptlets/prevent-fetch.test.js
+++ b/tests/scriptlets/prevent-fetch.test.js
@@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle, no-console */
import { runScriptlet, clearGlobalProps } from '../helpers';
-import { startsWith } from '../../src/helpers/string-utils';
import { isEmptyObject } from '../../src/helpers/object-utils';
const { test, module } = QUnit;
@@ -71,7 +70,7 @@ if (!isSupported) {
return;
}
const EXPECTED_LOG_STR_START = `${name}: fetch( url:"${INPUT_JSON_PATH}" method:"${TEST_METHOD}"`;
- assert.ok(startsWith(input, EXPECTED_LOG_STR_START), 'console.hit input');
+ assert.ok(input.startsWith(EXPECTED_LOG_STR_START), 'console.hit input');
};
// no args -> just logging, no preventing
@@ -129,6 +128,7 @@ if (!isSupported) {
assert.ok(isEmptyObject(parsedData1), 'Response is mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
+ // remove 'hit' property for following checking
clearGlobalProps('hit');
const response2 = await fetch(inputRequest2);
@@ -275,6 +275,23 @@ if (!isSupported) {
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';
@@ -301,4 +318,72 @@ if (!isSupported) {
assert.strictEqual(window.hit, undefined, 'hit function fired');
done();
});
+
+ test('simple fetch -- all original response properties are not modified', async (assert) => {
+ const TEST_FILE_NAME = 'test01.json';
+ const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/${TEST_FILE_NAME}`;
+ const inputRequest1 = new Request(INPUT_JSON_PATH);
+ const done = assert.async();
+
+ runScriptlet(name, ['*']);
+
+ const response = await fetch(inputRequest1);
+
+ /**
+ * Previously, only one header was present in the returned response
+ * which was `content-type: text/plain;charset=UTF-8`.
+ * Since we are not modifying the headers, we expect to receive more than one header.
+ * We cannot check the exact headers and their values
+ * because the response may contain different headers
+ * depending on whether the tests are run in Node or in a browser.
+ */
+ let headersCount = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (const key of response.headers.keys()) {
+ headersCount += 1;
+ }
+
+ assert.strictEqual(response.type, 'basic', 'response type is "basic" by default, not modified');
+ assert.true(response.url.includes(TEST_FILE_NAME), 'response url not modified');
+ assert.true(headersCount > 1, 'original headers not modified');
+
+ const responseJsonData = await response.json();
+ assert.ok(isEmptyObject(responseJsonData), 'response data is mocked');
+ assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
+ done();
+ });
+
+ test('simple fetch -- original response properties are not modified except type', async (assert) => {
+ const TEST_FILE_NAME = 'test01.json';
+ const TEST_RESPONSE_TYPE = 'opaque';
+ const INPUT_JSON_PATH = `${FETCH_OBJECTS_PATH}/${TEST_FILE_NAME}`;
+ const inputRequest1 = new Request(INPUT_JSON_PATH);
+ const done = assert.async();
+
+ runScriptlet(name, ['*', 'emptyArr', TEST_RESPONSE_TYPE]);
+
+ const response = await fetch(inputRequest1);
+
+ let headersCount = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (const key of response.headers.keys()) {
+ headersCount += 1;
+ }
+
+ assert.strictEqual(
+ response.type,
+ TEST_RESPONSE_TYPE,
+ `response type is modified, equals to ${TEST_RESPONSE_TYPE}`,
+ );
+ assert.true(response.url.includes(TEST_FILE_NAME), 'response url not modified');
+ assert.true(headersCount > 1, 'original headers not modified');
+
+ const responseJsonData = await response.json();
+ assert.ok(
+ Array.isArray(responseJsonData) && responseJsonData.length === 0,
+ 'response data is an empty array',
+ );
+ assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
+ done();
+ });
}