Skip to content

Commit 9d40d11

Browse files
committed
AG-39760 Improve 'prevent-addEventListener' — add ability to match specific element. #480
Squashed commit of the following: commit c16c23f Author: Adam Wróblewski <[email protected]> Date: Tue Mar 25 17:10:07 2025 +0100 Add log message for additionalArgName and additionalArgValue commit 025902f Author: Slava Leleka <[email protected]> Date: Tue Mar 25 18:45:20 2025 +0300 Update docs commit b05c54a Author: Adam Wróblewski <[email protected]> Date: Mon Mar 24 13:55:22 2025 +0100 Add JSDoc for elementMatches function Fix element type checking in getElementAttributesWithValues commit 3147a1a Author: Slava Leleka <[email protected]> Date: Mon Mar 24 15:31:57 2025 +0300 Update jsdoc commit 33fa116 Author: Adam Wróblewski <[email protected]> Date: Mon Mar 24 12:34:22 2025 +0100 Fix formatting in documentation commit 8b20b18 Author: Adam Wróblewski <[email protected]> Date: Mon Mar 24 12:19:24 2025 +0100 Improve log-addEventListener Log target element commit 5f9c075 Author: Adam Wróblewski <[email protected]> Date: Fri Mar 14 15:07:50 2025 +0100 Improve 'prevent-addEventListener' — add ability to match specific element
1 parent 1225b4f commit 9d40d11

7 files changed

