Big Change Update

This commit is contained in:
yeongpin
2025-01-14 14:47:41 +08:00
parent 380ea0b81d
commit 19fe4c85f8
651 changed files with 366654 additions and 17 deletions

View File

@@ -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(ex) {
}
};
const mutationHandler = mutations => {
if ( timerId !== undefined ) { return; }
let skip = true;
for ( let i = 0; i < mutations.length && skip; i++ ) {
const { type, addedNodes, removedNodes } = mutations[i];
if ( type === 'attributes' ) { skip = false; }
for ( let j = 0; j < addedNodes.length && skip; j++ ) {
if ( addedNodes[j].nodeType === 1 ) { skip = false; break; }
}
for ( let j = 0; j < removedNodes.length && skip; j++ ) {
if ( removedNodes[j].nodeType === 1 ) { skip = false; break; }
}
}
if ( skip ) { return; }
asap ? rmattr() : rmattrAsync();
};
const start = ( ) => {
rmattr();
if ( /\bstay\b/.test(behavior) === false ) { return; }
const observer = new MutationObserver(mutationHandler);
observer.observe(document, {
attributes: true,
attributeFilter: tokens,
childList: true,
subtree: true,
});
};
runAt(( ) => { start(); }, safe.String_split.call(behavior, /\s+/));
}
registerScriptlet(removeAttr, {
name: 'remove-attr.js',
aliases: [
'ra.js',
],
dependencies: [
runAt,
safeSelf,
],
});
/******************************************************************************/

View File

@@ -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);
};

View File

@@ -0,0 +1,419 @@
/*******************************************************************************
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',
];
}
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 ) {
cookieParts.push(`; domain=${options.domain}`);
}
cookieParts.push('; Secure');
} else if ( /^__(Host|Secure)-/.test(name) ) {
cookieParts.push('; Secure');
}
try {
document.cookie = cookieParts.join('');
} catch(_) {
}
const done = getCookieFn(name) === value;
if ( done && options.reload ) {
window.location.reload();
}
return done;
}
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 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=' + document.location.hostname;
const part2b = '; domain=.' + document.location.hostname;
let part2c, part2d;
const domain = document.domain;
if ( domain ) {
if ( domain !== document.location.hostname ) {
part2c = '; domain=.' + domain;
}
if ( domain.startsWith('www.') ) {
part2d = '; domain=' + domain.replace('www', '');
}
}
const part3 = '; path=/';
const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT';
document.cookie = part1 + part4;
document.cookie = part1 + part2a + part4;
document.cookie = part1 + part2b + part4;
document.cookie = part1 + part3 + part4;
document.cookie = part1 + part2a + part3 + part4;
document.cookie = part1 + part2b + part3 + part4;
if ( part2c !== undefined ) {
document.cookie = part1 + part2c + part3 + part4;
}
if ( part2d !== undefined ) {
document.cookie = part1 + part2d + part3 + part4;
}
});
};
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,
],
});
/******************************************************************************/

View File

@@ -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(ex) {
}
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(ex) {
}
return '';
};
const extractParam = (href, source) => {
if ( Boolean(source) === false ) { return href; }
const recursive = source.includes('?', 1);
const end = recursive ? source.indexOf('?', 1) : source.length;
try {
const url = new URL(href, document.location);
let value = url.searchParams.get(source.slice(1, end));
if ( value === null ) { return href }
if ( recursive ) { return extractParam(value, source.slice(end)); }
return value;
} catch(x) {
}
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(ex) {
return false;
}
for ( const elem of elems ) {
if ( elem.localName !== 'a' ) { continue; }
if ( elem.hasAttribute('href') === false ) { continue; }
const href = elem.getAttribute('href');
const text = 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,
],
});

View File

@@ -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(ex) {
}
}
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,
],
});

View File

@@ -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 { createArglistParser } from './shared.js';
import { registerScriptlet } from './base.js';
/******************************************************************************/
export function parseReplaceFn(s) {
if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; }
const parser = createArglistParser('/');
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: [
createArglistParser,
],
});

View File

@@ -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(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
? String(safe.Function_toString(callArgs[0]))
: 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
? String(safe.Function_toString(callArgs[0]))
: 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
? String(safe.Function_toString(callArgs[0]))
: 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,
],
});

View File

@@ -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',
});

View File

