/******************************************************************************* uBlock Origin - a browser extension to block requests. 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'; if ( typeof vAPI === 'object' && typeof vAPI.DOMProceduralFilterer !== 'object' ) { // >>>>>>>> start of local scope /******************************************************************************/ const nonVisualElements = { script: true, style: true, }; // 'P' stands for 'Procedural' class PSelectorTask { begin() { } end() { } } class PSelectorVoidTask extends PSelectorTask { constructor(task) { super(); console.info(`uBO: :${task[0]}() operator does not exist`); } transpose() { } } class PSelectorHasTextTask extends PSelectorTask { constructor(task) { super(); let arg0 = task[1], arg1; if ( Array.isArray(task[1]) ) { arg1 = arg0[1]; arg0 = arg0[0]; } this.needle = new RegExp(arg0, arg1); } transpose(node, output) { if ( this.needle.test(node.textContent) ) { output.push(node); } } } class PSelectorIfTask extends PSelectorTask { constructor(task) { super(); this.pselector = new PSelector(task[1]); } transpose(node, output) { if ( this.pselector.test(node) === this.target ) { output.push(node); } } } PSelectorIfTask.prototype.target = true; class PSelectorIfNotTask extends PSelectorIfTask { } PSelectorIfNotTask.prototype.target = false; class PSelectorMatchesCSSTask extends PSelectorTask { constructor(task) { super(); this.name = task[1].name; this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null; let arg0 = task[1].value, arg1; if ( Array.isArray(arg0) ) { arg1 = arg0[1]; arg0 = arg0[0]; } this.value = new RegExp(arg0, arg1); } transpose(node, output) { const style = window.getComputedStyle(node, this.pseudo); if ( style !== null && this.value.test(style[this.name]) ) { output.push(node); } } } class PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask { constructor(task) { super(task); this.pseudo = '::after'; } } class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask { constructor(task) { super(task); this.pseudo = '::before'; } } class PSelectorMatchesMediaTask extends PSelectorTask { constructor(task) { super(); this.mql = window.matchMedia(task[1]); if ( this.mql.media === 'not all' ) { return; } this.mql.addEventListener('change', ( ) => { if ( typeof vAPI !== 'object' ) { return; } if ( vAPI === null ) { return; } const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer; if ( filterer instanceof Object === false ) { return; } filterer.onDOMChanged([ null ]); }); } transpose(node, output) { if ( this.mql.matches === false ) { return; } output.push(node); } } class PSelectorMatchesPathTask extends PSelectorTask { constructor(task) { super(); let arg0 = task[1], arg1; if ( Array.isArray(task[1]) ) { arg1 = arg0[1]; arg0 = arg0[0]; } this.needle = new RegExp(arg0, arg1); } transpose(node, output) { if ( this.needle.test(self.location.pathname + self.location.search) ) { output.push(node); } } } class PSelectorMinTextLengthTask extends PSelectorTask { constructor(task) { super(); this.min = task[1]; } transpose(node, output) { if ( node.textContent.length >= this.min ) { output.push(node); } } } class PSelectorOthersTask extends PSelectorTask { constructor() { super(); this.targets = new Set(); } begin() { this.targets.clear(); } end(output) { const toKeep = new Set(this.targets); const toDiscard = new Set(); const body = document.body; let discard = null; for ( let keep of this.targets ) { while ( keep !== null && keep !== body ) { toKeep.add(keep); toDiscard.delete(keep); discard = keep.previousElementSibling; while ( discard !== null ) { if ( nonVisualElements[discard.localName] !== true && toKeep.has(discard) === false ) { toDiscard.add(discard); } discard = discard.previousElementSibling; } discard = keep.nextElementSibling; while ( discard !== null ) { if ( nonVisualElements[discard.localName] !== true && toKeep.has(discard) === false ) { toDiscard.add(discard); } discard = discard.nextElementSibling; } keep = keep.parentElement; } } for ( discard of toDiscard ) { output.push(discard); } this.targets.clear(); } transpose(candidate) { for ( const target of this.targets ) { if ( target.contains(candidate) ) { return; } if ( candidate.contains(target) ) { this.targets.delete(target); } } this.targets.add(candidate); } } // https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277 // Prepend `:scope ` if needed. class PSelectorSpathTask extends PSelectorTask { constructor(task) { super(); this.spath = task[1]; this.nth = /^(?:\s*[+~]|:)/.test(this.spath); if ( this.nth ) { return; } if ( /^\s*>/.test(this.spath) ) { this.spath = `:scope ${this.spath.trim()}`; } } transpose(node, output) { const nodes = this.nth ? PSelectorSpathTask.qsa(node, this.spath) : node.querySelectorAll(this.spath); for ( const node of nodes ) { output.push(node); } } // Helper method for other operators. static qsa(node, selector) { const parent = node.parentElement; if ( parent === null ) { return []; } let pos = 1; for (;;) { node = node.previousElementSibling; if ( node === null ) { break; } pos += 1; } return parent.querySelectorAll( `:scope > :nth-child(${pos})${selector}` ); } } class PSelectorUpwardTask extends PSelectorTask { constructor(task) { super(); const arg = task[1]; if ( typeof arg === 'number' ) { this.i = arg; } else { this.s = arg; } } transpose(node, output) { if ( this.s !== '' ) { const parent = node.parentElement; if ( parent === null ) { return; } node = parent.closest(this.s); if ( node === null ) { return; } } else { let nth = this.i; for (;;) { node = node.parentElement; if ( node === null ) { return; } nth -= 1; if ( nth === 0 ) { break; } } } output.push(node); } } PSelectorUpwardTask.prototype.i = 0; PSelectorUpwardTask.prototype.s = ''; class PSelectorWatchAttrs extends PSelectorTask { constructor(task) { super(); this.observer = null; this.observed = new WeakSet(); this.observerOptions = { attributes: true, subtree: true, }; const attrs = task[1]; if ( Array.isArray(attrs) && attrs.length !== 0 ) { this.observerOptions.attributeFilter = task[1]; } } // TODO: Is it worth trying to re-apply only the current selector? handler() { const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer; if ( filterer instanceof Object ) { filterer.onDOMChanged([ null ]); } } transpose(node, output) { output.push(node); if ( this.observed.has(node) ) { return; } if ( this.observer === null ) { this.observer = new MutationObserver(this.handler); } this.observer.observe(node, this.observerOptions); this.observed.add(node); } } class PSelectorXpathTask extends PSelectorTask { constructor(task) { super(); this.xpe = document.createExpression(task[1], null); this.xpr = null; } transpose(node, output) { this.xpr = this.xpe.evaluate( node, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, this.xpr ); let j = this.xpr.snapshotLength; while ( j-- ) { const node = this.xpr.snapshotItem(j); if ( node.nodeType === 1 ) { output.push(node); } } } } class PSelector { constructor(o) { if ( PSelector.prototype.operatorToTaskMap === undefined ) { PSelector.prototype.operatorToTaskMap = new Map([ [ 'has', PSelectorIfTask ], [ 'has-text', PSelectorHasTextTask ], [ 'if', PSelectorIfTask ], [ 'if-not', PSelectorIfNotTask ], [ 'matches-css', PSelectorMatchesCSSTask ], [ 'matches-css-after', PSelectorMatchesCSSAfterTask ], [ 'matches-css-before', PSelectorMatchesCSSBeforeTask ], [ 'matches-media', PSelectorMatchesMediaTask ], [ 'matches-path', PSelectorMatchesPathTask ], [ 'min-text-length', PSelectorMinTextLengthTask ], [ 'not', PSelectorIfNotTask ], [ 'others', PSelectorOthersTask ], [ 'spath', PSelectorSpathTask ], [ 'upward', PSelectorUpwardTask ], [ 'watch-attr', PSelectorWatchAttrs ], [ 'xpath', PSelectorXpathTask ], ]); } this.raw = o.raw; this.selector = o.selector; this.tasks = []; const tasks = []; if ( Array.isArray(o.tasks) === false ) { return; } for ( const task of o.tasks ) { const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask; tasks.push(new ctor(task)); } // Initialize only after all tasks have been successfully instantiated this.tasks = tasks; } prime(input) { const root = input || document; if ( this.selector === '' ) { return [ root ]; } if ( input !== document && /^ ?[>+~]/.test(this.selector) ) { return Array.from(PSelectorSpathTask.qsa(input, this.selector)); } return Array.from(root.querySelectorAll(this.selector)); } exec(input) { let nodes = this.prime(input); for ( const task of this.tasks ) { if ( nodes.length === 0 ) { break; } const transposed = []; task.begin(); for ( const node of nodes ) { task.transpose(node, transposed); } task.end(transposed); nodes = transposed; } return nodes; } test(input) { const nodes = this.prime(input); for ( const node of nodes ) { let output = [ node ]; for ( const task of this.tasks ) { const transposed = []; task.begin(); for ( const node of output ) { task.transpose(node, transposed); } task.end(transposed); output = transposed; if ( output.length === 0 ) { break; } } if ( output.length !== 0 ) { return true; } } return false; } } PSelector.prototype.operatorToTaskMap = undefined; class PSelectorRoot extends PSelector { constructor(o, styleToken) { super(o); this.budget = 200; // I arbitrary picked a 1/5 second this.raw = o.raw; this.cost = 0; this.lastAllowanceTime = 0; this.styleToken = styleToken; } prime(input) { try { return super.prime(input); } catch (ex) { } return []; } } PSelectorRoot.prototype.hit = false; class ProceduralFilterer { constructor(domFilterer) { this.domFilterer = domFilterer; this.mustApplySelectors = false; this.selectors = new Map(); this.masterToken = vAPI.randomToken(); this.styleTokenMap = new Map(); this.styledNodes = new Set(); if ( vAPI.domWatcher instanceof Object ) { vAPI.domWatcher.addListener(this); } } addProceduralSelectors(selectors) { const addedSelectors = []; let mustCommit = false; for ( const selector of selectors ) { if ( this.selectors.has(selector.raw) ) { continue; } let style, styleToken; if ( selector.action === undefined ) { style = vAPI.hideStyle; } else if ( selector.action[0] === 'style' ) { style = selector.action[1]; } if ( style !== undefined ) { styleToken = this.styleTokenFromStyle(style); } const pselector = new PSelectorRoot(selector, styleToken); this.selectors.set(selector.raw, pselector); addedSelectors.push(pselector); mustCommit = true; } if ( mustCommit === false ) { return; } this.mustApplySelectors = this.selectors.size !== 0; this.domFilterer.commit(); if ( this.domFilterer.hasListeners() ) { this.domFilterer.triggerListeners({ procedural: addedSelectors }); } } commitNow() { if ( this.selectors.size === 0 ) { return; } this.mustApplySelectors = false; //console.time('procedural selectors/dom layout changed'); // https://github.com/uBlockOrigin/uBlock-issues/issues/341 // Be ready to unhide nodes which no longer matches any of // the procedural selectors. const toUnstyle = this.styledNodes; this.styledNodes = new Set(); let t0 = Date.now(); for ( const pselector of this.selectors.values() ) { const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000); if ( allowance >= 1 ) { pselector.budget += allowance * 50; if ( pselector.budget > 200 ) { pselector.budget = 200; } pselector.lastAllowanceTime = t0; } if ( pselector.budget <= 0 ) { continue; } const nodes = pselector.exec(); const t1 = Date.now(); pselector.budget += t0 - t1; if ( pselector.budget < -500 ) { console.info('uBO: disabling %s', pselector.raw); pselector.budget = -0x7FFFFFFF; } t0 = t1; if ( nodes.length === 0 ) { continue; } pselector.hit = true; this.styleNodes(nodes, pselector.styleToken); } this.unstyleNodes(toUnstyle); //console.timeEnd('procedural selectors/dom layout changed'); } styleTokenFromStyle(style) { if ( style === undefined ) { return; } let styleToken = this.styleTokenMap.get(style); if ( styleToken !== undefined ) { return styleToken; } styleToken = vAPI.randomToken(); this.styleTokenMap.set(style, styleToken); this.domFilterer.addCSS( `[${this.masterToken}][${styleToken}]\n{${style}}`, { silent: true, mustInject: true } ); return styleToken; } styleNodes(nodes, styleToken) { if ( styleToken === undefined ) { for ( const node of nodes ) { node.textContent = ''; node.remove(); } return; } for ( const node of nodes ) { node.setAttribute(this.masterToken, ''); node.setAttribute(styleToken, ''); this.styledNodes.add(node); } } // TODO: Current assumption is one style per hit element. Could be an // issue if an element has multiple styling and one styling is // brought back. Possibly too rare to care about this for now. unstyleNodes(nodes) { for ( const node of nodes ) { if ( this.styledNodes.has(node) ) { continue; } node.removeAttribute(this.masterToken); } } createProceduralFilter(o) { return new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o); } onDOMCreated() { } onDOMChanged(addedNodes, removedNodes) { if ( this.selectors.size === 0 ) { return; } this.mustApplySelectors = this.mustApplySelectors || addedNodes.length !== 0 || removedNodes; this.domFilterer.commit(); } } vAPI.DOMProceduralFilterer = ProceduralFilterer; /******************************************************************************/ // >>>>>>>> end of local scope } /******************************************************************************* DO NOT: - Remove the following code - Add code beyond the following code Reason: - https://github.com/gorhill/uBlock/pull/3721 - uBO never uses the return value from injected content scripts **/ void 0;