+320
-11
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ 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]
14+
15+
### Added
16+
17+
- ability in `prevent-addEventListener` scriptlet to match specific element
18+
and updated `log-addEventListener` scriptlet to log target element [#480]
19+
20+
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.1.6...HEAD
21+
[#480]: https://github.com/AdguardTeam/Scriptlets/issues/480
22+
1323
## [v2.1.6] - 2025-03-06
1424

1525
### Fixed

src/helpers/attribute-utils.ts

+31
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,34 @@ export const parseAttributePairs = (input: string): ParsedAttributePair[] => {
156156

157157
return pairs;
158158
};
159+
160+
/**
161+
* Retrieves the attributes and their values of a given DOM element
162+
* and returns them as a string in the format:
163+
* 'nodeName[attrName1="attrValue1"][attrName2="attrValue2"]'.
164+
*
165+
* @param element The DOM element to extract attributes from.
166+
* @returns A string representation of the element's attributes
167+
* and their values, or an empty string if the element
168+
* or its attributes are not defined.
169+
*/
170+
export const getElementAttributesWithValues = (element: any): string => {
171+
if (
172+
!element
173+
|| !(element instanceof Element)
174+
|| !element.attributes
175+
|| !element.nodeName
176+
) {
177+
return '';
178+
}
179+
const attributes = element.attributes;
180+
const nodeName = element.nodeName.toLowerCase();
181+
let result = nodeName;
182+
183+
for (let i = 0; i < attributes.length; i += 1) {
184+
const attr = attributes[i];
185+
result += `[${attr.name}="${attr.value}"]`;
186+
}
187+
188+
return result;
189+
};

src/scriptlets/log-addEventListener.js

+28-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
logMessage,
88
objectToString,
99
isEmptyObject,
10+
getElementAttributesWithValues,
1011
} from '../helpers';
1112

1213
/**
@@ -31,8 +32,32 @@ export function logAddEventListener(source) {
3132

3233
function addEventListenerWrapper(type, listener, ...args) {
3334
if (validateType(type) && validateListener(listener)) {
34-
const message = `addEventListener("${type}", ${listenerToString(listener)})`;
35-
logMessage(source, message, true);
35+
let targetElement;
36+
let targetElementInfo;
37+
const listenerInfo = listenerToString(listener);
38+
39+
if (this) {
40+
if (this instanceof Window) {
41+
targetElementInfo = 'window';
42+
} else if (this instanceof Document) {
43+
targetElementInfo = 'document';
44+
} else if (this instanceof Element) {
45+
targetElement = this;
46+
targetElementInfo = getElementAttributesWithValues(this);
47+
}
48+
}
49+
50+
if (targetElementInfo) {
51+
const message = `addEventListener("${type}", ${listenerInfo})\nElement: ${targetElementInfo}`;
52+
logMessage(source, message, true);
53+
if (targetElement) {
54+
// eslint-disable-next-line no-console
55+
console.log('log-addEventListener Element:', targetElement);
56+
}
57+
} else {
58+
const message = `addEventListener("${type}", ${listenerInfo})`;
59+
logMessage(source, message, true);
60+
}
3661
hit(source);
3762
} else {
3863
// logging while debugging
@@ -86,4 +111,5 @@ logAddEventListener.injections = [
86111
logMessage,
87112
objectToString,
88113
isEmptyObject,
114+
getElementAttributesWithValues,
89115
];

src/scriptlets/prevent-addEventListener.js

+79-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
validateType,
55
validateListener,
66
listenerToString,
7+
logMessage,
78
} from '../helpers';
89

910
/* eslint-disable max-len */
@@ -21,14 +22,24 @@ import {
2122
*
2223
* ### Syntax
2324
*
25+
* <!-- markdownlint-disable line-length -->
26+
*
2427
* ```text
25-
* example.org#%#//scriptlet('prevent-addEventListener'[, typeSearch[, listenerSearch]])
28+
* example.org#%#//scriptlet('prevent-addEventListener'[, typeSearch[, listenerSearch[, additionalArgName, additionalArgValue]]])
2629
* ```
2730
*
31+
* <!-- markdownlint-enable line-length -->
32+
*
2833
* - `typeSearch` — optional, string or regular expression matching the type (event name);
2934
* defaults to match all types; invalid regular expression will cause exit and rule will not work
3035
* - `listenerSearch` — optional, string or regular expression matching the listener function body;
3136
* defaults to match all listeners; invalid regular expression will cause exit and rule will not work
37+
* - `additionalArgName` — optional, string, name of the additional argument to match;
38+
* currently only `elements` is supported;
39+
* - `additionalArgValue` — optional, value corresponding to the additional argument name;
40+
* for `elements` it can be a CSS selector or one of the following values:
41+
* - `window`
42+
* - `document`
3243
*
3344
* ### Examples
3445
*
@@ -52,20 +63,84 @@ import {
5263
* });
5364
* ```
5465
*
66+
* 1. Prevent 'click' listeners with the callback body containing `foo` and only if the element has the class `bar`
67+
*
68+
* ```adblock
69+
* example.org#%#//scriptlet('prevent-addEventListener', 'click', 'foo', 'elements', '.bar')
70+
* ```
71+
*
72+
* For instance, this listener will not be called:
73+
*
74+
* ```javascript
75+
* const el = document.querySelector('.bar');
76+
* el.addEventListener('click', () => {
77+
* window.test = 'foo';
78+
* });
79+
* ```
80+
*
81+
* This listener will be called:
82+
*
83+
* ```javascript
84+
* const el = document.querySelector('.xyz');
85+
* el.addEventListener('click', () => {
86+
* window.test = 'foo';
87+
* });
88+
* ```
89+
*
5590
* @added v1.0.4.
5691
*/
5792
/* eslint-enable max-len */
58-
export function preventAddEventListener(source, typeSearch, listenerSearch) {
93+
export function preventAddEventListener(source, typeSearch, listenerSearch, additionalArgName, additionalArgValue) {
5994
const typeSearchRegexp = toRegExp(typeSearch);
6095
const listenerSearchRegexp = toRegExp(listenerSearch);
6196

97+
let elementToMatch;
98+
if (additionalArgName) {
99+
if (additionalArgName !== 'elements') {
100+
logMessage(source, `Invalid "additionalArgName": ${additionalArgName}\nOnly "elements" is supported.`);
101+
return;
102+
}
103+
104+
if (!additionalArgValue) {
105+
logMessage(source, '"additionalArgValue" is required.');
106+
return;
107+
}
108+
109+
elementToMatch = additionalArgValue;
110+
}
111+
112+
/**
113+
* Checks if an element matches the specified selector or element type.
114+
*
115+
* @param {any} element - The element to check
116+
* @returns {boolean}
117+
*/
118+
const elementMatches = (element) => {
119+
// If elementToMatch is undefined, it means that the scriptlet was called without the `elements` argument
120+
// so it should match all elements
121+
if (elementToMatch === undefined) {
122+
return true;
123+
}
124+
if (elementToMatch === 'window') {
125+
return element === window;
126+
}
127+
if (elementToMatch === 'document') {
128+
return element === document;
129+
}
130+
if (element && element.matches && element.matches(elementToMatch)) {
131+
return true;
132+
}
133+
return false;
134+
};
135+
62136
const nativeAddEventListener = window.EventTarget.prototype.addEventListener;
63137

64138
function addEventListenerWrapper(type, listener, ...args) {
65139
let shouldPrevent = false;
66140
if (validateType(type) && validateListener(listener)) {
67141
shouldPrevent = typeSearchRegexp.test(type.toString())
68-
&& listenerSearchRegexp.test(listenerToString(listener));
142+
&& listenerSearchRegexp.test(listenerToString(listener))
143+
&& elementMatches(this);
69144
}
70145

71146
if (shouldPrevent) {
@@ -115,4 +190,5 @@ preventAddEventListener.injections = [
115190
validateType,
116191
validateListener,
117192
listenerToString,
193+
logMessage,
118194
];

tests/helpers/attribute-utils.spec.js

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect } from 'vitest';
22

3-
import { parseAttributePairs } from '../../src/helpers';
3+
import { parseAttributePairs, getElementAttributesWithValues } from '../../src/helpers';
44

55
describe('parseAttributePairs', () => {
66
describe('valid input', () => {
@@ -153,3 +153,36 @@ describe('parseAttributePairs', () => {
153153
});
154154
});
155155
});
156+
157+
describe('getElementAttributesWithValues', () => {
158+
test('Only node name', () => {
159+
const anchor = document.createElement('a');
160+
const expected = 'a';
161+
const result = getElementAttributesWithValues(anchor);
162+
expect(result).toStrictEqual(expected);
163+
});
164+
165+
test('Node name with attributes', () => {
166+
const NODE_NAME = 'div';
167+
const ATTRIBUTE_CLASS = 'class';
168+
const ATTRIBUTE_CLASS_VALUE = 'test-class';
169+
const ATTRIBUTE_STYLE = 'style';
170+
const ATTRIBUTE_STYLE_VALUE = 'display: none;';
171+
const ATTRIBUTE_DATA_TEST = 'data-test';
172+
const ATTRIBUTE_DATA_TEST_VALUE = 'test-value';
173+
const divWithClassAndStyle = document.createElement(NODE_NAME);
174+
divWithClassAndStyle.setAttribute(ATTRIBUTE_CLASS, ATTRIBUTE_CLASS_VALUE);
175+
divWithClassAndStyle.setAttribute(ATTRIBUTE_STYLE, ATTRIBUTE_STYLE_VALUE);
176+
divWithClassAndStyle.setAttribute(ATTRIBUTE_DATA_TEST, ATTRIBUTE_DATA_TEST_VALUE);
177+
// eslint-disable-next-line max-len
178+
const expected = `${NODE_NAME}[${ATTRIBUTE_CLASS}="${ATTRIBUTE_CLASS_VALUE}"][${ATTRIBUTE_STYLE}="${ATTRIBUTE_STYLE_VALUE}"][${ATTRIBUTE_DATA_TEST}="${ATTRIBUTE_DATA_TEST_VALUE}"]`;
179+
const result = getElementAttributesWithValues(divWithClassAndStyle);
180+
expect(result).toStrictEqual(expected);
181+
});
182+
183+
test('Not element - should return empty string', () => {
184+
const expected = '';
185+
const result = getElementAttributesWithValues('test');
186+
expect(result).toStrictEqual(expected);
187+
});
188+
});

tests/scriptlets/log-addEventListener.test.js

+52-5
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,38 @@ test('Checking if alias name works', (assert) => {
4848
});
4949

5050
test('logs events to console', (assert) => {
51+
assert.expect(7);
52+
53+
const elementId = 'testElement';
5154
const agLogAddEventListenerProp = 'agLogAddEventListenerProp';
5255
const eventName = 'click';
5356
const callback = function callback() {
5457
window[agLogAddEventListenerProp] = 'clicked';
5558
};
56-
console.log = function log(input) {
59+
60+
const element = document.createElement('div');
61+
element.setAttribute('id', elementId);
62+
console.log = function log(...args) {
63+
const input = args[0];
64+
const elementArg = args[1];
5765
// Ignore hit messages with "trace"
5866
if (input.includes('trace')) {
5967
return;
6068
}
61-
assert.ok(input.includes(eventName), 'console.hit input should be equal');
62-
assert.ok(input.includes(callback.toString()), 'console.hit input should be equal');
63-
assert.notOk(input.includes(INVALID_MESSAGE_START), 'Invalid message should not be displayed');
69+
70+
if (input.includes('log-addEventListener Element:')) {
71+
assert.true(elementArg.matches(`div#${elementId}`), 'target element should matches the element');
72+
} else {
73+
assert.ok(input.includes(eventName), 'event name should be logged');
74+
assert.ok(input.includes(callback.toString()), 'callback should be logged');
75+
assert.ok(input.includes(`Element: div[id="${elementId}"]`), 'target element should be logged');
76+
assert.notOk(input.includes(INVALID_MESSAGE_START), 'Invalid message should not be displayed');
77+
}
78+
nativeConsole(...args);
6479
};
6580

