summaryrefslogtreecommitdiff
path: root/data/extensions/uBlock0@raymondhill.net/js/resources
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/resources')
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/attribute.js305
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/base.js38
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/cookie.js441
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/create-html.js113
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/href-sanitizer.js188
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js1097
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/json-prune.js275
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/localstorage.js235
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/noeval.js58
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/object-prune.js272
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/parse-replace.js54
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/prevent-fetch.js208
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/prevent-innerHTML.js82
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/prevent-settimeout.js236
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/proxy-apply.js109
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/replace-argument.js122
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/run-at.js96
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/safe-self.js221
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/scriptlets.js2594
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/set-constant.js287
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/shared.js40
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/spoof-css.js163
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/stack-trace.js148
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/resources/utils.js192
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,
+ ],
+});
+
+/******************************************************************************/