@@ -0,0 +1,120 @@
/*******************************************************************************
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) || undefined;
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 ( 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,
],
});

View File

@@ -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 ( targets.hasOwnProperty(prop) === false ) { continue; }
return targets[prop];
}
return 0;
};
const runAt = intFromReadyState(when);
if ( intFromReadyState(document.readyState) >= runAt ) {
fn(); return;
}
const onStateChange = ( ) => {
if ( intFromReadyState(document.readyState) < runAt ) { return; }
fn();
safe.removeEventListener.apply(document, args);
};
const safe = safeSelf();
const args = [ 'readystatechange', onStateChange, { capture: true } ];
safe.addEventListener.apply(document, args);
}
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',
});

View File

@@ -0,0 +1,219 @@
/*******************************************************************************
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),
'RegExp': self.RegExp,
'RegExp_test': self.RegExp.prototype.test,
'RegExp_exec': self.RegExp.prototype.exec,
'Request_clone': self.Request.prototype.clone,
'String_fromCharCode': String.fromCharCode,
'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(ex) {
}
return /^/;
},
getExtraArgs(args, offset = 0) {
const entries = args.slice(offset).reduce((out, v, i, a) => {
if ( (i & 1) === 0 ) {
const rawValue = a[i+1];
const value = /^\d+$/.test(rawValue)
? parseInt(rawValue, 10)
: rawValue;
out.push([ a[i], value ]);
}
return out;
}, []);
return this.Object_fromEntries(entries);
},
onIdle(fn, options) {
if ( self.requestIdleCallback ) {
return self.requestIdleCallback(fn, options);
}
return self.requestAnimationFrame(fn);
},
offIdle(id) {
if ( self.requestIdleCallback ) {
return self.cancelIdleCallback(id);
}
return self.cancelAnimationFrame(id);
}
};
scriptletGlobals.safeSelf = safe;
if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
// This is executed only when the logger is opened
safe.logLevel = scriptletGlobals.logLevel || 1;
let lastLogType = '';
let lastLogText = '';
let lastLogTime = 0;
safe.toLogText = (type, ...args) => {
if ( args.length === 0 ) { return; }
const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
if ( text === lastLogText && type === lastLogType ) {
if ( (Date.now() - lastLogTime) < 5000 ) { return; }
}
lastLogType = type;
lastLogText = text;
lastLogTime = Date.now();
return text;
};
try {
const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
let bcBuffer = [];
safe.sendToLogger = (type, ...args) => {
const text = safe.toLogText(type, ...args);
if ( text === undefined ) { return; }
if ( bcBuffer === undefined ) {
return bc.postMessage({ what: 'messageToLogger', type, text });
}
bcBuffer.push({ type, text });
};
bc.onmessage = ev => {
const msg = ev.data;
switch ( msg ) {
case 'iamready!':
if ( bcBuffer === undefined ) { break; }
bcBuffer.forEach(({ type, text }) =>
bc.postMessage({ what: 'messageToLogger', type, text })
);
bcBuffer = undefined;
break;
case 'setScriptletLogLevelToOne':
safe.logLevel = 1;
break;
case 'setScriptletLogLevelToTwo':
safe.logLevel = 2;
break;
}
};
bc.postMessage('areyouready?');
} catch(_) {
safe.sendToLogger = (type, ...args) => {
const text = safe.toLogText(type, ...args);
if ( text === undefined ) { return; }
safe.log(`uBO ${text}`);
};
}
return safe;
}
registerScriptlet(safeSelf, {
name: 'safe-self.fn',
});

File diff suppressed because it is too large Load Diff

View File

@@ -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(ex) { return; }
} else if ( raw.startsWith('{') && raw.endsWith('}') ) {
try { value = safe.JSON_parse(raw).value; } catch(ex) { return; }
}
} else {
return;
}
if ( extraArgs.as !== undefined ) {
if ( extraArgs.as === 'function' ) {
return ( ) => value;
} else if ( extraArgs.as === 'callback' ) {
return ( ) => (( ) => value);
} else if ( extraArgs.as === 'resolved' ) {
return Promise.resolve(value);
} else if ( extraArgs.as === 'rejected' ) {
return Promise.reject(value);
}
}
return value;
}
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,
],
});

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
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 } from '../arglist-parser.js';
import { registerScriptlet } from './base.js';
/******************************************************************************/
registerScriptlet(ArglistParser, {
name: 'arglist-parser.fn',
});
/******************************************************************************/
export function createArglistParser(...args) {
return new ArglistParser(...args);
}
registerScriptlet(createArglistParser, {
name: 'create-arglist-parser.fn',
dependencies: [
ArglistParser,
],
});

View File

@@ -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,
],
});