mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
As discussed with filter list maintainers. * @scriptlet trusted-create-element * * @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.
2594 lines
89 KiB
JavaScript
Executable file
2594 lines
89 KiB
JavaScript
Executable file
/*******************************************************************************
|
||
|
||
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-element.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);
|
||
});
|
||
}
|
||
|
||
/******************************************************************************/
|