/******************************************************************************* 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 The scriptlets below are meant to be injected only into a web page context. */ /* eslint no-prototype-builtins: 0 */ // Externally added to the private namespace in which scriptlets execute. /* global scriptletGlobals */ export const builtinScriptlets = []; /******************************************************************************* Helper functions These are meant to be used as dependencies to injectable scriptlets. *******************************************************************************/ builtinScriptlets.push({ name: 'safe-self.fn', fn: safeSelf, }); function safeSelf() { if ( scriptletGlobals.safeSelf ) { return scriptletGlobals.safeSelf; } const self = globalThis; const safe = { 'Array_from': Array.from, 'Error': self.Error, 'Function_toStringFn': self.Function.prototype.toString, 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg), 'Math_floor': Math.floor, 'Math_max': Math.max, 'Math_min': Math.min, 'Math_random': Math.random, 'Object': Object, 'Object_defineProperty': Object.defineProperty.bind(Object), 'Object_defineProperties': Object.defineProperties.bind(Object), 'Object_fromEntries': Object.fromEntries.bind(Object), 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object), 'RegExp': self.RegExp, 'RegExp_test': self.RegExp.prototype.test, 'RegExp_exec': self.RegExp.prototype.exec, 'Request_clone': self.Request.prototype.clone, 'String_fromCharCode': String.fromCharCode, 'XMLHttpRequest': self.XMLHttpRequest, 'addEventListener': self.EventTarget.prototype.addEventListener, 'removeEventListener': self.EventTarget.prototype.removeEventListener, 'fetch': self.fetch, 'JSON': self.JSON, 'JSON_parseFn': self.JSON.parse, 'JSON_stringifyFn': self.JSON.stringify, 'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args), 'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args), 'log': console.log.bind(console), // Properties logLevel: 0, // Methods makeLogPrefix(...args) { return this.sendToLogger && `[${args.join(' \u205D ')}]` || ''; }, uboLog(...args) { if ( this.sendToLogger === undefined ) { return; } if ( args === undefined || args[0] === '' ) { return; } return this.sendToLogger('info', ...args); }, uboErr(...args) { if ( this.sendToLogger === undefined ) { return; } if ( args === undefined || args[0] === '' ) { return; } return this.sendToLogger('error', ...args); }, escapeRegexChars(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, initPattern(pattern, options = {}) { if ( pattern === '' ) { return { matchAll: true, expect: true }; } const expect = (options.canNegate !== true || pattern.startsWith('!') === false); if ( expect === false ) { pattern = pattern.slice(1); } const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); if ( match !== null ) { return { re: new this.RegExp( match[1], match[2] || options.flags ), expect, }; } if ( options.flags !== undefined ) { return { re: new this.RegExp(this.escapeRegexChars(pattern), options.flags ), expect, }; } return { pattern, expect }; }, testPattern(details, haystack) { if ( details.matchAll ) { return true; } if ( details.re ) { return this.RegExp_test.call(details.re, haystack) === details.expect; } return haystack.includes(details.pattern) === details.expect; }, patternToRegex(pattern, flags = undefined, verbatim = false) { if ( pattern === '' ) { return /^/; } const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); if ( match === null ) { const reStr = this.escapeRegexChars(pattern); return new RegExp(verbatim ? `^${reStr}$` : reStr, flags); } try { return new RegExp(match[1], match[2] || undefined); } catch(ex) { } return /^/; }, getExtraArgs(args, offset = 0) { const entries = args.slice(offset).reduce((out, v, i, a) => { if ( (i & 1) === 0 ) { const rawValue = a[i+1]; const value = /^\d+$/.test(rawValue) ? parseInt(rawValue, 10) : rawValue; out.push([ a[i], value ]); } return out; }, []); return this.Object_fromEntries(entries); }, onIdle(fn, options) { if ( self.requestIdleCallback ) { return self.requestIdleCallback(fn, options); } return self.requestAnimationFrame(fn); }, offIdle(id) { if ( self.requestIdleCallback ) { return self.cancelIdleCallback(id); } return self.cancelAnimationFrame(id); } }; scriptletGlobals.safeSelf = safe; if ( scriptletGlobals.bcSecret === undefined ) { return safe; } // This is executed only when the logger is opened safe.logLevel = scriptletGlobals.logLevel || 1; let lastLogType = ''; let lastLogText = ''; let lastLogTime = 0; safe.toLogText = (type, ...args) => { if ( args.length === 0 ) { return; } const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`; if ( text === lastLogText && type === lastLogType ) { if ( (Date.now() - lastLogTime) < 5000 ) { return; } } lastLogType = type; lastLogText = text; lastLogTime = Date.now(); return text; }; try { const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret); let bcBuffer = []; safe.sendToLogger = (type, ...args) => { const text = safe.toLogText(type, ...args); if ( text === undefined ) { return; } if ( bcBuffer === undefined ) { return bc.postMessage({ what: 'messageToLogger', type, text }); } bcBuffer.push({ type, text }); }; bc.onmessage = ev => { const msg = ev.data; switch ( msg ) { case 'iamready!': if ( bcBuffer === undefined ) { break; } bcBuffer.forEach(({ type, text }) => bc.postMessage({ what: 'messageToLogger', type, text }) ); bcBuffer = undefined; break; case 'setScriptletLogLevelToOne': safe.logLevel = 1; break; case 'setScriptletLogLevelToTwo': safe.logLevel = 2; break; } }; bc.postMessage('areyouready?'); } catch(_) { safe.sendToLogger = (type, ...args) => { const text = safe.toLogText(type, ...args); if ( text === undefined ) { return; } safe.log(`uBO ${text}`); }; } return safe; } /******************************************************************************/ builtinScriptlets.push({ name: 'get-random-token.fn', fn: getRandomToken, dependencies: [ 'safe-self.fn', ], }); function getRandomToken() { const safe = safeSelf(); return safe.String_fromCharCode(Date.now() % 26 + 97) + safe.Math_floor(safe.Math_random() * 982451653 + 982451653).toString(36); } /******************************************************************************/ builtinScriptlets.push({ name: 'get-exception-token.fn', fn: getExceptionToken, dependencies: [ 'get-random-token.fn', ], }); function getExceptionToken() { const token = getRandomToken(); const oe = self.onerror; self.onerror = function(msg, ...args) { if ( typeof msg === 'string' && msg.includes(token) ) { return true; } if ( oe instanceof Function ) { return oe.call(this, msg, ...args); } }.bind(); return token; } /******************************************************************************/ builtinScriptlets.push({ name: 'should-debug.fn', fn: shouldDebug, }); function shouldDebug(details) { if ( details instanceof Object === false ) { return false; } return scriptletGlobals.canDebug && details.debug; } /******************************************************************************/ builtinScriptlets.push({ name: 'run-at.fn', fn: runAt, dependencies: [ 'safe-self.fn', ], }); function runAt(fn, when) { const intFromReadyState = state => { const targets = { 'loading': 1, 'asap': 1, 'interactive': 2, 'end': 2, '2': 2, 'complete': 3, 'idle': 3, '3': 3, }; const tokens = Array.isArray(state) ? state : [ state ]; for ( const token of tokens ) { const prop = `${token}`; if ( targets.hasOwnProperty(prop) === false ) { continue; } return targets[prop]; } return 0; }; const runAt = intFromReadyState(when); if ( intFromReadyState(document.readyState) >= runAt ) { fn(); return; } const onStateChange = ( ) => { if ( intFromReadyState(document.readyState) < runAt ) { return; } fn(); safe.removeEventListener.apply(document, args); }; const safe = safeSelf(); const args = [ 'readystatechange', onStateChange, { capture: true } ]; safe.addEventListener.apply(document, args); } /******************************************************************************/ builtinScriptlets.push({ name: 'run-at-html-element.fn', fn: runAtHtmlElementFn, }); function runAtHtmlElementFn(fn) { if ( document.documentElement ) { fn(); return; } const observer = new MutationObserver(( ) => { observer.disconnect(); fn(); }); observer.observe(document, { childList: true }); } /******************************************************************************/ // Reference: // https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr // // Added `trusted` argument to allow for returning arbitrary text. Can only // be used through scriptlets requiring trusted source. builtinScriptlets.push({ name: 'generate-content.fn', fn: generateContentFn, dependencies: [ 'safe-self.fn', ], }); function generateContentFn(trusted, directive) { const safe = safeSelf(); const randomize = len => { const chunks = []; let textSize = 0; do { const s = safe.Math_random().toString(36).slice(2); chunks.push(s); textSize += s.length; } while ( textSize < len ); return chunks.join(' ').slice(0, len); }; if ( directive === 'true' ) { return randomize(10); } if ( directive === 'emptyObj' ) { return '{}'; } if ( directive === 'emptyArr' ) { return '[]'; } if ( directive === 'emptyStr' ) { return ''; } if ( directive.startsWith('length:') ) { const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive); if ( match === null ) { return ''; } const min = parseInt(match[1], 10); const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min; const len = safe.Math_min(min + extent * safe.Math_random(), 500000); return randomize(len | 0); } if ( directive.startsWith('war:') ) { if ( scriptletGlobals.warOrigin === undefined ) { return ''; } return new Promise(resolve => { const warOrigin = scriptletGlobals.warOrigin; const warName = directive.slice(4); const fullpath = [ warOrigin, '/', warName ]; const warSecret = scriptletGlobals.warSecret; if ( warSecret !== undefined ) { fullpath.push('?secret=', warSecret); } const warXHR = new safe.XMLHttpRequest(); warXHR.responseType = 'text'; warXHR.onloadend = ev => { resolve(ev.target.responseText || ''); }; warXHR.open('GET', fullpath.join('')); warXHR.send(); }).catch(( ) => ''); } if ( trusted ) { return directive; } return ''; } /******************************************************************************/ builtinScriptlets.push({ name: 'abort-current-script-core.fn', fn: abortCurrentScriptCore, dependencies: [ 'get-exception-token.fn', 'safe-self.fn', 'should-debug.fn', ], }); // Issues to mind before changing anything: // https://github.com/uBlockOrigin/uBlock-issues/issues/2154 function abortCurrentScriptCore( target = '', needle = '', context = '' ) { if ( typeof target !== 'string' ) { return; } if ( target === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('abort-current-script', target, needle, context); const reNeedle = safe.patternToRegex(needle); const reContext = safe.patternToRegex(context); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const thisScript = document.currentScript; const chain = target.split('.'); let owner = window; let prop; for (;;) { prop = chain.shift(); if ( chain.length === 0 ) { break; } if ( prop in owner === false ) { break; } owner = owner[prop]; if ( owner instanceof Object === false ) { return; } } let value; let desc = Object.getOwnPropertyDescriptor(owner, prop); if ( desc instanceof Object === false || desc.get instanceof Function === false ) { value = owner[prop]; desc = undefined; } const debug = shouldDebug(extraArgs); const exceptionToken = getExceptionToken(); const scriptTexts = new WeakMap(); const getScriptText = elem => { let text = elem.textContent; if ( text.trim() !== '' ) { return text; } if ( scriptTexts.has(elem) ) { return scriptTexts.get(elem); } const [ , mime, content ] = /^data:([^,]*),(.+)$/.exec(elem.src.trim()) || [ '', '', '' ]; try { switch ( true ) { case mime.endsWith(';base64'): text = self.atob(content); break; default: text = self.decodeURIComponent(content); break; } } catch(ex) { } scriptTexts.set(elem, text); return text; }; const validate = ( ) => { const e = document.currentScript; if ( e instanceof HTMLScriptElement === false ) { return; } if ( e === thisScript ) { return; } if ( context !== '' && reContext.test(e.src) === false ) { // eslint-disable-next-line no-debugger if ( debug === 'nomatch' || debug === 'all' ) { debugger; } return; } if ( safe.logLevel > 1 && context !== '' ) { safe.uboLog(logPrefix, `Matched src\n${e.src}`); } const scriptText = getScriptText(e); if ( reNeedle.test(scriptText) === false ) { // eslint-disable-next-line no-debugger if ( debug === 'nomatch' || debug === 'all' ) { debugger; } return; } if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Matched text\n${scriptText}`); } // eslint-disable-next-line no-debugger if ( debug === 'match' || debug === 'all' ) { debugger; } safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); }; // eslint-disable-next-line no-debugger if ( debug === 'install' ) { debugger; } try { Object.defineProperty(owner, prop, { get: function() { validate(); return desc instanceof Object ? desc.get.call(owner) : value; }, set: function(a) { validate(); if ( desc instanceof Object ) { desc.set.call(owner, a); } else { value = a; } } }); } catch(ex) { safe.uboErr(logPrefix, `Error: ${ex}`); } } /******************************************************************************/ builtinScriptlets.push({ name: 'validate-constant.fn', fn: validateConstantFn, dependencies: [ 'safe-self.fn', ], }); function validateConstantFn(trusted, raw, extraArgs = {}) { const safe = safeSelf(); let value; if ( raw === 'undefined' ) { value = undefined; } else if ( raw === 'false' ) { value = false; } else if ( raw === 'true' ) { value = true; } else if ( raw === 'null' ) { value = null; } else if ( raw === "''" || raw === '' ) { value = ''; } else if ( raw === '[]' || raw === 'emptyArr' ) { value = []; } else if ( raw === '{}' || raw === 'emptyObj' ) { value = {}; } else if ( raw === 'noopFunc' ) { value = function(){}; } else if ( raw === 'trueFunc' ) { value = function(){ return true; }; } else if ( raw === 'falseFunc' ) { value = function(){ return false; }; } else if ( raw === 'throwFunc' ) { value = function(){ throw ''; }; } else if ( /^-?\d+$/.test(raw) ) { value = parseInt(raw); if ( isNaN(raw) ) { return; } if ( Math.abs(raw) > 0x7FFF ) { return; } } else if ( trusted ) { if ( raw.startsWith('json:') ) { try { value = safe.JSON_parse(raw.slice(5)); } catch(ex) { return; } } else if ( raw.startsWith('{') && raw.endsWith('}') ) { try { value = safe.JSON_parse(raw).value; } catch(ex) { return; } } } else { return; } if ( extraArgs.as !== undefined ) { if ( extraArgs.as === 'function' ) { return ( ) => value; } else if ( extraArgs.as === 'callback' ) { return ( ) => (( ) => value); } else if ( extraArgs.as === 'resolved' ) { return Promise.resolve(value); } else if ( extraArgs.as === 'rejected' ) { return Promise.reject(value); } } return value; } /******************************************************************************/ builtinScriptlets.push({ name: 'set-constant.fn', fn: setConstantFn, dependencies: [ 'run-at.fn', 'safe-self.fn', 'validate-constant.fn', ], }); function setConstantFn( trusted = false, chain = '', rawValue = '' ) { if ( chain === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); function setConstant(chain, rawValue) { const trappedProp = (( ) => { const pos = chain.lastIndexOf('.'); if ( pos === -1 ) { return chain; } return chain.slice(pos+1); })(); const cloakFunc = fn => { safe.Object_defineProperty(fn, 'name', { value: trappedProp }); return new Proxy(fn, { defineProperty(target, prop) { if ( prop !== 'toString' ) { return Reflect.defineProperty(...arguments); } return true; }, deleteProperty(target, prop) { if ( prop !== 'toString' ) { return Reflect.deleteProperty(...arguments); } return true; }, get(target, prop) { if ( prop === 'toString' ) { return function() { return `function ${trappedProp}() { [native code] }`; }.bind(null); } return Reflect.get(...arguments); }, }); }; if ( trappedProp === '' ) { return; } const thisScript = document.currentScript; let normalValue = validateConstantFn(trusted, rawValue, extraArgs); if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) { normalValue = cloakFunc(normalValue); } let aborted = false; const mustAbort = function(v) { if ( trusted ) { return false; } if ( aborted ) { return true; } aborted = (v !== undefined && v !== null) && (normalValue !== undefined && normalValue !== null) && (typeof v !== typeof normalValue); if ( aborted ) { safe.uboLog(logPrefix, `Aborted because value set to ${v}`); } return aborted; }; // https://github.com/uBlockOrigin/uBlock-issues/issues/156 // Support multiple trappers for the same property. const trapProp = function(owner, prop, configurable, handler) { if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; } const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); let prevGetter, prevSetter; if ( odesc instanceof safe.Object ) { owner[prop] = normalValue; if ( odesc.get instanceof Function ) { prevGetter = odesc.get; } if ( odesc.set instanceof Function ) { prevSetter = odesc.set; } } try { safe.Object_defineProperty(owner, prop, { configurable, get() { if ( prevGetter !== undefined ) { prevGetter(); } return handler.getter(); }, set(a) { if ( prevSetter !== undefined ) { prevSetter(a); } handler.setter(a); } }); safe.uboLog(logPrefix, 'Trap installed'); } catch(ex) { safe.uboErr(logPrefix, ex); } }; const trapChain = function(owner, chain) { const pos = chain.indexOf('.'); if ( pos === -1 ) { trapProp(owner, chain, false, { v: undefined, init: function(v) { if ( mustAbort(v) ) { return false; } this.v = v; return true; }, getter: function() { if ( document.currentScript === thisScript ) { return this.v; } safe.uboLog(logPrefix, 'Property read'); return normalValue; }, setter: function(a) { if ( mustAbort(a) === false ) { return; } normalValue = a; } }); return; } const prop = chain.slice(0, pos); const v = owner[prop]; chain = chain.slice(pos + 1); if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) { trapChain(v, chain); return; } trapProp(owner, prop, true, { v: undefined, init: function(v) { this.v = v; return true; }, getter: function() { return this.v; }, setter: function(a) { this.v = a; if ( a instanceof safe.Object ) { trapChain(a, chain); } } }); }; trapChain(window, chain); } runAt(( ) => { setConstant(chain, rawValue); }, extraArgs.runAt); } /******************************************************************************/ builtinScriptlets.push({ name: 'replace-node-text.fn', fn: replaceNodeTextFn, dependencies: [ 'get-random-token.fn', 'run-at.fn', 'safe-self.fn', ], }); function replaceNodeTextFn( nodeName = '', pattern = '', replacement = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('replace-node-text.fn', ...Array.from(arguments)); const reNodeName = safe.patternToRegex(nodeName, 'i', true); const rePattern = safe.patternToRegex(pattern, 'gms'); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const reIncludes = extraArgs.includes || extraArgs.condition ? safe.patternToRegex(extraArgs.includes || extraArgs.condition, 'ms') : null; const reExcludes = extraArgs.excludes ? safe.patternToRegex(extraArgs.excludes, 'ms') : null; const stop = (takeRecord = true) => { if ( takeRecord ) { handleMutations(observer.takeRecords()); } observer.disconnect(); if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, 'Quitting'); } }; const textContentFactory = (( ) => { const out = { createScript: s => s }; const { trustedTypes: tt } = self; if ( tt instanceof Object ) { if ( typeof tt.getPropertyType === 'function' ) { if ( tt.getPropertyType('script', 'textContent') === 'TrustedScript' ) { return tt.createPolicy(getRandomToken(), out); } } } return out; })(); let sedCount = extraArgs.sedCount || 0; const handleNode = node => { const before = node.textContent; if ( reIncludes ) { reIncludes.lastIndex = 0; if ( safe.RegExp_test.call(reIncludes, before) === false ) { return true; } } if ( reExcludes ) { reExcludes.lastIndex = 0; if ( safe.RegExp_test.call(reExcludes, before) ) { return true; } } rePattern.lastIndex = 0; if ( safe.RegExp_test.call(rePattern, before) === false ) { return true; } rePattern.lastIndex = 0; const after = pattern !== '' ? before.replace(rePattern, replacement) : replacement; node.textContent = node.nodeName === 'SCRIPT' ? textContentFactory.createScript(after) : after; if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Text before:\n${before.trim()}`); } safe.uboLog(logPrefix, `Text after:\n${after.trim()}`); return sedCount === 0 || (sedCount -= 1) !== 0; }; const handleMutations = mutations => { for ( const mutation of mutations ) { for ( const node of mutation.addedNodes ) { if ( reNodeName.test(node.nodeName) === false ) { continue; } if ( handleNode(node) ) { continue; } stop(false); return; } } }; const observer = new MutationObserver(handleMutations); observer.observe(document, { childList: true, subtree: true }); if ( document.documentElement ) { const treeWalker = document.createTreeWalker( document.documentElement, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT ); let count = 0; for (;;) { const node = treeWalker.nextNode(); count += 1; if ( node === null ) { break; } if ( reNodeName.test(node.nodeName) === false ) { continue; } if ( node === document.currentScript ) { continue; } if ( handleNode(node) ) { continue; } stop(); break; } safe.uboLog(logPrefix, `${count} nodes present before installing mutation observer`); } if ( extraArgs.stay ) { return; } runAt(( ) => { const quitAfter = extraArgs.quitAfter || 0; if ( quitAfter !== 0 ) { setTimeout(( ) => { stop(); }, quitAfter); } else { stop(); } }, 'interactive'); } /******************************************************************************/ builtinScriptlets.push({ name: 'object-prune.fn', fn: objectPruneFn, dependencies: [ 'matches-stack-trace.fn', 'object-find-owner.fn', ], }); // When no "prune paths" argument is provided, the scriptlet is // used for logging purpose and the "needle paths" argument is // used to filter logging output. // // https://github.com/uBlockOrigin/uBlock-issues/issues/1545 // - Add support for "remove everything if needle matches" case function objectPruneFn( obj, rawPrunePaths, rawNeedlePaths, stackNeedleDetails = { matchAll: true }, extraArgs = {} ) { if ( typeof rawPrunePaths !== 'string' ) { return; } const prunePaths = rawPrunePaths !== '' ? rawPrunePaths.split(/ +/) : []; const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' ? rawNeedlePaths.split(/ +/) : []; if ( stackNeedleDetails.matchAll !== true ) { if ( matchesStackTrace(stackNeedleDetails, extraArgs.logstack) === false ) { return; } } if ( objectPruneFn.mustProcess === undefined ) { objectPruneFn.mustProcess = (root, needlePaths) => { for ( const needlePath of needlePaths ) { if ( objectFindOwnerFn(root, needlePath) === false ) { return false; } } return true; }; } if ( prunePaths.length === 0 ) { return; } let outcome = 'nomatch'; if ( objectPruneFn.mustProcess(obj, needlePaths) ) { for ( const path of prunePaths ) { if ( objectFindOwnerFn(obj, path, true) ) { outcome = 'match'; } } } if ( outcome === 'match' ) { return obj; } } /******************************************************************************/ builtinScriptlets.push({ name: 'object-find-owner.fn', fn: objectFindOwnerFn, }); function objectFindOwnerFn( root, path, prune = false ) { let owner = root; let chain = path; for (;;) { if ( typeof owner !== 'object' || owner === null ) { return false; } const pos = chain.indexOf('.'); if ( pos === -1 ) { if ( prune === false ) { return owner.hasOwnProperty(chain); } let modified = false; if ( chain === '*' ) { for ( const key in owner ) { if ( owner.hasOwnProperty(key) === false ) { continue; } delete owner[key]; modified = true; } } else if ( owner.hasOwnProperty(chain) ) { delete owner[chain]; modified = true; } return modified; } const prop = chain.slice(0, pos); const next = chain.slice(pos + 1); let found = false; if ( prop === '[-]' && Array.isArray(owner) ) { let i = owner.length; while ( i-- ) { if ( objectFindOwnerFn(owner[i], next) === false ) { continue; } owner.splice(i, 1); found = true; } return found; } if ( prop === '{-}' && owner instanceof Object ) { for ( const key of Object.keys(owner) ) { if ( objectFindOwnerFn(owner[key], next) === false ) { continue; } delete owner[key]; found = true; } return found; } if ( prop === '[]' && Array.isArray(owner) || prop === '{}' && owner instanceof Object || prop === '*' && owner instanceof Object ) { for ( const key of Object.keys(owner) ) { if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; } found = true; } return found; } if ( owner.hasOwnProperty(prop) === false ) { return false; } owner = owner[prop]; chain = chain.slice(pos + 1); } } /******************************************************************************/ builtinScriptlets.push({ name: 'get-safe-cookie-values.fn', fn: getSafeCookieValuesFn, }); function getSafeCookieValuesFn() { return [ 'accept', 'reject', 'accepted', 'rejected', 'notaccepted', 'allow', 'disallow', 'deny', 'allowed', 'denied', 'approved', 'disapproved', 'checked', 'unchecked', 'dismiss', 'dismissed', 'enable', 'disable', 'enabled', 'disabled', 'essential', 'nonessential', 'forbidden', 'forever', 'hide', 'hidden', 'necessary', 'required', 'ok', 'on', 'off', 'true', 't', 'false', 'f', 'yes', 'y', 'no', 'n', ]; } /******************************************************************************/ builtinScriptlets.push({ name: 'get-all-cookies.fn', fn: getAllCookiesFn, }); function getAllCookiesFn() { return document.cookie.split(/\s*;\s*/).map(s => { const pos = s.indexOf('='); if ( pos === 0 ) { return; } if ( pos === -1 ) { return `${s.trim()}=`; } const key = s.slice(0, pos).trim(); const value = s.slice(pos+1).trim(); return { key, value }; }).filter(s => s !== undefined); } /******************************************************************************/ builtinScriptlets.push({ name: 'get-all-local-storage.fn', fn: getAllLocalStorageFn, }); function getAllLocalStorageFn(which = 'localStorage') { const storage = self[which]; const out = []; for ( let i = 0; i < storage.length; i++ ) { const key = storage.key(i); const value = storage.getItem(key); return { key, value }; } return out; } /******************************************************************************/ builtinScriptlets.push({ name: 'get-cookie.fn', fn: getCookieFn, }); function getCookieFn( name = '' ) { for ( const s of document.cookie.split(/\s*;\s*/) ) { const pos = s.indexOf('='); if ( pos === -1 ) { continue; } if ( s.slice(0, pos) !== name ) { continue; } return s.slice(pos+1).trim(); } } /******************************************************************************/ builtinScriptlets.push({ name: 'set-cookie.fn', fn: setCookieFn, dependencies: [ 'get-cookie.fn', ], }); function setCookieFn( trusted = false, name = '', value = '', expires = '', path = '', options = {}, ) { // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 // https://github.com/uBlockOrigin/uBlock-issues/issues/2777 if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) { name = encodeURIComponent(name); } // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 // The characters [",] are given a pass from the RFC requirements because // apparently browsers do not follow the RFC to the letter. if ( /[^ -:<-[\]-~]/.test(value) ) { value = encodeURIComponent(value); } const cookieBefore = getCookieFn(name); if ( cookieBefore !== undefined && options.dontOverwrite ) { return; } if ( cookieBefore === value && options.reload ) { return; } const cookieParts = [ name, '=', value ]; if ( expires !== '' ) { cookieParts.push('; expires=', expires); } if ( path === '' ) { path = '/'; } else if ( path === 'none' ) { path = ''; } if ( path !== '' && path !== '/' ) { return; } if ( path === '/' ) { cookieParts.push('; path=/'); } if ( trusted ) { if ( options.domain ) { cookieParts.push(`; domain=${options.domain}`); } cookieParts.push('; Secure'); } else if ( /^__(Host|Secure)-/.test(name) ) { cookieParts.push('; Secure'); } try { document.cookie = cookieParts.join(''); } catch(_) { } const done = getCookieFn(name) === value; if ( done && options.reload ) { window.location.reload(); } return done; } /******************************************************************************/ builtinScriptlets.push({ name: 'set-local-storage-item.fn', fn: setLocalStorageItemFn, dependencies: [ 'get-safe-cookie-values.fn', 'safe-self.fn', ], }); function setLocalStorageItemFn( which = 'local', trusted = false, key = '', value = '', ) { if ( key === '' ) { return; } // For increased compatibility with AdGuard if ( value === 'emptyArr' ) { value = '[]'; } else if ( value === 'emptyObj' ) { value = '{}'; } const trustedValues = [ '', 'undefined', 'null', '{}', '[]', '""', '$remove$', ...getSafeCookieValuesFn(), ]; if ( trusted ) { if ( value.includes('$now$') ) { value = value.replaceAll('$now$', Date.now()); } if ( value.includes('$currentDate$') ) { value = value.replaceAll('$currentDate$', `${Date()}`); } if ( value.includes('$currentISODate$') ) { value = value.replaceAll('$currentISODate$', (new Date()).toISOString()); } } else { const normalized = value.toLowerCase(); const match = /^("?)(.+)\1$/.exec(normalized); const unquoted = match && match[2] || normalized; if ( trustedValues.includes(unquoted) === false ) { if ( /^\d+$/.test(unquoted) === false ) { return; } const n = parseInt(unquoted, 10); if ( n > 32767 ) { return; } } } try { const storage = self[`${which}Storage`]; if ( value === '$remove$' ) { const safe = safeSelf(); const pattern = safe.patternToRegex(key, undefined, true ); const toRemove = []; for ( let i = 0, n = storage.length; i < n; i++ ) { const key = storage.key(i); if ( pattern.test(key) ) { toRemove.push(key); } } for ( const key of toRemove ) { storage.removeItem(key); } } else { storage.setItem(key, `${value}`); } } catch(ex) { } } /******************************************************************************/ builtinScriptlets.push({ name: 'matches-stack-trace.fn', fn: matchesStackTrace, dependencies: [ 'get-exception-token.fn', 'safe-self.fn', ], }); function matchesStackTrace( needleDetails, logLevel = '' ) { const safe = safeSelf(); const exceptionToken = getExceptionToken(); const error = new safe.Error(exceptionToken); const docURL = new URL(self.location.href); docURL.hash = ''; // Normalize stack trace const reLine = /(.*?@)?(\S+)(:\d+):\d+\)?$/; const lines = []; for ( let line of error.stack.split(/[\n\r]+/) ) { if ( line.includes(exceptionToken) ) { continue; } line = line.trim(); const match = safe.RegExp_exec.call(reLine, line); if ( match === null ) { continue; } let url = match[2]; if ( url.startsWith('(') ) { url = url.slice(1); } if ( url === docURL.href ) { url = 'inlineScript'; } else if ( url.startsWith('') ) { url = 'injectedScript'; } let fn = match[1] !== undefined ? match[1].slice(0, -1) : line.slice(0, match.index).trim(); if ( fn.startsWith('at') ) { fn = fn.slice(2).trim(); } let rowcol = match[3]; lines.push(' ' + `${fn} ${url}${rowcol}:1`.trim()); } lines[0] = `stackDepth:${lines.length-1}`; const stack = lines.join('\t'); const r = needleDetails.matchAll !== true && safe.testPattern(needleDetails, stack); if ( logLevel === 'all' || logLevel === 'match' && r || logLevel === 'nomatch' && !r ) { safe.uboLog(stack.replace(/\t/g, '\n')); } return r; } /******************************************************************************/ builtinScriptlets.push({ name: 'parse-properties-to-match.fn', fn: parsePropertiesToMatch, dependencies: [ 'safe-self.fn', ], }); function parsePropertiesToMatch(propsToMatch, implicit = '') { const safe = safeSelf(); const needles = new Map(); if ( propsToMatch === undefined || propsToMatch === '' ) { return needles; } const options = { canNegate: true }; for ( const needle of propsToMatch.split(/\s+/) ) { const [ prop, pattern ] = needle.split(':'); if ( prop === '' ) { continue; } if ( pattern !== undefined ) { needles.set(prop, safe.initPattern(pattern, options)); } else if ( implicit !== '' ) { needles.set(implicit, safe.initPattern(prop, options)); } } return needles; } /******************************************************************************/ builtinScriptlets.push({ name: 'match-object-properties.fn', fn: matchObjectProperties, dependencies: [ 'safe-self.fn', ], }); function matchObjectProperties(propNeedles, ...objs) { if ( matchObjectProperties.extractProperties === undefined ) { matchObjectProperties.extractProperties = (src, des, props) => { for ( const p of props ) { const v = src[p]; if ( v === undefined ) { continue; } des[p] = src[p]; } }; } const safe = safeSelf(); const haystack = {}; const props = safe.Array_from(propNeedles.keys()); for ( const obj of objs ) { if ( obj instanceof Object === false ) { continue; } matchObjectProperties.extractProperties(obj, haystack, props); } for ( const [ prop, details ] of propNeedles ) { let value = haystack[prop]; if ( value === undefined ) { continue; } if ( typeof value !== 'string' ) { try { value = safe.JSON_stringify(value); } catch(ex) { } if ( typeof value !== 'string' ) { continue; } } if ( safe.testPattern(details, value) ) { continue; } return false; } return true; } /******************************************************************************/ builtinScriptlets.push({ name: 'json-prune-fetch-response.fn', fn: jsonPruneFetchResponseFn, dependencies: [ 'match-object-properties.fn', 'object-prune.fn', 'parse-properties-to-match.fn', 'safe-self.fn', ], }); function jsonPruneFetchResponseFn( rawPrunePaths = '', rawNeedlePaths = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url'); const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); const logall = rawPrunePaths === ''; const applyHandler = function(target, thisArg, args) { const fetchPromise = Reflect.apply(target, thisArg, args); let outcome = logall ? 'nomatch' : 'match'; if ( propNeedles.size !== 0 ) { const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; if ( objs[0] instanceof Request ) { try { objs[0] = safe.Request_clone.call(objs[0]); } catch(ex) { safe.uboErr(logPrefix, 'Error:', ex); } } if ( args[1] instanceof Object ) { objs.push(args[1]); } if ( matchObjectProperties(propNeedles, ...objs) === false ) { outcome = 'nomatch'; } } if ( logall === false && outcome === 'nomatch' ) { return fetchPromise; } if ( safe.logLevel > 1 && outcome !== 'nomatch' && propNeedles.size !== 0 ) { safe.uboLog(logPrefix, `Matched optional "propsToMatch"\n${extraArgs.propsToMatch}`); } return fetchPromise.then(responseBefore => { const response = responseBefore.clone(); return response.json().then(objBefore => { if ( typeof objBefore !== 'object' ) { return responseBefore; } if ( logall ) { safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); return responseBefore; } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedle, extraArgs ); if ( typeof objAfter !== 'object' ) { return responseBefore; } safe.uboLog(logPrefix, 'Pruned'); const responseAfter = Response.json(objAfter, { status: responseBefore.status, statusText: responseBefore.statusText, headers: responseBefore.headers, }); Object.defineProperties(responseAfter, { ok: { value: responseBefore.ok }, redirected: { value: responseBefore.redirected }, type: { value: responseBefore.type }, url: { value: responseBefore.url }, }); return responseAfter; }).catch(reason => { safe.uboErr(logPrefix, 'Error:', reason); return responseBefore; }); }).catch(reason => { safe.uboErr(logPrefix, 'Error:', reason); return fetchPromise; }); }; self.fetch = new Proxy(self.fetch, { apply: applyHandler }); } /******************************************************************************/ builtinScriptlets.push({ name: 'replace-fetch-response.fn', fn: replaceFetchResponseFn, dependencies: [ 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', ], }); function replaceFetchResponseFn( trusted = false, pattern = '', replacement = '', propsToMatch = '' ) { if ( trusted !== true ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('replace-fetch-response', pattern, replacement, propsToMatch); if ( pattern === '*' ) { pattern = '.*'; } const rePattern = safe.patternToRegex(pattern); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); const reIncludes = extraArgs.includes ? safe.patternToRegex(extraArgs.includes) : null; self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { const fetchPromise = Reflect.apply(target, thisArg, args); if ( pattern === '' ) { return fetchPromise; } let outcome = 'match'; if ( propNeedles.size !== 0 ) { const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; if ( objs[0] instanceof Request ) { try { objs[0] = safe.Request_clone.call(objs[0]); } catch(ex) { safe.uboErr(logPrefix, ex); } } if ( args[1] instanceof Object ) { objs.push(args[1]); } if ( matchObjectProperties(propNeedles, ...objs) === false ) { outcome = 'nomatch'; } } if ( outcome === 'nomatch' ) { return fetchPromise; } if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Matched "propsToMatch"\n${propsToMatch}`); } return fetchPromise.then(responseBefore => { const response = responseBefore.clone(); return response.text().then(textBefore => { if ( reIncludes && reIncludes.test(textBefore) === false ) { return responseBefore; } const textAfter = textBefore.replace(rePattern, replacement); const outcome = textAfter !== textBefore ? 'match' : 'nomatch'; if ( outcome === 'nomatch' ) { return responseBefore; } safe.uboLog(logPrefix, 'Replaced'); const responseAfter = new Response(textAfter, { status: responseBefore.status, statusText: responseBefore.statusText, headers: responseBefore.headers, }); Object.defineProperties(responseAfter, { ok: { value: responseBefore.ok }, redirected: { value: responseBefore.redirected }, type: { value: responseBefore.type }, url: { value: responseBefore.url }, }); return responseAfter; }).catch(reason => { safe.uboErr(logPrefix, reason); return responseBefore; }); }).catch(reason => { safe.uboErr(logPrefix, reason); return fetchPromise; }); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'proxy-apply.fn', fn: proxyApplyFn, }); function proxyApplyFn( target = '', handler = '' ) { let context = globalThis; let prop = target; for (;;) { const pos = prop.indexOf('.'); if ( pos === -1 ) { break; } context = context[prop.slice(0, pos)]; if ( context instanceof Object === false ) { return; } prop = prop.slice(pos+1); } const fn = context[prop]; if ( typeof fn !== 'function' ) { return; } if ( proxyApplyFn.CtorContext === undefined ) { proxyApplyFn.ctorContexts = []; proxyApplyFn.CtorContext = class { constructor(...args) { this.init(...args); } init(callFn, callArgs) { this.callFn = callFn; this.callArgs = callArgs; return this; } reflect() { const r = Reflect.construct(this.callFn, this.callArgs); this.callFn = this.callArgs = undefined; proxyApplyFn.ctorContexts.push(this); return r; } static factory(...args) { return proxyApplyFn.ctorContexts.length !== 0 ? proxyApplyFn.ctorContexts.pop().init(...args) : new proxyApplyFn.CtorContext(...args); } }; proxyApplyFn.applyContexts = []; proxyApplyFn.ApplyContext = class { constructor(...args) { this.init(...args); } init(callFn, thisArg, callArgs) { this.callFn = callFn; this.thisArg = thisArg; this.callArgs = callArgs; return this; } reflect() { const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs); this.callFn = this.thisArg = this.callArgs = undefined; proxyApplyFn.applyContexts.push(this); return r; } static factory(...args) { return proxyApplyFn.applyContexts.length !== 0 ? proxyApplyFn.applyContexts.pop().init(...args) : new proxyApplyFn.ApplyContext(...args); } }; } const fnStr = fn.toString(); const toString = (function toString() { return fnStr; }).bind(null); const proxyDetails = { apply(target, thisArg, args) { return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args)); }, get(target, prop) { if ( prop === 'toString' ) { return toString; } return Reflect.get(target, prop); }, }; if ( fn.prototype?.constructor === fn ) { proxyDetails.construct = function(target, args) { return handler(proxyApplyFn.CtorContext.factory(target, args)); }; } context[prop] = new Proxy(fn, proxyDetails); } /******************************************************************************/ builtinScriptlets.push({ name: 'prevent-xhr.fn', fn: preventXhrFn, dependencies: [ 'generate-content.fn', 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', ], }); function preventXhrFn( trusted = false, propsToMatch = '', directive = '' ) { if ( typeof propsToMatch !== 'string' ) { return; } const safe = safeSelf(); const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr'; const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive); const xhrInstances = new WeakMap(); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); const warOrigin = scriptletGlobals.warOrigin; const safeDispatchEvent = (xhr, type) => { try { xhr.dispatchEvent(new Event(type)); } catch(_) { } }; const XHRBefore = XMLHttpRequest.prototype; self.XMLHttpRequest = class extends self.XMLHttpRequest { open(method, url, ...args) { xhrInstances.delete(this); if ( warOrigin !== undefined && url.startsWith(warOrigin) ) { return super.open(method, url, ...args); } const haystack = { method, url }; if ( propsToMatch === '' && directive === '' ) { safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`); return super.open(method, url, ...args); } if ( matchObjectProperties(propNeedles, haystack) ) { const xhrDetails = Object.assign(haystack, { xhr: this, defer: args.length === 0 || !!args[0], directive, headers: { 'date': '', 'content-type': '', 'content-length': '', }, props: { response: { value: '' }, responseText: { value: '' }, responseXML: { value: null }, responseURL: { value: haystack.url }, }, }); xhrInstances.set(this, xhrDetails); } return super.open(method, url, ...args); } send(...args) { const xhrDetails = xhrInstances.get(this); if ( xhrDetails === undefined ) { return super.send(...args); } xhrDetails.headers['date'] = (new Date()).toUTCString(); let xhrText = ''; switch ( this.responseType ) { case 'arraybuffer': xhrDetails.props.response.value = new ArrayBuffer(0); xhrDetails.headers['content-type'] = 'application/octet-stream'; break; case 'blob': xhrDetails.props.response.value = new Blob([]); xhrDetails.headers['content-type'] = 'application/octet-stream'; break; case 'document': { const parser = new DOMParser(); const doc = parser.parseFromString('', 'text/html'); xhrDetails.props.response.value = doc; xhrDetails.props.responseXML.value = doc; xhrDetails.headers['content-type'] = 'text/html'; break; } case 'json': xhrDetails.props.response.value = {}; xhrDetails.props.responseText.value = '{}'; xhrDetails.headers['content-type'] = 'application/json'; break; default: { if ( directive === '' ) { break; } xhrText = generateContentFn(trusted, xhrDetails.directive); if ( xhrText instanceof Promise ) { xhrText = xhrText.then(text => { xhrDetails.props.response.value = text; xhrDetails.props.responseText.value = text; }); } else { xhrDetails.props.response.value = xhrText; xhrDetails.props.responseText.value = xhrText; } xhrDetails.headers['content-type'] = 'text/plain'; break; } } if ( xhrDetails.defer === false ) { xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length; Object.defineProperties(xhrDetails.xhr, { readyState: { value: 4 }, status: { value: 200 }, statusText: { value: 'OK' }, }); Object.defineProperties(xhrDetails.xhr, xhrDetails.props); return; } Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => { Object.defineProperties(details.xhr, { readyState: { value: 1, configurable: true }, }); safeDispatchEvent(details.xhr, 'readystatechange'); return details; }).then(details => { xhrDetails.headers['content-length'] = `${details.props.response.value}`.length; Object.defineProperties(details.xhr, { readyState: { value: 2, configurable: true }, status: { value: 200 }, statusText: { value: 'OK' }, }); safeDispatchEvent(details.xhr, 'readystatechange'); return details; }).then(details => { Object.defineProperties(details.xhr, { readyState: { value: 3, configurable: true }, }); Object.defineProperties(details.xhr, details.props); safeDispatchEvent(details.xhr, 'readystatechange'); return details; }).then(details => { Object.defineProperties(details.xhr, { readyState: { value: 4 }, }); safeDispatchEvent(details.xhr, 'readystatechange'); safeDispatchEvent(details.xhr, 'load'); safeDispatchEvent(details.xhr, 'loadend'); safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`); }); } getResponseHeader(headerName) { const xhrDetails = xhrInstances.get(this); if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) { return super.getResponseHeader(headerName); } const value = xhrDetails.headers[headerName.toLowerCase()]; if ( value !== undefined && value !== '' ) { return value; } return null; } getAllResponseHeaders() { const xhrDetails = xhrInstances.get(this); if ( xhrDetails === undefined || this.readyState < this.HEADERS_RECEIVED ) { return super.getAllResponseHeaders(); } const out = []; for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) { if ( !value ) { continue; } out.push(`${name}: ${value}`); } if ( out.length !== 0 ) { out.push(''); } return out.join('\r\n'); } }; self.XMLHttpRequest.prototype.open.toString = function() { return XHRBefore.open.toString(); }; self.XMLHttpRequest.prototype.send.toString = function() { return XHRBefore.send.toString(); }; self.XMLHttpRequest.prototype.getResponseHeader.toString = function() { return XHRBefore.getResponseHeader.toString(); }; self.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function() { return XHRBefore.getAllResponseHeaders.toString(); }; } /******************************************************************************* Injectable scriptlets These are meant to be used in the MAIN (webpage) execution world. *******************************************************************************/ builtinScriptlets.push({ name: 'abort-current-script.js', aliases: [ 'acs.js', 'abort-current-inline-script.js', 'acis.js', ], fn: abortCurrentScript, dependencies: [ 'abort-current-script-core.fn', 'run-at-html-element.fn', ], }); // Issues to mind before changing anything: // https://github.com/uBlockOrigin/uBlock-issues/issues/2154 function abortCurrentScript(...args) { runAtHtmlElementFn(( ) => { abortCurrentScriptCore(...args); }); } /******************************************************************************/ builtinScriptlets.push({ name: 'abort-on-property-read.js', aliases: [ 'aopr.js', ], fn: abortOnPropertyRead, dependencies: [ 'get-exception-token.fn', 'safe-self.fn', ], }); function abortOnPropertyRead( chain = '' ) { if ( typeof chain !== 'string' ) { return; } if ( chain === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('abort-on-property-read', chain); const exceptionToken = getExceptionToken(); const abort = function() { safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); }; const makeProxy = function(owner, chain) { const pos = chain.indexOf('.'); if ( pos === -1 ) { const desc = Object.getOwnPropertyDescriptor(owner, chain); if ( !desc || desc.get !== abort ) { Object.defineProperty(owner, chain, { get: abort, set: function(){} }); } return; } const prop = chain.slice(0, pos); let v = owner[prop]; chain = chain.slice(pos + 1); if ( v ) { makeProxy(v, chain); return; } const desc = Object.getOwnPropertyDescriptor(owner, prop); if ( desc && desc.set !== undefined ) { return; } Object.defineProperty(owner, prop, { get: function() { return v; }, set: function(a) { v = a; if ( a instanceof Object ) { makeProxy(a, chain); } } }); }; const owner = window; makeProxy(owner, chain); } /******************************************************************************/ builtinScriptlets.push({ name: 'abort-on-property-write.js', aliases: [ 'aopw.js', ], fn: abortOnPropertyWrite, dependencies: [ 'get-exception-token.fn', 'safe-self.fn', ], }); function abortOnPropertyWrite( prop = '' ) { if ( typeof prop !== 'string' ) { return; } if ( prop === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('abort-on-property-write', prop); const exceptionToken = getExceptionToken(); let owner = window; for (;;) { const pos = prop.indexOf('.'); if ( pos === -1 ) { break; } owner = owner[prop.slice(0, pos)]; if ( owner instanceof Object === false ) { return; } prop = prop.slice(pos + 1); } delete owner[prop]; Object.defineProperty(owner, prop, { set: function() { safe.uboLog(logPrefix, 'Aborted'); throw new ReferenceError(exceptionToken); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'abort-on-stack-trace.js', aliases: [ 'aost.js', ], fn: abortOnStackTrace, dependencies: [ 'get-exception-token.fn', 'matches-stack-trace.fn', 'safe-self.fn', ], }); function abortOnStackTrace( chain = '', needle = '' ) { if ( typeof chain !== 'string' ) { return; } const safe = safeSelf(); const needleDetails = safe.initPattern(needle, { canNegate: true }); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); if ( needle === '' ) { extraArgs.log = 'all'; } const makeProxy = function(owner, chain) { const pos = chain.indexOf('.'); if ( pos === -1 ) { let v = owner[chain]; Object.defineProperty(owner, chain, { get: function() { if ( matchesStackTrace(needleDetails, extraArgs.log) ) { throw new ReferenceError(getExceptionToken()); } return v; }, set: function(a) { if ( matchesStackTrace(needleDetails, extraArgs.log) ) { throw new ReferenceError(getExceptionToken()); } v = a; }, }); return; } const prop = chain.slice(0, pos); let v = owner[prop]; chain = chain.slice(pos + 1); if ( v ) { makeProxy(v, chain); return; } const desc = Object.getOwnPropertyDescriptor(owner, prop); if ( desc && desc.set !== undefined ) { return; } Object.defineProperty(owner, prop, { get: function() { return v; }, set: function(a) { v = a; if ( a instanceof Object ) { makeProxy(a, chain); } } }); }; const owner = window; makeProxy(owner, chain); } /******************************************************************************/ 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; }; runAt(( ) => { proxyApplyFn('EventTarget.prototype.addEventListener', 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(ex) { } 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(); }); }, extraArgs.runAt); } /******************************************************************************/ builtinScriptlets.push({ name: 'json-prune.js', fn: jsonPrune, dependencies: [ 'object-prune.fn', 'safe-self.fn', ], }); function jsonPrune( rawPrunePaths = '', rawNeedlePaths = '', stackNeedle = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle); const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true }); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); JSON.parse = new Proxy(JSON.parse, { apply: function(target, thisArg, args) { const objBefore = Reflect.apply(target, thisArg, args); if ( rawPrunePaths === '' ) { safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedleDetails, extraArgs ); if ( objAfter === undefined ) { return objBefore; } safe.uboLog(logPrefix, 'Pruned'); if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`); } return objAfter; }, }); } /******************************************************************************* * * json-prune-fetch-response.js * * Prune JSON response of fetch requests. * **/ builtinScriptlets.push({ name: 'json-prune-fetch-response.js', fn: jsonPruneFetchResponse, dependencies: [ 'json-prune-fetch-response.fn', ], }); function jsonPruneFetchResponse(...args) { jsonPruneFetchResponseFn(...args); } /******************************************************************************/ builtinScriptlets.push({ name: 'json-prune-xhr-response.js', fn: jsonPruneXhrResponse, dependencies: [ 'match-object-properties.fn', 'object-prune.fn', 'parse-properties-to-match.fn', 'safe-self.fn', ], }); function jsonPruneXhrResponse( rawPrunePaths = '', rawNeedlePaths = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths); const xhrInstances = new WeakMap(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url'); const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); self.XMLHttpRequest = class extends self.XMLHttpRequest { open(method, url, ...args) { const xhrDetails = { method, url }; let outcome = 'match'; if ( propNeedles.size !== 0 ) { if ( matchObjectProperties(propNeedles, xhrDetails) === false ) { outcome = 'nomatch'; } } if ( outcome === 'match' ) { if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`); } xhrInstances.set(this, xhrDetails); } return super.open(method, url, ...args); } get response() { const innerResponse = super.response; const xhrDetails = xhrInstances.get(this); if ( xhrDetails === undefined ) { return innerResponse; } const responseLength = typeof innerResponse === 'string' ? innerResponse.length : undefined; if ( xhrDetails.lastResponseLength !== responseLength ) { xhrDetails.response = undefined; xhrDetails.lastResponseLength = responseLength; } if ( xhrDetails.response !== undefined ) { return xhrDetails.response; } let objBefore; if ( typeof innerResponse === 'object' ) { objBefore = innerResponse; } else if ( typeof innerResponse === 'string' ) { try { objBefore = safe.JSON_parse(innerResponse); } catch(ex) { } } if ( typeof objBefore !== 'object' ) { return (xhrDetails.response = innerResponse); } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedle, extraArgs ); let outerResponse; if ( typeof objAfter === 'object' ) { outerResponse = typeof innerResponse === 'string' ? safe.JSON_stringify(objAfter) : objAfter; safe.uboLog(logPrefix, 'Pruned'); } else { outerResponse = innerResponse; } return (xhrDetails.response = outerResponse); } get responseText() { const response = this.response; return typeof response !== 'string' ? super.responseText : response; } }; } /******************************************************************************/ // There is still code out there which uses `eval` in lieu of `JSON.parse`. builtinScriptlets.push({ name: 'evaldata-prune.js', fn: evaldataPrune, dependencies: [ 'object-prune.fn', ], }); function evaldataPrune( rawPrunePaths = '', rawNeedlePaths = '' ) { self.eval = new Proxy(self.eval, { apply(target, thisArg, args) { const before = Reflect.apply(target, thisArg, args); if ( typeof before === 'object' ) { const after = objectPruneFn(before, rawPrunePaths, rawNeedlePaths); return after || before; } return before; } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'adjust-setInterval.js', aliases: [ 'nano-setInterval-booster.js', 'nano-sib.js', ], fn: adjustSetInterval, dependencies: [ 'safe-self.fn', ], }); // Imported from: // https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L126 // // Speed up or down setInterval, 3 optional arguments. // The payload matcher, a string literal or a JavaScript RegExp, defaults // to match all. // delayMatcher // The delay matcher, an integer, defaults to 1000. // Use `*` to match any delay. // boostRatio - The delay multiplier when there is a match, 0.5 speeds up by // 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up // 20 times. Speed up and down both cap at 50 times. function adjustSetInterval( needleArg = '', delayArg = '', boostArg = '' ) { if ( typeof needleArg !== 'string' ) { return; } const safe = safeSelf(); const reNeedle = safe.patternToRegex(needleArg); let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1; if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; } let boost = parseFloat(boostArg); boost = isNaN(boost) === false && isFinite(boost) ? Math.min(Math.max(boost, 0.001), 50) : 0.05; self.setInterval = new Proxy(self.setInterval, { apply: function(target, thisArg, args) { const [ a, b ] = args; if ( (delay === -1 || b === delay) && reNeedle.test(a.toString()) ) { args[1] = b * boost; } return target.apply(thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'adjust-setTimeout.js', aliases: [ 'nano-setTimeout-booster.js', 'nano-stb.js', ], fn: adjustSetTimeout, dependencies: [ 'safe-self.fn', ], }); // Imported from: // https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L82 // // Speed up or down setTimeout, 3 optional arguments. // funcMatcher // The payload matcher, a string literal or a JavaScript RegExp, defaults // to match all. // delayMatcher // The delay matcher, an integer, defaults to 1000. // Use `*` to match any delay. // boostRatio - The delay multiplier when there is a match, 0.5 speeds up by // 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up // 20 times. Speed up and down both cap at 50 times. function adjustSetTimeout( needleArg = '', delayArg = '', boostArg = '' ) { if ( typeof needleArg !== 'string' ) { return; } const safe = safeSelf(); const reNeedle = safe.patternToRegex(needleArg); let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1; if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; } let boost = parseFloat(boostArg); boost = isNaN(boost) === false && isFinite(boost) ? Math.min(Math.max(boost, 0.001), 50) : 0.05; self.setTimeout = new Proxy(self.setTimeout, { apply: function(target, thisArg, args) { const [ a, b ] = args; if ( (delay === -1 || b === delay) && reNeedle.test(a.toString()) ) { args[1] = b * boost; } return target.apply(thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'noeval-if.js', aliases: [ 'prevent-eval-if.js', ], fn: noEvalIf, dependencies: [ 'safe-self.fn', ], }); function noEvalIf( needle = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('noeval-if', needle); const reNeedle = safe.patternToRegex(needle); window.eval = new Proxy(window.eval, { // jshint ignore: line apply: function(target, thisArg, args) { const a = String(args[0]); if ( needle !== '' && reNeedle.test(a) ) { safe.uboLog(logPrefix, 'Prevented:\n', a); return; } if ( needle === '' || safe.logLevel > 1 ) { safe.uboLog(logPrefix, 'Not prevented:\n', a); } return Reflect.apply(target, thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'prevent-fetch.js', aliases: [ 'no-fetch-if.js', ], fn: noFetchIf, dependencies: [ 'generate-content.fn', 'proxy-apply.fn', 'safe-self.fn', ], }); function noFetchIf( propsToMatch = '', responseBody = '', responseType = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('prevent-fetch', propsToMatch, responseBody, responseType); const needles = []; for ( const condition of propsToMatch.split(/\s+/) ) { if ( condition === '' ) { continue; } const pos = condition.indexOf(':'); let key, value; if ( pos !== -1 ) { key = condition.slice(0, pos); value = condition.slice(pos + 1); } else { key = 'url'; value = condition; } needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) }); } const validResponseProps = { ok: [ false, true ], statusText: [ '', 'Not Found' ], type: [ 'basic', 'cors', 'default', 'error', 'opaque' ], }; const responseProps = { statusText: { value: 'OK' }, }; if ( /^\{.*\}$/.test(responseType) ) { try { Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => { if ( validResponseProps[p] === undefined ) { return; } if ( validResponseProps[p].includes(v) === false ) { return; } responseProps[p] = { value: v }; }); } catch(ex) {} } else if ( responseType !== '' ) { if ( validResponseProps.type.includes(responseType) ) { responseProps.type = { value: responseType }; } } proxyApplyFn('fetch', function fetch(context) { const { callArgs } = context; const details = callArgs[0] instanceof self.Request ? callArgs[0] : Object.assign({ url: callArgs[0] }, callArgs[1]); let proceed = true; try { const props = new Map(); for ( const prop in details ) { let v = details[prop]; if ( typeof v !== 'string' ) { try { v = safe.JSON_stringify(v); } catch(ex) { } } if ( typeof v !== 'string' ) { continue; } props.set(prop, v); } if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) { const out = Array.from(props).map(a => `${a[0]}:${a[1]}`); safe.uboLog(logPrefix, `Called: ${out.join('\n')}`); } if ( propsToMatch === '' && responseBody === '' ) { return context.reflect(); } proceed = needles.length === 0; for ( const { key, pattern } of needles ) { if ( pattern.expect && props.has(key) === false || safe.testPattern(pattern, props.get(key)) === false ) { proceed = true; break; } } } catch(ex) { } if ( proceed ) { return context.reflect(); } return Promise.resolve(generateContentFn(false, responseBody)).then(text => { safe.uboLog(logPrefix, `Prevented with response "${text}"`); const response = new Response(text, { headers: { 'Content-Length': text.length, } }); const props = Object.assign( { url: { value: details.url } }, responseProps ); safe.Object_defineProperties(response, props); return response; }); }); } /******************************************************************************/ builtinScriptlets.push({ name: 'prevent-refresh.js', aliases: [ 'refresh-defuser.js', ], fn: preventRefresh, world: 'ISOLATED', dependencies: [ 'run-at.fn', 'safe-self.fn', ], }); // https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/ function preventRefresh( arg1 = '' ) { if ( typeof arg1 !== 'string' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('prevent-refresh', arg1); const defuse = ( ) => { const meta = document.querySelector('meta[http-equiv="refresh" i][content]'); if ( meta === null ) { return; } safe.uboLog(logPrefix, `Prevented "${meta.textContent}"`); const s = arg1 === '' ? meta.getAttribute('content') : arg1; const ms = Math.max(parseFloat(s) || 0, 0) * 1000; setTimeout(( ) => { window.stop(); }, ms); }; runAt(( ) => { defuse(); }, 'interactive'); } /******************************************************************************/ builtinScriptlets.push({ name: 'remove-attr.js', aliases: [ 'ra.js', ], fn: removeAttr, dependencies: [ 'run-at.fn', 'safe-self.fn', ], }); function removeAttr( rawToken = '', rawSelector = '', behavior = '' ) { if ( typeof rawToken !== 'string' ) { return; } if ( rawToken === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('remove-attr', rawToken, rawSelector, behavior); const tokens = rawToken.split(/\s*\|\s*/); const selector = tokens .map(a => `${rawSelector}[${CSS.escape(a)}]`) .join(','); if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Target selector:\n\t${selector}`); } const asap = /\basap\b/.test(behavior); let timerId; const rmattrAsync = ( ) => { if ( timerId !== undefined ) { return; } timerId = safe.onIdle(( ) => { timerId = undefined; rmattr(); }, { timeout: 17 }); }; const rmattr = ( ) => { if ( timerId !== undefined ) { safe.offIdle(timerId); timerId = undefined; } try { const nodes = document.querySelectorAll(selector); for ( const node of nodes ) { for ( const attr of tokens ) { if ( node.hasAttribute(attr) === false ) { continue; } node.removeAttribute(attr); safe.uboLog(logPrefix, `Removed attribute '${attr}'`); } } } catch(ex) { } }; const mutationHandler = mutations => { if ( timerId !== undefined ) { return; } let skip = true; for ( let i = 0; i < mutations.length && skip; i++ ) { const { type, addedNodes, removedNodes } = mutations[i]; if ( type === 'attributes' ) { skip = false; } for ( let j = 0; j < addedNodes.length && skip; j++ ) { if ( addedNodes[j].nodeType === 1 ) { skip = false; break; } } for ( let j = 0; j < removedNodes.length && skip; j++ ) { if ( removedNodes[j].nodeType === 1 ) { skip = false; break; } } } if ( skip ) { return; } asap ? rmattr() : rmattrAsync(); }; const start = ( ) => { rmattr(); if ( /\bstay\b/.test(behavior) === false ) { return; } const observer = new MutationObserver(mutationHandler); observer.observe(document, { attributes: true, attributeFilter: tokens, childList: true, subtree: true, }); }; runAt(( ) => { start(); }, behavior.split(/\s+/)); } /******************************************************************************/ builtinScriptlets.push({ name: 'remove-class.js', aliases: [ 'rc.js', ], fn: removeClass, world: 'ISOLATED', dependencies: [ 'run-at.fn', 'safe-self.fn', ], }); function removeClass( rawToken = '', rawSelector = '', behavior = '' ) { if ( typeof rawToken !== 'string' ) { return; } if ( rawToken === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('remove-class', rawToken, rawSelector, behavior); const tokens = rawToken.split(/\s*\|\s*/); const selector = tokens .map(a => `${rawSelector}.${CSS.escape(a)}`) .join(','); if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Target selector:\n\t${selector}`); } const mustStay = /\bstay\b/.test(behavior); let timer; const rmclass = ( ) => { timer = undefined; try { const nodes = document.querySelectorAll(selector); for ( const node of nodes ) { node.classList.remove(...tokens); safe.uboLog(logPrefix, 'Removed class(es)'); } } catch(ex) { } if ( mustStay ) { return; } if ( document.readyState !== 'complete' ) { return; } observer.disconnect(); }; const mutationHandler = mutations => { if ( timer !== undefined ) { return; } let skip = true; for ( let i = 0; i < mutations.length && skip; i++ ) { const { type, addedNodes, removedNodes } = mutations[i]; if ( type === 'attributes' ) { skip = false; } for ( let j = 0; j < addedNodes.length && skip; j++ ) { if ( addedNodes[j].nodeType === 1 ) { skip = false; break; } } for ( let j = 0; j < removedNodes.length && skip; j++ ) { if ( removedNodes[j].nodeType === 1 ) { skip = false; break; } } } if ( skip ) { return; } timer = safe.onIdle(rmclass, { timeout: 67 }); }; const observer = new MutationObserver(mutationHandler); const start = ( ) => { rmclass(); observer.observe(document, { attributes: true, attributeFilter: [ 'class' ], childList: true, subtree: true, }); }; runAt(( ) => { start(); }, /\bcomplete\b/.test(behavior) ? 'idle' : 'loading'); } /******************************************************************************/ builtinScriptlets.push({ name: 'no-requestAnimationFrame-if.js', aliases: [ 'norafif.js', 'prevent-requestAnimationFrame.js', ], fn: noRequestAnimationFrameIf, dependencies: [ 'safe-self.fn', ], }); function noRequestAnimationFrameIf( needle = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } const log = needleNot === false && needle === '' ? console.log : undefined; const reNeedle = safe.patternToRegex(needle); window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, { apply: function(target, thisArg, args) { const a = args[0] instanceof Function ? String(safe.Function_toString(args[0])) : String(args[0]); let defuse = false; if ( log !== undefined ) { log('uBO: requestAnimationFrame("%s")', a); } else { defuse = reNeedle.test(a) !== needleNot; } if ( defuse ) { args[0] = function(){}; } return target.apply(thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'set-constant.js', aliases: [ 'set.js', ], fn: setConstant, dependencies: [ 'set-constant.fn' ], }); function setConstant( ...args ) { setConstantFn(false, ...args); } /******************************************************************************/ builtinScriptlets.push({ name: 'no-setInterval-if.js', aliases: [ 'nosiif.js', 'prevent-setInterval.js', 'setInterval-defuser.js', ], fn: noSetIntervalIf, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function noSetIntervalIf( needle = '', delay = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('prevent-setInterval', needle, delay); const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } if ( delay === '' ) { delay = undefined; } let delayNot = false; if ( delay !== undefined ) { delayNot = delay.charAt(0) === '!'; if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } const reNeedle = safe.patternToRegex(needle); proxyApplyFn('setInterval', function setInterval(context) { const { callArgs } = context; const a = callArgs[0] instanceof Function ? String(safe.Function_toString(callArgs[0])) : String(callArgs[0]); const b = callArgs[1]; if ( needle === '' && delay === undefined ) { safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); return context.reflect(); } let defuse; if ( needle !== '' ) { defuse = reNeedle.test(a) !== needleNot; } if ( defuse !== false && delay !== undefined ) { defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; } if ( defuse ) { callArgs[0] = function(){}; safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); } return context.reflect(); }); } /******************************************************************************/ builtinScriptlets.push({ name: 'no-setTimeout-if.js', aliases: [ 'nostif.js', 'prevent-setTimeout.js', 'setTimeout-defuser.js', ], fn: noSetTimeoutIf, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function noSetTimeoutIf( needle = '', delay = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needle, delay); const needleNot = needle.charAt(0) === '!'; if ( needleNot ) { needle = needle.slice(1); } if ( delay === '' ) { delay = undefined; } let delayNot = false; if ( delay !== undefined ) { delayNot = delay.charAt(0) === '!'; if ( delayNot ) { delay = delay.slice(1); } delay = parseInt(delay, 10); } const reNeedle = safe.patternToRegex(needle); proxyApplyFn('setTimeout', function setTimeout(context) { const { callArgs } = context; const a = callArgs[0] instanceof Function ? String(safe.Function_toString(callArgs[0])) : String(callArgs[0]); const b = callArgs[1]; if ( needle === '' && delay === undefined ) { safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); return context.reflect(); } let defuse; if ( needle !== '' ) { defuse = reNeedle.test(a) !== needleNot; } if ( defuse !== false && delay !== undefined ) { defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot; } if ( defuse ) { callArgs[0] = function(){}; safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); } return context.reflect(); }); } /******************************************************************************/ builtinScriptlets.push({ name: 'webrtc-if.js', fn: webrtcIf, dependencies: [ 'safe-self.fn', ], }); function webrtcIf( good = '' ) { if ( typeof good !== 'string' ) { return; } const safe = safeSelf(); const reGood = safe.patternToRegex(good); const rtcName = window.RTCPeerConnection ? 'RTCPeerConnection' : (window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : ''); if ( rtcName === '' ) { return; } const log = console.log.bind(console); const neuteredPeerConnections = new WeakSet(); const isGoodConfig = function(instance, config) { if ( neuteredPeerConnections.has(instance) ) { return false; } if ( config instanceof Object === false ) { return true; } if ( Array.isArray(config.iceServers) === false ) { return true; } for ( const server of config.iceServers ) { const urls = typeof server.urls === 'string' ? [ server.urls ] : server.urls; if ( Array.isArray(urls) ) { for ( const url of urls ) { if ( reGood.test(url) ) { return true; } } } if ( typeof server.username === 'string' ) { if ( reGood.test(server.username) ) { return true; } } if ( typeof server.credential === 'string' ) { if ( reGood.test(server.credential) ) { return true; } } } neuteredPeerConnections.add(instance); return false; }; const peerConnectionCtor = window[rtcName]; const peerConnectionProto = peerConnectionCtor.prototype; peerConnectionProto.createDataChannel = new Proxy(peerConnectionProto.createDataChannel, { apply: function(target, thisArg, args) { if ( isGoodConfig(target, args[1]) === false ) { log('uBO:', args[1]); return Reflect.apply(target, thisArg, args.slice(0, 1)); } return Reflect.apply(target, thisArg, args); }, }); window[rtcName] = new Proxy(peerConnectionCtor, { construct: function(target, args) { if ( isGoodConfig(target, args[0]) === false ) { log('uBO:', args[0]); return Reflect.construct(target); } return Reflect.construct(target, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'prevent-xhr.js', aliases: [ 'no-xhr-if.js', ], fn: preventXhr, dependencies: [ 'prevent-xhr.fn', ], }); function preventXhr(...args) { return preventXhrFn(false, ...args); } /** * @scriptlet prevent-window-open * * @description * Prevent a webpage from opening new tabs through `window.open()`. * * @param pattern * A plain string or regex to match against the `url` argument for the * prevention to be triggered. If not provided, all calls to `window.open()` * are prevented. * If set to the special value `debug` *and* the logger is opened, the scriptlet * will trigger a `debugger` statement and the prevention will not occur. * * @param [delay] * If provided, a decoy will be created or opened, and this parameter states * the number of seconds to wait for before the decoy is terminated, i.e. * either removed from the DOM or closed. * * @param [decoy] * A string representing the type of decoy to use: * - `blank`: replace the `url` parameter with `about:blank` * - `object`: create and append an `object` element to the DOM, and return * its `contentWindow` property. * - `frame`: create and append an `iframe` element to the DOM, and return * its `contentWindow` property. * * @example * ##+js(prevent-window-open, ads.example.com/) * * @example * ##+js(prevent-window-open, ads.example.com/, 1, iframe) * * */ builtinScriptlets.push({ name: 'prevent-window-open.js', aliases: [ 'nowoif.js', 'no-window-open-if.js', 'window.open-defuser.js', ], fn: noWindowOpenIf, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function noWindowOpenIf( pattern = '', delay = '', decoy = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('no-window-open-if', pattern, delay, decoy); const targetMatchResult = pattern.startsWith('!') === false; if ( targetMatchResult === false ) { pattern = pattern.slice(1); } const rePattern = safe.patternToRegex(pattern); const autoRemoveAfter = (parseFloat(delay) || 0) * 1000; const setTimeout = self.setTimeout; const createDecoy = function(tag, urlProp, url) { const decoyElem = document.createElement(tag); decoyElem[urlProp] = url; decoyElem.style.setProperty('height','1px', 'important'); decoyElem.style.setProperty('position','fixed', 'important'); decoyElem.style.setProperty('top','-1px', 'important'); decoyElem.style.setProperty('width','1px', 'important'); document.body.appendChild(decoyElem); setTimeout(( ) => { decoyElem.remove(); }, autoRemoveAfter); return decoyElem; }; const noopFunc = function(){}; proxyApplyFn('open', function open(context) { if ( pattern === 'debug' && safe.logLevel !== 0 ) { debugger; // eslint-disable-line no-debugger return context.reflect(); } const { callArgs } = context; const haystack = callArgs.join(' '); if ( rePattern.test(haystack) !== targetMatchResult ) { if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Allowed (${callArgs.join(', ')})`); } return context.reflect(); } safe.uboLog(logPrefix, `Prevented (${callArgs.join(', ')})`); if ( delay === '' ) { return null; } if ( decoy === 'blank' ) { callArgs[0] = 'about:blank'; const r = context.reflect(); setTimeout(( ) => { r.close(); }, autoRemoveAfter); return r; } const decoyElem = decoy === 'obj' ? createDecoy('object', 'data', ...callArgs) : createDecoy('iframe', 'src', ...callArgs); let popup = decoyElem.contentWindow; if ( typeof popup === 'object' && popup !== null ) { Object.defineProperty(popup, 'closed', { value: false }); } else { popup = new Proxy(self, { get: function(target, prop, ...args) { if ( prop === 'closed' ) { return false; } const r = Reflect.get(target, prop, ...args); if ( typeof r === 'function' ) { return noopFunc; } return r; }, set: function(...args) { return Reflect.set(...args); }, }); } if ( safe.logLevel !== 0 ) { popup = new Proxy(popup, { get: function(target, prop, ...args) { const r = Reflect.get(target, prop, ...args); safe.uboLog(logPrefix, `popup / get ${prop} === ${r}`); if ( typeof r === 'function' ) { return (...args) => { return r.call(target, ...args); }; } return r; }, set: function(target, prop, value, ...args) { safe.uboLog(logPrefix, `popup / set ${prop} = ${value}`); return Reflect.set(target, prop, value, ...args); }, }); } return popup; }); } /******************************************************************************/ builtinScriptlets.push({ name: 'close-window.js', aliases: [ 'window-close-if.js', ], fn: closeWindow, world: 'ISOLATED', dependencies: [ 'safe-self.fn', ], }); // https://github.com/uBlockOrigin/uAssets/issues/10323#issuecomment-992312847 // https://github.com/AdguardTeam/Scriptlets/issues/158 // https://github.com/uBlockOrigin/uBlock-issues/discussions/2270 function closeWindow( arg1 = '' ) { if ( typeof arg1 !== 'string' ) { return; } const safe = safeSelf(); let subject = ''; if ( /^\/.*\/$/.test(arg1) ) { subject = window.location.href; } else if ( arg1 !== '' ) { subject = `${window.location.pathname}${window.location.search}`; } try { const re = safe.patternToRegex(arg1); if ( re.test(subject) ) { window.close(); } } catch(ex) { console.log(ex); } } /******************************************************************************/ builtinScriptlets.push({ name: 'window.name-defuser.js', fn: windowNameDefuser, }); // https://github.com/gorhill/uBlock/issues/1228 function windowNameDefuser() { if ( window === window.top ) { window.name = ''; } } /******************************************************************************/ builtinScriptlets.push({ name: 'overlay-buster.js', fn: overlayBuster, }); // Experimental: Generic nuisance overlay buster. // if this works well and proves to be useful, this may end up // as a stock tool in uBO's popup panel. function overlayBuster() { if ( window !== window.top ) { return; } var tstart; var ttl = 30000; var delay = 0; var delayStep = 50; var buster = function() { var docEl = document.documentElement, bodyEl = document.body, vw = Math.min(docEl.clientWidth, window.innerWidth), vh = Math.min(docEl.clientHeight, window.innerHeight), tol = Math.min(vw, vh) * 0.05, el = document.elementFromPoint(vw/2, vh/2), style, rect; for (;;) { if ( el === null || el.parentNode === null || el === bodyEl ) { break; } style = window.getComputedStyle(el); if ( parseInt(style.zIndex, 10) >= 1000 || style.position === 'fixed' ) { rect = el.getBoundingClientRect(); if ( rect.left <= tol && rect.top <= tol && (vw - rect.right) <= tol && (vh - rect.bottom) < tol ) { el.parentNode.removeChild(el); tstart = Date.now(); el = document.elementFromPoint(vw/2, vh/2); bodyEl.style.setProperty('overflow', 'auto', 'important'); docEl.style.setProperty('overflow', 'auto', 'important'); continue; } } el = el.parentNode; } if ( (Date.now() - tstart) < ttl ) { delay = Math.min(delay + delayStep, 1000); setTimeout(buster, delay); } }; var domReady = function(ev) { if ( ev ) { document.removeEventListener(ev.type, domReady); } tstart = Date.now(); setTimeout(buster, delay); }; if ( document.readyState === 'loading' ) { document.addEventListener('DOMContentLoaded', domReady); } else { domReady(); } } /******************************************************************************/ builtinScriptlets.push({ name: 'alert-buster.js', fn: alertBuster, }); // https://github.com/uBlockOrigin/uAssets/issues/8 function alertBuster() { window.alert = new Proxy(window.alert, { apply: function(a) { console.info(a); }, get(target, prop) { if ( prop === 'toString' ) { return target.toString.bind(target); } return Reflect.get(target, prop); }, }); } /******************************************************************************/ builtinScriptlets.push({ name: 'nowebrtc.js', fn: noWebrtc, }); // Prevent web pages from using RTCPeerConnection(), and report attempts in console. function noWebrtc() { var rtcName = window.RTCPeerConnection ? 'RTCPeerConnection' : ( window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : '' ); if ( rtcName === '' ) { return; } var log = console.log.bind(console); var pc = function(cfg) { log('Document tried to create an RTCPeerConnection: %o', cfg); }; const noop = function() { }; pc.prototype = { close: noop, createDataChannel: noop, createOffer: noop, setRemoteDescription: noop, toString: function() { return '[object RTCPeerConnection]'; } }; var z = window[rtcName]; window[rtcName] = pc.bind(window); if ( z.prototype ) { z.prototype.createDataChannel = function() { return { close: function() {}, send: function() {} }; }.bind(null); } } /******************************************************************************/ builtinScriptlets.push({ name: 'disable-newtab-links.js', fn: disableNewtabLinks, }); // https://github.com/uBlockOrigin/uAssets/issues/913 function disableNewtabLinks() { document.addEventListener('click', function(ev) { var target = ev.target; while ( target !== null ) { if ( target.localName === 'a' && target.hasAttribute('target') ) { ev.stopPropagation(); ev.preventDefault(); break; } target = target.parentNode; } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'remove-cookie.js', aliases: [ 'cookie-remover.js', ], fn: cookieRemover, world: 'ISOLATED', dependencies: [ 'safe-self.fn', ], }); // https://github.com/NanoAdblocker/NanoFilters/issues/149 function cookieRemover( needle = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const reName = safe.patternToRegex(needle); const extraArgs = safe.getExtraArgs(Array.from(arguments), 1); const throttle = (fn, ms = 500) => { if ( throttle.timer !== undefined ) { return; } throttle.timer = setTimeout(( ) => { throttle.timer = undefined; fn(); }, ms); }; const removeCookie = ( ) => { document.cookie.split(';').forEach(cookieStr => { const pos = cookieStr.indexOf('='); if ( pos === -1 ) { return; } const cookieName = cookieStr.slice(0, pos).trim(); if ( reName.test(cookieName) === false ) { return; } const part1 = cookieName + '='; const part2a = '; domain=' + document.location.hostname; const part2b = '; domain=.' + document.location.hostname; let part2c, part2d; const domain = document.domain; if ( domain ) { if ( domain !== document.location.hostname ) { part2c = '; domain=.' + domain; } if ( domain.startsWith('www.') ) { part2d = '; domain=' + domain.replace('www', ''); } } const part3 = '; path=/'; const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT'; document.cookie = part1 + part4; document.cookie = part1 + part2a + part4; document.cookie = part1 + part2b + part4; document.cookie = part1 + part3 + part4; document.cookie = part1 + part2a + part3 + part4; document.cookie = part1 + part2b + part3 + part4; if ( part2c !== undefined ) { document.cookie = part1 + part2c + part3 + part4; } if ( part2d !== undefined ) { document.cookie = part1 + part2d + part3 + part4; } }); }; removeCookie(); window.addEventListener('beforeunload', removeCookie); if ( typeof extraArgs.when !== 'string' ) { return; } const supportedEventTypes = [ 'scroll', 'keydown' ]; const eventTypes = extraArgs.when.split(/\s/); for ( const type of eventTypes ) { if ( supportedEventTypes.includes(type) === false ) { continue; } document.addEventListener(type, ( ) => { throttle(removeCookie); }, { passive: true }); } } /******************************************************************************/ builtinScriptlets.push({ name: 'xml-prune.js', fn: xmlPrune, dependencies: [ 'safe-self.fn', ], }); function xmlPrune( selector = '', selectorCheck = '', urlPattern = '' ) { if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('xml-prune', selector, selectorCheck, urlPattern); const reUrl = safe.patternToRegex(urlPattern); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const queryAll = (xmlDoc, selector) => { const isXpath = /^xpath\(.+\)$/.test(selector); if ( isXpath === false ) { return Array.from(xmlDoc.querySelectorAll(selector)); } const xpr = xmlDoc.evaluate( selector.slice(6, -1), xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); const out = []; for ( let i = 0; i < xpr.snapshotLength; i++ ) { const node = xpr.snapshotItem(i); out.push(node); } return out; }; const pruneFromDoc = xmlDoc => { try { if ( selectorCheck !== '' && xmlDoc.querySelector(selectorCheck) === null ) { return xmlDoc; } if ( extraArgs.logdoc ) { const serializer = new XMLSerializer(); safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`); } const items = queryAll(xmlDoc, selector); if ( items.length === 0 ) { return xmlDoc; } safe.uboLog(logPrefix, `Removing ${items.length} items`); for ( const item of items ) { if ( item.nodeType === 1 ) { item.remove(); } else if ( item.nodeType === 2 ) { item.ownerElement.removeAttribute(item.nodeName); } safe.uboLog(logPrefix, `${item.constructor.name}.${item.nodeName} removed`); } } catch(ex) { safe.uboErr(logPrefix, `Error: ${ex}`); } return xmlDoc; }; const pruneFromText = text => { if ( (/^\s*\s*$/.test(text)) === false ) { return text; } try { const xmlParser = new DOMParser(); const xmlDoc = xmlParser.parseFromString(text, 'text/xml'); pruneFromDoc(xmlDoc); const serializer = new XMLSerializer(); text = serializer.serializeToString(xmlDoc); } catch(ex) { } return text; }; const urlFromArg = arg => { if ( typeof arg === 'string' ) { return arg; } if ( arg instanceof Request ) { return arg.url; } return String(arg); }; self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { const fetchPromise = Reflect.apply(target, thisArg, args); if ( reUrl.test(urlFromArg(args[0])) === false ) { return fetchPromise; } return fetchPromise.then(responseBefore => { const response = responseBefore.clone(); return response.text().then(text => { const responseAfter = new Response(pruneFromText(text), { status: responseBefore.status, statusText: responseBefore.statusText, headers: responseBefore.headers, }); Object.defineProperties(responseAfter, { ok: { value: responseBefore.ok }, redirected: { value: responseBefore.redirected }, type: { value: responseBefore.type }, url: { value: responseBefore.url }, }); return responseAfter; }).catch(( ) => responseBefore ); }); } }); self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, { apply: async (target, thisArg, args) => { if ( reUrl.test(urlFromArg(args[1])) === false ) { return Reflect.apply(target, thisArg, args); } thisArg.addEventListener('readystatechange', function() { if ( thisArg.readyState !== 4 ) { return; } const type = thisArg.responseType; if ( type === 'document' || type === '' && thisArg.responseXML instanceof XMLDocument ) { pruneFromDoc(thisArg.responseXML); const serializer = new XMLSerializer(); const textout = serializer.serializeToString(thisArg.responseXML); Object.defineProperty(thisArg, 'responseText', { value: textout }); if ( typeof thisArg.response === 'string' ) { Object.defineProperty(thisArg, 'response', { value: textout }); } return; } if ( type === 'text' || type === '' && typeof thisArg.responseText === 'string' ) { const textin = thisArg.responseText; const textout = pruneFromText(textin); if ( textout === textin ) { return; } Object.defineProperty(thisArg, 'response', { value: textout }); Object.defineProperty(thisArg, 'responseText', { value: textout }); return; } }); return Reflect.apply(target, thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'm3u-prune.js', fn: m3uPrune, dependencies: [ 'safe-self.fn', ], }); // https://en.wikipedia.org/wiki/M3U function m3uPrune( m3uPattern = '', urlPattern = '' ) { if ( typeof m3uPattern !== 'string' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('m3u-prune', m3uPattern, urlPattern); const toLog = []; const regexFromArg = arg => { if ( arg === '' ) { return /^/; } const match = /^\/(.+)\/([gms]*)$/.exec(arg); if ( match !== null ) { let flags = match[2] || ''; if ( flags.includes('m') ) { flags += 's'; } return new RegExp(match[1], flags); } return new RegExp( arg.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*+/g, '.*?') ); }; const reM3u = regexFromArg(m3uPattern); const reUrl = regexFromArg(urlPattern); const pruneSpliceoutBlock = (lines, i) => { if ( lines[i].startsWith('#EXT-X-CUE:TYPE="SpliceOut"') === false ) { return false; } toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; if ( lines[i].startsWith('#EXT-X-ASSET:CAID') ) { toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-SCTE35:') ) { toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-CUE-IN') ) { toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } if ( lines[i].startsWith('#EXT-X-SCTE35:') ) { toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } return true; }; const pruneInfBlock = (lines, i) => { if ( lines[i].startsWith('#EXTINF') === false ) { return false; } if ( reM3u.test(lines[i+1]) === false ) { return false; } toLog.push('Discarding', `\t${lines[i]}, \t${lines[i+1]}`); lines[i] = lines[i+1] = undefined; i += 2; if ( lines[i].startsWith('#EXT-X-DISCONTINUITY') ) { toLog.push(`\t${lines[i]}`); lines[i] = undefined; i += 1; } return true; }; const pruner = text => { if ( (/^\s*#EXTM3U/.test(text)) === false ) { return text; } if ( m3uPattern === '' ) { safe.uboLog(` Content:\n${text}`); return text; } if ( reM3u.multiline ) { reM3u.lastIndex = 0; for (;;) { const match = reM3u.exec(text); if ( match === null ) { break; } let discard = match[0]; let before = text.slice(0, match.index); if ( /^[\n\r]+/.test(discard) === false && /[\n\r]+$/.test(before) === false ) { const startOfLine = /[^\n\r]+$/.exec(before); if ( startOfLine !== null ) { before = before.slice(0, startOfLine.index); discard = startOfLine[0] + discard; } } let after = text.slice(match.index + match[0].length); if ( /[\n\r]+$/.test(discard) === false && /^[\n\r]+/.test(after) === false ) { const endOfLine = /^[^\n\r]+/.exec(after); if ( endOfLine !== null ) { after = after.slice(endOfLine.index); discard += discard + endOfLine[0]; } } text = before.trim() + '\n' + after.trim(); reM3u.lastIndex = before.length + 1; toLog.push('Discarding', ...discard.split(/\n+/).map(s => `\t${s}`)); if ( reM3u.global === false ) { break; } } return text; } const lines = text.split(/\n\r|\n|\r/); for ( let i = 0; i < lines.length; i++ ) { if ( lines[i] === undefined ) { continue; } if ( pruneSpliceoutBlock(lines, i) ) { continue; } if ( pruneInfBlock(lines, i) ) { continue; } } return lines.filter(l => l !== undefined).join('\n'); }; const urlFromArg = arg => { if ( typeof arg === 'string' ) { return arg; } if ( arg instanceof Request ) { return arg.url; } return String(arg); }; const realFetch = self.fetch; self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { if ( reUrl.test(urlFromArg(args[0])) === false ) { return Reflect.apply(target, thisArg, args); } return realFetch(...args).then(realResponse => realResponse.text().then(text => { const response = new Response(pruner(text), { status: realResponse.status, statusText: realResponse.statusText, headers: realResponse.headers, }); if ( toLog.length !== 0 ) { toLog.unshift(logPrefix); safe.uboLog(toLog.join('\n')); } return response; }) ); } }); self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, { apply: async (target, thisArg, args) => { if ( reUrl.test(urlFromArg(args[1])) === false ) { return Reflect.apply(target, thisArg, args); } thisArg.addEventListener('readystatechange', function() { if ( thisArg.readyState !== 4 ) { return; } const type = thisArg.responseType; if ( type !== '' && type !== 'text' ) { return; } const textin = thisArg.responseText; const textout = pruner(textin); if ( textout === textin ) { return; } Object.defineProperty(thisArg, 'response', { value: textout }); Object.defineProperty(thisArg, 'responseText', { value: textout }); if ( toLog.length !== 0 ) { toLog.unshift(logPrefix); safe.uboLog(toLog.join('\n')); } }); return Reflect.apply(target, thisArg, args); } }); } /******************************************************************************* * * @scriptlet href-sanitizer * * @description * Set the `href` attribute to a value found in the DOM at, or below the * targeted `a` element. * * ### Syntax * * ```text * example.org##+js(href-sanitizer, selector [, source]) * ``` * * - `selector`: required, CSS selector, specifies `a` elements for which the * `href` attribute must be overridden. * - `source`: optional, default to `text`, specifies from where to get the * value which will override the `href` attribute. * - `text`: the value will be the first valid URL found in the text * content of the targeted `a` element. * - `[attr]`: the value will be the attribute _attr_ of the targeted `a` * element. * - `?param`: the value will be the query parameter _param_ of the URL * found in the `href` attribute of the targeted `a` element. * * ### Examples * * example.org##+js(href-sanitizer, a) * example.org##+js(href-sanitizer, a[title], [title]) * example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to) * * */ builtinScriptlets.push({ name: 'href-sanitizer.js', fn: hrefSanitizer, world: 'ISOLATED', dependencies: [ 'run-at.fn', 'safe-self.fn', ], }); function hrefSanitizer( selector = '', source = '' ) { if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source); if ( source === '' ) { source = 'text'; } const sanitizeCopycats = (href, text) => { let elems = []; try { elems = document.querySelectorAll(`a[href="${href}"`); } catch(ex) { } for ( const elem of elems ) { elem.setAttribute('href', text); } return elems.length; }; const validateURL = text => { if ( text === '' ) { return ''; } if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; } try { const url = new URL(text, document.location); return url.href; } catch(ex) { } return ''; }; const extractParam = (href, source) => { if ( Boolean(source) === false ) { return href; } const recursive = source.includes('?', 1); const end = recursive ? source.indexOf('?', 1) : source.length; try { const url = new URL(href, document.location); let value = url.searchParams.get(source.slice(1, end)); if ( value === null ) { return href } if ( recursive ) { return extractParam(value, source.slice(end)); } if ( value.includes(' ') ) { value = value.replace(/ /g, '%20'); } return value; } catch(x) { } return href; }; const extractText = (elem, source) => { if ( /^\[.*\]$/.test(source) ) { return elem.getAttribute(source.slice(1,-1).trim()) || ''; } if ( source.startsWith('?') ) { return extractParam(elem.href, source); } if ( source === 'text' ) { return elem.textContent .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters ; } return ''; }; const sanitize = ( ) => { let elems = []; try { elems = document.querySelectorAll(selector); } catch(ex) { return false; } for ( const elem of elems ) { if ( elem.localName !== 'a' ) { continue; } if ( elem.hasAttribute('href') === false ) { continue; } const href = elem.getAttribute('href'); const text = extractText(elem, source); const hrefAfter = validateURL(text); if ( hrefAfter === '' ) { continue; } if ( hrefAfter === href ) { continue; } elem.setAttribute('href', hrefAfter); const count = sanitizeCopycats(href, hrefAfter); safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`); } return true; }; let observer, timer; const onDomChanged = mutations => { if ( timer !== undefined ) { return; } let shouldSanitize = false; for ( const mutation of mutations ) { if ( mutation.addedNodes.length === 0 ) { continue; } for ( const node of mutation.addedNodes ) { if ( node.nodeType !== 1 ) { continue; } shouldSanitize = true; break; } if ( shouldSanitize ) { break; } } if ( shouldSanitize === false ) { return; } timer = safe.onIdle(( ) => { timer = undefined; sanitize(); }); }; const start = ( ) => { if ( sanitize() === false ) { return; } observer = new MutationObserver(onDomChanged); observer.observe(document.body, { subtree: true, childList: true, }); }; runAt(( ) => { start(); }, 'interactive'); } /******************************************************************************* * * @scriptlet call-nothrow * * @description * Prevent a function call from throwing. The function will be called, however * should it throw, the scriptlet will silently process the exception and * returns as if no exception has occurred. * * ### Syntax * * ```text * example.org##+js(call-nothrow, propertyChain) * ``` * * - `propertyChain`: a chain of dot-separated properties which leads to the * function to be trapped. * * ### Examples * * example.org##+js(call-nothrow, Object.defineProperty) * * */ builtinScriptlets.push({ name: 'call-nothrow.js', fn: callNothrow, }); function callNothrow( chain = '' ) { if ( typeof chain !== 'string' ) { return; } if ( chain === '' ) { return; } const parts = chain.split('.'); let owner = window, prop; for (;;) { prop = parts.shift(); if ( parts.length === 0 ) { break; } owner = owner[prop]; if ( owner instanceof Object === false ) { return; } } if ( prop === '' ) { return; } const fn = owner[prop]; if ( typeof fn !== 'function' ) { return; } owner[prop] = new Proxy(fn, { apply: function(...args) { let r; try { r = Reflect.apply(...args); } catch(ex) { } return r; }, }); } /******************************************************************************/ builtinScriptlets.push({ name: 'spoof-css.js', fn: spoofCSS, dependencies: [ 'safe-self.fn', ], }); function spoofCSS( selector, ...args ) { if ( typeof selector !== 'string' ) { return; } if ( selector === '' ) { return; } const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase()); const propToValueMap = new Map(); for ( let i = 0; i < args.length; i += 2 ) { if ( typeof args[i+0] !== 'string' ) { break; } if ( args[i+0] === '' ) { break; } if ( typeof args[i+1] !== 'string' ) { break; } propToValueMap.set(toCamelCase(args[i+0]), args[i+1]); } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args); const canDebug = scriptletGlobals.canDebug; const shouldDebug = canDebug && propToValueMap.get('debug') || 0; const instanceProperties = [ 'cssText', 'length', 'parentRule' ]; const spoofStyle = (prop, real) => { const normalProp = toCamelCase(prop); const shouldSpoof = propToValueMap.has(normalProp); const value = shouldSpoof ? propToValueMap.get(normalProp) : real; if ( shouldSpoof ) { safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`); } return value; }; const cloackFunc = (fn, thisArg, name) => { const trap = fn.bind(thisArg); Object.defineProperty(trap, 'name', { value: name }); Object.defineProperty(trap, 'toString', { value: ( ) => `function ${name}() { [native code] }` }); return trap; }; self.getComputedStyle = new Proxy(self.getComputedStyle, { apply: function(target, thisArg, args) { // eslint-disable-next-line no-debugger if ( shouldDebug !== 0 ) { debugger; } const style = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(args[0]) === false ) { return style; } const proxiedStyle = new Proxy(style, { get(target, prop) { if ( typeof target[prop] === 'function' ) { if ( prop === 'getPropertyValue' ) { return cloackFunc(function getPropertyValue(prop) { return spoofStyle(prop, target[prop]); }, target, 'getPropertyValue'); } return cloackFunc(target[prop], target, prop); } if ( instanceProperties.includes(prop) ) { return Reflect.get(target, prop); } return spoofStyle(prop, Reflect.get(target, prop)); }, getOwnPropertyDescriptor(target, prop) { if ( propToValueMap.has(prop) ) { return { configurable: true, enumerable: true, value: propToValueMap.get(prop), writable: true, }; } return Reflect.getOwnPropertyDescriptor(target, prop); }, }); return proxiedStyle; }, get(target, prop) { if ( prop === 'toString' ) { return target.toString.bind(target); } return Reflect.get(target, prop); }, }); Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, { apply: function(target, thisArg, args) { // eslint-disable-next-line no-debugger if ( shouldDebug !== 0 ) { debugger; } const rect = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(thisArg) === false ) { return rect; } let { height, width } = rect; if ( propToValueMap.has('width') ) { width = parseFloat(propToValueMap.get('width')); } if ( propToValueMap.has('height') ) { height = parseFloat(propToValueMap.get('height')); } return new self.DOMRect(rect.x, rect.y, width, height); }, get(target, prop) { if ( prop === 'toString' ) { return target.toString.bind(target); } return Reflect.get(target, prop); }, }); } /******************************************************************************/ builtinScriptlets.push({ name: 'remove-node-text.js', aliases: [ 'rmnt.js', ], fn: removeNodeText, world: 'ISOLATED', dependencies: [ 'replace-node-text.fn', ], }); function removeNodeText( nodeName, includes, ...extraArgs ) { replaceNodeTextFn(nodeName, '', '', 'includes', includes || '', ...extraArgs); } /******************************************************************************* * * set-cookie.js * * Set specified cookie to a specific value. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js * **/ builtinScriptlets.push({ name: 'set-cookie.js', fn: setCookie, world: 'ISOLATED', dependencies: [ 'get-safe-cookie-values.fn', 'safe-self.fn', 'set-cookie.fn', ], }); function setCookie( name = '', value = '', path = '' ) { if ( name === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const normalized = value.toLowerCase(); const match = /^("?)(.+)\1$/.exec(normalized); const unquoted = match && match[2] || normalized; const validValues = getSafeCookieValuesFn(); if ( validValues.includes(unquoted) === false ) { if ( /^\d+$/.test(unquoted) === false ) { return; } const n = parseInt(value, 10); if ( n > 32767 ) { return; } } const done = setCookieFn( false, name, value, '', path, safe.getExtraArgs(Array.from(arguments), 3) ); if ( done ) { safe.uboLog(logPrefix, 'Done'); } } // For compatibility with AdGuard builtinScriptlets.push({ name: 'set-cookie-reload.js', fn: setCookieReload, world: 'ISOLATED', dependencies: [ 'set-cookie.js', ], }); function setCookieReload(name, value, path, ...args) { setCookie(name, value, path, 'reload', '1', ...args); } /******************************************************************************* * * set-local-storage-item.js * set-session-storage-item.js * * Set a local/session storage entry to a specific, allowed value. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-local-storage-item.js * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-session-storage-item.js * **/ builtinScriptlets.push({ name: 'set-local-storage-item.js', fn: setLocalStorageItem, world: 'ISOLATED', dependencies: [ 'set-local-storage-item.fn', ], }); function setLocalStorageItem(key = '', value = '') { setLocalStorageItemFn('local', false, key, value); } builtinScriptlets.push({ name: 'set-session-storage-item.js', fn: setSessionStorageItem, world: 'ISOLATED', dependencies: [ 'set-local-storage-item.fn', ], }); function setSessionStorageItem(key = '', value = '') { setLocalStorageItemFn('session', false, key, value); } /******************************************************************************* * * @scriptlet set-attr * * @description * Sets the specified attribute on the specified elements. This scriptlet runs * once when the page loads then afterward on DOM mutations. * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js * * ### Syntax * * ```text * example.org##+js(set-attr, selector, attr [, value]) * ``` * * - `selector`: CSS selector of DOM elements for which the attribute `attr` * must be modified. * - `attr`: the name of the attribute to modify * - `value`: the value to assign to the target attribute. Possible values: * - `''`: empty string (default) * - `true` * - `false` * - positive decimal integer 0 <= value < 32768 * - `[other]`: copy the value from attribute `other` on the same element * */ builtinScriptlets.push({ name: 'set-attr.js', fn: setAttr, world: 'ISOLATED', dependencies: [ 'run-at.fn', 'safe-self.fn', ], }); function setAttr( selector = '', attr = '', value = '' ) { if ( selector === '' ) { return; } if ( attr === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-attr', attr, value); const validValues = [ '', 'false', 'true' ]; let copyFrom = ''; if ( validValues.includes(value.toLowerCase()) === false ) { if ( /^\d+$/.test(value) ) { const n = parseInt(value, 10); if ( n >= 32768 ) { return; } value = `${n}`; } else if ( /^\[.+\]$/.test(value) ) { copyFrom = value.slice(1, -1); } else { return; } } const extractValue = elem => { if ( copyFrom !== '' ) { return elem.getAttribute(copyFrom) || ''; } return value; }; const applySetAttr = ( ) => { const elems = []; try { elems.push(...document.querySelectorAll(selector)); } catch(ex) { return false; } for ( const elem of elems ) { const before = elem.getAttribute(attr); const after = extractValue(elem); if ( after === before ) { continue; } if ( after !== '' && /^on/i.test(attr) ) { if ( attr.toLowerCase() in elem ) { continue; } } elem.setAttribute(attr, after); safe.uboLog(logPrefix, `${attr}="${after}"`); } return true; }; let observer, timer; const onDomChanged = mutations => { if ( timer !== undefined ) { return; } let shouldWork = false; for ( const mutation of mutations ) { if ( mutation.addedNodes.length === 0 ) { continue; } for ( const node of mutation.addedNodes ) { if ( node.nodeType !== 1 ) { continue; } shouldWork = true; break; } if ( shouldWork ) { break; } } if ( shouldWork === false ) { return; } timer = self.requestAnimationFrame(( ) => { timer = undefined; applySetAttr(); }); }; const start = ( ) => { if ( applySetAttr() === false ) { return; } observer = new MutationObserver(onDomChanged); observer.observe(document.body, { subtree: true, childList: true, }); }; runAt(( ) => { start(); }, 'idle'); } /******************************************************************************* * * @scriptlet prevent-canvas * * @description * Prevent usage of specific or all (default) canvas APIs. * * ### Syntax * * ```text * example.com##+js(prevent-canvas [, contextType]) * ``` * * - `contextType`: A specific type of canvas API to prevent (default to all * APIs). Can be a string or regex which will be matched against the type * used in getContext() call. Prepend with `!` to test for no-match. * * ### Examples * * 1. Prevent `example.com` from accessing all canvas APIs * * ```adblock * example.com##+js(prevent-canvas) * ``` * * 2. Prevent access to any flavor of WebGL API, everywhere * * ```adblock * *##+js(prevent-canvas, /webgl/) * ``` * * 3. Prevent `example.com` from accessing any flavor of canvas API except `2d` * * ```adblock * example.com##+js(prevent-canvas, !2d) * ``` * * ### References * * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext * * */ builtinScriptlets.push({ name: 'prevent-canvas.js', fn: preventCanvas, dependencies: [ 'safe-self.fn', ], }); function preventCanvas( contextType = '' ) { const safe = safeSelf(); const pattern = safe.initPattern(contextType, { canNegate: true }); const proto = globalThis.HTMLCanvasElement.prototype; proto.getContext = new Proxy(proto.getContext, { apply(target, thisArg, args) { if ( safe.testPattern(pattern, args[0]) ) { return null; } return Reflect.apply(target, thisArg, args); } }); } /******************************************************************************/ builtinScriptlets.push({ name: 'multiup.js', fn: multiup, world: 'ISOLATED', }); function multiup() { const handler = ev => { const target = ev.target; if ( target.matches('button[link]') === false ) { return; } const ancestor = target.closest('form'); if ( ancestor === null ) { return; } if ( ancestor !== target.parentElement ) { return; } const link = (target.getAttribute('link') || '').trim(); if ( link === '' ) { return; } ev.preventDefault(); ev.stopPropagation(); document.location.href = link; }; document.addEventListener('click', handler, { capture: true }); } /******************************************************************************/ builtinScriptlets.push({ name: 'remove-cache-storage-item.js', fn: removeCacheStorageItem, world: 'ISOLATED', dependencies: [ 'safe-self.fn', ], }); function removeCacheStorageItem( cacheNamePattern = '', requestPattern = '' ) { if ( cacheNamePattern === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('remove-cache-storage-item', cacheNamePattern, requestPattern); const cacheStorage = self.caches; if ( cacheStorage instanceof Object === false ) { return; } const reCache = safe.patternToRegex(cacheNamePattern, undefined, true); const reRequest = safe.patternToRegex(requestPattern, undefined, true); cacheStorage.keys().then(cacheNames => { for ( const cacheName of cacheNames ) { if ( reCache.test(cacheName) === false ) { continue; } if ( requestPattern === '' ) { cacheStorage.delete(cacheName).then(result => { if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Deleting ${cacheName}`); } if ( result !== true ) { return; } safe.uboLog(logPrefix, `Deleted ${cacheName}: ${result}`); }); continue; } cacheStorage.open(cacheName).then(cache => { cache.keys().then(requests => { for ( const request of requests ) { if ( reRequest.test(request.url) === false ) { continue; } if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Deleting ${cacheName}/${request.url}`); } cache.delete(request).then(result => { if ( result !== true ) { return; } safe.uboLog(logPrefix, `Deleted ${cacheName}/${request.url}: ${result}`); }); } }); }); } }); } /******************************************************************************* * * Scriplets below this section are only available for filter lists from * trusted sources. They all have the property `requiresTrust` set to `true`. * * Trusted sources are: * * - uBO's own filter lists, which name starts with "uBlock filters – ", and * maintained at: https://github.com/uBlockOrigin/uAssets * * - The user's own filters as seen in "My filters" pane in uBO's dashboard. * * The trustworthiness of filters using these privileged scriptlets are * evaluated at filter list compiled time: when a filter using one of the * privileged scriptlet originates from a non-trusted filter list source, it * is discarded at compile time, specifically from within: * * - Source: ./src/js/scriptlet-filtering.js * - Method: scriptletFilteringEngine.compile(), via normalizeRawFilter() * **/ /******************************************************************************* * * replace-node-text.js * * Replace text instance(s) with another text instance inside specific * DOM nodes. By default, the scriplet stops and quits at the interactive * stage of a document. * * See commit messages for usage: * - https://github.com/gorhill/uBlock/commit/99ce027fd702 * - https://github.com/gorhill/uBlock/commit/41876336db48 * **/ builtinScriptlets.push({ name: 'trusted-replace-node-text.js', requiresTrust: true, aliases: [ 'trusted-rpnt.js', 'replace-node-text.js', 'rpnt.js', ], fn: replaceNodeText, world: 'ISOLATED', dependencies: [ 'replace-node-text.fn', ], }); function replaceNodeText( nodeName, pattern, replacement, ...extraArgs ) { replaceNodeTextFn(nodeName, pattern, replacement, ...extraArgs); } /******************************************************************************* * * trusted-set-constant.js * * Set specified property to any value. This is essentially the same as * set-constant.js, but with no restriction as to which values can be used. * **/ builtinScriptlets.push({ name: 'trusted-set-constant.js', requiresTrust: true, aliases: [ 'trusted-set.js', ], fn: trustedSetConstant, dependencies: [ 'set-constant.fn' ], }); function trustedSetConstant( ...args ) { setConstantFn(true, ...args); } /******************************************************************************* * * trusted-set-cookie.js * * Set specified cookie to an arbitrary value. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-cookie.js#L23 * **/ builtinScriptlets.push({ name: 'trusted-set-cookie.js', requiresTrust: true, fn: trustedSetCookie, world: 'ISOLATED', dependencies: [ 'safe-self.fn', 'set-cookie.fn', ], }); function trustedSetCookie( name = '', value = '', offsetExpiresSec = '', path = '' ) { if ( name === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const time = new Date(); if ( value.includes('$now$') ) { value = value.replaceAll('$now$', time.getTime()); } if ( value.includes('$currentDate$') ) { value = value.replaceAll('$currentDate$', time.toUTCString()); } if ( value.includes('$currentISODate$') ) { value = value.replaceAll('$currentISODate$', time.toISOString()); } let expires = ''; if ( offsetExpiresSec !== '' ) { if ( offsetExpiresSec === '1day' ) { time.setDate(time.getDate() + 1); } else if ( offsetExpiresSec === '1year' ) { time.setFullYear(time.getFullYear() + 1); } else { if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; } time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10)); } expires = time.toUTCString(); } const done = setCookieFn( true, name, value, expires, path, safeSelf().getExtraArgs(Array.from(arguments), 4) ); if ( done ) { safe.uboLog(logPrefix, 'Done'); } } // For compatibility with AdGuard builtinScriptlets.push({ name: 'trusted-set-cookie-reload.js', requiresTrust: true, fn: trustedSetCookieReload, world: 'ISOLATED', dependencies: [ 'trusted-set-cookie.js', ], }); function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) { trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args); } /******************************************************************************* * * trusted-set-local-storage-item.js * * Set a local storage entry to an arbitrary value. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-local-storage-item.js * **/ builtinScriptlets.push({ name: 'trusted-set-local-storage-item.js', requiresTrust: true, fn: trustedSetLocalStorageItem, world: 'ISOLATED', dependencies: [ 'set-local-storage-item.fn', ], }); function trustedSetLocalStorageItem(key = '', value = '') { setLocalStorageItemFn('local', true, key, value); } builtinScriptlets.push({ name: 'trusted-set-session-storage-item.js', requiresTrust: true, fn: trustedSetSessionStorageItem, world: 'ISOLATED', dependencies: [ 'set-local-storage-item.fn', ], }); function trustedSetSessionStorageItem(key = '', value = '') { setLocalStorageItemFn('session', true, key, value); } /******************************************************************************* * * trusted-replace-fetch-response.js * * Replaces response text content of fetch requests if all given parameters * match. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-replace-fetch-response.js * **/ builtinScriptlets.push({ name: 'trusted-replace-fetch-response.js', requiresTrust: true, aliases: [ 'trusted-rpfr.js', ], fn: trustedReplaceFetchResponse, dependencies: [ 'replace-fetch-response.fn', ], }); function trustedReplaceFetchResponse(...args) { replaceFetchResponseFn(true, ...args); } /******************************************************************************/ builtinScriptlets.push({ name: 'trusted-replace-xhr-response.js', requiresTrust: true, fn: trustedReplaceXhrResponse, dependencies: [ 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', ], }); function trustedReplaceXhrResponse( pattern = '', replacement = '', propsToMatch = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-replace-xhr-response', pattern, replacement, propsToMatch); const xhrInstances = new WeakMap(); if ( pattern === '*' ) { pattern = '.*'; } const rePattern = safe.patternToRegex(pattern); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const reIncludes = extraArgs.includes ? safe.patternToRegex(extraArgs.includes) : null; self.XMLHttpRequest = class extends self.XMLHttpRequest { open(method, url, ...args) { const outerXhr = this; const xhrDetails = { method, url }; let outcome = 'match'; if ( propNeedles.size !== 0 ) { if ( matchObjectProperties(propNeedles, xhrDetails) === false ) { outcome = 'nomatch'; } } if ( outcome === 'match' ) { if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, `Matched "propsToMatch"`); } xhrInstances.set(outerXhr, xhrDetails); } return super.open(method, url, ...args); } get response() { const innerResponse = super.response; const xhrDetails = xhrInstances.get(this); if ( xhrDetails === undefined ) { return innerResponse; } const responseLength = typeof innerResponse === 'string' ? innerResponse.length : undefined; if ( xhrDetails.lastResponseLength !== responseLength ) { xhrDetails.response = undefined; xhrDetails.lastResponseLength = responseLength; } if ( xhrDetails.response !== undefined ) { return xhrDetails.response; } if ( typeof innerResponse !== 'string' ) { return (xhrDetails.response = innerResponse); } if ( reIncludes && reIncludes.test(innerResponse) === false ) { return (xhrDetails.response = innerResponse); } const textBefore = innerResponse; const textAfter = textBefore.replace(rePattern, replacement); if ( textAfter !== textBefore ) { safe.uboLog(logPrefix, 'Match'); } return (xhrDetails.response = textAfter); } get responseText() { const response = this.response; if ( typeof response !== 'string' ) { return super.responseText; } return response; } }; } /******************************************************************************* * * trusted-click-element.js * * Reference API: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-click-element.js * **/ builtinScriptlets.push({ name: 'trusted-click-element.js', requiresTrust: true, fn: trustedClickElement, world: 'ISOLATED', dependencies: [ 'get-all-cookies.fn', 'get-all-local-storage.fn', 'run-at-html-element.fn', 'safe-self.fn', ], }); function trustedClickElement( selectors = '', extraMatch = '', delay = '' ) { const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-click-element', selectors, extraMatch, delay); if ( extraMatch !== '' ) { const assertions = extraMatch.split(',').map(s => { const pos1 = s.indexOf(':'); const s1 = pos1 !== -1 ? s.slice(0, pos1) : s; const not = s1.startsWith('!'); const type = not ? s1.slice(1) : s1; const s2 = pos1 !== -1 ? s.slice(pos1+1).trim() : ''; if ( s2 === '' ) { return; } const out = { not, type }; const match = /^\/(.+)\/(i?)$/.exec(s2); if ( match !== null ) { out.re = new RegExp(match[1], match[2] || undefined); return out; } const pos2 = s2.indexOf('='); const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2; const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : ''; out.re = new RegExp(`^${this.escapeRegexChars(key)}=${this.escapeRegexChars(value)}`); return out; }).filter(details => details !== undefined); const allCookies = assertions.some(o => o.type === 'cookie') ? getAllCookiesFn() : []; const allStorageItems = assertions.some(o => o.type === 'localStorage') ? getAllLocalStorageFn() : []; const hasNeedle = (haystack, needle) => { for ( const { key, value } of haystack ) { if ( needle.test(`${key}=${value}`) ) { return true; } } return false; }; for ( const { not, type, re } of assertions ) { switch ( type ) { case 'cookie': if ( hasNeedle(allCookies, re) === not ) { return; } break; case 'localStorage': if ( hasNeedle(allStorageItems, re) === not ) { return; } break; } } } const getShadowRoot = elem => { // Firefox if ( elem.openOrClosedShadowRoot ) { return elem.openOrClosedShadowRoot; } // Chromium if ( typeof chrome === 'object' ) { if ( chrome.dom && chrome.dom.openOrClosedShadowRoot ) { return chrome.dom.openOrClosedShadowRoot(elem); } } return null; }; const querySelectorEx = (selector, context = document) => { const pos = selector.indexOf(' >>> '); if ( pos === -1 ) { return context.querySelector(selector); } const outside = selector.slice(0, pos).trim(); const inside = selector.slice(pos + 5).trim(); const elem = context.querySelector(outside); if ( elem === null ) { return null; } const shadowRoot = getShadowRoot(elem); return shadowRoot && querySelectorEx(inside, shadowRoot); }; const selectorList = selectors.split(/\s*,\s*/) .filter(s => { try { void querySelectorEx(s); } catch(_) { return false; } return true; }); if ( selectorList.length === 0 ) { return; } const clickDelay = parseInt(delay, 10) || 1; const t0 = Date.now(); const tbye = t0 + 10000; let tnext = selectorList.length !== 1 ? t0 : t0 + clickDelay; const terminate = ( ) => { selectorList.length = 0; next.stop(); observe.stop(); }; const next = notFound => { if ( selectorList.length === 0 ) { safe.uboLog(logPrefix, 'Completed'); return terminate(); } const tnow = Date.now(); if ( tnow >= tbye ) { safe.uboLog(logPrefix, 'Timed out'); return terminate(); } if ( notFound ) { observe(); } const delay = Math.max(notFound ? tbye - tnow : tnext - tnow, 1); next.timer = setTimeout(( ) => { next.timer = undefined; process(); }, delay); safe.uboLog(logPrefix, `Waiting for ${selectorList[0]}...`); }; next.stop = ( ) => { if ( next.timer === undefined ) { return; } clearTimeout(next.timer); next.timer = undefined; }; const observe = ( ) => { if ( observe.observer !== undefined ) { return; } observe.observer = new MutationObserver(( ) => { if ( observe.timer !== undefined ) { return; } observe.timer = setTimeout(( ) => { observe.timer = undefined; process(); }, 20); }); observe.observer.observe(document, { attributes: true, childList: true, subtree: true, }); }; observe.stop = ( ) => { if ( observe.timer !== undefined ) { clearTimeout(observe.timer); observe.timer = undefined; } if ( observe.observer ) { observe.observer.disconnect(); observe.observer = undefined; } }; const process = ( ) => { next.stop(); if ( Date.now() < tnext ) { return next(); } const selector = selectorList.shift(); if ( selector === undefined ) { return terminate(); } const elem = querySelectorEx(selector); if ( elem === null ) { selectorList.unshift(selector); return next(true); } safe.uboLog(logPrefix, `Clicked ${selector}`); elem.click(); tnext += clickDelay; next(); }; runAtHtmlElementFn(process); } /******************************************************************************/ builtinScriptlets.push({ name: 'trusted-prune-inbound-object.js', requiresTrust: true, fn: trustedPruneInboundObject, dependencies: [ 'object-find-owner.fn', 'object-prune.fn', 'safe-self.fn', ], }); function trustedPruneInboundObject( entryPoint = '', argPos = '', rawPrunePaths = '', rawNeedlePaths = '' ) { if ( entryPoint === '' ) { return; } let context = globalThis; let prop = entryPoint; for (;;) { const pos = prop.indexOf('.'); if ( pos === -1 ) { break; } context = context[prop.slice(0, pos)]; if ( context instanceof Object === false ) { return; } prop = prop.slice(pos+1); } if ( typeof context[prop] !== 'function' ) { return; } const argIndex = parseInt(argPos); if ( isNaN(argIndex) ) { return; } if ( argIndex < 1 ) { return; } const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); const needlePaths = []; if ( rawPrunePaths !== '' ) { needlePaths.push(...rawPrunePaths.split(/ +/)); } if ( rawNeedlePaths !== '' ) { needlePaths.push(...rawNeedlePaths.split(/ +/)); } const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); const mustProcess = root => { for ( const needlePath of needlePaths ) { if ( objectFindOwnerFn(root, needlePath) === false ) { return false; } } return true; }; context[prop] = new Proxy(context[prop], { apply: function(target, thisArg, args) { const targetArg = argIndex <= args.length ? args[argIndex-1] : undefined; if ( targetArg instanceof Object && mustProcess(targetArg) ) { let objBefore = targetArg; if ( extraArgs.dontOverwrite ) { try { objBefore = safe.JSON_parse(safe.JSON_stringify(targetArg)); } catch(_) { objBefore = undefined; } } if ( objBefore !== undefined ) { const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedle, extraArgs ); args[argIndex-1] = objAfter || objBefore; } } return Reflect.apply(target, thisArg, args); }, }); } /******************************************************************************/ builtinScriptlets.push({ name: 'trusted-prune-outbound-object.js', requiresTrust: true, fn: trustedPruneOutboundObject, dependencies: [ 'object-prune.fn', 'proxy-apply.fn', 'safe-self.fn', ], }); function trustedPruneOutboundObject( propChain = '', rawPrunePaths = '', rawNeedlePaths = '' ) { if ( propChain === '' ) { return; } const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); proxyApplyFn(propChain, function(context) { const objBefore = context.reflect(); if ( objBefore instanceof Object === false ) { return objBefore; } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, { matchAll: true }, extraArgs ); return objAfter || objBefore; }); } /******************************************************************************/ builtinScriptlets.push({ name: 'trusted-replace-argument.js', requiresTrust: true, fn: trustedReplaceArgument, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', 'validate-constant.fn', ], }); function trustedReplaceArgument( propChain = '', argposRaw = '', argraw = '' ) { if ( propChain === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw); const argoffset = parseInt(argposRaw, 10) || 0; const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const normalValue = validateConstantFn(true, argraw, extraArgs); const reCondition = extraArgs.condition ? safe.patternToRegex(extraArgs.condition) : /^/; proxyApplyFn(propChain, function(context) { const { callArgs } = context; if ( argposRaw === '' ) { safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`); return context.reflect(); } const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset; if ( argpos >= 0 && argpos < callArgs.length ) { const argBefore = callArgs[argpos]; if ( safe.RegExp_test.call(reCondition, argBefore) ) { callArgs[argpos] = normalValue; safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${normalValue}`); } } return context.reflect(); }); } /******************************************************************************/ builtinScriptlets.push({ name: 'trusted-replace-outbound-text.js', requiresTrust: true, fn: trustedReplaceOutboundText, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function trustedReplaceOutboundText( propChain = '', rawPattern = '', rawReplacement = '', ...args ) { if ( propChain === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-replace-outbound-text', propChain, rawPattern, rawReplacement, ...args); const rePattern = safe.patternToRegex(rawPattern); const replacement = rawReplacement.startsWith('json:') ? safe.JSON_parse(rawReplacement.slice(5)) : rawReplacement; const extraArgs = safe.getExtraArgs(args); const reCondition = safe.patternToRegex(extraArgs.condition || ''); proxyApplyFn(propChain, function(context) { const encodedTextBefore = context.reflect(); let textBefore = encodedTextBefore; if ( extraArgs.encoding === 'base64' ) { try { textBefore = self.atob(encodedTextBefore); } catch(ex) { return encodedTextBefore; } } if ( rawPattern === '' ) { safe.uboLog(logPrefix, 'Decoded outbound text:\n', textBefore); return encodedTextBefore; } reCondition.lastIndex = 0; if ( reCondition.test(textBefore) === false ) { return encodedTextBefore; } const textAfter = textBefore.replace(rePattern, replacement); if ( textAfter === textBefore ) { return encodedTextBefore; } safe.uboLog(logPrefix, 'Matched and replaced'); if ( safe.logLevel > 1 ) { safe.uboLog(logPrefix, 'Modified decoded outbound text:\n', textAfter); } let encodedTextAfter = textAfter; if ( extraArgs.encoding === 'base64' ) { encodedTextAfter = self.btoa(textAfter); } return encodedTextAfter; }); } /******************************************************************************* * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/5a92d79489/wiki/about-trusted-scriptlets.md#trusted-suppress-native-method * * This is a first version with current limitations: * - Does not support matching arguments which are object or array * - Does not support `stack` parameter * * If `signatureStr` parameter is not declared, the scriptlet will log all calls * to `methodPath` along with the arguments passed and will not prevent the * trapped method. * * */ builtinScriptlets.push({ name: 'trusted-suppress-native-method.js', requiresTrust: true, fn: trustedSuppressNativeMethod, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function trustedSuppressNativeMethod( methodPath = '', signature = '', how = '', stack = '' ) { if ( methodPath === '' ) { return; } if ( stack !== '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-suppress-native-method', methodPath, signature, how); const signatureArgs = signature.split(/\s*\|\s*/).map(v => { if ( /^".*"$/.test(v) ) { return { type: 'pattern', re: safe.patternToRegex(v.slice(1, -1)) }; } if ( v === 'false' ) { return { type: 'exact', value: false }; } if ( v === 'true' ) { return { type: 'exact', value: true }; } if ( v === 'null' ) { return { type: 'exact', value: null }; } if ( v === 'undefined' ) { return { type: 'exact', value: undefined }; } }); proxyApplyFn(methodPath, function(context) { const { callArgs } = context; if ( signature === '' ) { safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`); return context.reflect(); } if ( callArgs.length < signatureArgs.length ) { return context.reflect(); } for ( let i = 0; i < signatureArgs.length; i++ ) { const signatureArg = signatureArgs[i]; if ( signatureArg === undefined ) { continue; } const targetArg = callArgs[i]; if ( signatureArg.type === 'exact' ) { if ( targetArg !== signatureArg.value ) { return context.reflect(); } } if ( signatureArg.type === 'pattern' ) { if ( safe.RegExp_test.call(signatureArg.re, targetArg) === false ) { return context.reflect(); } } } safe.uboLog(logPrefix, `Suppressed:\n${callArgs.join('\n')}`); if ( how === 'abort' ) { throw new ReferenceError(); } }); } /******************************************************************************* * * Trusted version of prevent-xhr(), which allows the use of an arbitrary * string as response text. * * */ builtinScriptlets.push({ name: 'trusted-prevent-xhr.js', requiresTrust: true, fn: trustedPreventXhr, dependencies: [ 'prevent-xhr.fn', ], }); function trustedPreventXhr(...args) { return preventXhrFn(true, ...args); } /** * @trustedScriptlet trusted-prevent-dom-bypass * * @description * Prevent the bypassing of uBO scriptlets through anonymous embedded context. * * Ensure that a target method in the embedded context is using the * corresponding parent context's method (which is assumed to be * properly patched), or to replace the embedded context with that of the * parent context. * * Root issue: * https://issues.chromium.org/issues/40202434 * * @param methodPath * The method which calls must be intercepted. The arguments * of the intercepted calls are assumed to be HTMLElement, anything else will * be ignored. * * @param [targetProp] * The method in the embedded context which should be delegated to the * parent context. If no method is specified, the embedded context becomes * the parent one, i.e. all properties of the embedded context will be that * of the parent context. * * @example * ##+js(trusted-prevent-dom-bypass, Element.prototype.append, open) * * @example * ##+js(trusted-prevent-dom-bypass, Element.prototype.appendChild, XMLHttpRequest) * * */ builtinScriptlets.push({ name: 'trusted-prevent-dom-bypass.js', requiresTrust: true, fn: trustedPreventDomBypass, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', ], }); function trustedPreventDomBypass( methodPath = '', targetProp = '' ) { if ( methodPath === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-prevent-dom-bypass', methodPath, targetProp); proxyApplyFn(methodPath, function(context) { const elems = new Set(context.callArgs.filter(e => e instanceof HTMLElement)); const r = context.reflect(); if ( elems.length === 0 ) { return r; } for ( const elem of elems ) { try { if ( `${elem.contentWindow}` !== '[object Window]' ) { continue; } if ( elem.contentWindow.location.href !== 'about:blank' ) { if ( elem.contentWindow.location.href !== self.location.href ) { continue; } } if ( targetProp !== '' ) { elem.contentWindow[targetProp] = self[targetProp]; } else { Object.defineProperty(elem, 'contentWindow', { value: self }); } safe.uboLog(logPrefix, 'Bypass prevented'); } catch(_) { } } return r; }); } /** * @trustedScriptlet trusted-override-element-method * * @description * Override the behavior of a method on matching elements. * * @param methodPath * The method which calls must be intercepted. * * @param [selector] * A CSS selector which the target element must match. If not specified, * the override will occur for all elements. * * @param [disposition] * How the override should be handled. If not specified, the overridden call * will be equivalent to an empty function. If set to `throw`, an exception * will be thrown. Any other value will be validated and returned as a * supported safe constant. * * @example * ##+js(trusted-override-element-method, HTMLAnchorElement.prototype.click, a[target="_blank"][style]) * * */ builtinScriptlets.push({ name: 'trusted-override-element-method.js', requiresTrust: true, fn: trustedOverrideElementMethod, dependencies: [ 'proxy-apply.fn', 'safe-self.fn', 'validate-constant.fn', ], }); function trustedOverrideElementMethod( methodPath = '', selector = '', disposition = '' ) { if ( methodPath === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('trusted-override-element-method', methodPath, selector, disposition); proxyApplyFn(methodPath, function(context) { let override = selector === ''; if ( override === false ) { const { thisArg } = context; try { override = thisArg.closest(selector) === thisArg; } catch(_) { } } if ( override === false ) { return context.reflect(); } safe.uboLog(logPrefix, 'Overridden'); if ( disposition === '' ) { return; } if ( disposition === 'debug' && safe.logLevel !== 0 ) { debugger; // eslint-disable-line no-debugger } if ( disposition === 'throw' ) { throw new ReferenceError(); } return validateConstantFn(false, disposition); }); } /******************************************************************************/