mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
The procedural cosmetic filtering code has been split from the content script code injected unconditionally and will from now on be injected only when it is needed, i.e. when there are procedural cosmetic filters to enforce. The motivation for this is: https://www.debugbear.com/blog/2020-chrome-extension-performance-report#what-can-extension-developers-do-to-keep-their-extensions-fast Though uBO's content script injected unconditionally in all pages/frames is relatively small, I still wanted to further reduce the amount of content script code injected unconditionally: The procedural cosmetic filtering code represents roughly 14KB of code the browser won't have to parse/execute unconditionally unless there exists procedural cosmetic filters to enforce for a page or frame. At the time the above article was published, the total size of unconditional content scripts injected by uBO was ~101 KB, while after this commit, the total size will be ~57 KB (keeping in mind uBO does not minify and does not remove comments from its JavaScript code). Additionally, some refactoring on how user stylesheets are injected so as to ensure that `:style`-based procedural filters which are essentially declarative are injected earlier along with plain, non-procedural cosmetic filters.
469 lines
14 KiB
JavaScript
469 lines
14 KiB
JavaScript
/*******************************************************************************
|
|
|
|
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 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 ],
|
|
[ ':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;
|