/******************************************************************************* 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*\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); }); } /******************************************************************************/