diff options
author | awy <awy@awy.one> | 2025-08-15 03:01:21 +0300 |
---|---|---|
committer | awy <awy@awy.one> | 2025-08-15 03:01:21 +0300 |
commit | a9370a08517668b3e98cc1d0bd42df407a76c220 (patch) | |
tree | 37e7bdb0e76f5495f798e077e45d377c0c3870c0 /data/extensions/uBlock0@raymondhill.net/js/resources | |
parent | b73acfe395ea849fcd15c9886a7f4631f2b6f82b (diff) |
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/resources')
24 files changed, 7574 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/attribute.js b/data/extensions/uBlock0@raymondhill.net/js/resources/attribute.js new file mode 100644 index 0000000..3b33a50 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/attribute.js @@ -0,0 +1,305 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function setAttrFn( + trusted = false, + logPrefix, + selector = '', + attr = '', + value = '' +) { + if ( selector === '' ) { return; } + if ( attr === '' ) { return; } + + const safe = safeSelf(); + const copyFrom = trusted === false && /^\[.+\]$/.test(value) + ? value.slice(1, -1) + : ''; + + const extractValue = elem => copyFrom !== '' + ? elem.getAttribute(copyFrom) || '' + : value; + + const applySetAttr = ( ) => { + let elems; + try { + elems = document.querySelectorAll(selector); + } catch { + return false; + } + for ( const elem of elems ) { + const before = elem.getAttribute(attr); + const after = extractValue(elem); + if ( after === before ) { continue; } + if ( after !== '' && /^on/i.test(attr) ) { + if ( attr.toLowerCase() in elem ) { continue; } + } + elem.setAttribute(attr, after); + safe.uboLog(logPrefix, `${attr}="${after}"`); + } + return true; + }; + + let observer, timer; + const onDomChanged = mutations => { + if ( timer !== undefined ) { return; } + let shouldWork = false; + for ( const mutation of mutations ) { + if ( mutation.addedNodes.length === 0 ) { continue; } + for ( const node of mutation.addedNodes ) { + if ( node.nodeType !== 1 ) { continue; } + shouldWork = true; + break; + } + if ( shouldWork ) { break; } + } + if ( shouldWork === false ) { return; } + timer = self.requestAnimationFrame(( ) => { + timer = undefined; + applySetAttr(); + }); + }; + + const start = ( ) => { + if ( applySetAttr() === false ) { return; } + observer = new MutationObserver(onDomChanged); + observer.observe(document.body, { + subtree: true, + childList: true, + }); + }; + runAt(( ) => { start(); }, 'idle'); +} +registerScriptlet(setAttrFn, { + name: 'set-attr.fn', + dependencies: [ + runAt, + safeSelf, + ], +}); + +/** + * @scriptlet set-attr + * + * @description + * Sets the specified attribute on the specified elements. This scriptlet runs + * once when the page loads then afterward on DOM mutations. + * + * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js + * + * @param selector + * A CSS selector for the elements to target. + * + * @param attr + * The name of the attribute to modify. + * + * @param value + * The new value of the attribute. Supported values: + * - `''`: empty string (default) + * - `true` + * - `false` + * - positive decimal integer 0 <= value < 32768 + * - `[other]`: copy the value from attribute `other` on the same element + * + * */ + +export function setAttr( + selector = '', + attr = '', + value = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-attr', selector, attr, value); + const validValues = [ '', 'false', 'true' ]; + + if ( validValues.includes(value.toLowerCase()) === false ) { + if ( /^\d+$/.test(value) ) { + const n = parseInt(value, 10); + if ( n >= 32768 ) { return; } + value = `${n}`; + } else if ( /^\[.+\]$/.test(value) === false ) { + return; + } + } + + setAttrFn(false, logPrefix, selector, attr, value); +} +registerScriptlet(setAttr, { + name: 'set-attr.js', + dependencies: [ + safeSelf, + setAttrFn, + ], + world: 'ISOLATED', +}); + +/** + * @trustedScriptlet trusted-set-attr + * + * @description + * Sets the specified attribute on the specified elements. This scriptlet runs + * once when the page loads then afterward on DOM mutations. + * + * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#-%EF%B8%8F-trusted-set-attr + * + * @param selector + * A CSS selector for the elements to target. + * + * @param attr + * The name of the attribute to modify. + * + * @param value + * The new value of the attribute. Since the scriptlet requires a trusted + * source, the value can be anything. + * + * */ + +export function trustedSetAttr( + selector = '', + attr = '', + value = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-set-attr', selector, attr, value); + setAttrFn(true, logPrefix, selector, attr, value); +} +registerScriptlet(trustedSetAttr, { + name: 'trusted-set-attr.js', + requiresTrust: true, + dependencies: [ + safeSelf, + setAttrFn, + ], + world: 'ISOLATED', +}); + +/** + * @scriptlet remove-attr + * + * @description + * Remove one or more attributes from a set of elements. + * + * @param attribute + * The name of the attribute(s) to remove. This can be a list of space- + * separated attribute names. + * + * @param [selector] + * Optional. A CSS selector for the elements to target. Default to + * `[attribute]`, or `[attribute1],[attribute2],...` if more than one + * attribute name is specified. + * + * @param [behavior] + * Optional. Space-separated tokens which modify the default behavior. + * - `asap`: Try to remove the attribute as soon as possible. Default behavior + * is to remove the attribute(s) asynchronously. + * - `stay`: Keep trying to remove the specified attribute(s) on DOM mutations. + * */ + +export function removeAttr( + rawToken = '', + rawSelector = '', + behavior = '' +) { + if ( typeof rawToken !== 'string' ) { return; } + if ( rawToken === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('remove-attr', rawToken, rawSelector, behavior); + const tokens = 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 asap = /\basap\b/.test(behavior); + let timerId; + const rmattrAsync = ( ) => { + if ( timerId !== undefined ) { return; } + timerId = safe.onIdle(( ) => { + timerId = undefined; + rmattr(); + }, { timeout: 17 }); + }; + const rmattr = ( ) => { + if ( timerId !== undefined ) { + safe.offIdle(timerId); + timerId = undefined; + } + try { + const nodes = document.querySelectorAll(selector); + for ( const node of nodes ) { + for ( const attr of tokens ) { + if ( node.hasAttribute(attr) === false ) { continue; } + node.removeAttribute(attr); + safe.uboLog(logPrefix, `Removed attribute '${attr}'`); + } + } + } catch { + } + }; + const mutationHandler = mutations => { + if ( timerId !== undefined ) { return; } + let skip = true; + for ( let i = 0; i < mutations.length && skip; i++ ) { + const { type, addedNodes, removedNodes } = mutations[i]; + if ( type === 'attributes' ) { skip = false; } + for ( let j = 0; j < addedNodes.length && skip; j++ ) { + if ( addedNodes[j].nodeType === 1 ) { skip = false; break; } + } + for ( let j = 0; j < removedNodes.length && skip; j++ ) { + if ( removedNodes[j].nodeType === 1 ) { skip = false; break; } + } + } + if ( skip ) { return; } + asap ? rmattr() : rmattrAsync(); + }; + const start = ( ) => { + rmattr(); + if ( /\bstay\b/.test(behavior) === false ) { return; } + const observer = new MutationObserver(mutationHandler); + observer.observe(document, { + attributes: true, + attributeFilter: tokens, + childList: true, + subtree: true, + }); + }; + runAt(( ) => { start(); }, safe.String_split.call(behavior, /\s+/)); +} +registerScriptlet(removeAttr, { + name: 'remove-attr.js', + aliases: [ + 'ra.js', + ], + dependencies: [ + runAt, + safeSelf, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/base.js b/data/extensions/uBlock0@raymondhill.net/js/resources/base.js new file mode 100644 index 0000000..2c54418 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/base.js @@ -0,0 +1,38 @@ +/******************************************************************************* + + 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 + +*/ + +export const registeredScriptlets = []; + +export const registerScriptlet = (fn, details) => { + if ( typeof details !== 'object' ) { + throw new ReferenceError('Missing scriptlet details'); + } + details.fn = fn; + fn.details = details; + if ( Array.isArray(details.dependencies) ) { + details.dependencies.forEach((fn, i, array) => { + if ( typeof fn !== 'function' ) { return; } + array[i] = fn.details.name; + }); + } + registeredScriptlets.push(details); +}; diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/cookie.js b/data/extensions/uBlock0@raymondhill.net/js/resources/cookie.js new file mode 100644 index 0000000..21300bc --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/cookie.js @@ -0,0 +1,441 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function getSafeCookieValuesFn() { + return [ + 'accept', 'reject', + 'accepted', 'rejected', 'notaccepted', + 'allow', 'disallow', 'deny', + 'allowed', 'denied', + 'approved', 'disapproved', + 'checked', 'unchecked', + 'dismiss', 'dismissed', + 'enable', 'disable', + 'enabled', 'disabled', + 'essential', 'nonessential', + 'forbidden', 'forever', + 'hide', 'hidden', + 'necessary', 'required', + 'ok', + 'on', 'off', + 'true', 't', 'false', 'f', + 'yes', 'y', 'no', 'n', + 'all', 'none', 'functional', + 'granted', 'done', + 'decline', 'declined', + 'closed', 'next', 'mandatory', + 'disagree', 'agree', + ]; +} +registerScriptlet(getSafeCookieValuesFn, { + name: 'get-safe-cookie-values.fn', +}); + +/******************************************************************************/ + +export function getAllCookiesFn() { + const safe = safeSelf(); + return safe.String_split.call(document.cookie, /\s*;\s*/).map(s => { + const pos = s.indexOf('='); + if ( pos === 0 ) { return; } + if ( pos === -1 ) { return `${s.trim()}=`; } + const key = s.slice(0, pos).trim(); + const value = s.slice(pos+1).trim(); + return { key, value }; + }).filter(s => s !== undefined); +} +registerScriptlet(getAllCookiesFn, { + name: 'get-all-cookies.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function getCookieFn( + name = '' +) { + const safe = safeSelf(); + for ( const s of safe.String_split.call(document.cookie, /\s*;\s*/) ) { + const pos = s.indexOf('='); + if ( pos === -1 ) { continue; } + if ( s.slice(0, pos) !== name ) { continue; } + return s.slice(pos+1).trim(); + } +} +registerScriptlet(getCookieFn, { + name: 'get-cookie.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function setCookieFn( + trusted = false, + name = '', + value = '', + expires = '', + path = '', + options = {}, +) { + // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 + // https://github.com/uBlockOrigin/uBlock-issues/issues/2777 + if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) { + name = encodeURIComponent(name); + } + // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 + // The characters [",] are given a pass from the RFC requirements because + // apparently browsers do not follow the RFC to the letter. + if ( /[^ -:<-[\]-~]/.test(value) ) { + value = encodeURIComponent(value); + } + + const cookieBefore = getCookieFn(name); + if ( cookieBefore !== undefined && options.dontOverwrite ) { return; } + if ( cookieBefore === value && options.reload ) { return; } + + const cookieParts = [ name, '=', value ]; + if ( expires !== '' ) { + cookieParts.push('; expires=', expires); + } + + if ( path === '' ) { path = '/'; } + else if ( path === 'none' ) { path = ''; } + if ( path !== '' && path !== '/' ) { return; } + if ( path === '/' ) { + cookieParts.push('; path=/'); + } + + if ( trusted ) { + if ( options.domain ) { + let domain = options.domain; + if ( /^\/.+\//.test(domain) ) { + const baseURL = new URL(document.baseURI); + const reDomain = new RegExp(domain.slice(1, -1)); + const match = reDomain.exec(baseURL.hostname); + domain = match ? match[0] : undefined; + } + if ( domain ) { + cookieParts.push(`; domain=${domain}`); + } + } + cookieParts.push('; Secure'); + } else if ( /^__(Host|Secure)-/.test(name) ) { + cookieParts.push('; Secure'); + } + + try { + document.cookie = cookieParts.join(''); + } catch { + } + + const done = getCookieFn(name) === value; + if ( done && options.reload ) { + window.location.reload(); + } + + return done; +} +registerScriptlet(setCookieFn, { + name: 'set-cookie.fn', + dependencies: [ + getCookieFn, + ], +}); + +/** + * @scriptlet set-cookie + * + * @description + * Set a cookie to a safe value. + * + * @param name + * The name of the cookie to set. + * + * @param value + * The value of the cookie to set. Must be a safe value. Unsafe values will be + * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above. + * + * @param [path] + * Optional. The path of the cookie to set. Default to `/`. + * + * Reference: + * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js + * */ + +export function setCookie( + name = '', + value = '', + path = '' +) { + if ( name === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); + const normalized = value.toLowerCase(); + const match = /^("?)(.+)\1$/.exec(normalized); + const unquoted = match && match[2] || normalized; + const validValues = getSafeCookieValuesFn(); + if ( validValues.includes(unquoted) === false ) { + if ( /^-?\d+$/.test(unquoted) === false ) { return; } + const n = parseInt(value, 10) || 0; + if ( n < -32767 || n > 32767 ) { return; } + } + + const done = setCookieFn( + false, + name, + value, + '', + path, + safe.getExtraArgs(Array.from(arguments), 3) + ); + + if ( done ) { + safe.uboLog(logPrefix, 'Done'); + } +} +registerScriptlet(setCookie, { + name: 'set-cookie.js', + world: 'ISOLATED', + dependencies: [ + getSafeCookieValuesFn, + safeSelf, + setCookieFn, + ], +}); + +// For compatibility with AdGuard +export function setCookieReload(name, value, path, ...args) { + setCookie(name, value, path, 'reload', '1', ...args); +} +registerScriptlet(setCookieReload, { + name: 'set-cookie-reload.js', + world: 'ISOLATED', + dependencies: [ + setCookie, + ], +}); + +/** + * @trustedScriptlet trusted-set-cookie + * + * @description + * Set a cookie to any value. This scriptlet can be used only from a trusted + * source. + * + * @param name + * The name of the cookie to set. + * + * @param value + * The value of the cookie to set. Must be a safe value. Unsafe values will be + * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above. + * + * @param [offsetExpiresSec] + * Optional. The path of the cookie to set. Default to `/`. + * + * @param [path] + * Optional. The path of the cookie to set. Default to `/`. + * + * Reference: + * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js + * */ + +export function trustedSetCookie( + name = '', + value = '', + offsetExpiresSec = '', + path = '' +) { + if ( name === '' ) { return; } + + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); + const time = new Date(); + + if ( value.includes('$now$') ) { + value = value.replaceAll('$now$', time.getTime()); + } + if ( value.includes('$currentDate$') ) { + value = value.replaceAll('$currentDate$', time.toUTCString()); + } + if ( value.includes('$currentISODate$') ) { + value = value.replaceAll('$currentISODate$', time.toISOString()); + } + + let expires = ''; + if ( offsetExpiresSec !== '' ) { + if ( offsetExpiresSec === '1day' ) { + time.setDate(time.getDate() + 1); + } else if ( offsetExpiresSec === '1year' ) { + time.setFullYear(time.getFullYear() + 1); + } else { + if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; } + time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10)); + } + expires = time.toUTCString(); + } + + const done = setCookieFn( + true, + name, + value, + expires, + path, + safeSelf().getExtraArgs(Array.from(arguments), 4) + ); + + if ( done ) { + safe.uboLog(logPrefix, 'Done'); + } +} +registerScriptlet(trustedSetCookie, { + name: 'trusted-set-cookie.js', + requiresTrust: true, + world: 'ISOLATED', + dependencies: [ + safeSelf, + setCookieFn, + ], +}); + +// For compatibility with AdGuard +export function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) { + trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args); +} +registerScriptlet(trustedSetCookieReload, { + name: 'trusted-set-cookie-reload.js', + requiresTrust: true, + world: 'ISOLATED', + dependencies: [ + trustedSetCookie, + ], +}); + +/** + * @scriptlet remove-cookie + * + * @description + * Removes current site cookies specified by name. The removal operation occurs + * immediately when the scriptlet is injected, then when the page is unloaded. + * + * @param needle + * A string or a regex matching the name of the cookie(s) to remove. + * + * @param ['when', token] + * Vararg, optional. The parameter following 'when' tells when extra removal + * operations should take place. + * - `scroll`: when the page is scrolled + * - `keydown`: when a keyboard touch is pressed + * + * */ + +export function removeCookie( + needle = '' +) { + if ( typeof needle !== 'string' ) { return; } + const safe = safeSelf(); + const reName = safe.patternToRegex(needle); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 1); + const throttle = (fn, ms = 500) => { + if ( throttle.timer !== undefined ) { return; } + throttle.timer = setTimeout(( ) => { + throttle.timer = undefined; + fn(); + }, ms); + }; + const baseURL = new URL(document.baseURI); + let targetDomain = extraArgs.domain; + if ( targetDomain && /^\/.+\//.test(targetDomain) ) { + const reDomain = new RegExp(targetDomain.slice(1, -1)); + const match = reDomain.exec(baseURL.hostname); + targetDomain = match ? match[0] : undefined; + } + const remove = ( ) => { + safe.String_split.call(document.cookie, ';').forEach(cookieStr => { + const pos = cookieStr.indexOf('='); + if ( pos === -1 ) { return; } + const cookieName = cookieStr.slice(0, pos).trim(); + if ( reName.test(cookieName) === false ) { return; } + const part1 = cookieName + '='; + const part2a = `; domain=${baseURL.hostname}`; + const part2b = `; domain=.${baseURL.hostname}`; + let part2c, part2d; + if ( targetDomain ) { + part2c = `; domain=${targetDomain}`; + part2d = `; domain=.${targetDomain}`; + } else if ( document.domain ) { + const domain = document.domain; + if ( domain !== baseURL.hostname ) { + part2c = `; domain=.${domain}`; + } + if ( domain.startsWith('www.') ) { + part2d = `; domain=${domain.replace('www', '')}`; + } + } + const part3 = '; path=/'; + const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = part1 + part4; + document.cookie = part1 + part2a + part4; + document.cookie = part1 + part2b + part4; + document.cookie = part1 + part3 + part4; + document.cookie = part1 + part2a + part3 + part4; + document.cookie = part1 + part2b + part3 + part4; + if ( part2c !== undefined ) { + document.cookie = part1 + part2c + part3 + part4; + } + if ( part2d !== undefined ) { + document.cookie = part1 + part2d + part3 + part4; + } + }); + }; + remove(); + window.addEventListener('beforeunload', remove); + if ( typeof extraArgs.when !== 'string' ) { return; } + const supportedEventTypes = [ 'scroll', 'keydown' ]; + const eventTypes = safe.String_split.call(extraArgs.when, /\s/); + for ( const type of eventTypes ) { + if ( supportedEventTypes.includes(type) === false ) { continue; } + document.addEventListener(type, ( ) => { + throttle(remove); + }, { passive: true }); + } +} +registerScriptlet(removeCookie, { + name: 'remove-cookie.js', + aliases: [ + 'cookie-remover.js', + ], + world: 'ISOLATED', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/create-html.js b/data/extensions/uBlock0@raymondhill.net/js/resources/create-html.js new file mode 100644 index 0000000..c5732d3 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/create-html.js @@ -0,0 +1,113 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2025-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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +/** + * @scriptlet trusted-create-html + * + * @description + * Element(s) from a parsed HTML string are added as child element(s) to a + * specific parent element in the DOM. + * + * @param parent + * A CSS selector identifying the element to which created element(s) will be + * added. + * + * @param html + * An HTML string to be parsed using DOMParser, and which resulting elements + * are to be added as child element(s). + * + * @param duration + * Optional. If specified, the time in ms after which the added elements will + * be removed. No removal will occur if not specified. + * + * */ + +function trustedCreateHTML( + parentSelector, + htmlStr = '', + durationStr = '' +) { + if ( parentSelector === '' ) { return; } + if ( htmlStr === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-create-html', parentSelector, htmlStr, durationStr); + // We do not want to recursively create elements + self.trustedCreateHTML = true; + let ancestor = self.frameElement; + while ( ancestor !== null ) { + const doc = ancestor.ownerDocument; + if ( doc === null ) { break; } + const win = doc.defaultView; + if ( win === null ) { break; } + if ( win.trustedCreateHTML ) { return; } + ancestor = ancestor.frameElement; + } + const duration = parseInt(durationStr, 10); + const domParser = new DOMParser(); + const externalDoc = domParser.parseFromString(htmlStr, 'text/html'); + const docFragment = new DocumentFragment(); + const toRemove = []; + while ( externalDoc.body.firstChild !== null ) { + const imported = document.adoptNode(externalDoc.body.firstChild); + docFragment.appendChild(imported); + if ( isNaN(duration) ) { continue; } + toRemove.push(imported); + } + if ( docFragment.firstChild === null ) { return; } + const remove = ( ) => { + for ( const node of toRemove ) { + if ( node.parentNode === null ) { continue; } + node.parentNode.removeChild(node); + } + safe.uboLog(logPrefix, 'Node(s) removed'); + }; + const append = ( ) => { + const parent = document.querySelector(parentSelector); + if ( parent === null ) { return false; } + parent.append(docFragment); + safe.uboLog(logPrefix, 'Node(s) appended'); + if ( toRemove.length === 0 ) { return true; } + setTimeout(remove, duration); + return true; + }; + if ( append() ) { return; } + const observer = new MutationObserver(( ) => { + if ( append() === false ) { return; } + observer.disconnect(); + }); + observer.observe(document, { childList: true, subtree: true }); +} +registerScriptlet(trustedCreateHTML, { + name: 'trusted-create-html.js', + requiresTrust: true, + dependencies: [ + safeSelf, + ], + world: 'ISOLATED', +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js b/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js new file mode 100644 index 0000000..eb88c27 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js @@ -0,0 +1,188 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; +import { urlSkip } from '../urlskip.js'; + +/******************************************************************************/ + +registerScriptlet(urlSkip, { + name: 'urlskip.fn', +}); + +/** + * @scriptlet href-sanitizer + * + * @description + * Set the `href` attribute to a value found in the DOM at, or below the + * targeted `a` element, and optionally with transformation steps. + * + * @param selector + * A plain CSS selector for elements which `href` property must be sanitized. + * + * @param source + * One or more tokens to lookup the source of the `href` property, and + * optionally the transformation steps to perform: + * - `text`: Use the text content of the element as the URL + * - `[name]`: Use the value of the attribute `name` as the URL + * - Transformation steps: see `urlskip` documentation + * + * If `text` or `[name]` is not present, the URL will be the value of `href` + * attribute. + * + * @example + * `example.org##+js(href-sanitizer, a)` + * `example.org##+js(href-sanitizer, a[title], [title])` + * `example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)` + * `example.org##+js(href-sanitizer, a[href*="/redirect"], ?url ?url -base64)` + * + * */ + +function hrefSanitizer( + selector = '', + source = '' +) { + if ( typeof selector !== 'string' ) { return; } + if ( selector === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source); + if ( source === '' ) { source = 'text'; } + const sanitizeCopycats = (href, text) => { + let elems = []; + try { + elems = document.querySelectorAll(`a[href="${href}"`); + } + catch { + } + for ( const elem of elems ) { + elem.setAttribute('href', text); + } + return elems.length; + }; + const validateURL = text => { + if ( typeof text !== 'string' ) { return ''; } + if ( text === '' ) { return ''; } + if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; } + try { + const url = new URL(text, document.location); + return url.href; + } catch { + } + return ''; + }; + const extractParam = (href, source) => { + if ( Boolean(source) === false ) { return href; } + const recursive = source.includes('?', 1); + const end = recursive ? source.indexOf('?', 1) : source.length; + try { + const url = new URL(href, document.location); + let value = url.searchParams.get(source.slice(1, end)); + if ( value === null ) { return href } + if ( recursive ) { return extractParam(value, source.slice(end)); } + return value; + } catch { + } + return href; + }; + const extractURL = (elem, source) => { + if ( /^\[.*\]$/.test(source) ) { + return elem.getAttribute(source.slice(1,-1).trim()) || ''; + } + if ( source === 'text' ) { + return elem.textContent + .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters + .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters + ; + } + if ( source.startsWith('?') === false ) { return ''; } + const steps = source.replace(/(\S)\?/g, '\\1?').split(/\s+/); + const url = steps.length === 1 + ? extractParam(elem.href, source) + : urlSkip(elem.href, false, steps); + if ( url === undefined ) { return; } + return url.replace(/ /g, '%20'); + }; + const sanitize = ( ) => { + let elems = []; + try { + elems = document.querySelectorAll(selector); + } + catch { + return false; + } + for ( const elem of elems ) { + if ( elem.localName !== 'a' ) { continue; } + if ( elem.hasAttribute('href') === false ) { continue; } + const href = elem.getAttribute('href'); + const text = extractURL(elem, source); + const hrefAfter = validateURL(text); + if ( hrefAfter === '' ) { continue; } + if ( hrefAfter === href ) { continue; } + elem.setAttribute('href', hrefAfter); + const count = sanitizeCopycats(href, hrefAfter); + safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`); + } + return true; + }; + let observer, timer; + const onDomChanged = mutations => { + if ( timer !== undefined ) { return; } + let shouldSanitize = false; + for ( const mutation of mutations ) { + if ( mutation.addedNodes.length === 0 ) { continue; } + for ( const node of mutation.addedNodes ) { + if ( node.nodeType !== 1 ) { continue; } + shouldSanitize = true; + break; + } + if ( shouldSanitize ) { break; } + } + if ( shouldSanitize === false ) { return; } + timer = safe.onIdle(( ) => { + timer = undefined; + sanitize(); + }); + }; + const start = ( ) => { + if ( sanitize() === false ) { return; } + observer = new MutationObserver(onDomChanged); + observer.observe(document.body, { + subtree: true, + childList: true, + }); + }; + runAt(( ) => { start(); }, 'interactive'); +} +registerScriptlet(hrefSanitizer, { + name: 'href-sanitizer.js', + world: 'ISOLATED', + aliases: [ + 'urlskip.js', + ], + dependencies: [ + runAt, + safeSelf, + urlSkip, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js b/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js new file mode 100644 index 0000000..f8ea191 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js @@ -0,0 +1,1097 @@ +/******************************************************************************* + + 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 { + matchObjectPropertiesFn, + parsePropertiesToMatchFn, +} from './utils.js'; + +import { JSONPath } from './shared.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ +/******************************************************************************/ + +function editOutboundObjectFn( + trusted = false, + propChain = '', + jsonq = '', +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}edit-outbound-object`, + propChain, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + proxyApplyFn(propChain, function(context) { + const obj = context.reflect(); + if ( jsonp.apply(obj) === 0 ) { return obj; } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(obj, null, 2)}`); + } + return obj; + }); +} +registerScriptlet(editOutboundObjectFn, { + name: 'edit-outbound-object.fn', + dependencies: [ + JSONPath, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet edit-outbound-object-.js + * + * @description + * Prune properties from an object returned by a specific method. + * Properties can only be removed. + * + * @param propChain + * Property chain of the method to trap. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function editOutboundObject(propChain = '', jsonq = '') { + editOutboundObjectFn(false, propChain, jsonq); +} +registerScriptlet(editOutboundObject, { + name: 'edit-outbound-object.js', + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-edit-outbound-object.js + * + * @description + * Edit properties of an object returned by a specific method. + * Properties can be assigned new values. + * + * @param propChain + * Property chain of the method to trap. + * +* @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedEditOutboundObject(propChain = '', jsonq = '') { + editOutboundObjectFn(true, propChain, jsonq); +} +registerScriptlet(trustedEditOutboundObject, { + name: 'trusted-edit-outbound-object.js', + requiresTrust: true, + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ +/** + * @scriptlet json-edit.js + * + * @description + * Edit object generated through JSON.parse(). + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function jsonEdit(jsonq = '') { + editOutboundObjectFn(false, 'JSON.parse', jsonq); +} +registerScriptlet(jsonEdit, { + name: 'json-edit.js', + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit.js + * + * @description + * Edit object generated through JSON.parse(). + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedJsonEdit(jsonq = '') { + editOutboundObjectFn(true, 'JSON.parse', jsonq); +} +registerScriptlet(trustedJsonEdit, { + name: 'trusted-json-edit.js', + requiresTrust: true, + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function editInboundObjectFn( + trusted = false, + propChain = '', + argPosRaw = '', + jsonq = '', +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}edit-inbound-object`, + propChain, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const argPos = parseInt(argPosRaw, 10); + if ( isNaN(argPos) ) { return; } + const getArgPos = args => { + if ( argPos >= 0 ) { + if ( args.length <= argPos ) { return; } + return argPos; + } + if ( args.length < -argPos ) { return; } + return args.length + argPos; + }; + const editObj = obj => { + let clone; + try { + clone = safe.JSON_parse(safe.JSON_stringify(obj)); + } catch { + } + if ( typeof clone !== 'object' || clone === null ) { return; } + if ( jsonp.apply(clone) === 0 ) { return; } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(clone, null, 2)}`); + } + return clone; + }; + proxyApplyFn(propChain, function(context) { + const i = getArgPos(context.args); + if ( i !== undefined ) { + const obj = editObj(context.args[i]); + if ( obj ) { + context.args[i] = obj; + } + } + return context.reflect(); + }); +} +registerScriptlet(editInboundObjectFn, { + name: 'edit-inbound-object.fn', + dependencies: [ + JSONPath, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet edit-inbound-object.js + * + * @description + * Prune properties from an object passed as argument to a specific method. + * Properties can only be removed. + * + * @param propChain + * Property chain of the method to trap. + * + * @param argPos + * 0-based position of the argument. Use negative integer for position relative + * to the end. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function editInboundObject(propChain = '', argPos = '', jsonq = '') { + editInboundObjectFn(false, propChain, argPos, jsonq); +} +registerScriptlet(editInboundObject, { + name: 'edit-inbound-object.js', + dependencies: [ + editInboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-edit-inbound-object.js + * + * @description + * Edit properties of an object passed as argument to a specific method. + * Properties can be assigned new values. + * + * @param propChain + * Property chain of the method to trap. + * + * @param argPos + * 0-based position of the argument. Use negative integer for position relative + * to the end. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedEditInboundObject(propChain = '', argPos = '', jsonq = '') { + editInboundObjectFn(true, propChain, argPos, jsonq); +} +registerScriptlet(trustedEditInboundObject, { + name: 'trusted-edit-inbound-object.js', + requiresTrust: true, + dependencies: [ + editInboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditXhrResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-xhr-response`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + xhrInstances.set(this, xhrDetails); + } + return super.open(method, url, ...args); + } + get response() { + const innerResponse = super.response; + const xhrDetails = xhrInstances.get(this); + if ( xhrDetails === undefined ) { return innerResponse; } + const responseLength = typeof innerResponse === 'string' + ? innerResponse.length + : undefined; + if ( xhrDetails.lastResponseLength !== responseLength ) { + xhrDetails.response = undefined; + xhrDetails.lastResponseLength = responseLength; + } + if ( xhrDetails.response !== undefined ) { + return xhrDetails.response; + } + let obj; + if ( typeof innerResponse === 'object' ) { + obj = innerResponse; + } else if ( typeof innerResponse === 'string' ) { + try { obj = safe.JSON_parse(innerResponse); } catch { } + } + if ( typeof obj !== 'object' || obj === null || jsonp.apply(obj) === 0 ) { + return (xhrDetails.response = innerResponse); + } + safe.uboLog(logPrefix, 'Edited'); + const outerResponse = typeof innerResponse === 'string' + ? JSONPath.toJSON(obj, safe.JSON_stringify) + : obj; + return (xhrDetails.response = outerResponse); + } + get responseText() { + const response = this.response; + return typeof response !== 'string' + ? super.responseText + : response; + } + }; +} +registerScriptlet(jsonEditXhrResponseFn, { + name: 'json-edit-xhr-response.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-xhr-response.js + * + * @description + * Edit the object fetched through a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditXhrResponse(jsonq = '', ...args) { + jsonEditXhrResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditXhrResponse, { + name: 'json-edit-xhr-response.js', + dependencies: [ + jsonEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-xhr-response.js + * + * @description + * Edit the object fetched through a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditXhrResponse(jsonq = '', ...args) { + jsonEditXhrResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditXhrResponse, { + name: 'trusted-json-edit-xhr-response.js', + requiresTrust: true, + dependencies: [ + jsonEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditXhrRequestFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-xhr-request`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + xhrInstances.set(this, xhrDetails); + } + return super.open(method, url, ...args); + } + send(body) { + const xhrDetails = xhrInstances.get(this); + if ( xhrDetails ) { + body = this.#filterBody(body) || body; + } + super.send(body); + } + #filterBody(body) { + if ( typeof body !== 'string' ) { return; } + let data; + try { data = safe.JSON_parse(body); } + catch { } + if ( data instanceof Object === false ) { return; } + const n = jsonp.apply(data); + if ( n === 0 ) { return; } + body = safe.JSON_stringify(data); + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${body}`); + } + return body; + } + }; +} +registerScriptlet(jsonEditXhrRequestFn, { + name: 'json-edit-xhr-request.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-xhr-request.js + * + * @description + * Edit the object sent as the body in a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditXhrRequest(jsonq = '', ...args) { + jsonEditXhrRequestFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditXhrRequest, { + name: 'json-edit-xhr-request.js', + dependencies: [ + jsonEditXhrRequestFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-xhr-request.js + * + * @description + * Edit the object sent as the body in a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditXhrRequest(jsonq = '', ...args) { + jsonEditXhrRequestFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditXhrRequest, { + name: 'trusted-json-edit-xhr-request.js', + requiresTrust: true, + dependencies: [ + jsonEditXhrRequestFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditFetchResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-fetch-response`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + proxyApplyFn('fetch', function(context) { + const args = context.callArgs; + const fetchPromise = context.reflect(); + if ( propNeedles.size !== 0 ) { + const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; + if ( objs[0] instanceof Request ) { + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } + } + if ( args[1] instanceof Object ) { + objs.push(args[1]); + } + 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.json().then(obj => { + if ( typeof obj !== 'object' ) { return responseBefore; } + if ( jsonp.apply(obj) === 0 ) { return responseBefore; } + safe.uboLog(logPrefix, 'Edited'); + const responseAfter = Response.json(obj, { + status: responseBefore.status, + statusText: responseBefore.statusText, + headers: responseBefore.headers, + }); + Object.defineProperties(responseAfter, { + ok: { value: responseBefore.ok }, + redirected: { value: responseBefore.redirected }, + type: { value: responseBefore.type }, + url: { value: responseBefore.url }, + }); + return responseAfter; + }).catch(reason => { + safe.uboErr(logPrefix, 'Error:', reason); + return responseBefore; + }); + }).catch(reason => { + safe.uboErr(logPrefix, 'Error:', reason); + return fetchPromise; + }); + }); +} +registerScriptlet(jsonEditFetchResponseFn, { + name: 'json-edit-fetch-response.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-fetch-response.js + * + * @description + * Edit the object fetched through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditFetchResponse(jsonq = '', ...args) { + jsonEditFetchResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditFetchResponse, { + name: 'json-edit-fetch-response.js', + dependencies: [ + jsonEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-fetch-response.js + * + * @description + * Edit the object fetched through the fetch API. The trusted version allows + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditFetchResponse(jsonq = '', ...args) { + jsonEditFetchResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditFetchResponse, { + name: 'trusted-json-edit-fetch-response.js', + requiresTrust: true, + dependencies: [ + jsonEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditFetchRequestFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-fetch-request`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const filterBody = body => { + if ( typeof body !== 'string' ) { return; } + let data; + try { data = safe.JSON_parse(body); } + catch { } + if ( data instanceof Object === false ) { return; } + const n = jsonp.apply(data); + if ( n === 0 ) { return; } + return safe.JSON_stringify(data); + } + const proxyHandler = context => { + const args = context.callArgs; + const [ resource, options ] = args; + const bodyBefore = options?.body; + if ( Boolean(bodyBefore) === false ) { return context.reflect(); } + const bodyAfter = filterBody(bodyBefore); + if ( bodyAfter === undefined || bodyAfter === bodyBefore ) { + return context.reflect(); + } + if ( propNeedles.size !== 0 ) { + const objs = [ + resource instanceof Object ? resource : { url: `${resource}` } + ]; + if ( objs[0] instanceof Request ) { + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } + } + const matched = matchObjectPropertiesFn(propNeedles, ...objs); + if ( matched === undefined ) { return context.reflect(); } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${bodyAfter}`); + } + options.body = bodyAfter; + return context.reflect(); + }; + proxyApplyFn('fetch', proxyHandler); + proxyApplyFn('Request', proxyHandler); +} +registerScriptlet(jsonEditFetchRequestFn, { + name: 'json-edit-fetch-request.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-fetch-request.js + * + * @description + * Edit the request body sent through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when fetch() is called. + * + * */ + +function jsonEditFetchRequest(jsonq = '', ...args) { + jsonEditFetchRequestFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditFetchRequest, { + name: 'json-edit-fetch-request.js', + dependencies: [ + jsonEditFetchRequestFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-fetch-request.js + * + * @description + * Edit the request body sent through the fetch API. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when fetch() is called. + * + * */ + +function trustedJsonEditFetchRequest(jsonq = '', ...args) { + jsonEditFetchRequestFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditFetchRequest, { + name: 'trusted-json-edit-fetch-request.js', + requiresTrust: true, + dependencies: [ + jsonEditFetchRequestFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonlEditFn(jsonp, text = '') { + const safe = safeSelf(); + const lineSeparator = /\r?\n/.exec(text)?.[0] || '\n'; + const linesBefore = text.split('\n'); + const linesAfter = []; + for ( const lineBefore of linesBefore ) { + let obj; + try { obj = safe.JSON_parse(lineBefore); } catch { } + if ( typeof obj !== 'object' || obj === null ) { + linesAfter.push(lineBefore); + continue; + } + if ( jsonp.apply(obj) === 0 ) { + linesAfter.push(lineBefore); + continue; + } + const lineAfter = safe.JSON_stringify(obj); + linesAfter.push(lineAfter); + } + return linesAfter.join(lineSeparator); +} +registerScriptlet(jsonlEditFn, { + name: 'jsonl-edit.fn', + dependencies: [ + JSONPath, + safeSelf, + ], +}); + +/******************************************************************************/ + +function jsonlEditXhrResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}jsonl-edit-xhr-response`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + xhrInstances.set(this, xhrDetails); + } + return super.open(method, url, ...args); + } + get response() { + const innerResponse = super.response; + const xhrDetails = xhrInstances.get(this); + if ( xhrDetails === undefined ) { + return innerResponse; + } + const responseLength = typeof innerResponse === 'string' + ? innerResponse.length + : undefined; + if ( xhrDetails.lastResponseLength !== responseLength ) { + xhrDetails.response = undefined; + xhrDetails.lastResponseLength = responseLength; + } + if ( xhrDetails.response !== undefined ) { + return xhrDetails.response; + } + if ( typeof innerResponse !== 'string' ) { + return (xhrDetails.response = innerResponse); + } + const outerResponse = jsonlEditFn(jsonp, innerResponse); + if ( outerResponse !== innerResponse ) { + safe.uboLog(logPrefix, 'Pruned'); + } + return (xhrDetails.response = outerResponse); + } + get responseText() { + const response = this.response; + return typeof response !== 'string' + ? super.responseText + : response; + } + }; +} +registerScriptlet(jsonlEditXhrResponseFn, { + name: 'jsonl-edit-xhr-response.fn', + dependencies: [ + JSONPath, + jsonlEditFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet jsonl-edit-xhr-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonlEditXhrResponse(jsonq = '', ...args) { + jsonlEditXhrResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonlEditXhrResponse, { + name: 'jsonl-edit-xhr-response.js', + dependencies: [ + jsonlEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-jsonl-edit-xhr-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonlEditXhrResponse(jsonq = '', ...args) { + jsonlEditXhrResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonlEditXhrResponse, { + name: 'trusted-jsonl-edit-xhr-response.js', + requiresTrust: true, + dependencies: [ + jsonlEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonlEditFetchResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}jsonl-edit-fetch-response`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const logall = jsonq === ''; + proxyApplyFn('fetch', function(context) { + const args = context.callArgs; + const fetchPromise = context.reflect(); + if ( propNeedles.size !== 0 ) { + const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; + if ( objs[0] instanceof Request ) { + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } + } + if ( args[1] instanceof Object ) { + objs.push(args[1]); + } + 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 ( typeof textBefore !== 'string' ) { return textBefore; } + if ( logall ) { + safe.uboLog(logPrefix, textBefore); + return responseBefore; + } + const textAfter = jsonlEditFn(jsonp, textBefore); + if ( textAfter === textBefore ) { return responseBefore; } + safe.uboLog(logPrefix, 'Pruned'); + 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, 'Error:', reason); + return responseBefore; + }); + }).catch(reason => { + safe.uboErr(logPrefix, 'Error:', reason); + return fetchPromise; + }); + }); +} +registerScriptlet(jsonlEditFetchResponseFn, { + name: 'jsonl-edit-fetch-response.fn', + dependencies: [ + JSONPath, + jsonlEditFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet jsonl-edit-fetch-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonlEditFetchResponse(jsonq = '', ...args) { + jsonlEditFetchResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonlEditFetchResponse, { + name: 'jsonl-edit-fetch-response.js', + dependencies: [ + jsonlEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-jsonl-edit-fetch-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through the fetch API. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonlEditFetchResponse(jsonq = '', ...args) { + jsonlEditFetchResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonlEditFetchResponse, { + name: 'trusted-jsonl-edit-fetch-response.js', + requiresTrust: true, + dependencies: [ + jsonlEditFetchResponseFn, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/json-prune.js b/data/extensions/uBlock0@raymondhill.net/js/resources/json-prune.js new file mode 100644 index 0000000..08284e3 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/json-prune.js @@ -0,0 +1,275 @@ +/******************************************************************************* + + 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 { + matchObjectPropertiesFn, + parsePropertiesToMatchFn, +} from './utils.js'; + +import { objectPruneFn } from './object-prune.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +function jsonPrune( + rawPrunePaths = '', + rawNeedlePaths = '', + stackNeedle = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle); + const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true }); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + JSON.parse = new Proxy(JSON.parse, { + apply: function(target, thisArg, args) { + const objBefore = Reflect.apply(target, thisArg, args); + if ( rawPrunePaths === '' ) { + safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); + } + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + stackNeedleDetails, + extraArgs + ); + if ( objAfter === undefined ) { return objBefore; } + safe.uboLog(logPrefix, 'Pruned'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`); + } + return objAfter; + }, + }); +} +registerScriptlet(jsonPrune, { + name: 'json-prune.js', + dependencies: [ + objectPruneFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +function jsonPruneFetchResponse( + rawPrunePaths = '', + rawNeedlePaths = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); + const logall = rawPrunePaths === ''; + const applyHandler = function(target, thisArg, args) { + const fetchPromise = Reflect.apply(target, thisArg, args); + if ( propNeedles.size !== 0 ) { + const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ]; + if ( objs[0] instanceof Request ) { + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } + } + if ( args[1] instanceof Object ) { + objs.push(args[1]); + } + 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.json().then(objBefore => { + if ( typeof objBefore !== 'object' ) { return responseBefore; } + if ( logall ) { + safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2)); + return responseBefore; + } + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + stackNeedle, + extraArgs + ); + if ( typeof objAfter !== 'object' ) { return responseBefore; } + safe.uboLog(logPrefix, 'Pruned'); + const responseAfter = Response.json(objAfter, { + status: responseBefore.status, + statusText: responseBefore.statusText, + headers: responseBefore.headers, + }); + Object.defineProperties(responseAfter, { + ok: { value: responseBefore.ok }, + redirected: { value: responseBefore.redirected }, + type: { value: responseBefore.type }, + url: { value: responseBefore.url }, + }); + return responseAfter; + }).catch(reason => { + safe.uboErr(logPrefix, 'Error:', reason); + return responseBefore; + }); + }).catch(reason => { + safe.uboErr(logPrefix, 'Error:', reason); + return fetchPromise; + }); + }; + self.fetch = new Proxy(self.fetch, { + apply: applyHandler + }); +} +registerScriptlet(jsonPruneFetchResponse, { + name: 'json-prune-fetch-response.js', + dependencies: [ + matchObjectPropertiesFn, + objectPruneFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +function jsonPruneXhrResponse( + rawPrunePaths = '', + rawNeedlePaths = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths); + const xhrInstances = new WeakMap(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + let outcome = 'match'; + if ( propNeedles.size !== 0 ) { + if ( matchObjectPropertiesFn(propNeedles, xhrDetails) === undefined ) { + outcome = 'nomatch'; + } + } + if ( outcome === 'match' ) { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`); + } + xhrInstances.set(this, xhrDetails); + } + return super.open(method, url, ...args); + } + get response() { + const innerResponse = super.response; + const xhrDetails = xhrInstances.get(this); + if ( xhrDetails === undefined ) { + return innerResponse; + } + const responseLength = typeof innerResponse === 'string' + ? innerResponse.length + : undefined; + if ( xhrDetails.lastResponseLength !== responseLength ) { + xhrDetails.response = undefined; + xhrDetails.lastResponseLength = responseLength; + } + if ( xhrDetails.response !== undefined ) { + return xhrDetails.response; + } + let objBefore; + if ( typeof innerResponse === 'object' ) { + objBefore = innerResponse; + } else if ( typeof innerResponse === 'string' ) { + try { + objBefore = safe.JSON_parse(innerResponse); + } catch { + } + } + if ( typeof objBefore !== 'object' ) { + return (xhrDetails.response = innerResponse); + } + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + stackNeedle, + extraArgs + ); + let outerResponse; + if ( typeof objAfter === 'object' ) { + outerResponse = typeof innerResponse === 'string' + ? safe.JSON_stringify(objAfter) + : objAfter; + safe.uboLog(logPrefix, 'Pruned'); + } else { + outerResponse = innerResponse; + } + return (xhrDetails.response = outerResponse); + } + get responseText() { + const response = this.response; + return typeof response !== 'string' + ? super.responseText + : response; + } + }; +} +registerScriptlet(jsonPruneXhrResponse, { + name: 'json-prune-xhr-response.js', + dependencies: [ + matchObjectPropertiesFn, + objectPruneFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +// There is still code out there which uses `eval` in lieu of `JSON.parse`. + +function evaldataPrune( + rawPrunePaths = '', + rawNeedlePaths = '' +) { + proxyApplyFn('eval', function(context) { + const before = context.reflect(); + if ( typeof before !== 'object' ) { return before; } + if ( before === null ) { return null; } + const after = objectPruneFn(before, rawPrunePaths, rawNeedlePaths); + return after || before; + }); +} +registerScriptlet(evaldataPrune, { + name: 'evaldata-prune.js', + dependencies: [ + objectPruneFn, + proxyApplyFn, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/localstorage.js b/data/extensions/uBlock0@raymondhill.net/js/resources/localstorage.js new file mode 100644 index 0000000..8a6b762 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/localstorage.js @@ -0,0 +1,235 @@ +/******************************************************************************* + + 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 { getSafeCookieValuesFn } from './cookie.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function getAllLocalStorageFn(which = 'localStorage') { + const storage = self[which]; + const out = []; + for ( let i = 0; i < storage.length; i++ ) { + const key = storage.key(i); + const value = storage.getItem(key); + return { key, value }; + } + return out; +} +registerScriptlet(getAllLocalStorageFn, { + name: 'get-all-local-storage.fn', +}); + +/******************************************************************************/ + +export function setLocalStorageItemFn( + which = 'local', + trusted = false, + key = '', + value = '', +) { + if ( key === '' ) { return; } + + // For increased compatibility with AdGuard + if ( value === 'emptyArr' ) { + value = '[]'; + } else if ( value === 'emptyObj' ) { + value = '{}'; + } + + const trustedValues = [ + '', + 'undefined', 'null', + '{}', '[]', '""', + '$remove$', + ...getSafeCookieValuesFn(), + ]; + + if ( trusted ) { + if ( value.includes('$now$') ) { + value = value.replaceAll('$now$', Date.now()); + } + if ( value.includes('$currentDate$') ) { + value = value.replaceAll('$currentDate$', `${Date()}`); + } + if ( value.includes('$currentISODate$') ) { + value = value.replaceAll('$currentISODate$', (new Date()).toISOString()); + } + } else { + const normalized = value.toLowerCase(); + const match = /^("?)(.+)\1$/.exec(normalized); + const unquoted = match && match[2] || normalized; + if ( trustedValues.includes(unquoted) === false ) { + if ( /^-?\d+$/.test(unquoted) === false ) { return; } + const n = parseInt(unquoted, 10) || 0; + if ( n < -32767 || n > 32767 ) { return; } + } + } + + try { + const storage = self[`${which}Storage`]; + if ( value === '$remove$' ) { + const safe = safeSelf(); + const pattern = safe.patternToRegex(key, undefined, true ); + const toRemove = []; + for ( let i = 0, n = storage.length; i < n; i++ ) { + const key = storage.key(i); + if ( pattern.test(key) ) { toRemove.push(key); } + } + for ( const key of toRemove ) { + storage.removeItem(key); + } + } else { + storage.setItem(key, `${value}`); + } + } catch { + } +} +registerScriptlet(setLocalStorageItemFn, { + name: 'set-local-storage-item.fn', + dependencies: [ + getSafeCookieValuesFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +export function removeCacheStorageItem( + cacheNamePattern = '', + requestPattern = '' +) { + if ( cacheNamePattern === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('remove-cache-storage-item', cacheNamePattern, requestPattern); + const cacheStorage = self.caches; + if ( cacheStorage instanceof Object === false ) { return; } + const reCache = safe.patternToRegex(cacheNamePattern, undefined, true); + const reRequest = safe.patternToRegex(requestPattern, undefined, true); + cacheStorage.keys().then(cacheNames => { + for ( const cacheName of cacheNames ) { + if ( reCache.test(cacheName) === false ) { continue; } + if ( requestPattern === '' ) { + cacheStorage.delete(cacheName).then(result => { + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Deleting ${cacheName}`); + } + if ( result !== true ) { return; } + safe.uboLog(logPrefix, `Deleted ${cacheName}: ${result}`); + }); + continue; + } + cacheStorage.open(cacheName).then(cache => { + cache.keys().then(requests => { + for ( const request of requests ) { + if ( reRequest.test(request.url) === false ) { continue; } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Deleting ${cacheName}/${request.url}`); + } + cache.delete(request).then(result => { + if ( result !== true ) { return; } + safe.uboLog(logPrefix, `Deleted ${cacheName}/${request.url}: ${result}`); + }); + } + }); + }); + } + }); +} +registerScriptlet(removeCacheStorageItem, { + name: 'remove-cache-storage-item.fn', + world: 'ISOLATED', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************* + * + * set-local-storage-item.js + * set-session-storage-item.js + * + * Set a local/session storage entry to a specific, allowed value. + * + * Reference: + * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-local-storage-item.js + * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-session-storage-item.js + * + **/ + +export function setLocalStorageItem(key = '', value = '') { + setLocalStorageItemFn('local', false, key, value); +} +registerScriptlet(setLocalStorageItem, { + name: 'set-local-storage-item.js', + world: 'ISOLATED', + dependencies: [ + setLocalStorageItemFn, + ], +}); + +export function setSessionStorageItem(key = '', value = '') { + setLocalStorageItemFn('session', false, key, value); +} +registerScriptlet(setSessionStorageItem, { + name: 'set-session-storage-item.js', + world: 'ISOLATED', + dependencies: [ + setLocalStorageItemFn, + ], +}); + +/******************************************************************************* + * + * trusted-set-local-storage-item.js + * + * Set a local storage entry to an arbitrary value. + * + * Reference: + * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-local-storage-item.js + * + **/ + +export function trustedSetLocalStorageItem(key = '', value = '') { + setLocalStorageItemFn('local', true, key, value); +} +registerScriptlet(trustedSetLocalStorageItem, { + name: 'trusted-set-local-storage-item.js', + requiresTrust: true, + world: 'ISOLATED', + dependencies: [ + setLocalStorageItemFn, + ], +}); + +export function trustedSetSessionStorageItem(key = '', value = '') { + setLocalStorageItemFn('session', true, key, value); +} +registerScriptlet(trustedSetSessionStorageItem, { + name: 'trusted-set-session-storage-item.js', + requiresTrust: true, + world: 'ISOLATED', + dependencies: [ + setLocalStorageItemFn, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/noeval.js b/data/extensions/uBlock0@raymondhill.net/js/resources/noeval.js new file mode 100644 index 0000000..d66db53 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/noeval.js @@ -0,0 +1,58 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +function noEvalIf( + needle = '' +) { + if ( typeof needle !== 'string' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('noeval-if', needle); + const reNeedle = safe.patternToRegex(needle); + proxyApplyFn('eval', function(context) { + const { callArgs } = context; + const a = String(callArgs[0]); + if ( needle !== '' && reNeedle.test(a) ) { + safe.uboLog(logPrefix, 'Prevented:\n', a); + return; + } + if ( needle === '' || safe.logLevel > 1 ) { + safe.uboLog(logPrefix, 'Not prevented:\n', a); + } + return context.reflect(); + }); +} +registerScriptlet(noEvalIf, { + name: 'noeval-if.js', + aliases: [ + 'prevent-eval-if.js', + ], + dependencies: [ + proxyApplyFn, + safeSelf, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/object-prune.js b/data/extensions/uBlock0@raymondhill.net/js/resources/object-prune.js new file mode 100644 index 0000000..50256fe --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/object-prune.js @@ -0,0 +1,272 @@ +/******************************************************************************* + + 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 { matchesStackTraceFn } from './stack-trace.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +function objectFindOwnerFn( + root, + path, + prune = false +) { + const safe = safeSelf(); + let owner = root; + let chain = path; + for (;;) { + if ( typeof owner !== 'object' || owner === null ) { return false; } + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + if ( prune === false ) { + return safe.Object_hasOwn(owner, chain); + } + let modified = false; + if ( chain === '*' ) { + for ( const key in owner ) { + if ( safe.Object_hasOwn(owner, key) === false ) { continue; } + delete owner[key]; + modified = true; + } + } else if ( safe.Object_hasOwn(owner, chain) ) { + delete owner[chain]; + modified = true; + } + return modified; + } + const prop = chain.slice(0, pos); + const next = chain.slice(pos + 1); + let found = false; + if ( prop === '[-]' && Array.isArray(owner) ) { + let i = owner.length; + while ( i-- ) { + if ( objectFindOwnerFn(owner[i], next) === false ) { continue; } + owner.splice(i, 1); + found = true; + } + return found; + } + if ( prop === '{-}' && owner instanceof Object ) { + for ( const key of Object.keys(owner) ) { + if ( objectFindOwnerFn(owner[key], next) === false ) { continue; } + delete owner[key]; + found = true; + } + return found; + } + if ( + prop === '[]' && Array.isArray(owner) || + prop === '{}' && owner instanceof Object || + prop === '*' && owner instanceof Object + ) { + for ( const key of Object.keys(owner) ) { + if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; } + found = true; + } + return found; + } + if ( safe.Object_hasOwn(owner, prop) === false ) { return false; } + owner = owner[prop]; + chain = chain.slice(pos + 1); + } +} +registerScriptlet(objectFindOwnerFn, { + name: 'object-find-owner.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +// When no "prune paths" argument is provided, the scriptlet is +// used for logging purpose and the "needle paths" argument is +// used to filter logging output. +// +// https://github.com/uBlockOrigin/uBlock-issues/issues/1545 +// - Add support for "remove everything if needle matches" case + +export function objectPruneFn( + obj, + rawPrunePaths, + rawNeedlePaths, + stackNeedleDetails = { matchAll: true }, + extraArgs = {} +) { + if ( typeof rawPrunePaths !== 'string' ) { return; } + const safe = safeSelf(); + const prunePaths = rawPrunePaths !== '' + ? safe.String_split.call(rawPrunePaths, / +/) + : []; + const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' + ? safe.String_split.call(rawNeedlePaths, / +/) + : []; + if ( stackNeedleDetails.matchAll !== true ) { + if ( matchesStackTraceFn(stackNeedleDetails, extraArgs.logstack) === false ) { + return; + } + } + if ( objectPruneFn.mustProcess === undefined ) { + objectPruneFn.mustProcess = (root, needlePaths) => { + for ( const needlePath of needlePaths ) { + if ( objectFindOwnerFn(root, needlePath) === false ) { + return false; + } + } + return true; + }; + } + if ( prunePaths.length === 0 ) { return; } + let outcome = 'nomatch'; + if ( objectPruneFn.mustProcess(obj, needlePaths) ) { + for ( const path of prunePaths ) { + if ( objectFindOwnerFn(obj, path, true) ) { + outcome = 'match'; + } + } + } + if ( outcome === 'match' ) { return obj; } +} +registerScriptlet(objectPruneFn, { + name: 'object-prune.fn', + dependencies: [ + matchesStackTraceFn, + objectFindOwnerFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +function trustedPruneInboundObject( + entryPoint = '', + argPos = '', + rawPrunePaths = '', + rawNeedlePaths = '' +) { + if ( entryPoint === '' ) { return; } + let context = globalThis; + let prop = entryPoint; + for (;;) { + const pos = prop.indexOf('.'); + if ( pos === -1 ) { break; } + context = context[prop.slice(0, pos)]; + if ( context instanceof Object === false ) { return; } + prop = prop.slice(pos+1); + } + if ( typeof context[prop] !== 'function' ) { return; } + const argIndex = parseInt(argPos); + if ( isNaN(argIndex) ) { return; } + if ( argIndex < 1 ) { return; } + const safe = safeSelf(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); + const needlePaths = []; + if ( rawPrunePaths !== '' ) { + needlePaths.push(...safe.String_split.call(rawPrunePaths, / +/)); + } + if ( rawNeedlePaths !== '' ) { + needlePaths.push(...safe.String_split.call(rawNeedlePaths, / +/)); + } + const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); + const mustProcess = root => { + for ( const needlePath of needlePaths ) { + if ( objectFindOwnerFn(root, needlePath) === false ) { + return false; + } + } + return true; + }; + context[prop] = new Proxy(context[prop], { + apply: function(target, thisArg, args) { + const targetArg = argIndex <= args.length + ? args[argIndex-1] + : undefined; + if ( targetArg instanceof Object && mustProcess(targetArg) ) { + let objBefore = targetArg; + if ( extraArgs.dontOverwrite ) { + try { + objBefore = safe.JSON_parse(safe.JSON_stringify(targetArg)); + } catch { + objBefore = undefined; + } + } + if ( objBefore !== undefined ) { + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + stackNeedle, + extraArgs + ); + args[argIndex-1] = objAfter || objBefore; + } + } + return Reflect.apply(target, thisArg, args); + }, + }); +} +registerScriptlet(trustedPruneInboundObject, { + name: 'trusted-prune-inbound-object.js', + requiresTrust: true, + dependencies: [ + objectFindOwnerFn, + objectPruneFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +function trustedPruneOutboundObject( + propChain = '', + rawPrunePaths = '', + rawNeedlePaths = '' +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + proxyApplyFn(propChain, function(context) { + const objBefore = context.reflect(); + if ( objBefore instanceof Object === false ) { return objBefore; } + const objAfter = objectPruneFn( + objBefore, + rawPrunePaths, + rawNeedlePaths, + { matchAll: true }, + extraArgs + ); + return objAfter || objBefore; + }); +} +registerScriptlet(trustedPruneOutboundObject, { + name: 'trusted-prune-outbound-object.js', + requiresTrust: true, + dependencies: [ + objectPruneFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/parse-replace.js b/data/extensions/uBlock0@raymondhill.net/js/resources/parse-replace.js new file mode 100644 index 0000000..604644f --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/parse-replace.js @@ -0,0 +1,54 @@ +/******************************************************************************* + + 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 { ArglistParser } from './shared.js'; +import { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +export function parseReplaceFn(s) { + if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; } + const parser = new ArglistParser('/'); + parser.nextArg(s, 1); + let pattern = s.slice(parser.argBeg, parser.argEnd); + if ( parser.transform ) { + pattern = parser.normalizeArg(pattern); + } + if ( pattern === '' ) { return; } + parser.nextArg(s, parser.separatorEnd); + let replacement = s.slice(parser.argBeg, parser.argEnd); + if ( parser.separatorEnd === parser.separatorBeg ) { return; } + if ( parser.transform ) { + replacement = parser.normalizeArg(replacement); + } + const flags = s.slice(parser.separatorEnd); + try { + return { re: new RegExp(pattern, flags), replacement }; + } catch { + } +} +registerScriptlet(parseReplaceFn, { + name: 'parse-replace.fn', + dependencies: [ + ArglistParser, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-fetch.js b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-fetch.js new file mode 100644 index 0000000..1b142a7 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-fetch.js @@ -0,0 +1,208 @@ +/******************************************************************************* + + 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 { generateContentFn } from './utils.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +function preventFetchFn( + trusted = false, + propsToMatch = '', + responseBody = '', + responseType = '' +) { + const safe = safeSelf(); + const scriptletName = `${trusted ? 'trusted-' : ''}prevent-fetch`; + const logPrefix = safe.makeLogPrefix( + scriptletName, + propsToMatch, + responseBody, + responseType + ); + const needles = []; + for ( const condition of safe.String_split.call(propsToMatch, /\s+/) ) { + if ( condition === '' ) { continue; } + const pos = condition.indexOf(':'); + let key, value; + if ( pos !== -1 ) { + key = condition.slice(0, pos); + value = condition.slice(pos + 1); + } else { + key = 'url'; + value = condition; + } + needles.push({ key, pattern: safe.initPattern(value, { canNegate: true }) }); + } + const validResponseProps = { + ok: [ false, true ], + statusText: [ '', 'Not Found' ], + type: [ 'basic', 'cors', 'default', 'error', 'opaque' ], + }; + const responseProps = { + statusText: { value: 'OK' }, + }; + if ( /^\{.*\}$/.test(responseType) ) { + try { + Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => { + if ( validResponseProps[p] === undefined ) { return; } + if ( validResponseProps[p].includes(v) === false ) { return; } + responseProps[p] = { value: v }; + }); + } + catch { } + } else if ( responseType !== '' ) { + if ( validResponseProps.type.includes(responseType) ) { + responseProps.type = { value: responseType }; + } + } + proxyApplyFn('fetch', function fetch(context) { + const { callArgs } = context; + const details = callArgs[0] instanceof self.Request + ? callArgs[0] + : Object.assign({ url: callArgs[0] }, callArgs[1]); + let proceed = true; + try { + const props = new Map(); + for ( const prop in details ) { + let v = details[prop]; + if ( typeof v !== 'string' ) { + try { v = safe.JSON_stringify(v); } + catch { } + } + if ( typeof v !== 'string' ) { continue; } + props.set(prop, v); + } + if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) { + const out = Array.from(props).map(a => `${a[0]}:${a[1]}`); + safe.uboLog(logPrefix, `Called: ${out.join('\n')}`); + } + if ( propsToMatch === '' && responseBody === '' ) { + return context.reflect(); + } + proceed = needles.length === 0; + for ( const { key, pattern } of needles ) { + if ( + pattern.expect && props.has(key) === false || + safe.testPattern(pattern, props.get(key)) === false + ) { + proceed = true; + break; + } + } + } catch { + } + if ( proceed ) { + return context.reflect(); + } + return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => { + safe.uboLog(logPrefix, `Prevented with response "${text}"`); + const response = new Response(text, { + headers: { + 'Content-Length': text.length, + } + }); + const props = Object.assign( + { url: { value: details.url } }, + responseProps + ); + safe.Object_defineProperties(response, props); + return response; + }); + }); +} +registerScriptlet(preventFetchFn, { + name: 'prevent-fetch.fn', + dependencies: [ + generateContentFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet prevent-fetch + * + * @description + * Prevent a fetch() call from making a network request to a remote server. + * + * @param propsToMatch + * The fetch arguments to match for the prevention to be triggered. The + * untrusted flavor limits the realm of response to return to safe values. + * + * @param [responseBody] + * Optional. The reponse to return when the prevention occurs. + * + * @param [responseType] + * Optional. The response type to use when emitting a dummy response as a + * result of the prevention. + * + * */ + +function preventFetch(...args) { + preventFetchFn(false, ...args); +} +registerScriptlet(preventFetch, { + name: 'prevent-fetch.js', + aliases: [ + 'no-fetch-if.js', + ], + dependencies: [ + preventFetchFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-prevent-fetch + * + * @description + * Prevent a fetch() call from making a network request to a remote server. + * + * @param propsToMatch + * The fetch arguments to match for the prevention to be triggered. + * + * @param [responseBody] + * Optional. The reponse to return when the prevention occurs. The trusted + * flavor allows to return any response. + * + * @param [responseType] + * Optional. The response type to use when emitting a dummy response as a + * result of the prevention. + * + * */ + +function trustedPreventFetch(...args) { + preventFetchFn(true, ...args); +} +registerScriptlet(trustedPreventFetch, { + name: 'trusted-prevent-fetch.js', + requiresTrust: true, + dependencies: [ + preventFetchFn, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-innerHTML.js b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-innerHTML.js new file mode 100644 index 0000000..587ea20 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-innerHTML.js @@ -0,0 +1,82 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2025-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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/** + * @scriptlet prevent-innerHTML + * + * @description + * Conditionally prevent assignment to `innerHTML` property. + * + * @param [selector] + * Optional. The element must matches `selector` for the prevention to take + * place. + * + * @param [pattern] + * Optional. A pattern to match against the assigned value. The pattern can be + * a plain string, or a regex. Prepend with `!` to reverse the match condition. + * + * */ + +export function preventInnerHTML( + selector = '', + pattern = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-innerHTML', selector, pattern); + const matcher = safe.initPattern(pattern, { canNegate: true }); + const current = safe.Object_getOwnPropertyDescriptor(Element.prototype, 'innerHTML'); + if ( current === undefined ) { return; } + const shouldPreventSet = (elem, a) => { + if ( selector !== '' ) { + if ( typeof elem.matches !== 'function' ) { return false; } + if ( elem.matches(selector) === false ) { return false; } + } + return safe.testPattern(matcher, a); + }; + Object.defineProperty(Element.prototype, 'innerHTML', { + get: function() { + return current.get + ? current.get.call(this) + : current.value; + }, + set: function(a) { + if ( shouldPreventSet(this, a) ) { + safe.uboLog(logPrefix, 'Prevented'); + } else if ( current.set ) { + current.set.call(this, a); + } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Assigned:\n${a}`); + } + current.value = a; + }, + }); +} +registerScriptlet(preventInnerHTML, { + name: 'prevent-innerHTML.js', + dependencies: [ + safeSelf, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-settimeout.js b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-settimeout.js new file mode 100644 index 0000000..289c446 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/prevent-settimeout.js @@ -0,0 +1,236 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2019-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock + +*/ + +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +class RangeParser { + constructor(s) { + this.not = s.charAt(0) === '!'; + if ( this.not ) { s = s.slice(1); } + if ( s === '' ) { return; } + const pos = s.indexOf('-'); + if ( pos !== 0 ) { + this.min = this.max = parseInt(s, 10) || 0; + } + if ( pos !== -1 ) { + this.max = parseInt(s.slice(pos + 1), 10) || Number.MAX_SAFE_INTEGER; + } + } + unbound() { + return this.min === undefined && this.max === undefined; + } + test(v) { + const n = Math.min(Math.max(Number(v) || 0, 0), Number.MAX_SAFE_INTEGER); + if ( this.min === this.max ) { + return (this.min === undefined || n === this.min) !== this.not; + } + if ( this.min === undefined ) { + return (n <= this.max) !== this.not; + } + if ( this.max === undefined ) { + return (n >= this.min) !== this.not; + } + return (n >= this.min && n <= this.max) !== this.not; + } +} +registerScriptlet(RangeParser, { + name: 'range-parser.fn', +}); + +/** + * @scriptlet prevent-setTimeout + * + * @description + * Conditionally prevent execution of the callback function passed to native + * setTimeout method. With no parameters, all calls to setTimeout will be + * shown in the logger. + * + * @param [needle] + * A pattern to match against the stringified callback. The pattern can be a + * plain string, or a regex. Prepend with `!` to reverse the match condition. + * + * @param [delay] + * A value to match against the delay. Can be a single value for exact match, + * or a range: + * - `min-max`: matches if delay >= min and delay <= max + * - `min-`: matches if delay >= min + * - `-max`: matches if delay <= max + * No delay means to match any delay value. + * Prepend with `!` to reverse the match condition. + * + * */ + +export function preventSetTimeout( + needleRaw = '', + delayRaw = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needleRaw, delayRaw); + const needleNot = needleRaw.charAt(0) === '!'; + const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw); + const range = new RangeParser(delayRaw); + proxyApplyFn('setTimeout', function(context) { + const { callArgs } = context; + const a = callArgs[0] instanceof Function + ? safe.String(safe.Function_toString(callArgs[0])) + : safe.String(callArgs[0]); + const b = callArgs[1]; + if ( needleRaw === '' && range.unbound() ) { + safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); + return context.reflect(); + } + if ( reNeedle.test(a) !== needleNot && range.test(b) ) { + callArgs[0] = function(){}; + safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); + } + return context.reflect(); + }); +} +registerScriptlet(preventSetTimeout, { + name: 'prevent-setTimeout.js', + aliases: [ + 'no-setTimeout-if.js', + 'nostif.js', + 'setTimeout-defuser.js', + ], + dependencies: [ + proxyApplyFn, + RangeParser, + safeSelf, + ], +}); + +/** + * @scriptlet prevent-setInterval + * + * @description + * Conditionally prevent execution of the callback function passed to native + * setInterval method. With no parameters, all calls to setInterval will be + * shown in the logger. + * + * @param [needle] + * A pattern to match against the stringified callback. The pattern can be a + * plain string, or a regex. Prepend with `!` to reverse the match condition. + * No pattern means to match anything. + * + * @param [delay] + * A value to match against the delay. Can be a single value for exact match, + * or a range: + * - `min-max`: matches if delay >= min and delay <= max + * - `min-`: matches if delay >= min + * - `-max`: matches if delay <= max + * No delay means to match any delay value. + * Prepend with `!` to reverse the match condition. + * + * */ + +export function preventSetInterval( + needleRaw = '', + delayRaw = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-setInterval', needleRaw, delayRaw); + const needleNot = needleRaw.charAt(0) === '!'; + const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw); + const range = new RangeParser(delayRaw); + proxyApplyFn('setInterval', function(context) { + const { callArgs } = context; + const a = callArgs[0] instanceof Function + ? safe.String(safe.Function_toString(callArgs[0])) + : safe.String(callArgs[0]); + const b = callArgs[1]; + if ( needleRaw === '' && range.unbound() ) { + safe.uboLog(logPrefix, `Called:\n${a}\n${b}`); + return context.reflect(); + } + if ( reNeedle.test(a) !== needleNot && range.test(b) ) { + callArgs[0] = function(){}; + safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`); + } + return context.reflect(); + }); +} +registerScriptlet(preventSetInterval, { + name: 'prevent-setInterval.js', + aliases: [ + 'no-setInterval-if.js', + 'nosiif.js', + 'setInterval-defuser.js', + ], + dependencies: [ + proxyApplyFn, + RangeParser, + safeSelf, + ], +}); + +/** + * @scriptlet prevent-requestAnimationFrame + * + * @description + * Conditionally prevent execution of the callback function passed to native + * requestAnimationFrame method. With no parameters, all calls to + * requestAnimationFrame will be shown in the logger. + * + * @param [needle] + * A pattern to match against the stringified callback. The pattern can be a + * plain string, or a regex. + * Prepend with `!` to reverse the match condition. + * + * */ + +export function preventRequestAnimationFrame( + needleRaw = '' +) { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('prevent-requestAnimationFrame', needleRaw); + const needleNot = needleRaw.charAt(0) === '!'; + const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw); + proxyApplyFn('requestAnimationFrame', function(context) { + const { callArgs } = context; + const a = callArgs[0] instanceof Function + ? safe.String(safe.Function_toString(callArgs[0])) + : safe.String(callArgs[0]); + if ( needleRaw === '' ) { + safe.uboLog(logPrefix, `Called:\n${a}`); + } else if ( reNeedle.test(a) !== needleNot ) { + callArgs[0] = function(){}; + safe.uboLog(logPrefix, `Prevented:\n${a}`); + } + return context.reflect(); + }); +} +registerScriptlet(preventRequestAnimationFrame, { + name: 'prevent-requestAnimationFrame.js', + aliases: [ + 'no-requestAnimationFrame-if.js', + 'norafif.js', + ], + dependencies: [ + proxyApplyFn, + safeSelf, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/proxy-apply.js b/data/extensions/uBlock0@raymondhill.net/js/resources/proxy-apply.js new file mode 100644 index 0000000..60326f5 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/proxy-apply.js @@ -0,0 +1,109 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +export function proxyApplyFn( + target = '', + handler = '' +) { + let context = globalThis; + let prop = target; + for (;;) { + const pos = prop.indexOf('.'); + if ( pos === -1 ) { break; } + context = context[prop.slice(0, pos)]; + if ( context instanceof Object === false ) { return; } + prop = prop.slice(pos+1); + } + const fn = context[prop]; + if ( typeof fn !== 'function' ) { return; } + if ( proxyApplyFn.CtorContext === undefined ) { + proxyApplyFn.ctorContexts = []; + proxyApplyFn.CtorContext = class { + constructor(...args) { + this.init(...args); + } + init(callFn, callArgs) { + this.callFn = callFn; + this.callArgs = callArgs; + return this; + } + reflect() { + const r = Reflect.construct(this.callFn, this.callArgs); + this.callFn = this.callArgs = this.private = undefined; + proxyApplyFn.ctorContexts.push(this); + return r; + } + static factory(...args) { + return proxyApplyFn.ctorContexts.length !== 0 + ? proxyApplyFn.ctorContexts.pop().init(...args) + : new proxyApplyFn.CtorContext(...args); + } + }; + proxyApplyFn.applyContexts = []; + proxyApplyFn.ApplyContext = class { + constructor(...args) { + this.init(...args); + } + init(callFn, thisArg, callArgs) { + this.callFn = callFn; + this.thisArg = thisArg; + this.callArgs = callArgs; + return this; + } + reflect() { + const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs); + this.callFn = this.thisArg = this.callArgs = this.private = undefined; + proxyApplyFn.applyContexts.push(this); + return r; + } + static factory(...args) { + return proxyApplyFn.applyContexts.length !== 0 + ? proxyApplyFn.applyContexts.pop().init(...args) + : new proxyApplyFn.ApplyContext(...args); + } + }; + } + const fnStr = fn.toString(); + const toString = (function toString() { return fnStr; }).bind(null); + const proxyDetails = { + apply(target, thisArg, args) { + return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args)); + }, + get(target, prop) { + if ( prop === 'toString' ) { return toString; } + return Reflect.get(target, prop); + }, + }; + if ( fn.prototype?.constructor === fn ) { + proxyDetails.construct = function(target, args) { + return handler(proxyApplyFn.CtorContext.factory(target, args)); + }; + } + context[prop] = new Proxy(fn, proxyDetails); +} +registerScriptlet(proxyApplyFn, { + name: 'proxy-apply.fn', +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/replace-argument.js b/data/extensions/uBlock0@raymondhill.net/js/resources/replace-argument.js new file mode 100644 index 0000000..638eaee --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/replace-argument.js @@ -0,0 +1,122 @@ +/******************************************************************************* + + 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 { parseReplaceFn } from './parse-replace.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; +import { validateConstantFn } from './set-constant.js'; + +/** + * @scriptlet trusted-replace-argument.js + * + * @description + * Replace an argument passed to a method. Requires a trusted source. + * + * @param propChain + * The property chain to the function which argument must be replaced when + * called. + * + * @param argposRaw + * The zero-based position of the argument in the argument list. Use a negative + * number for a position relative to the last argument. Use literal `this` to + * replace the value used in `prototype`-based methods. + * + * @param argraw + * The replacement value, validated using the same heuristic as with the + * `set-constant.js` scriptlet. + * If the replacement value matches `json:...`, the value will be the + * json-parsed string after `json:`. + * If the replacement value matches `repl:/.../.../`, the target argument will + * be replaced according the regex-replacement directive following `repl:` + * + * @param [, condition, pattern] + * Optional. The replacement will occur only when pattern matches the target + * argument. + * + * */ + +export function trustedReplaceArgument( + propChain = '', + argposRaw = '', + argraw = '' +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw); + const argoffset = parseInt(argposRaw, 10) || 0; + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + const replacer = argraw.startsWith('repl:/') && + parseReplaceFn(argraw.slice(5)) || undefined; + const value = replacer === undefined && + validateConstantFn(true, argraw, extraArgs); + const reCondition = extraArgs.condition + ? safe.patternToRegex(extraArgs.condition) + : /^/; + const getArg = context => { + if ( argposRaw === 'this' ) { return context.thisArg; } + const { callArgs } = context; + const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset; + if ( argpos < 0 || argpos >= callArgs.length ) { return; } + context.private = { argpos }; + return callArgs[argpos]; + }; + const setArg = (context, value) => { + if ( argposRaw === 'this' ) { + if ( value !== context.thisArg ) { + context.thisArg = value; + } + } else if ( context.private ) { + context.callArgs[context.private.argpos] = value; + } + }; + proxyApplyFn(propChain, function(context) { + if ( argposRaw === '' ) { + safe.uboLog(logPrefix, `Arguments:\n${context.callArgs.join('\n')}`); + return context.reflect(); + } + const argBefore = getArg(context); + if ( extraArgs.condition !== undefined ) { + if ( safe.RegExp_test.call(reCondition, argBefore) === false ) { + return context.reflect(); + } + } + const argAfter = replacer && typeof argBefore === 'string' + ? argBefore.replace(replacer.re, replacer.replacement) + : value; + if ( argAfter !== argBefore ) { + setArg(context, argAfter); + safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${argAfter}`); + } + return context.reflect(); + }); +} +registerScriptlet(trustedReplaceArgument, { + name: 'trusted-replace-argument.js', + requiresTrust: true, + dependencies: [ + parseReplaceFn, + proxyApplyFn, + safeSelf, + validateConstantFn, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/run-at.js b/data/extensions/uBlock0@raymondhill.net/js/resources/run-at.js new file mode 100644 index 0000000..35036d2 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/run-at.js @@ -0,0 +1,96 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/* eslint no-prototype-builtins: 0 */ + +/** + * @helperScriptlet run-at.fn + * + * @description + * Execute a function at a specific page-load milestone. + * + * @param fn + * The function to call. + * + * @param when + * An identifier which tells when the function should be executed. + * See <https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState>. + * + * @example + * `runAt(( ) => { start(); }, 'interactive')` + * + * */ + +export function runAt(fn, when) { + const intFromReadyState = state => { + const targets = { + 'loading': 1, 'asap': 1, + 'interactive': 2, 'end': 2, '2': 2, + 'complete': 3, 'idle': 3, '3': 3, + }; + const tokens = Array.isArray(state) ? state : [ state ]; + for ( const token of tokens ) { + const prop = `${token}`; + if ( Object.hasOwn(targets, prop) === false ) { continue; } + return targets[prop]; + } + return 0; + }; + const runAt = intFromReadyState(when); + if ( intFromReadyState(document.readyState) >= runAt ) { + fn(); return; + } + const onStateChange = ( ) => { + if ( intFromReadyState(document.readyState) < runAt ) { return; } + fn(); + safe.removeEventListener.apply(document, args); + }; + const safe = safeSelf(); + const args = [ 'readystatechange', onStateChange, { capture: true } ]; + safe.addEventListener.apply(document, args); +} +registerScriptlet(runAt, { + name: 'run-at.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function runAtHtmlElementFn(fn) { + if ( document.documentElement ) { + fn(); + return; + } + const observer = new MutationObserver(( ) => { + observer.disconnect(); + fn(); + }); + observer.observe(document, { childList: true }); +} +registerScriptlet(runAtHtmlElementFn, { + name: 'run-at-html-element.fn', +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/safe-self.js b/data/extensions/uBlock0@raymondhill.net/js/resources/safe-self.js new file mode 100644 index 0000000..43aec1d --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/safe-self.js @@ -0,0 +1,221 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +// Externally added to the private namespace in which scriptlets execute. +/* global scriptletGlobals */ + +export function safeSelf() { + if ( scriptletGlobals.safeSelf ) { + return scriptletGlobals.safeSelf; + } + const self = globalThis; + const safe = { + 'Array_from': Array.from, + 'Error': self.Error, + 'Function_toStringFn': self.Function.prototype.toString, + 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg), + 'Math_floor': Math.floor, + 'Math_max': Math.max, + 'Math_min': Math.min, + 'Math_random': Math.random, + 'Object': Object, + 'Object_defineProperty': Object.defineProperty.bind(Object), + 'Object_defineProperties': Object.defineProperties.bind(Object), + 'Object_fromEntries': Object.fromEntries.bind(Object), + 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object), + 'Object_hasOwn': Object.hasOwn.bind(Object), + 'RegExp': self.RegExp, + 'RegExp_test': self.RegExp.prototype.test, + 'RegExp_exec': self.RegExp.prototype.exec, + 'Request_clone': self.Request.prototype.clone, + 'String': self.String, + 'String_fromCharCode': String.fromCharCode, + 'String_split': String.prototype.split, + 'XMLHttpRequest': self.XMLHttpRequest, + 'addEventListener': self.EventTarget.prototype.addEventListener, + 'removeEventListener': self.EventTarget.prototype.removeEventListener, + 'fetch': self.fetch, + 'JSON': self.JSON, + 'JSON_parseFn': self.JSON.parse, + 'JSON_stringifyFn': self.JSON.stringify, + 'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args), + 'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args), + 'log': console.log.bind(console), + // Properties + logLevel: 0, + // Methods + makeLogPrefix(...args) { + return this.sendToLogger && `[${args.join(' \u205D ')}]` || ''; + }, + uboLog(...args) { + if ( this.sendToLogger === undefined ) { return; } + if ( args === undefined || args[0] === '' ) { return; } + return this.sendToLogger('info', ...args); + + }, + uboErr(...args) { + if ( this.sendToLogger === undefined ) { return; } + if ( args === undefined || args[0] === '' ) { return; } + return this.sendToLogger('error', ...args); + }, + escapeRegexChars(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }, + initPattern(pattern, options = {}) { + if ( pattern === '' ) { + return { matchAll: true, expect: true }; + } + const expect = (options.canNegate !== true || pattern.startsWith('!') === false); + if ( expect === false ) { + pattern = pattern.slice(1); + } + const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); + if ( match !== null ) { + return { + re: new this.RegExp( + match[1], + match[2] || options.flags + ), + expect, + }; + } + if ( options.flags !== undefined ) { + return { + re: new this.RegExp(this.escapeRegexChars(pattern), + options.flags + ), + expect, + }; + } + return { pattern, expect }; + }, + testPattern(details, haystack) { + if ( details.matchAll ) { return true; } + if ( details.re ) { + return this.RegExp_test.call(details.re, haystack) === details.expect; + } + return haystack.includes(details.pattern) === details.expect; + }, + patternToRegex(pattern, flags = undefined, verbatim = false) { + if ( pattern === '' ) { return /^/; } + const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); + if ( match === null ) { + const reStr = this.escapeRegexChars(pattern); + return new RegExp(verbatim ? `^${reStr}$` : reStr, flags); + } + try { + return new RegExp(match[1], match[2] || undefined); + } + catch { + } + return /^/; + }, + getExtraArgs(args, offset = 0) { + const entries = args.slice(offset).reduce((out, v, i, a) => { + if ( (i & 1) === 0 ) { + const rawValue = a[i+1]; + const value = /^\d+$/.test(rawValue) + ? parseInt(rawValue, 10) + : rawValue; + out.push([ a[i], value ]); + } + return out; + }, []); + return this.Object_fromEntries(entries); + }, + onIdle(fn, options) { + if ( self.requestIdleCallback ) { + return self.requestIdleCallback(fn, options); + } + return self.requestAnimationFrame(fn); + }, + offIdle(id) { + if ( self.requestIdleCallback ) { + return self.cancelIdleCallback(id); + } + return self.cancelAnimationFrame(id); + } + }; + scriptletGlobals.safeSelf = safe; + if ( scriptletGlobals.bcSecret === undefined ) { return safe; } + // This is executed only when the logger is opened + safe.logLevel = scriptletGlobals.logLevel || 1; + let lastLogType = ''; + let lastLogText = ''; + let lastLogTime = 0; + safe.toLogText = (type, ...args) => { + if ( args.length === 0 ) { return; } + const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`; + if ( text === lastLogText && type === lastLogType ) { + if ( (Date.now() - lastLogTime) < 5000 ) { return; } + } + lastLogType = type; + lastLogText = text; + lastLogTime = Date.now(); + return text; + }; + try { + const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret); + let bcBuffer = []; + safe.sendToLogger = (type, ...args) => { + const text = safe.toLogText(type, ...args); + if ( text === undefined ) { return; } + if ( bcBuffer === undefined ) { + return bc.postMessage({ what: 'messageToLogger', type, text }); + } + bcBuffer.push({ type, text }); + }; + bc.onmessage = ev => { + const msg = ev.data; + switch ( msg ) { + case 'iamready!': + if ( bcBuffer === undefined ) { break; } + bcBuffer.forEach(({ type, text }) => + bc.postMessage({ what: 'messageToLogger', type, text }) + ); + bcBuffer = undefined; + break; + case 'setScriptletLogLevelToOne': + safe.logLevel = 1; + break; + case 'setScriptletLogLevelToTwo': + safe.logLevel = 2; + break; + } + }; + bc.postMessage('areyouready?'); + } catch { + safe.sendToLogger = (type, ...args) => { + const text = safe.toLogText(type, ...args); + if ( text === undefined ) { return; } + safe.log(`uBO ${text}`); + }; + } + return safe; +} +registerScriptlet(safeSelf, { + name: 'safe-self.fn', +}); 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); + }); +} + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/set-constant.js b/data/extensions/uBlock0@raymondhill.net/js/resources/set-constant.js new file mode 100644 index 0000000..9ee9859 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/set-constant.js @@ -0,0 +1,287 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { runAt } from './run-at.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function validateConstantFn(trusted, raw, extraArgs = {}) { + const safe = safeSelf(); + let value; + if ( raw === 'undefined' ) { + value = undefined; + } else if ( raw === 'false' ) { + value = false; + } else if ( raw === 'true' ) { + value = true; + } else if ( raw === 'null' ) { + value = null; + } else if ( raw === "''" || raw === '' ) { + value = ''; + } else if ( raw === '[]' || raw === 'emptyArr' ) { + value = []; + } else if ( raw === '{}' || raw === 'emptyObj' ) { + value = {}; + } else if ( raw === 'noopFunc' ) { + value = function(){}; + } else if ( raw === 'trueFunc' ) { + value = function(){ return true; }; + } else if ( raw === 'falseFunc' ) { + value = function(){ return false; }; + } else if ( raw === 'throwFunc' ) { + value = function(){ throw ''; }; + } else if ( /^-?\d+$/.test(raw) ) { + value = parseInt(raw); + if ( isNaN(raw) ) { return; } + if ( Math.abs(raw) > 0x7FFF ) { return; } + } else if ( trusted ) { + if ( raw.startsWith('json:') ) { + try { value = safe.JSON_parse(raw.slice(5)); } catch { return; } + } else if ( raw.startsWith('{') && raw.endsWith('}') ) { + try { value = safe.JSON_parse(raw).value; } catch { return; } + } + } else { + return; + } + if ( extraArgs.as !== undefined ) { + if ( extraArgs.as === 'function' ) { + return ( ) => value; + } else if ( extraArgs.as === 'callback' ) { + return ( ) => (( ) => value); + } else if ( extraArgs.as === 'resolved' ) { + return Promise.resolve(value); + } else if ( extraArgs.as === 'rejected' ) { + return Promise.reject(value); + } + } + return value; +} +registerScriptlet(validateConstantFn, { + name: 'validate-constant.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function setConstantFn( + trusted = false, + chain = '', + rawValue = '' +) { + if ( chain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); + function setConstant(chain, rawValue) { + const trappedProp = (( ) => { + const pos = chain.lastIndexOf('.'); + if ( pos === -1 ) { return chain; } + return chain.slice(pos+1); + })(); + const cloakFunc = fn => { + safe.Object_defineProperty(fn, 'name', { value: trappedProp }); + return new Proxy(fn, { + defineProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.defineProperty(...arguments); + } + return true; + }, + deleteProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.deleteProperty(...arguments); + } + return true; + }, + get(target, prop) { + if ( prop === 'toString' ) { + return function() { + return `function ${trappedProp}() { [native code] }`; + }.bind(null); + } + return Reflect.get(...arguments); + }, + }); + }; + if ( trappedProp === '' ) { return; } + const thisScript = document.currentScript; + let normalValue = validateConstantFn(trusted, rawValue, extraArgs); + if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) { + normalValue = cloakFunc(normalValue); + } + let aborted = false; + const mustAbort = function(v) { + if ( trusted ) { return false; } + if ( aborted ) { return true; } + aborted = + (v !== undefined && v !== null) && + (normalValue !== undefined && normalValue !== null) && + (typeof v !== typeof normalValue); + if ( aborted ) { + safe.uboLog(logPrefix, `Aborted because value set to ${v}`); + } + return aborted; + }; + // https://github.com/uBlockOrigin/uBlock-issues/issues/156 + // Support multiple trappers for the same property. + const trapProp = function(owner, prop, configurable, handler) { + if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; } + const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); + let prevGetter, prevSetter; + if ( odesc instanceof safe.Object ) { + owner[prop] = normalValue; + if ( odesc.get instanceof Function ) { + prevGetter = odesc.get; + } + if ( odesc.set instanceof Function ) { + prevSetter = odesc.set; + } + } + try { + safe.Object_defineProperty(owner, prop, { + configurable, + get() { + if ( prevGetter !== undefined ) { + prevGetter(); + } + return handler.getter(); + }, + set(a) { + if ( prevSetter !== undefined ) { + prevSetter(a); + } + handler.setter(a); + } + }); + safe.uboLog(logPrefix, 'Trap installed'); + } catch(ex) { + safe.uboErr(logPrefix, ex); + } + }; + const trapChain = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + trapProp(owner, chain, false, { + v: undefined, + init: function(v) { + if ( mustAbort(v) ) { return false; } + this.v = v; + return true; + }, + getter: function() { + if ( document.currentScript === thisScript ) { + return this.v; + } + safe.uboLog(logPrefix, 'Property read'); + return normalValue; + }, + setter: function(a) { + if ( mustAbort(a) === false ) { return; } + normalValue = a; + } + }); + return; + } + const prop = chain.slice(0, pos); + const v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) { + trapChain(v, chain); + return; + } + trapProp(owner, prop, true, { + v: undefined, + init: function(v) { + this.v = v; + return true; + }, + getter: function() { + return this.v; + }, + setter: function(a) { + this.v = a; + if ( a instanceof safe.Object ) { + trapChain(a, chain); + } + } + }); + }; + trapChain(window, chain); + } + runAt(( ) => { + setConstant(chain, rawValue); + }, extraArgs.runAt); +} +registerScriptlet(setConstantFn, { + name: 'set-constant.fn', + dependencies: [ + runAt, + safeSelf, + validateConstantFn, + ], +}); + +/******************************************************************************/ + +export function setConstant( + ...args +) { + setConstantFn(false, ...args); +} +registerScriptlet(setConstant, { + name: 'set-constant.js', + aliases: [ + 'set.js', + ], + dependencies: [ + setConstantFn, + ], +}); + +/******************************************************************************* + * + * trusted-set-constant.js + * + * Set specified property to any value. This is essentially the same as + * set-constant.js, but with no restriction as to which values can be used. + * + **/ + +export function trustedSetConstant( + ...args +) { + setConstantFn(true, ...args); +} +registerScriptlet(trustedSetConstant, { + name: 'trusted-set-constant.js', + requiresTrust: true, + aliases: [ + 'trusted-set.js', + ], + dependencies: [ + setConstantFn, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/shared.js b/data/extensions/uBlock0@raymondhill.net/js/resources/shared.js new file mode 100644 index 0000000..01a9de7 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/shared.js @@ -0,0 +1,40 @@ +/******************************************************************************* + + 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 + +*/ + +// Code imported from main code base and exposed as injectable scriptlets +import { ArglistParser as __ArglistParser__ } from '../arglist-parser.js'; +import { JSONPath as __JSONPath__ } from '../jsonpath.js'; +import { registerScriptlet } from './base.js'; + +/******************************************************************************/ + +export const ArglistParser = __ArglistParser__; + +registerScriptlet(ArglistParser, { + name: 'arglist-parser.fn', +}); + +export const JSONPath = __JSONPath__; + +registerScriptlet(JSONPath, { + name: 'jsonpath.fn', +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/spoof-css.js b/data/extensions/uBlock0@raymondhill.net/js/resources/spoof-css.js new file mode 100644 index 0000000..7cc7b9f --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/spoof-css.js @@ -0,0 +1,163 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/** + * @scriptlet spoof-css.js + * + * @description + * Spoof the value of CSS properties. + * + * @param selector + * A CSS selector for the element(s) to target. + * + * @param [property, value, ...] + * A list of property-value pairs of the style properties to spoof to the + * specified values. +* + * */ + +export function spoofCSS( + selector, + ...args +) { + if ( typeof selector !== 'string' ) { return; } + if ( selector === '' ) { return; } + const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase()); + const propToValueMap = new Map(); + const privatePropToValueMap = new Map(); + for ( let i = 0; i < args.length; i += 2 ) { + const prop = toCamelCase(args[i+0]); + if ( prop === '' ) { break; } + const value = args[i+1]; + if ( typeof value !== 'string' ) { break; } + if ( prop.charCodeAt(0) === 0x5F /* _ */ ) { + privatePropToValueMap.set(prop, value); + } else { + propToValueMap.set(prop, value); + } + } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args); + const instanceProperties = [ 'cssText', 'length', 'parentRule' ]; + const spoofStyle = (prop, real) => { + const normalProp = toCamelCase(prop); + const shouldSpoof = propToValueMap.has(normalProp); + const value = shouldSpoof ? propToValueMap.get(normalProp) : real; + if ( shouldSpoof ) { + safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`); + } + return value; + }; + const cloackFunc = (fn, thisArg, name) => { + const trap = fn.bind(thisArg); + Object.defineProperty(trap, 'name', { value: name }); + Object.defineProperty(trap, 'toString', { + value: ( ) => `function ${name}() { [native code] }` + }); + return trap; + }; + self.getComputedStyle = new Proxy(self.getComputedStyle, { + apply: function(target, thisArg, args) { + // eslint-disable-next-line no-debugger + if ( privatePropToValueMap.has('_debug') ) { debugger; } + const style = Reflect.apply(target, thisArg, args); + const targetElements = new WeakSet(document.querySelectorAll(selector)); + if ( targetElements.has(args[0]) === false ) { return style; } + const proxiedStyle = new Proxy(style, { + get(target, prop) { + if ( typeof target[prop] === 'function' ) { + if ( prop === 'getPropertyValue' ) { + return cloackFunc(function getPropertyValue(prop) { + return spoofStyle(prop, target[prop]); + }, target, 'getPropertyValue'); + } + return cloackFunc(target[prop], target, prop); + } + if ( instanceProperties.includes(prop) ) { + return Reflect.get(target, prop); + } + return spoofStyle(prop, Reflect.get(target, prop)); + }, + getOwnPropertyDescriptor(target, prop) { + if ( propToValueMap.has(prop) ) { + return { + configurable: true, + enumerable: true, + value: propToValueMap.get(prop), + writable: true, + }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, + }); + return proxiedStyle; + }, + get(target, prop) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop); + }, + }); + Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, { + apply: function(target, thisArg, args) { + // eslint-disable-next-line no-debugger + if ( privatePropToValueMap.has('_debug') ) { debugger; } + const rect = Reflect.apply(target, thisArg, args); + const targetElements = new WeakSet(document.querySelectorAll(selector)); + if ( targetElements.has(thisArg) === false ) { return rect; } + let { x, y, height, width } = rect; + if ( privatePropToValueMap.has('_rectx') ) { + x = parseFloat(privatePropToValueMap.get('_rectx')); + } + if ( privatePropToValueMap.has('_recty') ) { + y = parseFloat(privatePropToValueMap.get('_recty')); + } + if ( privatePropToValueMap.has('_rectw') ) { + width = parseFloat(privatePropToValueMap.get('_rectw')); + } else if ( propToValueMap.has('width') ) { + width = parseFloat(propToValueMap.get('width')); + } + if ( privatePropToValueMap.has('_recth') ) { + height = parseFloat(privatePropToValueMap.get('_recth')); + } else if ( propToValueMap.has('height') ) { + height = parseFloat(propToValueMap.get('height')); + } + return new self.DOMRect(x, y, width, height); + }, + get(target, prop) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop); + }, + }); +} +registerScriptlet(spoofCSS, { + name: 'spoof-css.js', + dependencies: [ + safeSelf, + ], +}); diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/stack-trace.js b/data/extensions/uBlock0@raymondhill.net/js/resources/stack-trace.js new file mode 100644 index 0000000..c2e5358 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/stack-trace.js @@ -0,0 +1,148 @@ +/******************************************************************************* + + 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 { getExceptionTokenFn } from './utils.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ + +export function matchesStackTraceFn( + needleDetails, + logLevel = '' +) { + const safe = safeSelf(); + const exceptionToken = getExceptionTokenFn(); + const error = new safe.Error(exceptionToken); + const docURL = new URL(self.location.href); + docURL.hash = ''; + // Normalize stack trace + const reLine = /(.*?@)?(\S+)(:\d+):\d+\)?$/; + const lines = []; + for ( let line of safe.String_split.call(error.stack, /[\n\r]+/) ) { + if ( line.includes(exceptionToken) ) { continue; } + line = line.trim(); + const match = safe.RegExp_exec.call(reLine, line); + if ( match === null ) { continue; } + let url = match[2]; + if ( url.startsWith('(') ) { url = url.slice(1); } + if ( url === docURL.href ) { + url = 'inlineScript'; + } else if ( url.startsWith('<anonymous>') ) { + url = 'injectedScript'; + } + let fn = match[1] !== undefined + ? match[1].slice(0, -1) + : line.slice(0, match.index).trim(); + if ( fn.startsWith('at') ) { fn = fn.slice(2).trim(); } + let rowcol = match[3]; + lines.push(' ' + `${fn} ${url}${rowcol}:1`.trim()); + } + lines[0] = `stackDepth:${lines.length-1}`; + const stack = lines.join('\t'); + const r = needleDetails.matchAll !== true && + safe.testPattern(needleDetails, stack); + if ( + logLevel === 'all' || + logLevel === 'match' && r || + logLevel === 'nomatch' && !r + ) { + safe.uboLog(stack.replace(/\t/g, '\n')); + } + return r; +} +registerScriptlet(matchesStackTraceFn, { + name: 'matches-stack-trace.fn', + dependencies: [ + getExceptionTokenFn, + safeSelf, + ], +}); + +/******************************************************************************/ + +function abortOnStackTrace( + chain = '', + needle = '' +) { + if ( typeof chain !== 'string' ) { return; } + const safe = safeSelf(); + const needleDetails = safe.initPattern(needle, { canNegate: true }); + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + if ( needle === '' ) { extraArgs.log = 'all'; } + const makeProxy = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + let v = owner[chain]; + Object.defineProperty(owner, chain, { + get: function() { + const log = safe.logLevel > 1 ? 'all' : 'match'; + if ( matchesStackTraceFn(needleDetails, log) ) { + throw new ReferenceError(getExceptionTokenFn()); + } + return v; + }, + set: function(a) { + const log = safe.logLevel > 1 ? 'all' : 'match'; + if ( matchesStackTraceFn(needleDetails, log) ) { + throw new ReferenceError(getExceptionTokenFn()); + } + v = a; + }, + }); + return; + } + const prop = chain.slice(0, pos); + let v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v ) { + makeProxy(v, chain); + return; + } + const desc = Object.getOwnPropertyDescriptor(owner, prop); + if ( desc && desc.set !== undefined ) { return; } + Object.defineProperty(owner, prop, { + get: function() { return v; }, + set: function(a) { + v = a; + if ( a instanceof Object ) { + makeProxy(a, chain); + } + } + }); + }; + const owner = window; + makeProxy(owner, chain); +} +registerScriptlet(abortOnStackTrace, { + name: 'abort-on-stack-trace.js', + aliases: [ + 'aost.js', + ], + dependencies: [ + getExceptionTokenFn, + matchesStackTraceFn, + safeSelf, + ], +}); + +/******************************************************************************/ diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/utils.js b/data/extensions/uBlock0@raymondhill.net/js/resources/utils.js new file mode 100644 index 0000000..f448495 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/utils.js @@ -0,0 +1,192 @@ +/******************************************************************************* + + 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 { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +// Externally added to the private namespace in which scriptlets execute. +/* global scriptletGlobals */ + +/******************************************************************************/ + +export function getRandomTokenFn() { + const safe = safeSelf(); + return safe.String_fromCharCode(Date.now() % 26 + 97) + + safe.Math_floor(safe.Math_random() * 982451653 + 982451653).toString(36); +} +registerScriptlet(getRandomTokenFn, { + name: 'get-random-token.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function getExceptionTokenFn() { + const token = getRandomTokenFn(); + const oe = self.onerror; + self.onerror = function(msg, ...args) { + if ( typeof msg === 'string' && msg.includes(token) ) { return true; } + if ( oe instanceof Function ) { + return oe.call(this, msg, ...args); + } + }.bind(); + return token; +} +registerScriptlet(getExceptionTokenFn, { + name: 'get-exception-token.fn', + dependencies: [ + getRandomTokenFn, + ], +}); + +/******************************************************************************/ + +export function parsePropertiesToMatchFn(propsToMatch, implicit = '') { + const safe = safeSelf(); + const needles = new Map(); + if ( propsToMatch === undefined || propsToMatch === '' ) { return needles; } + const options = { canNegate: true }; + for ( const needle of safe.String_split.call(propsToMatch, /\s+/) ) { + let [ prop, pattern ] = safe.String_split.call(needle, ':'); + if ( prop === '' ) { continue; } + if ( pattern !== undefined && /[^$\w -]/.test(prop) ) { + prop = `${prop}:${pattern}`; + pattern = undefined; + } + if ( pattern !== undefined ) { + needles.set(prop, safe.initPattern(pattern, options)); + } else if ( implicit !== '' ) { + needles.set(implicit, safe.initPattern(prop, options)); + } + } + return needles; +} +registerScriptlet(parsePropertiesToMatchFn, { + name: 'parse-properties-to-match.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +export function matchObjectPropertiesFn(propNeedles, ...objs) { + const safe = safeSelf(); + const matched = []; + for ( const obj of objs ) { + if ( obj instanceof Object === false ) { continue; } + for ( const [ prop, details ] of propNeedles ) { + let value = obj[prop]; + if ( value === undefined ) { continue; } + if ( typeof value !== 'string' ) { + try { value = safe.JSON_stringify(value); } + catch { } + if ( typeof value !== 'string' ) { continue; } + } + if ( safe.testPattern(details, value) === false ) { return; } + matched.push(`${prop}: ${value}`); + } + } + return matched; +} +registerScriptlet(matchObjectPropertiesFn, { + name: 'match-object-properties.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ + +// Reference: +// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr +// +// Added `trusted` argument to allow for returning arbitrary text. Can only +// be used through scriptlets requiring trusted source. + +export function generateContentFn(trusted, directive) { + const safe = safeSelf(); + const randomize = len => { + const chunks = []; + let textSize = 0; + do { + const s = safe.Math_random().toString(36).slice(2); + chunks.push(s); + textSize += s.length; + } + while ( textSize < len ); + return chunks.join(' ').slice(0, len); + }; + if ( directive === 'true' ) { + return randomize(10); + } + if ( directive === 'emptyObj' ) { + return '{}'; + } + if ( directive === 'emptyArr' ) { + return '[]'; + } + if ( directive === 'emptyStr' ) { + return ''; + } + if ( directive.startsWith('length:') ) { + const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive); + if ( match === null ) { return ''; } + const min = parseInt(match[1], 10); + const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min; + const len = safe.Math_min(min + extent * safe.Math_random(), 500000); + return randomize(len | 0); + } + if ( directive.startsWith('war:') ) { + if ( scriptletGlobals.warOrigin === undefined ) { return ''; } + return new Promise(resolve => { + const warOrigin = scriptletGlobals.warOrigin; + const warName = directive.slice(4); + const fullpath = [ warOrigin, '/', warName ]; + const warSecret = scriptletGlobals.warSecret; + if ( warSecret !== undefined ) { + fullpath.push('?secret=', warSecret); + } + const warXHR = new safe.XMLHttpRequest(); + warXHR.responseType = 'text'; + warXHR.onloadend = ev => { + resolve(ev.target.responseText || ''); + }; + warXHR.open('GET', fullpath.join('')); + warXHR.send(); + }).catch(( ) => ''); + } + if ( trusted ) { + return directive; + } + return ''; +} +registerScriptlet(generateContentFn, { + name: 'generate-content.fn', + dependencies: [ + safeSelf, + ], +}); + +/******************************************************************************/ |