Skip to content

Commit 0435d8e

Browse files
committed
AG-51234 Improve 'log-addEventListener' — add new optional parameter to control overriding of addEventListener. #551
Squashed commit of the following: commit 23d4b73 Author: Adam Wróblewski <adam@adguard.com> Date: Tue Apr 7 15:16:38 2026 +0200 Revert some changes commit b82bce7 Author: Adam Wróblewski <adam@adguard.com> Date: Tue Apr 7 14:58:35 2026 +0200 Add `noProtect` option to `log-addEventListener` scriptlet
1 parent 6db81a2 commit 0435d8e

3 files changed

Lines changed: 240 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
2121
- Support for `JSONPath` in `json-prune`, `json-prune-fetch-response`, `json-prune-xhr-response`,
2222
`trusted-json-set`, `trusted-json-set-fetch-response` and `trusted-json-set-xhr-response` scriptlets [#522].
2323

24+
### Changed
25+
26+
- `log-addEventListener` scriptlet: added new optional `noProtect` parameter,
27+
improving compatibility with other scriptlets that need to override `addEventListener` [#551].
28+
2429
### Fixed
2530

2631
- `trusted-click-element` no longer throws when event handlers set `cancelBubble`
2732
on spoofed events [#555].
2833

2934
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.3.1...HEAD
3035
[#522]: https://github.com/AdguardTeam/Scriptlets/issues/522
36+
[#551]: https://github.com/AdguardTeam/Scriptlets/issues/551
3137
[#555]: https://github.com/AdguardTeam/Scriptlets/issues/555
3238

3339
## [v2.3.1] - 2026-03-24
Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isEmptyObject,
1010
getElementAttributesWithValues,
1111
} from '../helpers';
12+
import { type Source } from './scriptlets';
1213

1314
/**
1415
* @scriptlet log-addEventListener
@@ -21,20 +22,31 @@ import {
2122
*
2223
* ### Syntax
2324
*
24-
* ```adblock
25-
* example.org#%#//scriptlet('log-addEventListener')
25+
* ```text
26+
* example.org#%#//scriptlet('log-addEventListener'[, noProtect])
2627
* ```
2728
*
29+
* - `noProtect` — optional, if set to `'true'`, the scriptlet will use simple assignment instead of
30+
* `Object.defineProperty` with a no-op setter. This allows other scriptlets or tools to override
31+
* `addEventListener` later if needed. By default, the scriptlet protects the override from being
32+
* overwritten by website scripts. If compatibility with other scriptlets is needed,
33+
* set this parameter to `'true'`.
34+
*
2835
* @added v1.0.4.
2936
*/
30-
export function logAddEventListener(source) {
37+
export function logAddEventListener(source: Source, noProtect?: string) {
3138
const nativeAddEventListener = window.EventTarget.prototype.addEventListener;
3239

33-
function addEventListenerWrapper(type, listener, ...args) {
40+
function addEventListenerWrapper(
41+
this: EventTarget | null | undefined,
42+
type: string,
43+
listener: EventListenerOrEventListenerObject | null,
44+
...args: [options?: boolean | AddEventListenerOptions]
45+
) {
3446
if (validateType(type) && validateListener(listener)) {
35-
let targetElement;
36-
let targetElementInfo;
37-
const listenerInfo = listenerToString(listener);
47+
let targetElement: Element | undefined;
48+
let targetElementInfo: string | undefined;
49+
const listenerInfo = listenerToString(listener as EventListener | EventListenerObject);
3850

3951
if (this) {
4052
if (this instanceof Window) {
@@ -76,16 +88,21 @@ export function logAddEventListener(source) {
7688
return nativeAddEventListener.apply(context, [type, listener, ...args]);
7789
}
7890

79-
const descriptor = {
80-
configurable: true,
81-
set: () => {},
82-
get: () => addEventListenerWrapper,
83-
};
84-
// https://github.com/AdguardTeam/Scriptlets/issues/215
85-
// https://github.com/AdguardTeam/Scriptlets/issues/143
86-
Object.defineProperty(window.EventTarget.prototype, 'addEventListener', descriptor);
87-
Object.defineProperty(window, 'addEventListener', descriptor);
88-
Object.defineProperty(document, 'addEventListener', descriptor);
91+
// https://github.com/AdguardTeam/Scriptlets/issues/551
92+
if (noProtect === 'true') {
93+
window.EventTarget.prototype.addEventListener = addEventListenerWrapper;
94+
} else {
95+
const descriptor = {
96+
configurable: true,
97+
set: () => { },
98+
get: () => addEventListenerWrapper,
99+
};
100+
// https://github.com/AdguardTeam/Scriptlets/issues/215
101+
// https://github.com/AdguardTeam/Scriptlets/issues/143
102+
Object.defineProperty(window.EventTarget.prototype, 'addEventListener', descriptor);
103+
Object.defineProperty(window, 'addEventListener', descriptor);
104+
Object.defineProperty(document, 'addEventListener', descriptor);
105+
}
89106
}
90107

91108
export const logAddEventListenerNames = [

tests/scriptlets/log-addEventListener.test.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,206 @@ test('logs events to console - listener added to window', (assert) => {
121121
clearGlobalProps(agLogAddEventListenerProp);
122122
});
123123

124+
test('forwards addEventListener options', (assert) => {
125+
assert.expect(6);
126+
127+
const useCaptureElement = document.createElement('div');
128+
const onceElement = document.createElement('div');
129+
const passiveCaptureElement = document.createElement('div');
130+
const combinedOptionsElement = document.createElement('div');
131+
let useCaptureCallCount = 0;
132+
let onceCallCount = 0;
133+
let passiveCaptureCallCount = 0;
134+
let combinedCallCount = 0;
135+
136+
runScriptlet(name);
137+
138+
useCaptureElement.addEventListener('click', () => {
139+
useCaptureCallCount += 1;
140+
}, true);
141+
142+
onceElement.addEventListener('click', () => {
143+
onceCallCount += 1;
144+
}, { once: true });
145+
146+
passiveCaptureElement.addEventListener('click', () => {
147+
passiveCaptureCallCount += 1;
148+
}, { capture: true, passive: true });
149+
150+
combinedOptionsElement.addEventListener('click', () => {
151+
combinedCallCount += 1;
152+
}, { capture: true, passive: true, once: true });
153+
154+
useCaptureElement.click();
155+
useCaptureElement.click();
156+
onceElement.click();
157+
onceElement.click();
158+
passiveCaptureElement.click();
159+
combinedOptionsElement.click();
160+
combinedOptionsElement.click();
161+
162+
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
163+
assert.strictEqual(
164+
useCaptureCallCount,
165+
2,
166+
'boolean capture option should be forwarded to native addEventListener',
167+
);
168+
assert.strictEqual(
169+
onceCallCount,
170+
1,
171+
'once option should be forwarded to native addEventListener',
172+
);
173+
assert.strictEqual(
174+
passiveCaptureCallCount,
175+
1,
176+
'capture and passive options should be forwarded to native addEventListener',
177+
);
178+
assert.strictEqual(
179+
combinedCallCount,
180+
1,
181+
'combined options object should be forwarded to native addEventListener',
182+
);
183+
assert.strictEqual(
184+
typeof onceElement.addEventListener,
185+
'function',
186+
'wrapped addEventListener should stay callable',
187+
);
188+
});
189+
190+
test('noProtect parameter allows subsequent override of addEventListener', (assert) => {
191+
assert.expect(9);
192+
193+
const scriptletArgs = ['true'];
194+
runScriptlet(name, scriptletArgs);
195+
196+
const elementId = 'noProtectElement';
197+
const elementProp = 'elementProp';
198+
const elementEventName = 'click';
199+
const callback = function callback() {
200+
window[elementProp] = 'clicked';
201+
};
202+
203+
const element = document.createElement('div');
204+
element.setAttribute('id', elementId);
205+
console.log = function log(...args) {
206+
const input = args[0];
207+
const elementArg = args[1];
208+
if (input.includes('trace')) {
209+
return;
210+
}
211+
212+
if (input.includes('log-addEventListener Element:')) {
213+
assert.true(elementArg.matches(`div#${elementId}`), 'target element should match the noProtect element');
214+
} else {
215+
assert.ok(input.includes(elementEventName), 'event name should be logged for noProtect');
216+
assert.ok(input.includes(callback.toString()), 'callback should be logged for noProtect');
217+
assert.ok(
218+
input.includes(`Element: div[id="${elementId}"]`),
219+
'target element should be logged for noProtect',
220+
);
221+
assert.notOk(
222+
input.includes(INVALID_MESSAGE_START),
223+
'Invalid message should not be displayed for noProtect',
224+
);
225+
}
226+
227+
nativeConsole(...args);
228+
};
229+
230+
element.addEventListener(elementEventName, callback);
231+
element.click();
232+
233+
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
234+
assert.strictEqual(window[elementProp], 'clicked', 'element listener should still be logged and called');
235+
clearGlobalProps('hit', elementProp);
236+
237+
let overrideWorked = false;
238+
window.EventTarget.prototype.addEventListener = function customWrapper() {
239+
overrideWorked = true;
240+
};
241+
242+
const elementAfterOverride = document.createElement('div');
243+
elementAfterOverride.addEventListener('click', () => {});
244+
245+
assert.strictEqual(overrideWorked, true, 'addEventListener should be overridable with noProtect');
246+
assert.strictEqual(window.hit, undefined, 'hit should NOT fire');
247+
});
248+
249+
test('default behavior (no noProtect) protects addEventListener from override', (assert) => {
250+
assert.expect(12);
251+
252+
runScriptlet(name);
253+
254+
const descriptor = Object.getOwnPropertyDescriptor(
255+
window.EventTarget.prototype,
256+
'addEventListener',
257+
);
258+
259+
assert.strictEqual(typeof descriptor.get, 'function', 'descriptor should have a getter');
260+
assert.strictEqual(typeof descriptor.set, 'function', 'descriptor should have a setter');
261+
assert.strictEqual(descriptor.configurable, true, 'descriptor should be configurable');
262+
263+
const originalWrapper = descriptor.get();
264+
265+
window.EventTarget.prototype.addEventListener = function maliciousWrapper() {
266+
throw new Error('This should not be called');
267+
};
268+
269+
const currentAddEventListener = window.EventTarget.prototype.addEventListener;
270+
assert.strictEqual(
271+
currentAddEventListener,
272+
originalWrapper,
273+
'addEventListener should still be the scriptlet wrapper after attempted override',
274+
);
275+
276+
assert.strictEqual(
277+
descriptor.get(),
278+
originalWrapper,
279+
'getter should still return original wrapper after setter was called',
280+
);
281+
282+
const elementId = 'protectedElement';
283+
const protectedProp = 'protectedProp';
284+
const eventName = 'click';
285+
const callback = function callback() {
286+
window[protectedProp] = 'clicked';
287+
};
288+
289+
const element = document.createElement('div');
290+
element.setAttribute('id', elementId);
291+
console.log = function log(...args) {
292+
const input = args[0];
293+
const elementArg = args[1];
294+
if (input.includes('trace')) {
295+
return;
296+
}
297+
298+
if (input.includes('log-addEventListener Element:')) {
299+
assert.true(elementArg.matches(`div#${elementId}`), 'target element should match the protected element');
300+
} else {
301+
assert.ok(input.includes(eventName), 'event name should be logged after blocked override');
302+
assert.ok(input.includes(callback.toString()), 'callback should be logged after blocked override');
303+
assert.ok(
304+
input.includes(`Element: div[id="${elementId}"]`),
305+
'target element should be logged after blocked override',
306+
);
307+
assert.notOk(
308+
input.includes(INVALID_MESSAGE_START),
309+
'Invalid message should not be displayed after blocked override',
310+
);
311+
}
312+
313+
nativeConsole(...args);
314+
};
315+
316+
element.addEventListener(eventName, callback);
317+
element.click();
318+
319+
assert.strictEqual(window.hit, 'FIRED', 'hit function fired after blocked override');
320+
assert.strictEqual(window[protectedProp], 'clicked', 'listener should still use the original protected wrapper');
321+
clearGlobalProps(protectedProp);
322+
});
323+
124324
test('logs events to console - listener is null', (assert) => {
125325
const eventName = 'click';
126326
const listener = null;

0 commit comments

Comments
 (0)