Skip to content

Commit 056c175

Browse files
AdamWrslvvko
authored andcommitted
AG-23237 Add remove-request-query-parameter scriptlet. #329
Squashed commit of the following: commit aa40f7b Author: slvvko <v.leleka@adguard.com> Date: Thu Jan 29 18:55:01 2026 -0500 improve splitByNotEscapedDelimiter commit fdc3111 Author: slvvko <v.leleka@adguard.com> Date: Wed Jan 28 12:28:20 2026 -0500 support url obj for xhr commit 288a831 Author: slvvko <v.leleka@adguard.com> Date: Wed Jan 28 12:27:44 2026 -0500 remake splitByNotEscapedDelimiter, add tests commit e742b6a Merge: ad37cca a085fff Author: slvvko <v.leleka@adguard.com> Date: Wed Jan 28 12:20:01 2026 -0500 fix changelog commit ad37cca Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:17:08 2026 -0500 mark urlPattern as optional commit bb3fa51 Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:16:51 2026 -0500 fix log message commit e51bf33 Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:15:30 2026 -0500 update tests commit f4e8f77 Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:15:18 2026 -0500 wrap toRegExp in try..catch commit cbee33a Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:14:47 2026 -0500 fix regex.test lastIndex statefulness commit 92d1ca7 Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:14:21 2026 -0500 update fetchWrapper, support url obj commit 43404fd Author: slvvko <v.leleka@adguard.com> Date: Tue Jan 27 22:13:30 2026 -0500 add abp alias commit af89a2c Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 19:27:49 2026 -0500 fix link commit 5bdf99c Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 19:26:18 2026 -0500 fix docs commit 6f38023 Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 19:24:14 2026 -0500 finalize implementation commit 3c1c930 Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 19:22:35 2026 -0500 update changelog commit 99f2101 Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 19:22:21 2026 -0500 update compatibility table commit a8fe6b6 Merge: e88c971 eba8337 Author: slvvko <v.leleka@adguard.com> Date: Mon Jan 26 18:34:13 2026 -0500 merge parent branch into current one, resolve conflicts commit e88c971 Author: Adam Wróblewski <adam@adguard.com> Date: Fri Jan 23 17:45:19 2026 +0100 Add remove-request-query-parameter scriptlet
1 parent a085fff commit 056c175

9 files changed

Lines changed: 717 additions & 5 deletions

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,22 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1010
<!-- TODO: change `@added unknown` tag due to the actual version -->
1111
<!-- during new scriptlets or redirects releasing -->
1212

13-
## Unreleased
13+
## [Unreleased]
1414

1515
### Added
1616

