Support CSS selectors mixed w/ operators in procedural cosmetic filters

Related issue:
- https://github.com/gorhill/uBlock/issues/3683

This commit further increases uBO's procedural cosmetic filters
Adguard's cosmetic filter syntax -- specifically those procedural
cosmetic filters where plain CSS selectors appeared following
a procedural oeprator (this was rejected as invalid by uBO).

Also, experimental support for `:watch-attrs` procedural
operator, as discussed in <https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-449765525>.
Support may be dropped before next release depending on whether
a better solution is suggested.

Additionally, the usual opportunistic refactoring toward ES6
syntax.
This commit is contained in:
Raymond Hill 2018-12-26 10:45:19 -05:00
parent e4cec5a15e
commit 8a88e9d931
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 275 additions and 218 deletions

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
uBlock Origin - a browser extension to block requests. uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2018 Raymond Hill Copyright (C) 2014-present Raymond Hill
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -105,7 +105,8 @@
// https://github.com/chrisaljoudi/uBlock/issues/456 // https://github.com/chrisaljoudi/uBlock/issues/456
// https://github.com/gorhill/uBlock/issues/2029 // https://github.com/gorhill/uBlock/issues/2029
if ( typeof vAPI === 'object' && !vAPI.contentScript ) { // >>>>>>>> start of HUGE-IF-BLOCK // >>>>>>>> start of HUGE-IF-BLOCK
if ( typeof vAPI === 'object' && !vAPI.contentScript ) {
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -178,28 +179,28 @@ vAPI.SafeAnimationFrame.prototype = {
vAPI.domWatcher = (function() { vAPI.domWatcher = (function() {
var addedNodeLists = [], const addedNodeLists = [];
addedNodes = [], const removedNodeLists = [];
domIsReady = false, const addedNodes = [];
const ignoreTags = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style' ]);
const listeners = [];
let domIsReady = false,
domLayoutObserver, domLayoutObserver,
ignoreTags = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style' ]),
listeners = [],
listenerIterator = [], listenerIteratorDirty = false, listenerIterator = [], listenerIteratorDirty = false,
removedNodeLists = [],
removedNodes = false, removedNodes = false,
safeObserverHandlerTimer; safeObserverHandlerTimer;
var safeObserverHandler = function() { const safeObserverHandler = function() {
//console.time('dom watcher/safe observer handler'); //console.time('dom watcher/safe observer handler');
safeObserverHandlerTimer.clear(); safeObserverHandlerTimer.clear();
var i = addedNodeLists.length, let i = addedNodeLists.length,
j = addedNodes.length, j = addedNodes.length;
nodeList, iNode, node;
while ( i-- ) { while ( i-- ) {
nodeList = addedNodeLists[i]; const nodeList = addedNodeLists[i];
iNode = nodeList.length; let iNode = nodeList.length;
while ( iNode-- ) { while ( iNode-- ) {
node = nodeList[iNode]; const node = nodeList[iNode];
if ( node.nodeType !== 1 ) { continue; } if ( node.nodeType !== 1 ) { continue; }
if ( ignoreTags.has(node.localName) ) { continue; } if ( ignoreTags.has(node.localName) ) { continue; }
if ( node.parentElement === null ) { continue; } if ( node.parentElement === null ) { continue; }
@ -209,8 +210,8 @@ vAPI.domWatcher = (function() {
addedNodeLists.length = 0; addedNodeLists.length = 0;
i = removedNodeLists.length; i = removedNodeLists.length;
while ( i-- && removedNodes === false ) { while ( i-- && removedNodes === false ) {
nodeList = removedNodeLists[i]; const nodeList = removedNodeLists[i];
iNode = nodeList.length; let iNode = nodeList.length;
while ( iNode-- ) { while ( iNode-- ) {
if ( nodeList[iNode].nodeType !== 1 ) { continue; } if ( nodeList[iNode].nodeType !== 1 ) { continue; }
removedNodes = true; removedNodes = true;
@ -220,7 +221,7 @@ vAPI.domWatcher = (function() {
removedNodeLists.length = 0; removedNodeLists.length = 0;
//console.timeEnd('dom watcher/safe observer handler'); //console.timeEnd('dom watcher/safe observer handler');
if ( addedNodes.length === 0 && removedNodes === false ) { return; } if ( addedNodes.length === 0 && removedNodes === false ) { return; }
for ( var listener of getListenerIterator() ) { for ( const listener of getListenerIterator() ) {
listener.onDOMChanged(addedNodes, removedNodes); listener.onDOMChanged(addedNodes, removedNodes);
} }
addedNodes.length = 0; addedNodes.length = 0;
@ -229,17 +230,15 @@ vAPI.domWatcher = (function() {
// https://github.com/chrisaljoudi/uBlock/issues/205 // https://github.com/chrisaljoudi/uBlock/issues/205
// Do not handle added node directly from within mutation observer. // Do not handle added node directly from within mutation observer.
var observerHandler = function(mutations) { const observerHandler = function(mutations) {
//console.time('dom watcher/observer handler'); //console.time('dom watcher/observer handler');
var nodeList, mutation, let i = mutations.length;
i = mutations.length;
while ( i-- ) { while ( i-- ) {
mutation = mutations[i]; const mutation = mutations[i];
nodeList = mutation.addedNodes; let nodeList = mutation.addedNodes;
if ( nodeList.length !== 0 ) { if ( nodeList.length !== 0 ) {
addedNodeLists.push(nodeList); addedNodeLists.push(nodeList);
} }
if ( removedNodes ) { continue; }
nodeList = mutation.removedNodes; nodeList = mutation.removedNodes;
if ( nodeList.length !== 0 ) { if ( nodeList.length !== 0 ) {
removedNodeLists.push(nodeList); removedNodeLists.push(nodeList);
@ -253,7 +252,7 @@ vAPI.domWatcher = (function() {
//console.timeEnd('dom watcher/observer handler'); //console.timeEnd('dom watcher/observer handler');
}; };
var startMutationObserver = function() { const startMutationObserver = function() {
if ( domLayoutObserver !== undefined || !domIsReady ) { return; } if ( domLayoutObserver !== undefined || !domIsReady ) { return; }
domLayoutObserver = new MutationObserver(observerHandler); domLayoutObserver = new MutationObserver(observerHandler);
domLayoutObserver.observe(document.documentElement, { domLayoutObserver.observe(document.documentElement, {
@ -266,13 +265,13 @@ vAPI.domWatcher = (function() {
vAPI.shutdown.add(cleanup); vAPI.shutdown.add(cleanup);
}; };
var stopMutationObserver = function() { const stopMutationObserver = function() {
if ( domLayoutObserver === undefined ) { return; } if ( domLayoutObserver === undefined ) { return; }
cleanup(); cleanup();
vAPI.shutdown.remove(cleanup); vAPI.shutdown.remove(cleanup);
}; };
var getListenerIterator = function() { const getListenerIterator = function() {
if ( listenerIteratorDirty ) { if ( listenerIteratorDirty ) {
listenerIterator = listeners.slice(); listenerIterator = listeners.slice();
listenerIteratorDirty = false; listenerIteratorDirty = false;
@ -280,7 +279,7 @@ vAPI.domWatcher = (function() {
return listenerIterator; return listenerIterator;
}; };
var addListener = function(listener) { const addListener = function(listener) {
if ( listeners.indexOf(listener) !== -1 ) { return; } if ( listeners.indexOf(listener) !== -1 ) { return; }
listeners.push(listener); listeners.push(listener);
listenerIteratorDirty = true; listenerIteratorDirty = true;
@ -289,8 +288,8 @@ vAPI.domWatcher = (function() {
startMutationObserver(); startMutationObserver();
}; };
var removeListener = function(listener) { const removeListener = function(listener) {
var pos = listeners.indexOf(listener); const pos = listeners.indexOf(listener);
if ( pos === -1 ) { return; } if ( pos === -1 ) { return; }
listeners.splice(pos, 1); listeners.splice(pos, 1);
listenerIteratorDirty = true; listenerIteratorDirty = true;
@ -299,7 +298,7 @@ vAPI.domWatcher = (function() {
} }
}; };
var cleanup = function() { const cleanup = function() {
if ( domLayoutObserver !== undefined ) { if ( domLayoutObserver !== undefined ) {
domLayoutObserver.disconnect(); domLayoutObserver.disconnect();
domLayoutObserver = null; domLayoutObserver = null;
@ -310,19 +309,15 @@ vAPI.domWatcher = (function() {
} }
}; };
var start = function() { const start = function() {
domIsReady = true; domIsReady = true;
for ( var listener of getListenerIterator() ) { for ( const listener of getListenerIterator() ) {
listener.onDOMCreated(); listener.onDOMCreated();
} }
startMutationObserver(); startMutationObserver();
}; };
return { return { start, addListener, removeListener };
start: start,
addListener: addListener,
removeListener: removeListener
};
})(); })();
/******************************************************************************/ /******************************************************************************/
@ -330,7 +325,7 @@ vAPI.domWatcher = (function() {
/******************************************************************************/ /******************************************************************************/
vAPI.matchesProp = (function() { vAPI.matchesProp = (function() {
var docElem = document.documentElement; const docElem = document.documentElement;
if ( typeof docElem.matches !== 'function' ) { if ( typeof docElem.matches !== 'function' ) {
if ( typeof docElem.mozMatchesSelector === 'function' ) { if ( typeof docElem.mozMatchesSelector === 'function' ) {
return 'mozMatchesSelector'; return 'mozMatchesSelector';
@ -454,6 +449,63 @@ vAPI.DOMFilterer = (function() {
PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype);
PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask;
const PSelectorSpathTask = function(task) {
this.spath = task[1];
};
PSelectorSpathTask.prototype.exec = function(input) {
const output = [];
for ( let node of input ) {
const parent = node.parentElement;
if ( parent === null ) { continue; }
let pos = 1;
for (;;) {
node = node.previousElementSibling;
if ( node === null ) { break; }
pos += 1;
}
const nodes = parent.querySelectorAll(
':scope > :nth-child(' + pos + ')' + this.spath
);
for ( const node of nodes ) {
output.push(node);
}
}
return output;
};
const PSelectorWatchAttrs = function(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?
PSelectorWatchAttrs.prototype.handler = function() {
const filterer =
vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
if ( filterer instanceof Object ) {
filterer.onDOMChanged([ null ]);
}
};
PSelectorWatchAttrs.prototype.exec = function(input) {
if ( input.length === 0 ) { return input; }
if ( this.observer === null ) {
this.observer = new MutationObserver(this.handler);
}
for ( const node of input ) {
if ( this.observed.has(node) ) { continue; }
this.observer.observe(node, this.observerOptions);
this.observed.add(node);
}
return input;
};
const PSelectorXpathTask = function(task) { const PSelectorXpathTask = function(task) {
this.xpe = document.createExpression(task[1], null); this.xpe = document.createExpression(task[1], null);
this.xpr = null; this.xpr = null;
@ -488,7 +540,9 @@ vAPI.DOMFilterer = (function() {
[ ':matches-css-after', PSelectorMatchesCSSAfterTask ], [ ':matches-css-after', PSelectorMatchesCSSAfterTask ],
[ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ ':not', PSelectorIfNotTask ], [ ':not', PSelectorIfNotTask ],
[ ':xpath', PSelectorXpathTask ] [ ':spath', PSelectorSpathTask ],
[ ':watch-attrs', PSelectorWatchAttrs ],
[ ':xpath', PSelectorXpathTask ],
]); ]);
} }
this.budget = 200; // I arbitrary picked a 1/5 second this.budget = 200; // I arbitrary picked a 1/5 second
@ -618,10 +672,9 @@ vAPI.DOMFilterer = (function() {
pselector.budget = -0x7FFFFFFF; pselector.budget = -0x7FFFFFFF;
} }
t0 = t1; t0 = t1;
let i = nodes.length; for ( const node of nodes ) {
while ( i-- ) { this.domFilterer.hideNode(node);
this.domFilterer.hideNode(nodes[i]); this.hiddenNodes.add(node);
this.hiddenNodes.add(nodes[i]);
} }
} }
@ -720,33 +773,34 @@ vAPI.domFilterer = new vAPI.DOMFilterer();
/******************************************************************************/ /******************************************************************************/
vAPI.domCollapser = (function() { vAPI.domCollapser = (function() {
var resquestIdGenerator = 1, const messaging = vAPI.messaging;
processTimer, const toProcess = [];
toProcess = [], const toFilter = [];
toFilter = [], const toCollapse = new Map();
toCollapse = new Map(), const src1stProps = {
cachedBlockedSet, embed: 'src',
cachedBlockedSetHash, iframe: 'src',
cachedBlockedSetTimer; img: 'src',
var src1stProps = { object: 'data'
'embed': 'src',
'iframe': 'src',
'img': 'src',
'object': 'data'
}; };
var src2ndProps = { const src2ndProps = {
'img': 'srcset' img: 'srcset'
}; };
var tagToTypeMap = { const tagToTypeMap = {
embed: 'object', embed: 'object',
iframe: 'sub_frame', iframe: 'sub_frame',
img: 'image', img: 'image',
object: 'object' object: 'object'
}; };
var netSelectorCacheCount = 0,
messaging = vAPI.messaging;
var cachedBlockedSetClear = function() { let resquestIdGenerator = 1,
processTimer,
cachedBlockedSet,
cachedBlockedSetHash,
cachedBlockedSetTimer,
netSelectorCacheCount = 0;
const cachedBlockedSetClear = function() {
cachedBlockedSet = cachedBlockedSet =
cachedBlockedSetHash = cachedBlockedSetHash =
cachedBlockedSetTimer = undefined; cachedBlockedSetTimer = undefined;
@ -754,13 +808,13 @@ vAPI.domCollapser = (function() {
// https://github.com/chrisaljoudi/uBlock/issues/174 // https://github.com/chrisaljoudi/uBlock/issues/174
// Do not remove fragment from src URL // Do not remove fragment from src URL
var onProcessed = function(response) { const onProcessed = function(response) {
if ( !response ) { // This happens if uBO is disabled or restarted. if ( !response ) { // This happens if uBO is disabled or restarted.
toCollapse.clear(); toCollapse.clear();
return; return;
} }
var targets = toCollapse.get(response.id); const targets = toCollapse.get(response.id);
if ( targets === undefined ) { return; } if ( targets === undefined ) { return; }
toCollapse.delete(response.id); toCollapse.delete(response.id);
if ( cachedBlockedSetHash !== response.hash ) { if ( cachedBlockedSetHash !== response.hash ) {
@ -774,16 +828,15 @@ vAPI.domCollapser = (function() {
if ( cachedBlockedSet === undefined || cachedBlockedSet.size === 0 ) { if ( cachedBlockedSet === undefined || cachedBlockedSet.size === 0 ) {
return; return;
} }
var selectors = [], const selectors = [];
iframeLoadEventPatch = vAPI.iframeLoadEventPatch, const iframeLoadEventPatch = vAPI.iframeLoadEventPatch;
netSelectorCacheCountMax = response.netSelectorCacheCountMax, let netSelectorCacheCountMax = response.netSelectorCacheCountMax;
tag, prop, src, value;
for ( var target of targets ) { for ( const target of targets ) {
tag = target.localName; const tag = target.localName;
prop = src1stProps[tag]; let prop = src1stProps[tag];
if ( prop === undefined ) { continue; } if ( prop === undefined ) { continue; }
src = target[prop]; let src = target[prop];
if ( typeof src !== 'string' || src.length === 0 ) { if ( typeof src !== 'string' || src.length === 0 ) {
prop = src2ndProps[tag]; prop = src2ndProps[tag];
if ( prop === undefined ) { continue; } if ( prop === undefined ) { continue; }
@ -799,12 +852,12 @@ vAPI.domCollapser = (function() {
target.hidden = true; target.hidden = true;
// https://github.com/chrisaljoudi/uBlock/issues/1048 // https://github.com/chrisaljoudi/uBlock/issues/1048
// Use attribute to construct CSS rule // Use attribute to construct CSS rule
if ( if ( netSelectorCacheCount <= netSelectorCacheCountMax ) {
netSelectorCacheCount <= netSelectorCacheCountMax && const value = target.getAttribute(prop);
(value = target.getAttribute(prop)) if ( value ) {
) { selectors.push(tag + '[' + prop + '="' + value + '"]');
selectors.push(tag + '[' + prop + '="' + value + '"]'); netSelectorCacheCount += 1;
netSelectorCacheCount += 1; }
} }
if ( iframeLoadEventPatch !== undefined ) { if ( iframeLoadEventPatch !== undefined ) {
iframeLoadEventPatch(target); iframeLoadEventPatch(target);
@ -824,10 +877,10 @@ vAPI.domCollapser = (function() {
} }
}; };
var send = function() { const send = function() {
processTimer = undefined; processTimer = undefined;
toCollapse.set(resquestIdGenerator, toProcess); toCollapse.set(resquestIdGenerator, toProcess);
var msg = { const msg = {
what: 'getCollapsibleBlockedRequests', what: 'getCollapsibleBlockedRequests',
id: resquestIdGenerator, id: resquestIdGenerator,
frameURL: window.location.href, frameURL: window.location.href,
@ -835,12 +888,12 @@ vAPI.domCollapser = (function() {
hash: cachedBlockedSetHash hash: cachedBlockedSetHash
}; };
messaging.send('contentscript', msg, onProcessed); messaging.send('contentscript', msg, onProcessed);
toProcess = []; toProcess.length = 0;
toFilter = []; toFilter.length = 0;
resquestIdGenerator += 1; resquestIdGenerator += 1;
}; };
var process = function(delay) { const process = function(delay) {
if ( toProcess.length === 0 ) { return; } if ( toProcess.length === 0 ) { return; }
if ( delay === 0 ) { if ( delay === 0 ) {
if ( processTimer !== undefined ) { if ( processTimer !== undefined ) {
@ -852,26 +905,24 @@ vAPI.domCollapser = (function() {
} }
}; };
var add = function(target) { const add = function(target) {
toProcess[toProcess.length] = target; toProcess[toProcess.length] = target;
}; };
var addMany = function(targets) { const addMany = function(targets) {
var i = targets.length; for ( const target of targets ) {
while ( i-- ) { add(target);
add(targets[i]);
} }
}; };
var iframeSourceModified = function(mutations) { const iframeSourceModified = function(mutations) {
var i = mutations.length; for ( const mutation of mutations ) {
while ( i-- ) { addIFrame(mutation.target, true);
addIFrame(mutations[i].target, true);
} }
process(); process();
}; };
var iframeSourceObserver = new MutationObserver(iframeSourceModified); const iframeSourceObserver = new MutationObserver(iframeSourceModified);
var iframeSourceObserverOptions = { const iframeSourceObserverOptions = {
attributes: true, attributes: true,
attributeFilter: [ 'src' ] attributeFilter: [ 'src' ]
}; };
@ -880,7 +931,7 @@ vAPI.domCollapser = (function() {
// document, from within `bootstrapPhase1`, and which scriptlets are // document, from within `bootstrapPhase1`, and which scriptlets are
// selectively looked-up from: // selectively looked-up from:
// https://github.com/uBlockOrigin/uAssets/blob/master/filters/resources.txt // https://github.com/uBlockOrigin/uAssets/blob/master/filters/resources.txt
var primeLocalIFrame = function(iframe) { const primeLocalIFrame = function(iframe) {
if ( vAPI.injectedScripts ) { if ( vAPI.injectedScripts ) {
vAPI.injectScriptlet(iframe.contentDocument, vAPI.injectedScripts); vAPI.injectScriptlet(iframe.contentDocument, vAPI.injectedScripts);
} }
@ -888,11 +939,11 @@ vAPI.domCollapser = (function() {
// https://github.com/gorhill/uBlock/issues/162 // https://github.com/gorhill/uBlock/issues/162
// Be prepared to deal with possible change of src attribute. // Be prepared to deal with possible change of src attribute.
var addIFrame = function(iframe, dontObserve) { const addIFrame = function(iframe, dontObserve) {
if ( dontObserve !== true ) { if ( dontObserve !== true ) {
iframeSourceObserver.observe(iframe, iframeSourceObserverOptions); iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
} }
var src = iframe.src; const src = iframe.src;
if ( src === '' || typeof src !== 'string' ) { if ( src === '' || typeof src !== 'string' ) {
primeLocalIFrame(iframe); primeLocalIFrame(iframe);
return; return;
@ -905,21 +956,20 @@ vAPI.domCollapser = (function() {
add(iframe); add(iframe);
}; };
var addIFrames = function(iframes) { const addIFrames = function(iframes) {
var i = iframes.length; for ( const iframe of iframes ) {
while ( i-- ) { addIFrame(iframe);
addIFrame(iframes[i]);
} }
}; };
var onResourceFailed = function(ev) { const onResourceFailed = function(ev) {
if ( tagToTypeMap[ev.target.localName] !== undefined ) { if ( tagToTypeMap[ev.target.localName] !== undefined ) {
add(ev.target); add(ev.target);
process(); process();
} }
}; };
var domWatcherInterface = { const domWatcherInterface = {
onDOMCreated: function() { onDOMCreated: function() {
if ( vAPI instanceof Object === false ) { return; } if ( vAPI instanceof Object === false ) { return; }
if ( vAPI.domCollapser instanceof Object === false ) { if ( vAPI.domCollapser instanceof Object === false ) {
@ -935,10 +985,9 @@ vAPI.domCollapser = (function() {
// https://github.com/chrisaljoudi/uBlock/issues/7 // https://github.com/chrisaljoudi/uBlock/issues/7
// Preferring getElementsByTagName over querySelectorAll: // Preferring getElementsByTagName over querySelectorAll:
// http://jsperf.com/queryselectorall-vs-getelementsbytagname/145 // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145
var elems = document.images || document.getElementsByTagName('img'), const elems = document.images ||
i = elems.length, elem; document.getElementsByTagName('img');
while ( i-- ) { for ( const elem of elems ) {
elem = elems[i];
if ( elem.complete ) { if ( elem.complete ) {
add(elem); add(elem);
} }
@ -958,15 +1007,13 @@ vAPI.domCollapser = (function() {
}); });
}, },
onDOMChanged: function(addedNodes) { onDOMChanged: function(addedNodes) {
var ni = addedNodes.length; if ( addedNodes.length === 0 ) { return; }
if ( ni === 0 ) { return; } for ( const node of addedNodes ) {
for ( var i = 0, node; i < ni; i++ ) {
node = addedNodes[i];
if ( node.localName === 'iframe' ) { if ( node.localName === 'iframe' ) {
addIFrame(node); addIFrame(node);
} }
if ( node.childElementCount === 0 ) { continue; } if ( node.childElementCount === 0 ) { continue; }
var iframes = node.getElementsByTagName('iframe'); const iframes = node.getElementsByTagName('iframe');
if ( iframes.length !== 0 ) { if ( iframes.length !== 0 ) {
addIFrames(iframes); addIFrames(iframes);
} }
@ -979,13 +1026,7 @@ vAPI.domCollapser = (function() {
vAPI.domWatcher.addListener(domWatcherInterface); vAPI.domWatcher.addListener(domWatcherInterface);
} }
return { return { add, addMany, addIFrame, addIFrames, process };
add: add,
addMany: addMany,
addIFrame: addIFrame,
addIFrames: addIFrames,
process: process
};
})(); })();
/******************************************************************************/ /******************************************************************************/
@ -993,13 +1034,14 @@ vAPI.domCollapser = (function() {
/******************************************************************************/ /******************************************************************************/
vAPI.domSurveyor = (function() { vAPI.domSurveyor = (function() {
var messaging = vAPI.messaging, const messaging = vAPI.messaging;
domFilterer, const queriedIds = new Set();
const queriedClasses = new Set();
const pendingIdNodes = { nodes: [], added: [] };
const pendingClassNodes = { nodes: [], added: [] };
let domFilterer,
hostname = '', hostname = '',
queriedIds = new Set(),
queriedClasses = new Set(),
pendingIdNodes = { nodes: [], added: [] },
pendingClassNodes = { nodes: [], added: [] },
surveyCost = 0; surveyCost = 0;
// This is to shutdown the surveyor if result of surveying keeps being // This is to shutdown the surveyor if result of surveying keeps being
@ -1007,17 +1049,17 @@ vAPI.domSurveyor = (function() {
// picked 5 minutes before the surveyor is allowed to shutdown. I also // picked 5 minutes before the surveyor is allowed to shutdown. I also
// arbitrarily picked 256 misses before the surveyor is allowed to // arbitrarily picked 256 misses before the surveyor is allowed to
// shutdown. // shutdown.
var canShutdownAfter = Date.now() + 300000, let canShutdownAfter = Date.now() + 300000,
surveyingMissCount = 0; surveyingMissCount = 0;
// Handle main process' response. // Handle main process' response.
var surveyPhase3 = function(response) { const surveyPhase3 = function(response) {
var result = response && response.result, const result = response && response.result;
mustCommit = false; let mustCommit = false;
if ( result ) { if ( result ) {
var selectors = result.simple; let selectors = result.simple;
if ( Array.isArray(selectors) && selectors.length !== 0 ) { if ( Array.isArray(selectors) && selectors.length !== 0 ) {
domFilterer.addCSSRule( domFilterer.addCSSRule(
selectors, selectors,
@ -1068,19 +1110,14 @@ vAPI.domSurveyor = (function() {
vAPI.domSurveyor = null; vAPI.domSurveyor = null;
}; };
var surveyTimer = new vAPI.SafeAnimationFrame(function() {
surveyPhase1();
});
// The purpose of "chunkification" is to ensure the surveyor won't unduly // The purpose of "chunkification" is to ensure the surveyor won't unduly
// block the main event loop. // block the main event loop.
var hasChunk = function(pending) { const hasChunk = function(pending) {
return pending.nodes.length !== 0 || return pending.nodes.length !== 0 || pending.added.length !== 0;
pending.added.length !== 0;
}; };
var addChunk = function(pending, added) { const addChunk = function(pending, added) {
if ( added.length === 0 ) { return; } if ( added.length === 0 ) { return; }
if ( if (
Array.isArray(added) === false || Array.isArray(added) === false ||
@ -1094,8 +1131,8 @@ vAPI.domSurveyor = (function() {
} }
}; };
var nextChunk = function(pending) { const nextChunk = function(pending) {
var added = pending.added.length !== 0 ? pending.added.shift() : [], let added = pending.added.length !== 0 ? pending.added.shift() : [],
nodes; nodes;
if ( pending.nodes.length === 0 ) { if ( pending.nodes.length === 0 ) {
if ( added.length <= 1000 ) { return added; } if ( added.length <= 1000 ) { return added; }
@ -1121,39 +1158,39 @@ vAPI.domSurveyor = (function() {
// Extract all classes/ids: these will be passed to the cosmetic // Extract all classes/ids: these will be passed to the cosmetic
// filtering engine, and in return we will obtain only the relevant // filtering engine, and in return we will obtain only the relevant
// CSS selectors. // CSS selectors.
const reWhitespace = /\s/;
// https://github.com/gorhill/uBlock/issues/672 // https://github.com/gorhill/uBlock/issues/672
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
// http://jsperf.com/enumerate-classes/6 // http://jsperf.com/enumerate-classes/6
var surveyPhase1 = function() { const surveyPhase1 = function() {
//console.time('dom surveyor/surveying'); //console.time('dom surveyor/surveying');
surveyTimer.clear(); surveyTimer.clear();
var t0 = window.performance.now(); const t0 = window.performance.now();
var rews = reWhitespace, const rews = reWhitespace;
qq, iout, nodes, i, node, v, vv, j; const ids = [];
var ids = []; let iout = 0;
iout = 0; let qq = queriedIds;
qq = queriedIds; let nodes = nextChunk(pendingIdNodes);
nodes = nextChunk(pendingIdNodes); let i = nodes.length;
i = nodes.length;
while ( i-- ) { while ( i-- ) {
node = nodes[i]; const node = nodes[i];
v = node.id; let v = node.id;
if ( typeof v !== 'string' ) { continue; } if ( typeof v !== 'string' ) { continue; }
v = v.trim(); v = v.trim();
if ( qq.has(v) === false && v.length !== 0 ) { if ( qq.has(v) === false && v.length !== 0 ) {
ids[iout++] = v; qq.add(v); ids[iout++] = v; qq.add(v);
} }
} }
var classes = []; const classes = [];
iout = 0; iout = 0;
qq = queriedClasses; qq = queriedClasses;
nodes = nextChunk(pendingClassNodes); nodes = nextChunk(pendingClassNodes);
i = nodes.length; i = nodes.length;
while ( i-- ) { while ( i-- ) {
node = nodes[i]; const node = nodes[i];
vv = node.className; let vv = node.className;
if ( typeof vv !== 'string' ) { continue; } if ( typeof vv !== 'string' ) { continue; }
if ( rews.test(vv) === false ) { if ( rews.test(vv) === false ) {
if ( qq.has(vv) === false && vv.length !== 0 ) { if ( qq.has(vv) === false && vv.length !== 0 ) {
@ -1161,9 +1198,9 @@ vAPI.domSurveyor = (function() {
} }
} else { } else {
vv = node.classList; vv = node.classList;
j = vv.length; let j = vv.length;
while ( j-- ) { while ( j-- ) {
v = vv[j]; let v = vv[j];
if ( qq.has(v) === false ) { if ( qq.has(v) === false ) {
classes[iout++] = v; qq.add(v); classes[iout++] = v; qq.add(v);
} }
@ -1190,9 +1227,10 @@ vAPI.domSurveyor = (function() {
} }
//console.timeEnd('dom surveyor/surveying'); //console.timeEnd('dom surveyor/surveying');
}; };
var reWhitespace = /\s/;
var domWatcherInterface = { const surveyTimer = new vAPI.SafeAnimationFrame(surveyPhase1);
const domWatcherInterface = {
onDOMCreated: function() { onDOMCreated: function() {
if ( if (
vAPI instanceof Object === false || vAPI instanceof Object === false ||
@ -1217,17 +1255,18 @@ vAPI.domSurveyor = (function() {
onDOMChanged: function(addedNodes) { onDOMChanged: function(addedNodes) {
if ( addedNodes.length === 0 ) { return; } if ( addedNodes.length === 0 ) { return; }
//console.time('dom surveyor/dom layout changed'); //console.time('dom surveyor/dom layout changed');
var idNodes = [], iid = 0, const idNodes = [];
classNodes = [], iclass = 0; let iid = 0;
var i = addedNodes.length, const classNodes = [];
node, nodeList, j; let iclass = 0;
let i = addedNodes.length;
while ( i-- ) { while ( i-- ) {
node = addedNodes[i]; const node = addedNodes[i];
idNodes[iid++] = node; idNodes[iid++] = node;
classNodes[iclass++] = node; classNodes[iclass++] = node;
if ( node.childElementCount === 0 ) { continue; } if ( node.childElementCount === 0 ) { continue; }
nodeList = node.querySelectorAll('[id]'); let nodeList = node.querySelectorAll('[id]');
j = nodeList.length; let j = nodeList.length;
while ( j-- ) { while ( j-- ) {
idNodes[iid++] = nodeList[j]; idNodes[iid++] = nodeList[j];
} }
@ -1246,15 +1285,13 @@ vAPI.domSurveyor = (function() {
} }
}; };
var start = function(details) { const start = function(details) {
if ( vAPI.domWatcher instanceof Object === false ) { return; } if ( vAPI.domWatcher instanceof Object === false ) { return; }
hostname = details.hostname; hostname = details.hostname;
vAPI.domWatcher.addListener(domWatcherInterface); vAPI.domWatcher.addListener(domWatcherInterface);
}; };
return { return { start };
start: start
};
})(); })();
/******************************************************************************/ /******************************************************************************/
@ -1266,18 +1303,11 @@ vAPI.domSurveyor = (function() {
(function bootstrap() { (function bootstrap() {
var bootstrapPhase2 = function(ev) { const bootstrapPhase2 = function() {
// This can happen on Firefox. For instance: // This can happen on Firefox. For instance:
// https://github.com/gorhill/uBlock/issues/1893 // https://github.com/gorhill/uBlock/issues/1893
if ( window.location === null ) { return; } if ( window.location === null ) { return; }
if ( vAPI instanceof Object === false ) { return; }
if ( ev ) {
document.removeEventListener('DOMContentLoaded', bootstrapPhase2);
}
if ( vAPI instanceof Object === false ) {
return;
}
vAPI.messaging.send( vAPI.messaging.send(
'contentscript', 'contentscript',
@ -1303,8 +1333,8 @@ vAPI.domSurveyor = (function() {
// as nuisance popups. // as nuisance popups.
// Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu // Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu
var onMouseClick = function(ev) { const onMouseClick = function(ev) {
var elem = ev.target; let elem = ev.target;
while ( elem !== null && elem.localName !== 'a' ) { while ( elem !== null && elem.localName !== 'a' ) {
elem = elem.parentElement; elem = elem.parentElement;
} }
@ -1327,9 +1357,9 @@ vAPI.domSurveyor = (function() {
}); });
}; };
var bootstrapPhase1 = function(response) { const bootstrapPhase1 = function(response) {
// cosmetic filtering engine aka 'cfe' // cosmetic filtering engine aka 'cfe'
var cfeDetails = response && response.specificCosmeticFilters; const cfeDetails = response && response.specificCosmeticFilters;
if ( !cfeDetails || !cfeDetails.ready ) { if ( !cfeDetails || !cfeDetails.ready ) {
vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer = vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer =
vAPI.domSurveyor = vAPI.domIsLoaded = null; vAPI.domSurveyor = vAPI.domIsLoaded = null;
@ -1340,7 +1370,7 @@ vAPI.domSurveyor = (function() {
vAPI.domFilterer = null; vAPI.domFilterer = null;
vAPI.domSurveyor = null; vAPI.domSurveyor = null;
} else { } else {
var domFilterer = vAPI.domFilterer; const domFilterer = vAPI.domFilterer;
if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) { if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) {
vAPI.domSurveyor = null; vAPI.domSurveyor = null;
} }
@ -1398,7 +1428,11 @@ vAPI.domSurveyor = (function() {
) { ) {
bootstrapPhase2(); bootstrapPhase2();
} else { } else {
document.addEventListener('DOMContentLoaded', bootstrapPhase2); document.addEventListener(
'DOMContentLoaded',
bootstrapPhase2,
{ once: true }
);
} }
}; };
@ -1419,4 +1453,5 @@ vAPI.domSurveyor = (function() {
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
} // <<<<<<<< end of HUGE-IF-BLOCK }
// <<<<<<<< end of HUGE-IF-BLOCK

View File

@ -61,7 +61,7 @@ let filterFromCompiledData = function(args) {
// One hostname => one selector // One hostname => one selector
let FilterOneOne = function(hostname, selector) { const FilterOneOne = function(hostname, selector) {
this.hostname = hostname; this.hostname = hostname;
this.selector = selector; this.selector = selector;
}; };
@ -87,7 +87,7 @@ FilterOneOne.prototype = {
retrieve: function(target, out) { retrieve: function(target, out) {
if ( target.endsWith(this.hostname) === false ) { return; } if ( target.endsWith(this.hostname) === false ) { return; }
let i = target.length - this.hostname.length; const i = target.length - this.hostname.length;
if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) { return; } if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) { return; }
out.add(this.selector); out.add(this.selector);
}, },
@ -107,7 +107,7 @@ registerFilterClass(FilterOneOne);
// One hostname => many selectors // One hostname => many selectors
let FilterOneMany = function(hostname, selectors) { const FilterOneMany = function(hostname, selectors) {
this.hostname = hostname; this.hostname = hostname;
this.selectors = selectors; this.selectors = selectors;
}; };
@ -131,7 +131,7 @@ FilterOneMany.prototype = {
retrieve: function(target, out) { retrieve: function(target, out) {
if ( target.endsWith(this.hostname) === false ) { return; } if ( target.endsWith(this.hostname) === false ) { return; }
let i = target.length - this.hostname.length; const i = target.length - this.hostname.length;
if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) { return; } if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) { return; }
for ( let selector of this.selectors ) { for ( let selector of this.selectors ) {
out.add(selector); out.add(selector);
@ -161,7 +161,7 @@ FilterManyAny.prototype = {
fid: 10, fid: 10,
add: function(hostname, selector) { add: function(hostname, selector) {
let selectors = this.entries.get(hostname); const selectors = this.entries.get(hostname);
if ( selectors === undefined ) { if ( selectors === undefined ) {
this.entries.set(hostname, selector); this.entries.set(hostname, selector);
} else if ( typeof selectors === 'string' ) { } else if ( typeof selectors === 'string' ) {
@ -172,19 +172,19 @@ FilterManyAny.prototype = {
}, },
retrieve: function(target, out) { retrieve: function(target, out) {
for ( let entry of this.entries ) { for ( const entry of this.entries ) {
let hostname = entry[0]; const hostname = entry[0];
if ( target.endsWith(hostname) === false ) { continue; } if ( target.endsWith(hostname) === false ) { continue; }
let i = target.length - hostname.length; const i = target.length - hostname.length;
if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) { if ( i !== 0 && target.charCodeAt(i-1) !== 0x2E /* '.' */ ) {
continue; continue;
} }
let selectors = entry[1]; const selectors = entry[1];
if ( typeof selectors === 'string' ) { if ( typeof selectors === 'string' ) {
out.add(selectors); out.add(selectors);
continue; continue;
} }
for ( let selector of selectors ) { for ( const selector of selectors ) {
out.add(selector); out.add(selector);
} }
} }
@ -523,8 +523,8 @@ FilterContainer.prototype.compile = function(parsed, writer) {
// 1000 = cosmetic filtering // 1000 = cosmetic filtering
writer.select(1000); writer.select(1000);
let hostnames = parsed.hostnames, const hostnames = parsed.hostnames;
i = hostnames.length; let i = hostnames.length;
if ( i === 0 ) { if ( i === 0 ) {
this.compileGenericSelector(parsed, writer); this.compileGenericSelector(parsed, writer);
return true; return true;
@ -535,7 +535,7 @@ FilterContainer.prototype.compile = function(parsed, writer) {
// of same filter OR globally if there is no non-negated hostnames. // of same filter OR globally if there is no non-negated hostnames.
let applyGlobally = true; let applyGlobally = true;
while ( i-- ) { while ( i-- ) {
let hostname = hostnames[i]; const hostname = hostnames[i];
if ( hostname.startsWith('~') === false ) { if ( hostname.startsWith('~') === false ) {
applyGlobally = false; applyGlobally = false;
} }
@ -599,7 +599,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
if ( compiled === undefined || compiled !== selector ) { if ( compiled === undefined || compiled !== selector ) {
const who = writer.properties.get('assetKey') || '?'; const who = writer.properties.get('assetKey') || '?';
µb.logger.writeOne({ µb.logger.writeOne({
error: `Invalid generic cosmetic filter in ${who} : ##${selector}` error: `Invalid generic cosmetic filter in ${who}: ##${selector}`
}); });
return; return;
} }

View File

@ -167,6 +167,7 @@
'matches-css-after', 'matches-css-after',
'matches-css-before', 'matches-css-before',
'not', 'not',
'watch-attrs',
'xpath' 'xpath'
].join('|'), ].join('|'),
')\\(' ')\\('
@ -235,6 +236,23 @@
} }
}; };
const compileSpathExpression = function(s) {
if ( isValidCSSSelector('*' + s) ) {
return s;
}
};
const compileAttrList = function(s) {
const attrs = s.split('\s*,\s*');
const out = [];
for ( const attr of attrs ) {
if ( attr !== '' ) {
out.push(attr);
}
}
return out;
};
const compileXpathExpression = function(s) { const compileXpathExpression = function(s) {
try { try {
document.createExpression(s, null); document.createExpression(s, null);
@ -260,6 +278,8 @@
[ ':matches-css-after', compileCSSDeclaration ], [ ':matches-css-after', compileCSSDeclaration ],
[ ':matches-css-before', compileCSSDeclaration ], [ ':matches-css-before', compileCSSDeclaration ],
[ ':not', compileNotSelector ], [ ':not', compileNotSelector ],
[ ':spath', compileSpathExpression ],
[ ':watch-attrs', compileAttrList ],
[ ':xpath', compileXpathExpression ] [ ':xpath', compileXpathExpression ]
]); ]);
@ -277,10 +297,11 @@
} }
const raw = [ compiled.selector ]; const raw = [ compiled.selector ];
let value; let value;
for ( let task of tasks ) { for ( const task of tasks ) {
switch ( task[0] ) { switch ( task[0] ) {
case ':xpath': case ':has':
raw.push(task[0], '(', task[1], ')'); case ':if':
raw.push(':has', '(', decompile(task[1]), ')');
break; break;
case ':has-text': case ':has-text':
if ( Array.isArray(task[1]) ) { if ( Array.isArray(task[1]) ) {
@ -306,14 +327,15 @@
} }
raw.push(task[0], '(', task[1].name, ': ', value, ')'); raw.push(task[0], '(', task[1].name, ': ', value, ')');
break; break;
case ':has':
case ':if':
raw.push(':has', '(', decompile(task[1]), ')');
break;
case ':if-not':
case ':not': case ':not':
case ':if-not':
raw.push(':not', '(', decompile(task[1]), ')'); raw.push(':not', '(', decompile(task[1]), ')');
break; break;
case ':spath':
case ':watch-attrs':
case ':xpath':
raw.push(task[0], '(', task[1], ')');
break;
} }
} }
return raw.join(''); return raw.join('');
@ -364,13 +386,7 @@
// then consider it to be part of the prefix. If there is // then consider it to be part of the prefix. If there is
// at least one task present, then we fail, as we do not // at least one task present, then we fail, as we do not
// support suffix CSS selectors. // support suffix CSS selectors.
// TODO: AdGuard does support suffix CSS selectors, so if ( isValidCSSSelector(raw.slice(opNameBeg, i)) ) { continue; }
// supporting this would increase compatibility with
// AdGuard filter lists.
if ( isValidCSSSelector(raw.slice(opNameBeg, i)) ) {
if ( opPrefixBeg !== 0 ) { return; }
continue;
}
// Extract and remember operator details. // Extract and remember operator details.
let operator = raw.slice(opNameBeg, opNameEnd); let operator = raw.slice(opNameBeg, opNameEnd);
operator = normalizedOperators.get(operator) || operator; operator = normalizedOperators.get(operator) || operator;
@ -380,7 +396,11 @@
if ( opPrefixBeg === 0 ) { if ( opPrefixBeg === 0 ) {
prefix = raw.slice(0, opNameBeg); prefix = raw.slice(0, opNameBeg);
} else if ( opNameBeg !== opPrefixBeg ) { } else if ( opNameBeg !== opPrefixBeg ) {
return; const spath = compileSpathExpression(
raw.slice(opPrefixBeg, opNameBeg)
);
if ( spath === undefined ) { return; }
tasks.push([ ':spath', spath ]);
} }
tasks.push([ operator, args ]); tasks.push([ operator, args ]);
opPrefixBeg = i; opPrefixBeg = i;
@ -392,7 +412,9 @@
prefix = raw; prefix = raw;
tasks = undefined; tasks = undefined;
} else if ( opPrefixBeg < n ) { } else if ( opPrefixBeg < n ) {
return; const spath = compileSpathExpression(raw.slice(opPrefixBeg));
if ( spath === undefined ) { return; }
tasks.push([ ':spath', spath ]);
} }
// https://github.com/NanoAdblocker/NanoCore/issues/1#issuecomment-354394894 // https://github.com/NanoAdblocker/NanoCore/issues/1#issuecomment-354394894
if ( prefix !== '' ) { if ( prefix !== '' ) {
@ -407,7 +429,7 @@
return lastProceduralSelectorCompiled; return lastProceduralSelectorCompiled;
} }
lastProceduralSelector = raw; lastProceduralSelector = raw;
var compiled = compile(raw); let compiled = compile(raw);
if ( compiled !== undefined ) { if ( compiled !== undefined ) {
compiled.raw = decompile(compiled); compiled.raw = decompile(compiled);
compiled = JSON.stringify(compiled); compiled = JSON.stringify(compiled);