|
| 1 | +/******************************************************************************* |
| 2 | +
|
| 3 | + uBlock Origin - a comprehensive, efficient content blocker |
| 4 | + Copyright (C) 2019-present Raymond Hill |
| 5 | +
|
| 6 | + This program is free software: you can redistribute it and/or modify |
| 7 | + it under the terms of the GNU General Public License as published by |
| 8 | + the Free Software Foundation, either version 3 of the License, or |
| 9 | + (at your option) any later version. |
| 10 | +
|
| 11 | + This program is distributed in the hope that it will be useful, |
| 12 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | + GNU General Public License for more details. |
| 15 | +
|
| 16 | + You should have received a copy of the GNU General Public License |
| 17 | + along with this program. If not, see {http://www.gnu.org/licenses/}. |
| 18 | +
|
| 19 | + Home: https://github.com/gorhill/uBlock |
| 20 | +
|
| 21 | +*/ |
| 22 | + |
| 23 | +import { proxyApplyFn } from './proxy-apply.js'; |
| 24 | +import { registerScriptlet } from './base.js'; |
| 25 | +import { runAt } from './run-at.js'; |
| 26 | +import { safeSelf } from './safe-self.js'; |
| 27 | + |
| 28 | +/******************************************************************************/ |
| 29 | + |
| 30 | +/** |
| 31 | + * @scriptlet prevent-addEventListener |
| 32 | + * |
| 33 | + * @description |
| 34 | + * Conditionally prevent execution of the callback function passed to native |
| 35 | + * addEventListener method. With no parameters, all calls to addEventListener |
| 36 | + * will be shown in the logger. |
| 37 | + * |
| 38 | + * @param [type] |
| 39 | + * The type of the event to prevent. The pattern can be a plain string, or a |
| 40 | + * regex to specify more than one event type. |
| 41 | + * |
| 42 | + * @param [pattern] |
| 43 | + * A pattern to match against the stringified callback. The pattern can be a |
| 44 | + * plain string, or a regex. |
| 45 | + * |
| 46 | + * @param [runat, value] |
| 47 | + * An optional vararg which tell the scriptlet when the prevention should |
| 48 | + * start. Values correspond to `readyState`: loading, interactive, complete. |
| 49 | + * |
| 50 | + * @param [protect, 1] |
| 51 | + * An optional vararg which tells the scriptlet whether the prevention should |
| 52 | + * be protected, i.e. not overwritten by other code. |
| 53 | + * |
| 54 | + * */ |
| 55 | + |
| 56 | +function preventAddEventListener( |
| 57 | + type = '', |
| 58 | + pattern = '' |
| 59 | +) { |
| 60 | + const safe = safeSelf(); |
| 61 | + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); |
| 62 | + const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern); |
| 63 | + const reType = safe.patternToRegex(type, undefined, true); |
| 64 | + const rePattern = safe.patternToRegex(pattern); |
| 65 | + const targetSelector = extraArgs.elements || undefined; |
| 66 | + const elementMatches = elem => { |
| 67 | + if ( targetSelector === 'window' ) { return elem === window; } |
| 68 | + if ( targetSelector === 'document' ) { return elem === document; } |
| 69 | + if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; } |
| 70 | + const elems = Array.from(document.querySelectorAll(targetSelector)); |
| 71 | + return elems.includes(elem); |
| 72 | + }; |
| 73 | + const elementDetails = elem => { |
| 74 | + if ( elem instanceof Window ) { return 'window'; } |
| 75 | + if ( elem instanceof Document ) { return 'document'; } |
| 76 | + if ( elem instanceof Element === false ) { return '?'; } |
| 77 | + const parts = []; |
| 78 | + // https://github.com/uBlockOrigin/uAssets/discussions/17907#discussioncomment-9871079 |
| 79 | + const id = String(elem.id); |
| 80 | + if ( id !== '' ) { parts.push(`#${CSS.escape(id)}`); } |
| 81 | + for ( let i = 0; i < elem.classList.length; i++ ) { |
| 82 | + parts.push(`.${CSS.escape(elem.classList.item(i))}`); |
| 83 | + } |
| 84 | + for ( let i = 0; i < elem.attributes.length; i++ ) { |
| 85 | + const attr = elem.attributes.item(i); |
| 86 | + if ( attr.name === 'id' ) { continue; } |
| 87 | + if ( attr.name === 'class' ) { continue; } |
| 88 | + parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`); |
| 89 | + } |
| 90 | + return parts.join(''); |
| 91 | + }; |
| 92 | + const shouldPrevent = (thisArg, type, handler) => { |
| 93 | + const matchesType = safe.RegExp_test.call(reType, type); |
| 94 | + const matchesHandler = safe.RegExp_test.call(rePattern, handler); |
| 95 | + const matchesEither = matchesType || matchesHandler; |
| 96 | + const matchesBoth = matchesType && matchesHandler; |
| 97 | + if ( safe.logLevel > 1 && matchesEither ) { |
| 98 | + debugger; // eslint-disable-line no-debugger |
| 99 | + } |
| 100 | + if ( matchesBoth && targetSelector !== undefined ) { |
| 101 | + if ( elementMatches(thisArg) === false ) { return false; } |
| 102 | + } |
| 103 | + return matchesBoth; |
| 104 | + }; |
| 105 | + const proxyFn = function(context) { |
| 106 | + const { callArgs, thisArg } = context; |
| 107 | + let t, h; |
| 108 | + try { |
| 109 | + t = String(callArgs[0]); |
| 110 | + if ( typeof callArgs[1] === 'function' ) { |
| 111 | + h = String(safe.Function_toString(callArgs[1])); |
| 112 | + } else if ( typeof callArgs[1] === 'object' && callArgs[1] !== null ) { |
| 113 | + if ( typeof callArgs[1].handleEvent === 'function' ) { |
| 114 | + h = String(safe.Function_toString(callArgs[1].handleEvent)); |
| 115 | + } |
| 116 | + } else { |
| 117 | + h = String(callArgs[1]); |
| 118 | + } |
| 119 | + } catch { |
| 120 | + } |
| 121 | + if ( type === '' && pattern === '' ) { |
| 122 | + safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`); |
| 123 | + } else if ( shouldPrevent(thisArg, t, h) ) { |
| 124 | + return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`); |
| 125 | + } |
| 126 | + return context.reflect(); |
| 127 | + }; |
| 128 | + runAt(( ) => { |
| 129 | + proxyApplyFn('EventTarget.prototype.addEventListener', proxyFn); |
| 130 | + if ( extraArgs.protect ) { |
| 131 | + const { addEventListener } = EventTarget.prototype; |
| 132 | + Object.defineProperty(EventTarget.prototype, 'addEventListener', { |
| 133 | + set() { }, |
| 134 | + get() { return addEventListener; } |
| 135 | + }); |
| 136 | + } |
| 137 | + proxyApplyFn('document.addEventListener', proxyFn); |
| 138 | + if ( extraArgs.protect ) { |
| 139 | + const { addEventListener } = document; |
| 140 | + Object.defineProperty(document, 'addEventListener', { |
| 141 | + set() { }, |
| 142 | + get() { return addEventListener; } |
| 143 | + }); |
| 144 | + } |
| 145 | + }, extraArgs.runAt); |
| 146 | +} |
| 147 | +registerScriptlet(preventAddEventListener , { |
| 148 | + name: 'prevent-addEventListener.js', |
| 149 | + aliases: [ |
| 150 | + 'addEventListener-defuser.js', |
| 151 | + 'aeld.js', |
| 152 | + ], |
| 153 | + dependencies: [ |
| 154 | + proxyApplyFn, |
| 155 | + runAt, |
| 156 | + safeSelf, |
| 157 | + ], |
| 158 | +}); |
0 commit comments