/******************************************************************************* 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 /******************************************************************************/ // TODO: Experiment/evaluate loading procedural operator code using an // on demand approach. // 'P' stands for 'Procedural' const PSelectorHasTextTask = class { constructor(task) { 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); } } }; const PSelectorIfTask = class { constructor(task) { this.pselector = new PSelector(task[1]); } transpose(node, output) { if ( this.pselector.test(node) === this.target ) { output.push(node); } } }; PSelectorIfTask.prototype.target = true; const PSelectorIfNotTask = class extends PSelectorIfTask { }; PSelectorIfNotTask.prototype.target = false; const PSelectorMatchesCSSTask = class { constructor(task) { this.name = task[1].name; 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); } } }; PSelectorMatchesCSSTask.prototype.pseudo = null; const PSelectorMatchesCSSAfterTask = class extends PSelectorMatchesCSSTask { }; PSelectorMatchesCSSAfterTask.prototype.pseudo = ':after'; const PSelectorMatchesCSSBeforeTask = class extends PSelectorMatchesCSSTask { }; PSelectorMatchesCSSBeforeTask.prototype.pseudo = ':before'; const PSelectorMinTextLengthTask = class { constructor(task) { this.min = task[1]; } transpose(node, output) { if ( node.textContent.length >= this.min ) { output.push(node); } } }; const PSelectorMatchesPathTask = class { constructor(task) { 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); } } }; // https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277 // Prepend `:scope ` if needed. const PSelectorSpathTask = class { constructor(task) { 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()}`; } } qsa(node) { if ( this.nth === false ) { return node.querySelectorAll(this.spath); } 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})${this.spath}` ); } transpose(node, output) { const nodes = this.qsa(node); if ( nodes === undefined ) { return; } for ( const node of nodes ) { output.push(node); } } }; const PSelectorUpwardTask = class { constructor(task) { 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 = ''; const PSelectorWatchAttrs = class { constructor(task) { 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); } }; const PSelectorXpathTask = class { constructor(task) { 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); } } } }; const PSelector = class { 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-path', PSelectorMatchesPathTask ], [ ':min-text-length', PSelectorMinTextLengthTask ], [ ':not', PSelectorIfNotTask ], [ ':nth-ancestor', PSelectorUpwardTask ], [ ':spath', PSelectorSpathTask ], [ ':upward', PSelectorUpwardTask ], [ ':watch-attr', PSelectorWatchAttrs ], [ ':xpath', PSelectorXpathTask ], ]); } this.raw = o.raw; this.selector = o.selector; this.tasks = []; const tasks = o.tasks; if ( Array.isArray(tasks) === false ) { return; } for ( const task of tasks ) { this.tasks.push( new (this.operatorToTaskMap.get(task[0]))(task) ); } } prime(input) { const root = input || document; if ( this.selector === '' ) { return [ root ]; } 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 = []; for ( const node of nodes ) { task.transpose(node, 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 = []; for ( const node of output ) { task.transpose(node, transposed); } output = transposed; if ( output.length === 0 ) { break; } } if ( output.length !== 0 ) { return true; } } return false; } }; PSelector.prototype.operatorToTaskMap = undefined; const PSelectorRoot = class 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; } }; PSelectorRoot.prototype.hit = false; const ProceduralFilterer = class { constructor(domFilterer) { this.domFilterer = domFilterer; this.domIsReady = false; this.domIsWatched = false; 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 = this.domIsWatched; 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 || this.domIsReady === false ) { 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 // brough 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(o); } onDOMCreated() { this.domIsReady = true; this.domFilterer.commit(); } 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;