summaryrefslogtreecommitdiffstats
path: root/src/js/contentscript.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/contentscript.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/contentscript.js')
-rw-r--r--src/js/contentscript.js1364
1 files changed, 1364 insertions, 0 deletions
diff --git a/src/js/contentscript.js b/src/js/contentscript.js
new file mode 100644
index 0000000..8f3a4cf
--- /dev/null
+++ b/src/js/contentscript.js
@@ -0,0 +1,1364 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-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
+*/
+
+'use strict';
+
+/*******************************************************************************
+
+ +--> domCollapser
+ |
+ |
+ domWatcher--+
+ | +-- domSurveyor
+ | |
+ +--> domFilterer --+-- [domLogger]
+ | |
+ | +-- [domInspector]
+ |
+ [domProceduralFilterer]
+
+ domWatcher:
+ Watches for changes in the DOM, and notify the other components about these
+ changes.
+
+ domCollapser:
+ Enforces the collapsing of DOM elements for which a corresponding
+ resource was blocked through network filtering.
+
+ domFilterer:
+ Enforces the filtering of DOM elements, by feeding it cosmetic filters.
+
+ domProceduralFilterer:
+ Enforce the filtering of DOM elements through procedural cosmetic filters.
+ Loaded on demand, only when needed.
+
+ domSurveyor:
+ Surveys the DOM to find new cosmetic filters to apply to the current page.
+
+ domLogger:
+ Surveys the page to find and report the injected cosmetic filters blocking
+ actual elements on the current page. This component is dynamically loaded
+ IF AND ONLY IF uBO's logger is opened.
+
+ If page is whitelisted:
+ - domWatcher: off
+ - domCollapser: off
+ - domFilterer: off
+ - domSurveyor: off
+ - domLogger: off
+
+ I verified that the code in this file is completely flushed out of memory
+ when a page is whitelisted.
+
+ If cosmetic filtering is disabled:
+ - domWatcher: on
+ - domCollapser: on
+ - domFilterer: off
+ - domSurveyor: off
+ - domLogger: off
+
+ If generic cosmetic filtering is disabled:
+ - domWatcher: on
+ - domCollapser: on
+ - domFilterer: on
+ - domSurveyor: off
+ - domLogger: on if uBO logger is opened
+
+ If generic cosmetic filtering is enabled:
+ - domWatcher: on
+ - domCollapser: on
+ - domFilterer: on
+ - domSurveyor: on
+ - domLogger: on if uBO logger is opened
+
+ Additionally, the domSurveyor can turn itself off once it decides that
+ it has become pointless (repeatedly not finding new cosmetic filters).
+
+ The domFilterer makes use of platform-dependent user stylesheets[1].
+
+ [1] "user stylesheets" refer to local CSS rules which have priority over,
+ and can't be overridden by a web page's own CSS rules.
+
+*/
+
+// Abort execution if our global vAPI object does not exist.
+// https://github.com/chrisaljoudi/uBlock/issues/456
+// https://github.com/gorhill/uBlock/issues/2029
+
+ // >>>>>>>> start of HUGE-IF-BLOCK
+if ( typeof vAPI === 'object' && !vAPI.contentScript ) {
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.contentScript = true;
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-663657508
+{
+ let context = self;
+ try {
+ while (
+ context !== self.top &&
+ context.location.href.startsWith('about:blank') &&
+ context.parent.location.href
+ ) {
+ context = context.parent;
+ }
+ } catch(ex) {
+ }
+ vAPI.effectiveSelf = context;
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.userStylesheet = {
+ added: new Set(),
+ removed: new Set(),
+ apply: function(callback) {
+ if ( this.added.size === 0 && this.removed.size === 0 ) { return; }
+ vAPI.messaging.send('vapi', {
+ what: 'userCSS',
+ add: Array.from(this.added),
+ remove: Array.from(this.removed),
+ }).then(( ) => {
+ if ( callback instanceof Function === false ) { return; }
+ callback();
+ });
+ this.added.clear();
+ this.removed.clear();
+ },
+ add: function(cssText, now) {
+ if ( cssText === '' ) { return; }
+ this.added.add(cssText);
+ if ( now ) { this.apply(); }
+ },
+ remove: function(cssText, now) {
+ if ( cssText === '' ) { return; }
+ this.removed.add(cssText);
+ if ( now ) { this.apply(); }
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/*******************************************************************************
+
+ The purpose of SafeAnimationFrame is to take advantage of the behavior of
+ window.requestAnimationFrame[1]. If we use an animation frame as a timer,
+ then this timer is described as follow:
+
+ - time events are throttled by the browser when the viewport is not visible --
+ there is no point for uBO to play with the DOM if the document is not
+ visible.
+ - time events are micro tasks[2].
+ - time events are synchronized to monitor refresh, meaning that they can fire
+ at most 1/60 (typically).
+
+ If a delay value is provided, a plain timer is first used. Plain timers are
+ macro-tasks, so this is good when uBO wants to yield to more important tasks
+ on a page. Once the plain timer elapse, an animation frame is used to trigger
+ the next time at which to execute the job.
+
+ [1] https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
+ [2] https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
+
+*/
+
+// https://github.com/gorhill/uBlock/issues/2147
+
+vAPI.SafeAnimationFrame = class {
+ constructor(callback) {
+ this.fid = this.tid = undefined;
+ this.callback = callback;
+ }
+ start(delay) {
+ if ( self.vAPI instanceof Object === false ) { return; }
+ if ( delay === undefined ) {
+ if ( this.fid === undefined ) {
+ this.fid = requestAnimationFrame(( ) => { this.onRAF(); } );
+ }
+ if ( this.tid === undefined ) {
+ this.tid = vAPI.setTimeout(( ) => { this.onSTO(); }, 20000);
+ }
+ return;
+ }
+ if ( this.fid === undefined && this.tid === undefined ) {
+ this.tid = vAPI.setTimeout(( ) => { this.macroToMicro(); }, delay);
+ }
+ }
+ clear() {
+ if ( this.fid !== undefined ) {
+ cancelAnimationFrame(this.fid);
+ this.fid = undefined;
+ }
+ if ( this.tid !== undefined ) {
+ clearTimeout(this.tid);
+ this.tid = undefined;
+ }
+ }
+ macroToMicro() {
+ this.tid = undefined;
+ this.start();
+ }
+ onRAF() {
+ if ( this.tid !== undefined ) {
+ clearTimeout(this.tid);
+ this.tid = undefined;
+ }
+ this.fid = undefined;
+ this.callback();
+ }
+ onSTO() {
+ if ( this.fid !== undefined ) {
+ cancelAnimationFrame(this.fid);
+ this.fid = undefined;
+ }
+ this.tid = undefined;
+ this.callback();
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/552
+// Listen and report CSP violations so that blocked resources through CSP
+// are properly reported in the logger.
+
+{
+ const newEvents = new Set();
+ const allEvents = new Set();
+ let timer;
+
+ const send = function() {
+ if ( self.vAPI instanceof Object === false ) { return; }
+ vAPI.messaging.send('scriptlets', {
+ what: 'securityPolicyViolation',
+ type: 'net',
+ docURL: document.location.href,
+ violations: Array.from(newEvents),
+ }).then(response => {
+ if ( response === true ) { return; }
+ stop();
+ });
+ for ( const event of newEvents ) {
+ allEvents.add(event);
+ }
+ newEvents.clear();
+ };
+
+ const sendAsync = function() {
+ if ( timer !== undefined ) { return; }
+ timer = self.requestIdleCallback(
+ ( ) => { timer = undefined; send(); },
+ { timeout: 2063 }
+ );
+ };
+
+ const listener = function(ev) {
+ if ( ev.isTrusted !== true ) { return; }
+ if ( ev.disposition !== 'enforce' ) { return; }
+ const json = JSON.stringify({
+ url: ev.blockedURL || ev.blockedURI,
+ policy: ev.originalPolicy,
+ directive: ev.effectiveDirective || ev.violatedDirective,
+ });
+ if ( allEvents.has(json) ) { return; }
+ newEvents.add(json);
+ sendAsync();
+ };
+
+ const stop = function() {
+ newEvents.clear();
+ allEvents.clear();
+ if ( timer !== undefined ) {
+ self.cancelIdleCallback(timer);
+ timer = undefined;
+ }
+ document.removeEventListener('securitypolicyviolation', listener);
+ if ( vAPI ) { vAPI.shutdown.remove(stop); }
+ };
+
+ document.addEventListener('securitypolicyviolation', listener);
+ vAPI.shutdown.add(stop);
+
+ // We need to call at least once to find out whether we really need to
+ // listen to CSP violations.
+ sendAsync();
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// vAPI.domWatcher
+
+{
+ vAPI.domMutationTime = Date.now();
+
+ const addedNodeLists = [];
+ const removedNodeLists = [];
+ const addedNodes = [];
+ const ignoreTags = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style' ]);
+ const listeners = [];
+
+ let domLayoutObserver;
+ let listenerIterator = [];
+ let listenerIteratorDirty = false;
+ let removedNodes = false;
+ let safeObserverHandlerTimer;
+
+ const safeObserverHandler = function() {
+ let i = addedNodeLists.length;
+ while ( i-- ) {
+ const nodeList = addedNodeLists[i];
+ let iNode = nodeList.length;
+ while ( iNode-- ) {
+ const node = nodeList[iNode];
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( ignoreTags.has(node.localName) ) { continue; }
+ if ( node.parentElement === null ) { continue; }
+ addedNodes.push(node);
+ }
+ }
+ addedNodeLists.length = 0;
+ i = removedNodeLists.length;
+ while ( i-- && removedNodes === false ) {
+ const nodeList = removedNodeLists[i];
+ let iNode = nodeList.length;
+ while ( iNode-- ) {
+ if ( nodeList[iNode].nodeType !== 1 ) { continue; }
+ removedNodes = true;
+ break;
+ }
+ }
+ removedNodeLists.length = 0;
+ if ( addedNodes.length === 0 && removedNodes === false ) { return; }
+ for ( const listener of getListenerIterator() ) {
+ try { listener.onDOMChanged(addedNodes, removedNodes); }
+ catch (ex) { }
+ }
+ addedNodes.length = 0;
+ removedNodes = false;
+ vAPI.domMutationTime = Date.now();
+ };
+
+ // https://github.com/chrisaljoudi/uBlock/issues/205
+ // Do not handle added node directly from within mutation observer.
+ const observerHandler = function(mutations) {
+ let i = mutations.length;
+ while ( i-- ) {
+ const mutation = mutations[i];
+ let nodeList = mutation.addedNodes;
+ if ( nodeList.length !== 0 ) {
+ addedNodeLists.push(nodeList);
+ }
+ nodeList = mutation.removedNodes;
+ if ( nodeList.length !== 0 ) {
+ removedNodeLists.push(nodeList);
+ }
+ }
+ if ( addedNodeLists.length !== 0 || removedNodeLists.length !== 0 ) {
+ safeObserverHandlerTimer.start(
+ addedNodeLists.length < 100 ? 1 : undefined
+ );
+ }
+ };
+
+ const startMutationObserver = function() {
+ if ( domLayoutObserver !== undefined ) { return; }
+ domLayoutObserver = new MutationObserver(observerHandler);
+ domLayoutObserver.observe(document, {
+ //attributeFilter: [ 'class', 'id' ],
+ //attributes: true,
+ childList: true,
+ subtree: true
+ });
+ safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler);
+ vAPI.shutdown.add(cleanup);
+ };
+
+ const stopMutationObserver = function() {
+ if ( domLayoutObserver === undefined ) { return; }
+ cleanup();
+ vAPI.shutdown.remove(cleanup);
+ };
+
+ const getListenerIterator = function() {
+ if ( listenerIteratorDirty ) {
+ listenerIterator = listeners.slice();
+ listenerIteratorDirty = false;
+ }
+ return listenerIterator;
+ };
+
+ const addListener = function(listener) {
+ if ( listeners.indexOf(listener) !== -1 ) { return; }
+ listeners.push(listener);
+ listenerIteratorDirty = true;
+ if ( domLayoutObserver === undefined ) { return; }
+ try { listener.onDOMCreated(); }
+ catch (ex) { }
+ startMutationObserver();
+ };
+
+ const removeListener = function(listener) {
+ const pos = listeners.indexOf(listener);
+ if ( pos === -1 ) { return; }
+ listeners.splice(pos, 1);
+ listenerIteratorDirty = true;
+ if ( listeners.length === 0 ) {
+ stopMutationObserver();
+ }
+ };
+
+ const cleanup = function() {
+ if ( domLayoutObserver !== undefined ) {
+ domLayoutObserver.disconnect();
+ domLayoutObserver = undefined;
+ }
+ if ( safeObserverHandlerTimer !== undefined ) {
+ safeObserverHandlerTimer.clear();
+ safeObserverHandlerTimer = undefined;
+ }
+ };
+
+ const start = function() {
+ for ( const listener of getListenerIterator() ) {
+ try { listener.onDOMCreated(); }
+ catch (ex) { }
+ }
+ startMutationObserver();
+ };
+
+ vAPI.domWatcher = { start, addListener, removeListener };
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.injectScriptlet = function(doc, text) {
+ if ( !doc ) { return; }
+ let script, url;
+ try {
+ const blob = new self.Blob([ text ], { type: 'text/javascript; charset=utf-8' });
+ url = self.URL.createObjectURL(blob);
+ script = doc.createElement('script');
+ script.async = false;
+ script.src = url;
+ (doc.head || doc.documentElement || doc).appendChild(script);
+ } catch (ex) {
+ }
+ if ( url ) {
+ if ( script ) { script.remove(); }
+ self.URL.revokeObjectURL(url);
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/*******************************************************************************
+
+ The DOM filterer is the heart of uBO's cosmetic filtering.
+
+ DOMFilterer: adds procedural cosmetic filtering
+
+*/
+
+vAPI.hideStyle = 'display:none!important;';
+
+vAPI.DOMFilterer = class {
+ constructor() {
+ this.commitTimer = new vAPI.SafeAnimationFrame(
+ ( ) => { this.commitNow(); }
+ );
+ this.disabled = false;
+ this.listeners = [];
+ this.stylesheets = [];
+ this.exceptedCSSRules = [];
+ this.exceptions = [];
+ this.convertedProceduralFilters = [];
+ this.proceduralFilterer = null;
+ }
+
+ explodeCSS(css) {
+ const out = [];
+ const cssHide = `{${vAPI.hideStyle}}`;
+ const blocks = css.trim().split(/\n\n+/);
+ for ( const block of blocks ) {
+ if ( block.endsWith(cssHide) === false ) { continue; }
+ out.push(block.slice(0, -cssHide.length).trim());
+ }
+ return out;
+ }
+
+ addCSS(css, details = {}) {
+ if ( typeof css !== 'string' || css.length === 0 ) { return; }
+ if ( this.stylesheets.includes(css) ) { return; }
+ this.stylesheets.push(css);
+ if ( details.mustInject && this.disabled === false ) {
+ vAPI.userStylesheet.add(css);
+ }
+ if ( this.hasListeners() === false ) { return; }
+ if ( details.silent ) { return; }
+ this.triggerListeners({ declarative: this.explodeCSS(css) });
+ }
+
+ exceptCSSRules(exceptions) {
+ if ( exceptions.length === 0 ) { return; }
+ this.exceptedCSSRules.push(...exceptions);
+ if ( this.hasListeners() ) {
+ this.triggerListeners({ exceptions });
+ }
+ }
+
+ addListener(listener) {
+ if ( this.listeners.indexOf(listener) !== -1 ) { return; }
+ this.listeners.push(listener);
+ }
+
+ removeListener(listener) {
+ const pos = this.listeners.indexOf(listener);
+ if ( pos === -1 ) { return; }
+ this.listeners.splice(pos, 1);
+ }
+
+ hasListeners() {
+ return this.listeners.length !== 0;
+ }
+
+ triggerListeners(changes) {
+ for ( const listener of this.listeners ) {
+ listener.onFiltersetChanged(changes);
+ }
+ }
+
+ toggle(state, callback) {
+ if ( state === undefined ) { state = this.disabled; }
+ if ( state !== this.disabled ) { return; }
+ this.disabled = !state;
+ const uss = vAPI.userStylesheet;
+ for ( const css of this.stylesheets ) {
+ if ( this.disabled ) {
+ uss.remove(css);
+ } else {
+ uss.add(css);
+ }
+ }
+ uss.apply(callback);
+ }
+
+ // Here we will deal with:
+ // - Injecting low priority user styles;
+ // - Notifying listeners about changed filterset.
+ // https://www.reddit.com/r/uBlockOrigin/comments/9jj0y1/no_longer_blocking_ads/
+ // Ensure vAPI is still valid -- it can go away by the time we are
+ // called, since the port could be force-disconnected from the main
+ // process. Another approach would be to have vAPI.SafeAnimationFrame
+ // register a shutdown job: to evaluate. For now I will keep the fix
+ // trivial.
+ commitNow() {
+ this.commitTimer.clear();
+ if ( vAPI instanceof Object === false ) { return; }
+ vAPI.userStylesheet.apply();
+ if ( this.proceduralFilterer instanceof Object ) {
+ this.proceduralFilterer.commitNow();
+ }
+ }
+
+ commit(commitNow) {
+ if ( commitNow ) {
+ this.commitTimer.clear();
+ this.commitNow();
+ } else {
+ this.commitTimer.start();
+ }
+ }
+
+ proceduralFiltererInstance() {
+ if ( this.proceduralFilterer instanceof Object === false ) {
+ if ( vAPI.DOMProceduralFilterer instanceof Object === false ) {
+ return null;
+ }
+ this.proceduralFilterer = new vAPI.DOMProceduralFilterer(this);
+ }
+ return this.proceduralFilterer;
+ }
+
+ addProceduralSelectors(selectors) {
+ const procedurals = [];
+ for ( const raw of selectors ) {
+ procedurals.push(JSON.parse(raw));
+ }
+ if ( procedurals.length === 0 ) { return; }
+ const pfilterer = this.proceduralFiltererInstance();
+ if ( pfilterer !== null ) {
+ pfilterer.addProceduralSelectors(procedurals);
+ }
+ }
+
+ createProceduralFilter(o) {
+ const pfilterer = this.proceduralFiltererInstance();
+ if ( pfilterer === null ) { return; }
+ return pfilterer.createProceduralFilter(o);
+ }
+
+ getAllSelectors(bits = 0) {
+ const out = {
+ declarative: [],
+ exceptions: this.exceptedCSSRules,
+ };
+ const hasProcedural = this.proceduralFilterer instanceof Object;
+ const includePrivateSelectors = (bits & 0b01) !== 0;
+ const masterToken = hasProcedural
+ ? `[${this.proceduralFilterer.masterToken}]`
+ : undefined;
+ for ( const css of this.stylesheets ) {
+ for ( const block of this.explodeCSS(css) ) {
+ if (
+ includePrivateSelectors === false &&
+ masterToken !== undefined &&
+ block.startsWith(masterToken)
+ ) {
+ continue;
+ }
+ out.declarative.push(block);
+ }
+ }
+ const excludeProcedurals = (bits & 0b10) !== 0;
+ if ( excludeProcedurals === false ) {
+ out.procedural = [];
+ if ( hasProcedural ) {
+ out.procedural.push(
+ ...this.proceduralFilterer.selectors.values()
+ );
+ }
+ const proceduralFilterer = this.proceduralFiltererInstance();
+ if ( proceduralFilterer !== null ) {
+ for ( const json of this.convertedProceduralFilters ) {
+ const pfilter = proceduralFilterer.createProceduralFilter(json);
+ pfilter.converted = true;
+ out.procedural.push(pfilter);
+ }
+ }
+ }
+ return out;
+ }
+
+ getAllExceptionSelectors() {
+ return this.exceptions.join(',\n');
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// vAPI.domCollapser
+
+{
+ const messaging = vAPI.messaging;
+ const toCollapse = new Map();
+ const src1stProps = {
+ audio: 'currentSrc',
+ embed: 'src',
+ iframe: 'src',
+ img: 'currentSrc',
+ object: 'data',
+ video: 'currentSrc',
+ };
+ const src2ndProps = {
+ audio: 'src',
+ img: 'src',
+ video: 'src',
+ };
+ const tagToTypeMap = {
+ audio: 'media',
+ embed: 'object',
+ iframe: 'sub_frame',
+ img: 'image',
+ object: 'object',
+ video: 'media',
+ };
+ let requestIdGenerator = 1,
+ processTimer,
+ cachedBlockedSet,
+ cachedBlockedSetHash,
+ cachedBlockedSetTimer,
+ toProcess = [],
+ toFilter = [],
+ netSelectorCacheCount = 0;
+
+ const cachedBlockedSetClear = function() {
+ cachedBlockedSet =
+ cachedBlockedSetHash =
+ cachedBlockedSetTimer = undefined;
+ };
+
+ // https://github.com/chrisaljoudi/uBlock/issues/399
+ // https://github.com/gorhill/uBlock/issues/2848
+ // Use a user stylesheet to collapse placeholders.
+ const getCollapseToken = ( ) => {
+ if ( collapseToken === undefined ) {
+ collapseToken = vAPI.randomToken();
+ vAPI.userStylesheet.add(
+ `[${collapseToken}]\n{display:none!important;}`,
+ true
+ );
+ }
+ return collapseToken;
+ };
+ let collapseToken;
+
+ // https://github.com/chrisaljoudi/uBlock/issues/174
+ // Do not remove fragment from src URL
+ const onProcessed = function(response) {
+ // This happens if uBO is disabled or restarted.
+ if ( response instanceof Object === false ) {
+ toCollapse.clear();
+ return;
+ }
+
+ const targets = toCollapse.get(response.id);
+ if ( targets === undefined ) { return; }
+
+ toCollapse.delete(response.id);
+ if ( cachedBlockedSetHash !== response.hash ) {
+ cachedBlockedSet = new Set(response.blockedResources);
+ cachedBlockedSetHash = response.hash;
+ if ( cachedBlockedSetTimer !== undefined ) {
+ clearTimeout(cachedBlockedSetTimer);
+ }
+ cachedBlockedSetTimer = vAPI.setTimeout(cachedBlockedSetClear, 30000);
+ }
+ if ( cachedBlockedSet === undefined || cachedBlockedSet.size === 0 ) {
+ return;
+ }
+
+ const selectors = [];
+ let netSelectorCacheCountMax = response.netSelectorCacheCountMax;
+
+ for ( const target of targets ) {
+ const tag = target.localName;
+ let prop = src1stProps[tag];
+ if ( prop === undefined ) { continue; }
+ let src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) {
+ prop = src2ndProps[tag];
+ if ( prop === undefined ) { continue; }
+ src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) { continue; }
+ }
+ if ( cachedBlockedSet.has(tagToTypeMap[tag] + ' ' + src) === false ) {
+ continue;
+ }
+ target.setAttribute(getCollapseToken(), '');
+ // https://github.com/chrisaljoudi/uBlock/issues/1048
+ // Use attribute to construct CSS rule
+ if ( netSelectorCacheCount > netSelectorCacheCountMax ) { continue; }
+ const value = target.getAttribute(prop);
+ if ( value ) {
+ selectors.push(`${tag}[${prop}="${CSS.escape(value)}"]`);
+ netSelectorCacheCount += 1;
+ }
+ }
+
+ if ( selectors.length === 0 ) { return; }
+ messaging.send('contentscript', {
+ what: 'cosmeticFiltersInjected',
+ type: 'net',
+ hostname: window.location.hostname,
+ selectors,
+ });
+ };
+
+ const send = function() {
+ processTimer = undefined;
+ toCollapse.set(requestIdGenerator, toProcess);
+ messaging.send('contentscript', {
+ what: 'getCollapsibleBlockedRequests',
+ id: requestIdGenerator,
+ frameURL: window.location.href,
+ resources: toFilter,
+ hash: cachedBlockedSetHash,
+ }).then(response => {
+ onProcessed(response);
+ });
+ toProcess = [];
+ toFilter = [];
+ requestIdGenerator += 1;
+ };
+
+ const process = function(delay) {
+ if ( toProcess.length === 0 ) { return; }
+ if ( delay === 0 ) {
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ }
+ send();
+ } else if ( processTimer === undefined ) {
+ processTimer = vAPI.setTimeout(send, delay || 20);
+ }
+ };
+
+ const add = function(target) {
+ toProcess[toProcess.length] = target;
+ };
+
+ const addMany = function(targets) {
+ for ( const target of targets ) {
+ add(target);
+ }
+ };
+
+ const iframeSourceModified = function(mutations) {
+ for ( const mutation of mutations ) {
+ addIFrame(mutation.target, true);
+ }
+ process();
+ };
+ const iframeSourceObserver = new MutationObserver(iframeSourceModified);
+ const iframeSourceObserverOptions = {
+ attributes: true,
+ attributeFilter: [ 'src' ]
+ };
+
+ // https://github.com/gorhill/uBlock/issues/162
+ // Be prepared to deal with possible change of src attribute.
+ const addIFrame = function(iframe, dontObserve) {
+ if ( dontObserve !== true ) {
+ iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
+ }
+ const src = iframe.src;
+ if ( typeof src !== 'string' || src === '' ) { return; }
+ if ( src.startsWith('http') === false ) { return; }
+ toFilter.push({ type: 'sub_frame', url: iframe.src });
+ add(iframe);
+ };
+
+ const addIFrames = function(iframes) {
+ for ( const iframe of iframes ) {
+ addIFrame(iframe);
+ }
+ };
+
+ const onResourceFailed = function(ev) {
+ if ( tagToTypeMap[ev.target.localName] !== undefined ) {
+ add(ev.target);
+ process();
+ }
+ };
+
+ const stop = function() {
+ document.removeEventListener('error', onResourceFailed, true);
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ }
+ if ( vAPI.domWatcher instanceof Object ) {
+ vAPI.domWatcher.removeListener(domWatcherInterface);
+ }
+ vAPI.shutdown.remove(stop);
+ vAPI.domCollapser = null;
+ };
+
+ const start = function() {
+ if ( vAPI.domWatcher instanceof Object ) {
+ vAPI.domWatcher.addListener(domWatcherInterface);
+ }
+ };
+
+ const domWatcherInterface = {
+ onDOMCreated: function() {
+ if ( self.vAPI instanceof Object === false ) { return; }
+ if ( vAPI.domCollapser instanceof Object === false ) {
+ if ( vAPI.domWatcher instanceof Object ) {
+ vAPI.domWatcher.removeListener(domWatcherInterface);
+ }
+ return;
+ }
+ // Listener to collapse blocked resources.
+ // - Future requests not blocked yet
+ // - Elements dynamically added to the page
+ // - Elements which resource URL changes
+ // https://github.com/chrisaljoudi/uBlock/issues/7
+ // Preferring getElementsByTagName over querySelectorAll:
+ // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145
+ const elems = document.images ||
+ document.getElementsByTagName('img');
+ for ( const elem of elems ) {
+ if ( elem.complete ) {
+ add(elem);
+ }
+ }
+ addMany(document.embeds || document.getElementsByTagName('embed'));
+ addMany(document.getElementsByTagName('object'));
+ addIFrames(document.getElementsByTagName('iframe'));
+ process(0);
+
+ document.addEventListener('error', onResourceFailed, true);
+
+ vAPI.shutdown.add(stop);
+ },
+ onDOMChanged: function(addedNodes) {
+ if ( addedNodes.length === 0 ) { return; }
+ for ( const node of addedNodes ) {
+ if ( node.localName === 'iframe' ) {
+ addIFrame(node);
+ }
+ if ( node.firstElementChild === null ) { continue; }
+ const iframes = node.getElementsByTagName('iframe');
+ if ( iframes.length !== 0 ) {
+ addIFrames(iframes);
+ }
+ }
+ process();
+ }
+ };
+
+ vAPI.domCollapser = { start };
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// vAPI.domSurveyor
+
+{
+ // http://www.cse.yorku.ca/~oz/hash.html#djb2
+ // Must mirror cosmetic filtering compiler's version
+ const hashFromStr = (type, s) => {
+ const len = s.length;
+ const step = len + 7 >>> 3;
+ let hash = (type << 5) + type ^ len;
+ for ( let i = 0; i < len; i += step ) {
+ hash = (hash << 5) + hash ^ s.charCodeAt(i);
+ }
+ return hash & 0xFFFFFF;
+ };
+
+ const addHashes = hashes => {
+ for ( const hash of hashes ) {
+ queriedHashes.add(hash);
+ }
+ };
+
+ const queriedHashes = new Set();
+ const maxSurveyNodes = 65536;
+ const pendingLists = [];
+ const pendingNodes = [];
+ const processedSet = new Set();
+ let domFilterer;
+ let hostname = '';
+ let domChanged = false;
+ let scannedCount = 0;
+ let stopped = false;
+
+ const addPendingList = list => {
+ if ( list.length === 0 ) { return; }
+ pendingLists.push(Array.from(list));
+ };
+
+ const nextPendingNodes = ( ) => {
+ if ( pendingLists.length === 0 ) { return 0; }
+ const bufferSize = 256;
+ let j = 0;
+ do {
+ const nodeList = pendingLists[0];
+ let n = bufferSize - j;
+ if ( n > nodeList.length ) {
+ n = nodeList.length;
+ }
+ for ( let i = 0; i < n; i++ ) {
+ pendingNodes[j+i] = nodeList[i];
+ }
+ j += n;
+ if ( n !== nodeList.length ) {
+ pendingLists[0] = nodeList.slice(n);
+ break;
+ }
+ pendingLists.shift();
+ } while ( j < bufferSize && pendingLists.length !== 0 );
+ return j;
+ };
+
+ const hasPendingNodes = ( ) => {
+ return pendingLists.length !== 0;
+ };
+
+ // Extract all classes/ids: these will be passed to the cosmetic
+ // filtering engine, and in return we will obtain only the relevant
+ // CSS selectors.
+
+ // https://github.com/gorhill/uBlock/issues/672
+ // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
+ // http://jsperf.com/enumerate-classes/6
+
+ const idFromNode = (node, out) => {
+ const raw = node.id;
+ if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
+ const hash = hashFromStr(0x23 /* '#' */, raw.trim());
+ if ( queriedHashes.has(hash) ) { return; }
+ queriedHashes.add(hash);
+ out.push(hash);
+ };
+
+ // https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
+ // Performance: avoid using Element.classList
+ const classesFromNode = (node, out) => {
+ const s = node.getAttribute('class');
+ if ( typeof s !== 'string' ) { return; }
+ const len = s.length;
+ for ( let beg = 0, end = 0; beg < len; beg += 1 ) {
+ end = s.indexOf(' ', beg);
+ if ( end === beg ) { continue; }
+ if ( end === -1 ) { end = len; }
+ const hash = hashFromStr(0x2E /* '.' */, s.slice(beg, end));
+ beg = end;
+ if ( queriedHashes.has(hash) ) { continue; }
+ queriedHashes.add(hash);
+ out.push(hash);
+ }
+ };
+
+ const getSurveyResults = (hashes, safeOnly) => {
+ if ( self.vAPI.messaging instanceof Object === false ) {
+ stop(); return;
+ }
+ const promise = hashes.length === 0
+ ? Promise.resolve(null)
+ : self.vAPI.messaging.send('contentscript', {
+ what: 'retrieveGenericCosmeticSelectors',
+ hostname,
+ hashes,
+ exceptions: domFilterer.exceptions,
+ safeOnly,
+ });
+ promise.then(response => {
+ processSurveyResults(response);
+ });
+ };
+
+ const doSurvey = ( ) => {
+ if ( self.vAPI instanceof Object === false ) { return; }
+ const t0 = performance.now();
+ const hashes = [];
+ const nodes = pendingNodes;
+ const deadline = t0 + 4;
+ let processed = 0;
+ let scanned = 0;
+ for (;;) {
+ const n = nextPendingNodes();
+ if ( n === 0 ) { break; }
+ for ( let i = 0; i < n; i++ ) {
+ const node = nodes[i]; nodes[i] = null;
+ if ( domChanged ) {
+ if ( processedSet.has(node) ) { continue; }
+ processedSet.add(node);
+ }
+ idFromNode(node, hashes);
+ classesFromNode(node, hashes);
+ scanned += 1;
+ }
+ processed += n;
+ if ( performance.now() >= deadline ) { break; }
+ }
+ //console.info(`[domSurveyor][${hostname}] Surveyed ${scanned}/${processed} nodes in ${(performance.now()-t0).toFixed(2)} ms: ${hashes.length} hashes`);
+ scannedCount += scanned;
+ if ( scannedCount >= maxSurveyNodes ) {
+ stop();
+ }
+ processedSet.clear();
+ getSurveyResults(hashes);
+ };
+
+ const surveyTimer = new vAPI.SafeAnimationFrame(doSurvey);
+
+ // This is to shutdown the surveyor if result of surveying keeps being
+ // fruitless. This is useful on long-lived web page. I arbitrarily
+ // picked 5 minutes before the surveyor is allowed to shutdown. I also
+ // arbitrarily picked 256 misses before the surveyor is allowed to
+ // shutdown.
+ let canShutdownAfter = Date.now() + 300000;
+ let surveyResultMissCount = 0;
+
+ // Handle main process' response.
+
+ const processSurveyResults = response => {
+ if ( stopped ) { return; }
+ const result = response && response.result;
+ let mustCommit = false;
+ if ( result ) {
+ const css = result.injectedCSS;
+ if ( typeof css === 'string' && css.length !== 0 ) {
+ domFilterer.addCSS(css);
+ mustCommit = true;
+ }
+ const selectors = result.excepted;
+ if ( Array.isArray(selectors) && selectors.length !== 0 ) {
+ domFilterer.exceptCSSRules(selectors);
+ }
+ }
+ if ( hasPendingNodes() ) {
+ surveyTimer.start(1);
+ }
+ if ( mustCommit ) {
+ surveyResultMissCount = 0;
+ canShutdownAfter = Date.now() + 300000;
+ return;
+ }
+ surveyResultMissCount += 1;
+ if ( surveyResultMissCount < 256 || Date.now() < canShutdownAfter ) {
+ return;
+ }
+ //console.info(`[domSurveyor][${hostname}] Shutting down, too many misses`);
+ stop();
+ self.vAPI.messaging.send('contentscript', {
+ what: 'disableGenericCosmeticFilteringSurveyor',
+ hostname,
+ });
+ };
+
+ const domWatcherInterface = {
+ onDOMCreated: function() {
+ domFilterer = vAPI.domFilterer;
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1692
+ // Look-up safe-only selectors to mitigate probability of
+ // html/body elements of erroneously being targeted.
+ const hashes = [];
+ if ( document.documentElement !== null ) {
+ idFromNode(document.documentElement, hashes);
+ classesFromNode(document.documentElement, hashes);
+ }
+ if ( document.body !== null ) {
+ idFromNode(document.body, hashes);
+ classesFromNode(document.body, hashes);
+ }
+ if ( hashes.length !== 0 ) {
+ getSurveyResults(hashes, true);
+ }
+ addPendingList(document.querySelectorAll(
+ '[id]:not(html):not(body),[class]:not(html):not(body)'
+ ));
+ if ( hasPendingNodes() ) {
+ surveyTimer.start();
+ }
+ },
+ onDOMChanged: function(addedNodes) {
+ if ( addedNodes.length === 0 ) { return; }
+ domChanged = true;
+ for ( const node of addedNodes ) {
+ addPendingList([ node ]);
+ if ( node.firstElementChild === null ) { continue; }
+ addPendingList(
+ node.querySelectorAll(
+ '[id]:not(html):not(body),[class]:not(html):not(body)'
+ )
+ );
+ }
+ if ( hasPendingNodes() ) {
+ surveyTimer.start(1);
+ }
+ }
+ };
+
+ const start = details => {
+ if ( self.vAPI instanceof Object === false ) { return; }
+ if ( self.vAPI.domFilterer instanceof Object === false ) { return; }
+ if ( self.vAPI.domWatcher instanceof Object === false ) { return; }
+ hostname = details.hostname;
+ self.vAPI.domWatcher.addListener(domWatcherInterface);
+ };
+
+ const stop = ( ) => {
+ stopped = true;
+ pendingLists.length = 0;
+ surveyTimer.clear();
+ if ( self.vAPI instanceof Object === false ) { return; }
+ if ( self.vAPI.domWatcher instanceof Object ) {
+ self.vAPI.domWatcher.removeListener(domWatcherInterface);
+ }
+ self.vAPI.domSurveyor = null;
+ };
+
+ self.vAPI.domSurveyor = { start, addHashes };
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+// vAPI.bootstrap:
+// Bootstrapping allows all components of the content script
+// to be launched if/when needed.
+
+{
+ const onDomReady = ( ) => {
+ // This can happen on Firefox. For instance:
+ // https://github.com/gorhill/uBlock/issues/1893
+ if ( window.location === null ) { return; }
+ if ( self.vAPI instanceof Object === false ) { return; }
+
+ vAPI.messaging.send('contentscript', {
+ what: 'shouldRenderNoscriptTags',
+ });
+
+ if ( vAPI.domFilterer instanceof Object ) {
+ vAPI.domFilterer.commitNow();
+ }
+
+ if ( vAPI.domWatcher instanceof Object ) {
+ vAPI.domWatcher.start();
+ }
+
+ // Element picker works only in top window for now.
+ if (
+ window !== window.top ||
+ vAPI.domFilterer instanceof Object === false
+ ) {
+ return;
+ }
+
+ // To be used by element picker/zapper.
+ vAPI.mouseClick = { x: -1, y: -1 };
+
+ const onMouseClick = function(ev) {
+ if ( ev.isTrusted === false ) { return; }
+ vAPI.mouseClick.x = ev.clientX;
+ vAPI.mouseClick.y = ev.clientY;
+
+ // https://github.com/chrisaljoudi/uBlock/issues/1143
+ // Find a link under the mouse, to try to avoid confusing new tabs
+ // as nuisance popups.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/777
+ // Mind that href may not be a string.
+ const elem = ev.target.closest('a[href]');
+ if ( elem === null || typeof elem.href !== 'string' ) { return; }
+ vAPI.messaging.send('contentscript', {
+ what: 'maybeGoodPopup',
+ url: elem.href || '',
+ });
+ };
+
+ document.addEventListener('mousedown', onMouseClick, true);
+
+ // https://github.com/gorhill/uMatrix/issues/144
+ vAPI.shutdown.add(function() {
+ document.removeEventListener('mousedown', onMouseClick, true);
+ });
+ };
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/403
+ // If there was a spurious port disconnection -- in which case the
+ // response is expressly set to `null`, rather than undefined or
+ // an object -- let's stay around, we may be given the opportunity
+ // to try bootstrapping again later.
+
+ const onResponseReady = response => {
+ if ( response instanceof Object === false ) { return; }
+ vAPI.bootstrap = undefined;
+
+ // cosmetic filtering engine aka 'cfe'
+ const cfeDetails = response && response.specificCosmeticFilters;
+ if ( !cfeDetails || !cfeDetails.ready ) {
+ vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer =
+ vAPI.domSurveyor = vAPI.domIsLoaded = null;
+ return;
+ }
+
+ vAPI.domCollapser.start();
+
+ const {
+ noSpecificCosmeticFiltering,
+ noGenericCosmeticFiltering,
+ scriptletDetails,
+ } = response;
+
+ vAPI.noSpecificCosmeticFiltering = noSpecificCosmeticFiltering;
+ vAPI.noGenericCosmeticFiltering = noGenericCosmeticFiltering;
+
+ if ( noSpecificCosmeticFiltering && noGenericCosmeticFiltering ) {
+ vAPI.domFilterer = null;
+ vAPI.domSurveyor = null;
+ } else {
+ const domFilterer = vAPI.domFilterer = new vAPI.DOMFilterer();
+ if ( noGenericCosmeticFiltering || cfeDetails.disableSurveyor ) {
+ vAPI.domSurveyor = null;
+ }
+ domFilterer.exceptions = cfeDetails.exceptionFilters;
+ domFilterer.addCSS(cfeDetails.injectedCSS);
+ domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters);
+ domFilterer.exceptCSSRules(cfeDetails.exceptedFilters);
+ domFilterer.convertedProceduralFilters = cfeDetails.convertedProceduralFilters;
+ vAPI.userStylesheet.apply();
+ }
+
+ if ( scriptletDetails && typeof self.uBO_scriptletsInjected !== 'string' ) {
+ self.uBO_scriptletsInjected = scriptletDetails.filters;
+ if ( scriptletDetails.mainWorld ) {
+ vAPI.injectScriptlet(document, scriptletDetails.mainWorld);
+ vAPI.injectedScripts = scriptletDetails.mainWorld;
+ }
+ }
+
+ if ( vAPI.domSurveyor ) {
+ if ( Array.isArray(cfeDetails.genericCosmeticHashes) ) {
+ vAPI.domSurveyor.addHashes(cfeDetails.genericCosmeticHashes);
+ }
+ vAPI.domSurveyor.start(cfeDetails);
+ }
+
+ const readyState = document.readyState;
+ if ( readyState === 'interactive' || readyState === 'complete' ) {
+ return onDomReady();
+ }
+ document.addEventListener('DOMContentLoaded', onDomReady, { once: true });
+ };
+
+ vAPI.bootstrap = function() {
+ vAPI.messaging.send('contentscript', {
+ what: 'retrieveContentScriptParameters',
+ url: vAPI.effectiveSelf.location.href,
+ needScriptlets: typeof self.uBO_scriptletsInjected !== 'string',
+ }).then(response => {
+ onResponseReady(response);
+ });
+ };
+}
+
+// This starts bootstrap process.
+vAPI.bootstrap();
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+}
+// <<<<<<<< end of HUGE-IF-BLOCK