6681
runScriptlet(name);
6782

68-
const element = document.createElement('div');
6983
element.addEventListener(eventName, callback);
7084
element.click();
7185

@@ -74,6 +88,39 @@ test('logs events to console', (assert) => {
7488
clearGlobalProps(agLogAddEventListenerProp);
7589
});
7690

91+
test('logs events to console - listener added to window', (assert) => {
92+
assert.expect(6);
93+
94+
const agLogAddEventListenerProp = 'agLogAddEventListenerProp';
95+
const eventName = 'click';
96+
const callback = function callback() {
97+
window[agLogAddEventListenerProp] = 'clicked';
98+
};
99+
100+
console.log = function log(...args) {
101+
const input = args[0];
102+
// Ignore hit messages with "trace"
103+
if (input.includes('trace')) {
104+
return;
105+
}
106+
assert.ok(input.includes(eventName), 'event name should be logged');
107+
assert.ok(input.includes(callback.toString()), 'callback should be logged');
108+
assert.ok(input.includes('Element: window'), 'target element should be logged');
109+
assert.notOk(input.includes(INVALID_MESSAGE_START), 'Invalid message should not be displayed');
110+
111+
nativeConsole(...args);
112+
};
113+
114+
runScriptlet(name);
115+
116+
window.addEventListener(eventName, callback);
117+
window.dispatchEvent(new Event(eventName));
118+
119+
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
120+
assert.strictEqual(window[agLogAddEventListenerProp], 'clicked', 'property should change');
121+
clearGlobalProps(agLogAddEventListenerProp);
122+
});
123+
77124
test('logs events to console - listener is null', (assert) => {
78125
const eventName = 'click';
79126
const listener = null;

0 commit comments

Comments
 (0)