diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js | 2594 |
1 files changed, 2594 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js b/data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js new file mode 100644 index 0000000..071b605 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js @@ -0,0 +1,2594 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import './attribute.js'; +import './create-html.js'; +import './href-sanitizer.js'; +import './json-edit.js'; +import './json-prune.js'; +import './noeval.js'; +import './object-prune.js'; +import './prevent-fetch.js'; +import './prevent-innerHTML.js'; +import './prevent-settimeout.js'; +import './replace-argument.js'; +import './spoof-css.js'; + +import { + generateContentFn, + getExceptionTokenFn, + getRandomTokenFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, +} from './utils.js'; +import { runAt, runAtHtmlElementFn } from './run-at.js'; + +import { getAllCookiesFn } from './cookie.js'; +import { getAllLocalStorageFn } from './localstorage.js'; +import { matchesStackTraceFn } from './stack-trace.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registeredScriptlets } from './base.js'; +import { safeSelf } from './safe-self.js'; +import { validateConstantFn } from './set-constant.js'; + +// Externally added to the private namespace in which scriptlets execute. +/* global scriptletGlobals */ + +/* eslint no-prototype-builtins: 0 */ + +export const builtinScriptlets = registeredScriptlets; + +/******************************************************************************* + + Helper functions + + These are meant to be used as dependencies to injectable scriptlets. + +*******************************************************************************/ + +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: '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 = safe.String_split.call(target, '.'); + 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 = getExceptionTokenFn(); + 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 { + } + 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: '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(getRandomTokenFn(), 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: '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 = parsePropertiesToMatchFn(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; } + 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]); + } + const matched = matchObjectPropertiesFn(propNeedles, ...objs); + if ( matched === undefined ) { return fetchPromise; } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + } + 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); + if ( textAfter === textBefore ) { 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: '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 = parsePropertiesToMatchFn(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 ( matchObjectPropertiesFn(propNeedles, haystack) ) { + const xhrDetails = Object.assign(haystack, { + xhr: this, + defer: args.length === 0 || !!args[0], + directive, + headers: { + 'date': '', + 'content-type': '', + 'content-length': '', + }, + url: haystack.url, + props: { + response: { value: '' }, + responseText: { value: '' }, + responseXML: { value: null }, + }, + }); + 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 }, + responseURL: { value: xhrDetails.url }, + 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 }, + responseURL: { value: xhrDetails.url }, + }); + 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 = getExceptionTokenFn(); + 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 = getExceptionTokenFn(); + 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: 'addEventListener-defuser.js', + aliases: [ + 'aeld.js', + 'prevent-addEventListener.js', + ], + fn: addEventListenerDefuser, + dependencies: [ + 'proxy-apply.fn', + 'run-at.fn', + 'safe-self.fn', + 'should-debug.fn', + ], +}); +// https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120 +function addEventListenerDefuser( + type = '', + pattern = '' +) { + const safe = safeSelf(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern); + const reType = safe.patternToRegex(type, undefined, true); + const rePattern = safe.patternToRegex(pattern); + const debug = shouldDebug(extraArgs); + const targetSelector = extraArgs.elements || undefined; + const elementMatches = elem => { + if ( targetSelector === 'window' ) { return elem === window; } + if ( targetSelector === 'document' ) { return elem === document; } + if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; } + const elems = Array.from(document.querySelectorAll(targetSelector)); + return elems.includes(elem); + }; + const elementDetails = elem => { + if ( elem instanceof Window ) { return 'window'; } + if ( elem instanceof Document ) { return 'document'; } + if ( elem instanceof Element === false ) { return '?'; } + const parts = []; + // https://github.com/uBlockOrigin/uAssets/discussions/17907#discussioncomment-9871079 + const id = String(elem.id); + if ( id !== '' ) { parts.push(`#${CSS.escape(id)}`); } + for ( let i = 0; i < elem.classList.length; i++ ) { + parts.push(`.${CSS.escape(elem.classList.item(i))}`); + } + for ( let i = 0; i < elem.attributes.length; i++ ) { + const attr = elem.attributes.item(i); + if ( attr.name === 'id' ) { continue; } + if ( attr.name === 'class' ) { continue; } + parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`); + } + return parts.join(''); + }; + const shouldPrevent = (thisArg, type, handler) => { + const matchesType = safe.RegExp_test.call(reType, type); + const matchesHandler = safe.RegExp_test.call(rePattern, handler); + const matchesEither = matchesType || matchesHandler; + const matchesBoth = matchesType && matchesHandler; + if ( debug === 1 && matchesBoth || debug === 2 && matchesEither ) { + debugger; // eslint-disable-line no-debugger + } + if ( matchesBoth && targetSelector !== undefined ) { + if ( elementMatches(thisArg) === false ) { return false; } + } + return matchesBoth; + }; + const proxyFn = function(context) { + const { callArgs, thisArg } = context; + let t, h; + try { + t = String(callArgs[0]); + if ( typeof callArgs[1] === 'function' ) { + h = String(safe.Function_toString(callArgs[1])); + } else if ( typeof callArgs[1] === 'object' && callArgs[1] !== null ) { + if ( typeof callArgs[1].handleEvent === 'function' ) { + h = String(safe.Function_toString(callArgs[1].handleEvent)); + } + } else { + h = String(callArgs[1]); + } + } catch { + } + if ( type === '' && pattern === '' ) { + safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`); + } else if ( shouldPrevent(thisArg, t, h) ) { + return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`); + } + return context.reflect(); + }; + runAt(( ) => { + proxyApplyFn('EventTarget.prototype.addEventListener', proxyFn); + proxyApplyFn('document.addEventListener', proxyFn); + }, extraArgs.runAt); +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'adjust-setInterval.js', + aliases: [ + '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: 'prevent-refresh.js', + aliases: [ + 'refresh-defuser.js', + ], + fn: preventRefresh, + world: 'ISOLATED', + dependencies: [ + 'safe-self.fn', + ], +}); +// https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/ +function preventRefresh( + delay = '' +) { + if ( typeof delay !== 'string' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-refresh', delay); + const stop = content => { + window.stop(); + safe.uboLog(logPrefix, `Prevented "${content}"`); + }; + const defuse = ( ) => { + const meta = document.querySelector('meta[http-equiv="refresh" i][content]'); + if ( meta === null ) { return; } + const content = meta.getAttribute('content') || ''; + const ms = delay === '' + ? Math.max(parseFloat(content) || 0, 0) * 500 + : 0; + if ( ms === 0 ) { + stop(content); + } else { + setTimeout(( ) => { stop(content); }, ms); + } + }; + self.addEventListener('load', defuse, { capture: true, once: true }); +} + +/******************************************************************************/ + +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 = safe.String_split.call(rawToken, /\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 { + } + 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: '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(allFrames) { + if ( allFrames === '' && 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', ev => { + let target = ev.target; + while ( target !== null ) { + if ( target.localName === 'a' && target.hasAttribute('target') ) { + ev.stopPropagation(); + ev.preventDefault(); + break; + } + target = target.parentNode; + } + }, { capture: 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*</.test(text) && />\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 { + } + 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', ...safe.String_split.call(discard, /\n+/).map(s => `\t${s}`)); + if ( reM3u.global === false ) { break; } + } + return text; + } + const lines = safe.String_split.call(text, /\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 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, + dependencies: [ + 'safe-self.fn', + ], +}); +function callNothrow( + chain = '' +) { + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } + const safe = safeSelf(); + const parts = safe.String_split.call(chain, '.'); + 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 { + } + return r; + }, + }); +} + +/******************************************************************************/ + +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); +} + +/******************************************************************************* + * + * @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 }); +} + + + +/******************************************************************************* + * + * 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-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 = parsePropertiesToMatchFn(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 ( matchObjectPropertiesFn(propNeedles, xhrDetails) === undefined ) { + 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 = safe.String_split.call(extraMatch, ',').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 = safe.String_split.call(selectors, /\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-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 { 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: [ + 'matches-stack-trace.fn', + 'proxy-apply.fn', + 'safe-self.fn', + ], +}); +function trustedSuppressNativeMethod( + methodPath = '', + signature = '', + how = '', + stack = '' +) { + if ( methodPath === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-suppress-native-method', methodPath, signature, how, stack); + const signatureArgs = safe.String_split.call(signature, /\s*\|\s*/).map(v => { + if ( /^".*"$/.test(v) ) { + return { type: 'pattern', re: safe.patternToRegex(v.slice(1, -1)) }; + } + if ( /^\/.+\/$/.test(v) ) { + return { type: 'pattern', re: safe.patternToRegex(v) }; + } + 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 }; + } + }); + const stackNeedle = safe.initPattern(stack, { canNegate: true }); + proxyApplyFn(methodPath, function(context) { + const { callArgs } = context; + if ( signature === '' ) { + safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`); + return context.reflect(); + } + for ( let i = 0; i < signatureArgs.length; i++ ) { + const signatureArg = signatureArgs[i]; + if ( signatureArg === undefined ) { continue; } + const targetArg = i < callArgs.length ? callArgs[i] : undefined; + 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(); + } + } + } + if ( stackNeedle.matchAll !== true ) { + const logLevel = safe.logLevel > 1 ? 'all' : ''; + if ( matchesStackTraceFn(stackNeedle, logLevel) === false ) { + return context.reflect(); + } + } + if ( how === 'debug' ) { + debugger; // eslint-disable-line no-debugger + 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 !== '' ) { + let me = self, it = elem.contentWindow; + let chain = targetProp; + for (;;) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { break; } + const prop = chain.slice(0, pos); + me = me[prop]; it = it[prop]; + chain = chain.slice(pos+1); + } + it[chain] = me[chain]; + } 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); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + 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(true, disposition, extraArgs); + }); +} + +/******************************************************************************/ |