Inject procedural cosmetic filterer's code only when needed

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.
This commit is contained in:
Raymond Hill 2021-02-17 09:12:00 -05:00
parent c6745a0276
commit 9c3205b37c
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
6 changed files with 727 additions and 624 deletions

View File

@ -0,0 +1,469 @@
/*******************************************************************************
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;

View File

@ -484,612 +484,195 @@ vAPI.injectScriptlet = function(doc, text) {
*/
{
vAPI.hideStyle = 'display:none!important;';
vAPI.hideStyle = 'display:none!important;';
// TODO: Experiment/evaluate loading procedural operator code using an
// on demand approach.
vAPI.DOMFilterer = class {
constructor() {
this.commitTimer = new vAPI.SafeAnimationFrame(
( ) => { this.commitNow(); }
);
this.domIsReady = document.readyState !== 'loading';
this.disabled = false;
this.listeners = [];
this.stylesheets = [];
this.exceptedCSSRules = [];
this.exceptions = [];
this.proceduralFilterer = null;
// https://github.com/uBlockOrigin/uBlock-issues/issues/167
// By the time the DOMContentLoaded is fired, the content script might
// have been disconnected from the background page. Unclear why this
// would happen, so far seems to be a Chromium-specific behavior at
// launch time.
if ( this.domIsReady !== true ) {
document.addEventListener('DOMContentLoaded', ( ) => {
if ( vAPI instanceof Object === false ) { return; }
this.domIsReady = true;
this.commit();
});
}
}
// 'P' stands for 'Procedural'
explodeCSS(css) {
const out = [];
const reBlock = /^\{(.*)\}$/m;
const blocks = css.trim().split(/\n\n+/);
for ( const block of blocks ) {
const match = reBlock.exec(block);
out.push([ block.slice(0, match.index).trim(), match[1] ]);
}
return out;
}
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);
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);
}
transpose(node, output) {
if ( this.needle.test(node.textContent) ) {
output.push(node);
}
}
};
if ( this.hasListeners() === false ) { return; }
if ( details.silent ) { return; }
this.triggerListeners({ declarative: this.explodeCSS(css) });
}
const PSelectorIfTask = class {
constructor(task) {
this.pselector = new PSelector(task[1]);
exceptCSSRules(exceptions) {
if ( exceptions.length === 0 ) { return; }
this.exceptedCSSRules.push(...exceptions);
if ( this.hasListeners() ) {
this.triggerListeners({ exceptions });
}
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;
addListener(listener) {
if ( this.listeners.indexOf(listener) !== -1 ) { return; }
this.listeners.push(listener);
}
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;
removeListener(listener) {
const pos = this.listeners.indexOf(listener);
if ( pos === -1 ) { return; }
this.listeners.splice(pos, 1);
}
const PSelectorMatchesCSSAfterTask = class extends PSelectorMatchesCSSTask {
};
PSelectorMatchesCSSAfterTask.prototype.pseudo = ':after';
hasListeners() {
return this.listeners.length !== 0;
}
const PSelectorMatchesCSSBeforeTask = class extends PSelectorMatchesCSSTask {
};
PSelectorMatchesCSSBeforeTask.prototype.pseudo = ':before';
triggerListeners(changes) {
for ( const listener of this.listeners ) {
listener.onFiltersetChanged(changes);
}
}
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;
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 {
this.s = arg;
uss.add(css);
}
}
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 = '';
uss.apply(callback);
}
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];
}
// 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();
}
// 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) ) {
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 DOMProceduralFilterer = 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.addCSSRule(
`[${this.masterToken}][${styleToken}]`,
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.DOMFilterer = class {
constructor() {
this.commitTimer = new vAPI.SafeAnimationFrame(
( ) => { this.commitNow(); }
);
this.domIsReady = document.readyState !== 'loading';
this.disabled = false;
this.listeners = [];
this.filterset = new Set();
this.exceptedCSSRules = [];
this.exceptions = [];
this.proceduralFilterer = null;
// https://github.com/uBlockOrigin/uBlock-issues/issues/167
// By the time the DOMContentLoaded is fired, the content script might
// have been disconnected from the background page. Unclear why this
// would happen, so far seems to be a Chromium-specific behavior at
// launch time.
if ( this.domIsReady !== true ) {
document.addEventListener('DOMContentLoaded', ( ) => {
if ( vAPI instanceof Object === false ) { return; }
this.domIsReady = true;
this.commit();
});
}
}
addCSSRule(selectors, declarations, details = {}) {
if ( selectors === undefined ) { return; }
const selectorsStr = Array.isArray(selectors)
? selectors.join(',\n')
: selectors;
if ( selectorsStr.length === 0 ) { return; }
this.filterset.add({ selectors: selectorsStr, declarations });
if ( details.mustInject && this.disabled === false ) {
vAPI.userStylesheet.add(`${selectorsStr}\n{${declarations}}`);
}
this.commit();
if ( details.silent !== true && this.hasListeners() ) {
this.triggerListeners({
declarative: [ [ selectorsStr, declarations ] ]
});
}
}
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 userStylesheet = vAPI.userStylesheet;
for ( const entry of this.filterset ) {
const rule = `${entry.selectors}\n{${entry.declarations}}`;
if ( this.disabled ) {
userStylesheet.remove(rule);
} else {
userStylesheet.add(rule);
}
}
userStylesheet.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() {
commit(commitNow) {
if ( commitNow ) {
this.commitTimer.clear();
if ( vAPI instanceof Object === false ) { return; }
vAPI.userStylesheet.apply();
if ( this.proceduralFilterer instanceof Object ) {
this.proceduralFilterer.commitNow();
}
this.commitNow();
} else {
this.commitTimer.start();
}
}
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;
}
proceduralFiltererInstance() {
if ( this.proceduralFilterer instanceof Object === false ) {
this.proceduralFilterer = new DOMProceduralFilterer(this);
}
return this.proceduralFilterer;
addProceduralSelectors(selectors) {
if ( Array.isArray(selectors) === false || selectors.length === 0 ) {
return;
}
addProceduralSelectors(selectors) {
if ( Array.isArray(selectors) === false || selectors.length === 0 ) {
return;
}
const procedurals = [];
for ( const raw of selectors ) {
const o = JSON.parse(raw);
if (
o.action !== undefined &&
o.action[0] === ':style' &&
o.tasks === undefined
) {
this.addCSSRule(o.selector, o.action[1], { mustInject: true });
continue;
}
if ( o.pseudo !== undefined ) {
this.addCSSRule(o.selector, vAPI.hideStyle, { mustInject: true });
continue;
}
procedurals.push(o);
}
if ( procedurals.length !== 0 ) {
this.proceduralFiltererInstance()
.addProceduralSelectors(procedurals);
}
const procedurals = [];
for ( const raw of selectors ) {
procedurals.push(JSON.parse(raw));
}
createProceduralFilter(o) {
return this.proceduralFiltererInstance().createProceduralFilter(o);
if ( procedurals.length === 0 ) { return; }
const pfilterer = this.proceduralFiltererInstance();
if ( pfilterer !== null ) {
pfilterer.addProceduralSelectors(procedurals);
}
}
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 entry of this.filterset ) {
const selectors = entry.selectors;
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 ) {
const blocks = this.explodeCSS(css);
for ( const block of blocks ) {
if (
includePrivateSelectors === false &&
masterToken !== undefined &&
selectors.startsWith(masterToken)
block[0].startsWith(masterToken)
) {
continue;
}
out.declarative.push([ selectors, entry.declarations ]);
out.declarative.push([ block[0], block[1] ]);
}
const excludeProcedurals = (bits & 0b10) !== 0;
if ( excludeProcedurals !== true ) {
out.procedural = hasProcedural
? Array.from(this.proceduralFilterer.selectors.values())
: [];
}
return out;
}
const excludeProcedurals = (bits & 0b10) !== 0;
if ( excludeProcedurals !== true ) {
out.procedural = hasProcedural
? Array.from(this.proceduralFilterer.selectors.values())
: [];
}
return out;
}
getAllExceptionSelectors() {
return this.exceptions.join(',\n');
}
};
}
getAllExceptionSelectors() {
return this.exceptions.join(',\n');
}
};
/******************************************************************************/
/******************************************************************************/
@ -1525,12 +1108,12 @@ vAPI.injectScriptlet = function(doc, text) {
let mustCommit = false;
if ( result ) {
let selectors = result.injected;
if ( typeof selectors === 'string' && selectors.length !== 0 ) {
domFilterer.addCSSRule(selectors, vAPI.hideStyle);
const css = result.injectedCSS;
if ( typeof css === 'string' && css.length !== 0 ) {
domFilterer.addCSS(css);
mustCommit = true;
}
selectors = result.excepted;
const selectors = result.excepted;
if ( Array.isArray(selectors) && selectors.length !== 0 ) {
domFilterer.exceptCSSRules(selectors);
}
@ -1695,7 +1278,7 @@ vAPI.injectScriptlet = function(doc, text) {
vAPI.domSurveyor = null;
}
domFilterer.exceptions = cfeDetails.exceptionFilters;
domFilterer.addCSSRule(cfeDetails.injectedHideFilters, vAPI.hideStyle);
domFilterer.addCSS(cfeDetails.injectedCSS);
domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters);
domFilterer.exceptCSSRules(cfeDetails.exceptedFilters);
}

View File

@ -894,7 +894,7 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
return;
}
const out = { injected: '', excepted, };
const out = { injectedCSS: '', excepted, };
const injected = [];
if ( simpleSelectors.size !== 0 ) {
@ -918,9 +918,9 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
});
}
out.injected = injected.join(',\n');
out.injectedCSS = `${injected.join(',\n')}\n{display:none!important;}`;
vAPI.tabs.insertCSS(request.tabId, {
code: out.injected + '\n{display:none!important;}',
code: out.injectedCSS,
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
@ -955,9 +955,10 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
exceptedFilters: [],
noDOMSurveying: this.needDOMSurveyor === false,
};
const injectedHideFilters = [];
const injectedCSS = [];
if ( options.noCosmeticFiltering !== true ) {
const injectedHideFilters = [];
const specificSet = this.$specificSet;
const proceduralSet = this.$proceduralSet;
const exceptionSet = this.$exceptionSet;
@ -1019,8 +1020,29 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
if ( specificSet.size !== 0 ) {
injectedHideFilters.push(Array.from(specificSet).join(',\n'));
}
// Some procedural filters are really declarative cosmetic filters, so
// we extract and inject them immediately.
if ( proceduralSet.size !== 0 ) {
out.proceduralFilters = Array.from(proceduralSet);
for ( const json of proceduralSet ) {
const pfilter = JSON.parse(json);
if ( pfilter.tasks === undefined ) {
const { action } = pfilter;
if ( action !== undefined && action[0] === ':style' ) {
injectedCSS.push(`${pfilter.selector}\n{${action[1]}}`);
proceduralSet.delete(json);
continue;
}
}
if ( pfilter.pseudo !== undefined ) {
injectedHideFilters.push(pfilter.selector);
proceduralSet.delete(json);
continue;
}
}
if ( proceduralSet.size !== 0 ) {
out.proceduralFilters = Array.from(proceduralSet);
}
}
// Highly generic cosmetic filters: sent once along with specific ones.
@ -1063,6 +1085,12 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
}
}
if ( injectedHideFilters.length !== 0 ) {
injectedCSS.push(
`${injectedHideFilters.join(',\n')}\n{display:none!important;}`
);
}
// Important: always clear used registers before leaving.
specificSet.clear();
proceduralSet.clear();
@ -1077,10 +1105,11 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
runAt: 'document_start',
};
if ( injectedHideFilters.length !== 0 ) {
out.injectedHideFilters = injectedHideFilters.join(',\n');
details.code = out.injectedHideFilters + '\n{display:none!important;}';
if ( options.dontInject !== true ) {
// Inject all declarative-based filters as a single stylesheet.
if ( injectedCSS.length !== 0 ) {
out.injectedCSS = injectedCSS.join('\n\n');
details.code = out.injectedCSS;
if ( request.tabId !== undefined ) {
vAPI.tabs.insertCSS(request.tabId, details);
}
}
@ -1091,7 +1120,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
cacheEntry.retrieve('net', networkFilters);
if ( networkFilters.length !== 0 ) {
details.code = networkFilters.join('\n') + '\n{display:none!important;}';
if ( options.dontInject !== true ) {
if ( request.tabId !== undefined ) {
vAPI.tabs.insertCSS(request.tabId, details);
}
}
@ -1125,7 +1154,6 @@ FilterContainer.prototype.benchmark = async function() {
const options = {
noCosmeticFiltering: false,
noGenericCosmeticFiltering: false,
dontInject: true,
};
let count = 0;
const t0 = self.performance.now();

View File

@ -533,7 +533,7 @@ vAPI.messaging.listen({
const µb = µBlock;
const retrieveContentScriptParameters = function(sender, request) {
const retrieveContentScriptParameters = async function(sender, request) {
if ( µb.readyToFilter !== true ) { return; }
const { tabId, frameId } = sender;
if ( tabId === undefined || frameId === undefined ) { return; }
@ -550,6 +550,7 @@ const retrieveContentScriptParameters = function(sender, request) {
request.url = pageStore.getEffectiveFrameURL(sender);
}
const loggerEnabled = µb.logger.enabled;
const noCosmeticFiltering = pageStore.noCosmeticFiltering === true;
const response = {
@ -568,7 +569,7 @@ const retrieveContentScriptParameters = function(sender, request) {
request.url
);
response.noGenericCosmeticFiltering = genericHide === 2;
if ( genericHide !== 0 && µb.logger.enabled ) {
if ( loggerEnabled && genericHide !== 0 ) {
µBlock.filteringContext
.duplicate()
.fromTabId(tabId)
@ -595,7 +596,7 @@ const retrieveContentScriptParameters = function(sender, request) {
request.url
);
response.noSpecificCosmeticFiltering = specificHide === 2;
if ( specificHide !== 0 && µb.logger.enabled ) {
if ( loggerEnabled && specificHide !== 0 ) {
µBlock.filteringContext
.duplicate()
.fromTabId(tabId)
@ -620,6 +621,23 @@ const retrieveContentScriptParameters = function(sender, request) {
response.specificCosmeticFilters =
µb.cosmeticFilteringEngine.retrieveSpecificSelectors(request, response);
// The procedural filterer's code is loaded only when needed and must be
// present before returning response to caller.
if (
Array.isArray(response.specificCosmeticFilters.proceduralFilters) || (
loggerEnabled &&
response.specificCosmeticFilters.exceptedFilters.length !== 0
)
) {
await vAPI.tabs.executeScript(tabId, {
allFrames: false,
file: '/js/contentscript-extra.js',
frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-748179731
// For non-network URIs, scriptlet injection is deferred to here. The
// effective URL is available here in `request.url`.
@ -630,8 +648,18 @@ const retrieveContentScriptParameters = function(sender, request) {
response.scriptlets = µb.scriptletFilteringEngine.retrieve(request);
}
if ( µb.logger.enabled && response.noCosmeticFiltering !== true ) {
µb.logCosmeticFilters(tabId, frameId);
// https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623
// Inject as early as possible to make the cosmetic logger code less
// sensitive to the removal of DOM nodes which may match injected
// cosmetic filters.
if ( loggerEnabled && response.noCosmeticFiltering !== true ) {
vAPI.tabs.executeScript(tabId, {
allFrames: false,
file: '/js/scriptlets/cosmetic-logger.js',
frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
}
return response;
@ -640,6 +668,13 @@ const retrieveContentScriptParameters = function(sender, request) {
const onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
case 'retrieveContentScriptParameters':
return retrieveContentScriptParameters(
sender,
request
).then(response => {
callback(response);
});
default:
break;
}
@ -686,10 +721,6 @@ const onMessage = function(request, sender, callback) {
}
break;
case 'retrieveContentScriptParameters':
response = retrieveContentScriptParameters(sender, request);
break;
case 'retrieveGenericCosmeticSelectors':
request.tabId = sender.tabId;
request.frameId = sender.frameId;
@ -728,6 +759,25 @@ const onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
// The procedural filterer must be present in case the user wants to
// type-in custom filters.
case 'elementPickerArguments':
return vAPI.tabs.executeScript(sender.tabId, {
allFrames: false,
file: '/js/contentscript-extra.js',
frameId: sender.frameId,
matchAboutBlank: true,
runAt: 'document_start',
}).then(( ) => {
callback({
target: µb.epickerArgs.target,
mouse: µb.epickerArgs.mouse,
zap: µb.epickerArgs.zap,
eprom: µb.epickerArgs.eprom,
pickerURL: vAPI.getURL(`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret()}`),
});
µb.epickerArgs.target = '';
});
default:
break;
}
@ -736,16 +786,6 @@ const onMessage = function(request, sender, callback) {
let response;
switch ( request.what ) {
case 'elementPickerArguments':
response = {
target: µb.epickerArgs.target,
mouse: µb.epickerArgs.mouse,
zap: µb.epickerArgs.zap,
eprom: µb.epickerArgs.eprom,
pickerURL: vAPI.getURL(`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret()}`),
};
µb.epickerArgs.target = '';
break;
case 'elementPickerEprom':
µb.epickerArgs.eprom = request;
break;

View File

@ -805,9 +805,8 @@ const filterToDOMInterface = (( ) => {
}
}
if ( cssSelectors.size !== 0 ) {
vAPI.domFilterer.addCSSRule(
Array.from(cssSelectors),
vAPI.hideStyle,
vAPI.domFilterer.addCSS(
`${Array.from(cssSelectors).join('\n')}\n{${vAPI.hideStyle}}`,
{ mustInject: true }
);
}

View File

@ -643,22 +643,6 @@ const matchBucket = function(url, hostname, bucket, start) {
/******************************************************************************/
// https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623
// Inject as early as possible to make the cosmetic logger code less
// sensitive to the removal of DOM nodes which may match injected
// cosmetic filters.
µBlock.logCosmeticFilters = function(tabId, frameId) {
vAPI.tabs.executeScript(tabId, {
file: '/js/scriptlets/cosmetic-logger.js',
frameId: frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
};
/******************************************************************************/
µBlock.scriptlets = (function() {
const pendingEntries = new Map();