From 1977196abe83d5ac28516231b93931aec04752fa Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 18 Dec 2025 09:03:36 -0500 Subject: [PATCH] Improve `prevent-addEventListener` scriptlet --- src/js/resources/prevent-addeventlistener.js | 158 +++++++++++++++++++ src/js/resources/scriptlets.js | 97 +----------- 2 files changed, 159 insertions(+), 96 deletions(-) create mode 100644 src/js/resources/prevent-addeventlistener.js diff --git a/src/js/resources/prevent-addeventlistener.js b/src/js/resources/prevent-addeventlistener.js new file mode 100644 index 000000000..885ebaa05 --- /dev/null +++ b/src/js/resources/prevent-addeventlistener.js @@ -0,0 +1,158 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +/** + * @scriptlet prevent-addEventListener + * + * @description + * Conditionally prevent execution of the callback function passed to native + * addEventListener method. With no parameters, all calls to addEventListener + * will be shown in the logger. + * + * @param [type] + * The type of the event to prevent. The pattern can be a plain string, or a + * regex to specify more than one event type. + * + * @param [pattern] + * A pattern to match against the stringified callback. The pattern can be a + * plain string, or a regex. + * + * @param [runat, value] + * An optional vararg which tell the scriptlet when the prevention should + * start. Values correspond to `readyState`: loading, interactive, complete. + * + * @param [protect, 1] + * An optional vararg which tells the scriptlet whether the prevention should + * be protected, i.e. not overwritten by other code. + * + * */ + +function preventAddEventListener( + type = '', + pattern = '' +) { + const safe = safeSelf(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern); + const reType = safe.patternToRegex(type, undefined, true); + const rePattern = safe.patternToRegex(pattern); + const targetSelector = extraArgs.elements || undefined; + const elementMatches = elem => { + if ( targetSelector === 'window' ) { return elem === window; } + if ( targetSelector === 'document' ) { return elem === document; } + if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; } + const elems = Array.from(document.querySelectorAll(targetSelector)); + return elems.includes(elem); + }; + const elementDetails = elem => { + if ( elem instanceof Window ) { return 'window'; } + if ( elem instanceof Document ) { return 'document'; } + if ( elem instanceof Element === false ) { return '?'; } + const parts = []; + // https://github.com/uBlockOrigin/uAssets/discussions/17907#discussioncomment-9871079 + const id = String(elem.id); + if ( id !== '' ) { parts.push(`#${CSS.escape(id)}`); } + for ( let i = 0; i < elem.classList.length; i++ ) { + parts.push(`.${CSS.escape(elem.classList.item(i))}`); + } + for ( let i = 0; i < elem.attributes.length; i++ ) { + const attr = elem.attributes.item(i); + if ( attr.name === 'id' ) { continue; } + if ( attr.name === 'class' ) { continue; } + parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`); + } + return parts.join(''); + }; + const shouldPrevent = (thisArg, type, handler) => { + const matchesType = safe.RegExp_test.call(reType, type); + const matchesHandler = safe.RegExp_test.call(rePattern, handler); + const matchesEither = matchesType || matchesHandler; + const matchesBoth = matchesType && matchesHandler; + if ( safe.logLevel > 1 && matchesEither ) { + debugger; // eslint-disable-line no-debugger + } + if ( matchesBoth && targetSelector !== undefined ) { + if ( elementMatches(thisArg) === false ) { return false; } + } + return matchesBoth; + }; + const proxyFn = function(context) { + const { callArgs, thisArg } = context; + let t, h; + try { + t = String(callArgs[0]); + if ( typeof callArgs[1] === 'function' ) { + h = String(safe.Function_toString(callArgs[1])); + } else if ( typeof callArgs[1] === 'object' && callArgs[1] !== null ) { + if ( typeof callArgs[1].handleEvent === 'function' ) { + h = String(safe.Function_toString(callArgs[1].handleEvent)); + } + } else { + h = String(callArgs[1]); + } + } catch { + } + if ( type === '' && pattern === '' ) { + safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`); + } else if ( shouldPrevent(thisArg, t, h) ) { + return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`); + } + return context.reflect(); + }; + runAt(( ) => { + proxyApplyFn('EventTarget.prototype.addEventListener', proxyFn); + if ( extraArgs.protect ) { + const { addEventListener } = EventTarget.prototype; + Object.defineProperty(EventTarget.prototype, 'addEventListener', { + set() { }, + get() { return addEventListener; } + }); + } + proxyApplyFn('document.addEventListener', proxyFn); + if ( extraArgs.protect ) { + const { addEventListener } = document; + Object.defineProperty(document, 'addEventListener', { + set() { }, + get() { return addEventListener; } + }); + } + }, extraArgs.runAt); +} +registerScriptlet(preventAddEventListener , { + name: 'prevent-addEventListener.js', + aliases: [ + 'addEventListener-defuser.js', + 'aeld.js', + ], + dependencies: [ + proxyApplyFn, + runAt, + safeSelf, + ], +}); diff --git a/src/js/resources/scriptlets.js b/src/js/resources/scriptlets.js index 74a7beb81..f5cb03c9c 100755 --- a/src/js/resources/scriptlets.js +++ b/src/js/resources/scriptlets.js @@ -27,6 +27,7 @@ import './json-edit.js'; import './json-prune.js'; import './noeval.js'; import './object-prune.js'; +import './prevent-addeventlistener.js'; import './prevent-dialog.js'; import './prevent-fetch.js'; import './prevent-innerHTML.js'; @@ -701,102 +702,6 @@ function abortOnPropertyWrite( /******************************************************************************/ -builtinScriptlets.push({ - name: 'addEventListener-defuser.js', - aliases: [ - 'aeld.js', - 'prevent-addEventListener.js', - ], - fn: addEventListenerDefuser, - dependencies: [ - 'proxy-apply.fn', - 'run-at.fn', - 'safe-self.fn', - 'should-debug.fn', - ], -}); -// https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120 -function addEventListenerDefuser( - type = '', - pattern = '' -) { - const safe = safeSelf(); - const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); - const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern); - const reType = safe.patternToRegex(type, undefined, true); - const rePattern = safe.patternToRegex(pattern); - const debug = shouldDebug(extraArgs); - const targetSelector = extraArgs.elements || undefined; - const elementMatches = elem => { - if ( targetSelector === 'window' ) { return elem === window; } - if ( targetSelector === 'document' ) { return elem === document; } - if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; } - const elems = Array.from(document.querySelectorAll(targetSelector)); - return elems.includes(elem); - }; - const elementDetails = elem => { - if ( elem instanceof Window ) { return 'window'; } - if ( elem instanceof Document ) { return 'document'; } - if ( elem instanceof Element === false ) { return '?'; } - const parts = []; - // https://github.com/uBlockOrigin/uAssets/discussions/17907#discussioncomment-9871079 - const id = String(elem.id); - if ( id !== '' ) { parts.push(`#${CSS.escape(id)}`); } - for ( let i = 0; i < elem.classList.length; i++ ) { - parts.push(`.${CSS.escape(elem.classList.item(i))}`); - } - for ( let i = 0; i < elem.attributes.length; i++ ) { - const attr = elem.attributes.item(i); - if ( attr.name === 'id' ) { continue; } - if ( attr.name === 'class' ) { continue; } - parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`); - } - return parts.join(''); - }; - const shouldPrevent = (thisArg, type, handler) => { - const matchesType = safe.RegExp_test.call(reType, type); - const matchesHandler = safe.RegExp_test.call(rePattern, handler); - const matchesEither = matchesType || matchesHandler; - const matchesBoth = matchesType && matchesHandler; - if ( debug === 1 && matchesBoth || debug === 2 && matchesEither ) { - debugger; // eslint-disable-line no-debugger - } - if ( matchesBoth && targetSelector !== undefined ) { - if ( elementMatches(thisArg) === false ) { return false; } - } - return matchesBoth; - }; - const proxyFn = function(context) { - const { callArgs, thisArg } = context; - let t, h; - try { - t = String(callArgs[0]); - if ( typeof callArgs[1] === 'function' ) { - h = String(safe.Function_toString(callArgs[1])); - } else if ( typeof callArgs[1] === 'object' && callArgs[1] !== null ) { - if ( typeof callArgs[1].handleEvent === 'function' ) { - h = String(safe.Function_toString(callArgs[1].handleEvent)); - } - } else { - h = String(callArgs[1]); - } - } catch { - } - if ( type === '' && pattern === '' ) { - safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`); - } else if ( shouldPrevent(thisArg, t, h) ) { - return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`); - } - return context.reflect(); - }; - runAt(( ) => { - proxyApplyFn('EventTarget.prototype.addEventListener', proxyFn); - proxyApplyFn('document.addEventListener', proxyFn); - }, extraArgs.runAt); -} - -/******************************************************************************/ - builtinScriptlets.push({ name: 'adjust-setInterval.js', aliases: [