1717
- `prevent-constructor` scriptlet to prevent constructor calls
1818
like `new Promise()` or `new MutationObserver()` [#461].
19+
- `remove-request-query-parameter` scriptlet to remove query parameters
20+
from `fetch` and `XMLHttpRequest` requests [#329].
1921

2022
### Fixed
2123

2224
- Anti-adblock detection in `spoof-css` scriptlet
2325
by using cloaked bound functions instead of Proxies [#422].
2426

27+
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.15...HEAD
28+
[#329]: https://github.com/AdguardTeam/Scriptlets/issues/329
2529
[#422]: https://github.com/AdguardTeam/Scriptlets/issues/422
2630
[#461]: https://github.com/AdguardTeam/Scriptlets/issues/461
2731

scripts/compatibility-table.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@
218218
"adg": "remove-node-text",
219219
"ubo": "remove-node-text.js (rmnt.js)"
220220
},
221+
{
222+
"adg": "remove-request-query-parameter",
223+
"abp": "strip-fetch-query-parameter"
224+
},
221225
{
222226
"adg": "scorecardresearch-beacon"
223227
},
@@ -285,9 +289,6 @@
285289
{
286290
"abp": "simulate-mouse-event"
287291
},
288-
{
289-
"abp": "strip-fetch-query-parameter"
290-
},
291292
{
292293
"abp": "hide-if-contains"
293294
},

src/helpers/string-utils.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,42 @@ export const extractRegexAndReplacement = (
567567

568568
return objReplacementWithRegex;
569569
};
570+
571+
/**
572+
* Splits a string by a delimiter, ignoring escaped delimiters.
573+
*
574+
* @param string String to split.
575+
* @param delimiter Delimiter to split by.
576+
*
577+
* @returns Array of substrings.
578+
*/
579+
export const splitByNotEscapedDelimiter = (string: string, delimiter: string) => {
580+
const BACKSLASH = '\\';
581+
const result: string[] = [];
582+
let current = '';
583+
let i = 0;
584+
585+
while (i < string.length) {
586+
if (string[i] === BACKSLASH && i + 1 < string.length) {
587+
const next = string.substring(i + 1, i + 1 + delimiter.length);
588+
if (next === delimiter) {
589+
current += delimiter;
590+
i += 1 + delimiter.length;
591+
continue;
592+
}
593+
}
594+
595+
const substr = string.substring(i, i + delimiter.length);
596+
if (substr === delimiter) {
597+
result.push(current);
598+
current = '';
599+
i += delimiter.length;
600+
} else {
601+
current += string[i];
602+
i += 1;
603+
}
604+
}
605+
606+
result.push(current);
607+
return result;
608+
};
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import {
2+
hit,
3+
logMessage,
4+
splitByNotEscapedDelimiter,
5+
toRegExp,
6+
} from '../helpers';
7+
import { type Source } from './scriptlets';
8+
9+
/**
10+
* @scriptlet remove-request-query-parameter
11+
*
12+
* @description
13+
* Removes a specified query parameter from matched outgoing requests.
14+
*
15+
* Related ABP source:
16+
* https://gitlab.com/eyeo/anti-cv/snippets/-/blob/92f9b84bd0d34dbd0e3c1bfe3ff2062863c7714a/source/behavioral/strip-fetch-query-parameter.js
17+
*
18+
* ### Syntax
19+
*
20+
* ```text
21+
* example.org#%#//scriptlet('remove-request-query-parameter', parametersToRemove[, urlPattern])
22+
* ```
23+
*
24+
* - `parametersToRemove` — required, either a single regular expression (starting with `/`)
25+
* or a list of literal query parameter names separated by `,`.
26+
* Mixing regular expressions and literal strings is not allowed.
27+
* - `urlPattern` — optional, a string or regular expression to match request URLs.
28+
*
29+
* ### Examples
30+
*
31+
* 1. Remove a specific query parameter from all requests:
32+
*
33+
* ```adblock
34+
* example.org#%#//scriptlet('remove-request-query-parameter', 'utm_source')
35+
* ```
36+
*
37+
* 1. Remove multiple query parameters from all requests:
38+
*
39+
* ```adblock
40+
* example.org#%#//scriptlet('remove-request-query-parameter', 'utm_source,utm_medium,utm_campaign')
41+
* ```
42+
*
43+
* 1. Remove a specific query parameter from requests matching a URL pattern:
44+
*
45+
* ```adblock
46+
* example.org#%#//scriptlet('remove-request-query-parameter', 'ad_config_id', '/playback/')
47+
* ```
48+
*
49+
* 1. Remove query parameters matching a regular expression:
50+
*
51+
* ```adblock
52+
* example.org#%#//scriptlet('remove-request-query-parameter', '/^utm_/', '/api/')
53+
* ```
54+
*
55+
* @added unknown.
56+
*/
57+
export function removeRequestQueryParameter(source: Source, parametersToRemove: string, urlPattern?: string) {
58+
if (!parametersToRemove) {
59+
logMessage(source, 'Missing parameters to remove');
60+
return;
61+
}
62+
63+
let urlPatternRegExp: RegExp | null = null;
64+
if (urlPattern) {
65+
try {
66+
urlPatternRegExp = toRegExp(urlPattern);
67+
} catch (e) {
68+
logMessage(source, `Invalid URL pattern: ${urlPattern}`);
69+
return;
70+
}
71+
}
72+
73+
let regexpParamsToRemove: RegExp[];
74+
try {
75+
// If starts with `/`, treat the entire value as a single regex pattern
76+
if (parametersToRemove.startsWith('/')) {
77+
regexpParamsToRemove = [toRegExp(parametersToRemove)];
78+
} else {
79+
// Comma-separated literal parameter names
80+
const SEPARATOR_MARK = ',';
81+
const paramsToRemove = splitByNotEscapedDelimiter(parametersToRemove, SEPARATOR_MARK);
82+
// Convert each literal string to a RegExp (toRegExp escapes special chars for non-regex strings)
83+
regexpParamsToRemove = paramsToRemove.map((param) => toRegExp(param));
84+
}
85+
} catch (e) {
86+
logMessage(source, `Invalid parameter pattern: ${parametersToRemove}`);
87+
return;
88+
}
89+
90+
/**
91+
* Removes query parameters from the URL.
92+
*
93+
* @param url URL to remove query parameters from.
94+
*
95+
* @returns Modified URL.
96+
*/
97+
const removeParams = (url: string): string => {
98+
try {
99+
let modified = false;
100+
const urlObj = new URL(url, window.location.origin);
101+
102+
// Get all parameter names to check against regexps
103+
const paramNames = Array.from(urlObj.searchParams.keys());
104+
105+
paramNames.forEach((paramName) => {
106+
// Check if any of the param patterns match this parameter name
107+
// Reset lastIndex before each test to avoid stateful issues with `g` or `y` flags
108+
const shouldRemove = regexpParamsToRemove.some((regex) => {
109+
regex.lastIndex = 0;
110+
return regex.test(paramName);
111+
});
112+
if (shouldRemove) {
113+
urlObj.searchParams.delete(paramName);
114+
modified = true;
115+
}
116+
});
117+
118+
if (modified) {
119+
hit(source);
120+
return urlObj.toString();
121+
}
122+
} catch (e) {
123+
logMessage(source, `Cannot remove query parameters from URL: ${url}`);
124+
}
125+
return url;
126+
};
127+
128+
const xhrWrapper = (
129+
target: XMLHttpRequest['open'],
130+
thisArg: XMLHttpRequest,
131+
argumentsList: unknown[],
132+
) => {
133+
const urlArg = argumentsList[1];
134+
if (!urlArg) {
135+
return Reflect.apply(target, thisArg, argumentsList);
136+
}
137+
138+
let url: string;
139+
if (typeof urlArg === 'string') {
140+
url = urlArg;
141+
} else if (urlArg instanceof URL) {
142+
url = urlArg.toString();
143+
} else {
144+
return Reflect.apply(target, thisArg, argumentsList);
145+
}
146+
147+
if (urlPatternRegExp && !urlPatternRegExp.test(url)) {
148+
return Reflect.apply(target, thisArg, argumentsList);
149+
}
150+
151+
const newUrl = removeParams(url);
152+
argumentsList[1] = newUrl;
153+
154+
return Reflect.apply(target, thisArg, argumentsList);
155+
};
156+
157+
const fetchWrapper = (
158+
target: typeof window.fetch,
159+
thisArg: typeof window,
160+
argumentsList: unknown[],
161+
) => {
162+
const requestUrl = {
163+
url: '',
164+
type: '',
165+
};
166+
167+
const urlArg = argumentsList[0];
168+
if (!urlArg) {
169+
return Reflect.apply(target, thisArg, argumentsList);
170+
}
171+
172+
if (typeof urlArg === 'string') {
173+
requestUrl.url = urlArg;
174+
requestUrl.type = 'string';
175+
} else if (urlArg instanceof URL) {
176+
requestUrl.url = urlArg.toString();
177+
requestUrl.type = 'string';
178+
} else if (urlArg instanceof Request) {
179+
requestUrl.url = urlArg.url;
180+
requestUrl.type = 'request';
181+
}
182+
183+
if (!requestUrl.url) {
184+
return Reflect.apply(target, thisArg, argumentsList);
185+
}
186+
187+
if (urlPatternRegExp && !urlPatternRegExp.test(requestUrl.url)) {
188+
return Reflect.apply(target, thisArg, argumentsList);
189+
}
190+
191+
const newUrl = removeParams(requestUrl.url);
192+
if (newUrl === requestUrl.url) {
193+
// No modification was made
194+
return Reflect.apply(target, thisArg, argumentsList);
195+
}
196+
if (requestUrl.type === 'string') {
197+
argumentsList[0] = newUrl;
198+
} else if (requestUrl.type === 'request') {
199+
// Request.url is read-only, so we need to create a new Request with the modified URL
200+
const originalRequest = argumentsList[0] as Request;
201+
argumentsList[0] = new Request(newUrl, originalRequest);
202+
}
203+
return Reflect.apply(target, thisArg, argumentsList);
204+
};
205+
206+
const xhrHandler = {
207+
apply: xhrWrapper,
208+
};
209+
210+
const fetchHandler = {
211+
apply: fetchWrapper,
212+
};
213+
214+
window.XMLHttpRequest.prototype.open = new Proxy(
215+
window.XMLHttpRequest.prototype.open,
216+
xhrHandler,
217+
);
218+
219+
window.fetch = new Proxy(
220+
window.fetch,
221+
fetchHandler,
222+
);
223+
}
224+
225+
export const removeRequestQueryParameterNames = [
226+
'remove-request-query-parameter',
227+
'abp-strip-fetch-query-parameter',
228+
];
229+
230+
// eslint-disable-next-line prefer-destructuring
231+
removeRequestQueryParameter.primaryName = removeRequestQueryParameterNames[0];
232+
233+
removeRequestQueryParameter.injections = [
234+
hit,
235+
logMessage,
236+
splitByNotEscapedDelimiter,
237+
toRegExp,
238+
];

src/scriptlets/scriptlets-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export { preventCanvas } from './prevent-canvas';
7777
export { trustedReplaceArgument } from './trusted-replace-argument';
7878
export { preventInnerHTML } from './prevent-innerHTML';
7979
export { preventConstructor } from './prevent-constructor';
80+
export { removeRequestQueryParameter } from './remove-request-query-parameter';
8081
// redirects as scriptlets
8182
// https://github.com/AdguardTeam/Scriptlets/issues/300
8283
export { AmazonApstag } from './amazon-apstag';

src/scriptlets/scriptlets-names-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export { preventCanvasNames } from './prevent-canvas';
7777
export { trustedReplaceArgumentNames } from './trusted-replace-argument';
7878
export { preventInnerHTMLNames } from './prevent-innerHTML';
7979
export { preventConstructorNames } from './prevent-constructor';
80+
export { removeRequestQueryParameterNames } from './remove-request-query-parameter';
8081
// redirects as scriptlets
8182
// https://github.com/AdguardTeam/Scriptlets/issues/300
8283
export { AmazonApstagNames } from './amazon-apstag';

0 commit comments

Comments
 (0)