diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index c96706b0d..ad22909df 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -38,7 +38,7 @@ "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], - "js": ["js/vapi-client.js", "js/contentscript.js"], + "js": ["js/vapi-client.js", "js/vapi-usercss.js", "js/contentscript.js"], "run_at": "document_start", "all_frames": true }, diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index a3fdc6815..a0efabb61 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -853,22 +853,17 @@ vAPI.messaging.onPortMessage = (function() { if ( supportsUserStylesheets ) { details.cssOrigin = 'user'; } - var fn; if ( msg.add ) { details.runAt = 'document_start'; - fn = chrome.tabs.insertCSS; - } else { - fn = chrome.tabs.removeCSS; } - var css = msg.css; - if ( typeof css === 'string' ) { - details.code = css; - fn(tabId, details); - return; + var cssText; + for ( cssText of msg.add ) { + details.code = cssText; + chrome.tabs.insertCSS(tabId, details); } - for ( var i = 0, n = css.length; i < n; i++ ) { - details.code = css[i]; - fn(tabId, details); + for ( cssText of msg.remove ) { + details.code = cssText; + chrome.tabs.removeCSS(tabId, details); } break; } diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index cd3a7a23a..6fd2d77a4 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -171,14 +171,15 @@ vAPI.messaging = { portPoller: function() { this.portTimer = null; - if ( this.port !== null ) { - if ( this.channelCount !== 0 || this.pendingCount !== 0 ) { - this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay); - this.portTimerDelay = Math.min(this.portTimerDelay * 2, 60 * 60 * 1000); - return; - } + if ( + this.port !== null && + this.channelCount === 0 && + this.pendingCount === 0 + ) { + return this.destroyPort(); } - this.destroyPort(); + this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay); + this.portTimerDelay = Math.min(this.portTimerDelay * 2, 60 * 60 * 1000); }, portPollerCallback: null, @@ -324,15 +325,12 @@ vAPI.messaging = { sendToChannelListeners: function(channelName, msg) { var listeners = this.channels[channelName]; - if ( listeners === undefined ) { - return; - } + if ( listeners === undefined ) { return; } + listeners = listeners.slice(0); var response; - for ( var i = 0, n = listeners.length; i < n; i++ ) { - response = listeners[i](msg); - if ( response !== undefined ) { - break; - } + for ( var listener of listeners ) { + response = listener(msg); + if ( response !== undefined ) { break; } } return response; } diff --git a/platform/chromium/vapi-usercss.js b/platform/chromium/vapi-usercss.js new file mode 100644 index 000000000..56072eba6 --- /dev/null +++ b/platform/chromium/vapi-usercss.js @@ -0,0 +1,506 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2017 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'; + +// For content pages + +// Abort execution if our global vAPI object does not exist. +// https://github.com/chrisaljoudi/uBlock/issues/456 +// https://github.com/gorhill/uBlock/issues/2029 + +if ( typeof vAPI === 'object' ) { // >>>>>>>> start of HUGE-IF-BLOCK + +/******************************************************************************/ +/******************************************************************************/ + +vAPI.DOMFilterer = function() { + this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); + this.domIsReady = document.readyState !== 'loading'; + this.listeners = []; + this.hideNodeId = vAPI.randomToken(); + this.hideNodeStylesheet = false; + this.excludedNodeSet = new WeakSet(); + this.addedNodes = new Set(); + this.removedNodes = false; + + this.specificSimpleHide = new Set(); + this.specificSimpleHideAggregated = undefined; + this.addedSpecificSimpleHide = []; + this.specificComplexHide = new Set(); + this.specificComplexHideAggregated = undefined; + this.addedSpecificComplexHide = []; + + this.genericSimpleHide = new Set(); + this.genericComplexHide = new Set(); + + this.userStylesheet = { + style: null, + css: new Map(), + disabled: false, + add: function(cssText) { + if ( cssText === '' || this.css.has(cssText) ) { return; } + if ( this.style === null ) { + this.style = document.createElement('style'); + this.style.disabled = this.disabled; + var parent = document.head || document.documentElement; + if ( parent !== null ) { + parent.appendChild(this.style); + } + } + var sheet = this.style.sheet, + i = sheet.cssRules.length; + if ( !sheet ) { return; } + sheet.insertRule(cssText, i); + this.css.set(cssText, sheet.cssRules[i]); + }, + remove: function(cssText) { + if ( cssText === '' ) { return; } + var cssRule = this.css.get(cssText); + if ( cssRule === undefined ) { return; } + this.css.delete(cssText); + if ( this.style === null ) { return; } + var rules = this.style.sheet.cssRules, + i = rules.length; + while ( i-- ) { + if ( rules[i] !== cssRule ) { continue; } + this.style.sheet.deleteRule(i); + break; + } + if ( rules.length === 0 ) { + var parent = this.style.parentNode; + if ( parent !== null ) { + parent.removeChild(this.style); + } + this.style = null; + } + }, + toggle: function(state) { + if ( state === undefined ) { state = this.disabled; } + if ( state !== this.disabled ) { return; } + this.disabled = !state; + if ( this.style !== null ) { + this.style.disabled = this.disabled; + } + }, + getAllSelectors: function() { + var out = []; + var rules = this.style && + this.style.sheet && + this.style.sheet.cssRules; + if ( rules instanceof Object === false ) { return out; } + var i = rules.length; + while ( i-- ) { + out.push(rules.item(i).selectorText); + } + return out; + } + }; + + this.hideNodeExpando = undefined; + this.hideNodeBatchProcessTimer = undefined; + this.hiddenNodeObserver = undefined; + this.hiddenNodesetToProcess = new Set(); + this.hiddenNodeset = new WeakSet(); + + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(this); + } +}; + +vAPI.DOMFilterer.prototype = { + reHideStyle: /^display: none !important;$/, + + // https://www.w3.org/community/webed/wiki/CSS/Selectors#Combinators + reCSSCombinators: /[ >+~]/, + + commitNow: function() { + this.commitTimer.clear(); + + if ( this.domIsReady !== true || this.userStylesheet.disabled ) { + return; + } + + var nodes, node; + + // Filterset changed. + + if ( this.addedSpecificSimpleHide.length !== 0 ) { + console.time('specific simple filterset changed'); + console.log('added %d specific simple selectors', this.addedSpecificSimpleHide.length); + nodes = document.querySelectorAll(this.addedSpecificSimpleHide.join(',')); + for ( node of nodes ) { + this.hideNode(node); + } + this.addedSpecificSimpleHide = []; + this.specificSimpleHideAggregated = undefined; + console.timeEnd('specific simple filterset changed'); + } + + if ( this.addedSpecificComplexHide.length !== 0 ) { + console.time('specific complex filterset changed'); + console.log('added %d specific complex selectors', this.addedSpecificComplexHide.length); + nodes = document.querySelectorAll(this.addedSpecificComplexHide.join(',')); + for ( node of nodes ) { + this.hideNode(node); + } + this.addedSpecificComplexHide = []; + this.specificComplexHideAggregated = undefined; + console.timeEnd('specific complex filterset changed'); + } + + // DOM layout changed. + + var domNodesAdded = this.addedNodes.size !== 0, + domLayoutChanged = domNodesAdded || this.removedNodes; + + if ( domNodesAdded === false || domLayoutChanged === false ) { + return; + } + + console.log('%d nodes added', this.addedNodes.size); + + if ( this.specificSimpleHide.size !== 0 && domNodesAdded ) { + console.time('dom layout changed/specific simple selectors'); + if ( this.specificSimpleHideAggregated === undefined ) { + this.specificSimpleHideAggregated = + Array.from(this.specificSimpleHide).join(',\n'); + } + for ( node of this.addedNodes ) { + if ( node[vAPI.matchesProp](this.specificSimpleHideAggregated) ) { + this.hideNode(node); + } + nodes = node.querySelectorAll(this.specificSimpleHideAggregated); + for ( node of nodes ) { + this.hideNode(node); + } + } + console.timeEnd('dom layout changed/specific simple selectors'); + } + + if ( this.specificComplexHide.size !== 0 && domLayoutChanged ) { + console.time('dom layout changed/specific complex selectors'); + if ( this.specificComplexHideAggregated === undefined ) { + this.specificComplexHideAggregated = + Array.from(this.specificComplexHide).join(',\n'); + } + nodes = document.querySelectorAll(this.specificComplexHideAggregated); + for ( node of nodes ) { + this.hideNode(node); + } + console.timeEnd('dom layout changed/specific complex selectors'); + } + + this.addedNodes.clear(); + this.removedNodes = false; + }, + + commit: function(now) { + if ( now ) { + this.commitTimer.clear(); + this.commitNow(); + } else { + this.commitTimer.start(); + } + }, + + addCSSRule: function(selectors, declarations, details) { + if ( selectors === undefined ) { return; } + + if ( details === undefined ) { details = {}; } + + var isGeneric= details.lazy === true, + isSimple = details.type === 'simple', + isComplex = details.type === 'complex', + selector; + + var selectorsStr = Array.isArray(selectors) ? + selectors.join(',\n') : + selectors; + if ( selectorsStr.length === 0 ) { return; } + + this.userStylesheet.add( + selectorsStr + + '\n{ ' + declarations + ' }' + ); + this.commit(); + + this.triggerListeners('declarative', selectorsStr); + + if ( this.reHideStyle.test(declarations) === false ) { + return; + } + + if ( isGeneric ) { + if ( isSimple ) { + this.genericSimpleHide.add(selectorsStr); + return; + } + if ( isComplex ) { + this.genericComplexHide.add(selectorsStr); + return; + } + } + + var selectorsArr = Array.isArray(selectors) ? + selectors : + selectors.split(',\n'); + + if ( isGeneric ) { + for ( selector of selectorsArr ) { + if ( this.reCSSCombinators.test(selector) ) { + this.genericComplexHide.add(selector); + } else { + this.genericSimpleHide.add(selector); + } + } + return; + } + + // Specific cosmetic filters. + for ( selector of selectorsArr ) { + if ( + isComplex || + isSimple === false && this.reCSSCombinators.test(selector) + ) { + if ( this.specificComplexHide.has(selector) === false ) { + this.specificComplexHide.add(selector); + this.addedSpecificComplexHide.push(selector); + } + } else if ( this.specificSimpleHide.has(selector) === false ) { + this.specificSimpleHide.add(selector); + this.addedSpecificSimpleHide.push(selector); + } + } + }, + + removeCSSRule: function(selectors, declarations) { + var selectorsStr = Array.isArray(selectors) + ? selectors.join(',\n') + : selectors; + if ( selectorsStr.length === 0 ) { return; } + this.userStylesheet.remove( + selectorsStr + + '\n{ ' + declarations + ' }' + ); + if ( this.reHideStyle.test(declarations) === false ) { return; } + var selectorsArr = Array.isArray(selectors) ? + selectors : + selectors.split(',\n'); + for ( var selector of selectorsArr ) { + if ( this.reCSSCombinators.test(selector) ) { + this.specificComplexHide.remove(selector); + this.genericComplexHide.remove(selector); + } else { + this.specificSimpleHide.remove(selector); + this.genericSimpleHide.remove(selector); + } + } + this.commit(); + }, + + onDOMCreated: function() { + this.domIsReady = true; + this.addedNodes.clear(); + this.removedNodes = false; + this.commit(); + }, + + onDOMChanged: function(addedNodes, removedNodes) { + for ( var node of addedNodes ) { + this.addedNodes.add(node); + } + this.removedNodes = this.removedNodes || removedNodes; + this.commit(); + }, + + addListener: function(listener) { + if ( this.listeners.indexOf(listener) !== -1 ) { return; } + this.listeners.push(listener); + }, + + removeListener: function(listener) { + var pos = this.listeners.indexOf(listener); + if ( pos === -1 ) { return; } + this.listeners.splice(pos, 1); + }, + + triggerListeners: function(type, selectors) { + var i = this.listeners.length; + while ( i-- ) { + this.listeners[i].onFiltersetChanged(type, selectors); + } + }, + + // https://jsperf.com/clientheight-and-clientwidth-vs-getcomputedstyle + // Avoid getComputedStyle(), detecting whether a node is visible can be + // achieved with clientWidth/clientHeight. + // https://gist.github.com/paulirish/5d52fb081b3570c81e3a + // Do not interleave read-from/write-to the DOM. Write-to DOM + // operations would cause the first read-from to be expensive, and + // interleaving means that potentially all single read-from operation + // would be expensive rather than just the 1st one. + // Benchmarking toggling off/on cosmetic filtering confirms quite an + // improvement when: + // - batching as much as possible handling of all nodes; + // - avoiding to interleave read-from/write-to operations. + // However, toggling off/on cosmetic filtering repeatedly is not + // a real use case, but this shows this will help performance + // on sites which try to use inline styles to bypass blockers. + hideNodeBatchProcess: function() { + this.hideNodeBatchProcessTimer.clear(); + var expando = this.hideNodeExpando; + for ( var node of this.hiddenNodesetToProcess ) { + if ( + this.hiddenNodeset.has(node) === false || + node[expando] === undefined || + node.clientHeight === 0 || node.clientWidth === 0 + ) { + continue; + } + var attr = node.getAttribute('style'); + if ( attr === null ) { + attr = ''; + } else if ( + attr.length !== 0 && + attr.charCodeAt(attr.length - 1) !== 0x3B /* ';' */ + ) { + attr += '; '; + } + node.setAttribute('style', attr + 'display: none !important;'); + } + this.hiddenNodesetToProcess.clear(); + }, + + hideNodeObserverHandler: function(mutations) { + if ( this.userStylesheet.disabled ) { return; } + var i = mutations.length, + stagedNodes = this.hiddenNodesetToProcess; + while ( i-- ) { + stagedNodes.add(mutations[i].target); + } + this.hideNodeBatchProcessTimer.start(); + }, + + hiddenNodeObserverOptions: { + attributes: true, + attributeFilter: [ 'style' ] + }, + + hideNodeInit: function() { + this.hideNodeExpando = vAPI.randomToken(); + this.hideNodeBatchProcessTimer = + new vAPI.SafeAnimationFrame(this.hideNodeBatchProcess.bind(this)); + this.hiddenNodeObserver = + new MutationObserver(this.hideNodeObserverHandler.bind(this)); + if ( this.hideNodeStylesheet === false ) { + this.hideNodeStylesheet = true; + this.userStylesheet.add( + '[' + this.hideNodeId + ']\n{ display: none !important; }' + ); + } + }, + + excludeNode: function(node) { + this.excludedNodeSet.add(node); + this.unhideNode(node); + }, + + hideNode: function(node) { + if ( this.excludedNodeSet.has(node) ) { return; } + if ( this.hiddenNodeset.has(node) ) { return; } + this.hiddenNodeset.add(node); + if ( this.hideNodeExpando === undefined ) { this.hideNodeInit(); } + node.setAttribute(this.hideNodeId, ''); + if ( node[this.hideNodeExpando] === undefined ) { + node[this.hideNodeExpando] = + node.hasAttribute('style') && + (node.getAttribute('style') || ''); + } + this.hiddenNodesetToProcess.add(node); + this.hideNodeBatchProcessTimer.start(); + this.hiddenNodeObserver.observe(node, this.hiddenNodeObserverOptions); + }, + + unhideNode: function(node) { + if ( this.hiddenNodeset.has(node) === false ) { return; } + node.removeAttribute(this.hideNodeId); + this.hiddenNodesetToProcess.delete(node); + if ( this.hideNodeExpando === undefined ) { return; } + var attr = node[this.hideNodeExpando]; + if ( attr === false ) { + node.removeAttribute('style'); + } else if ( typeof attr === 'string' ) { + node.setAttribute('style', attr); + } + node[this.hideNodeExpando] = undefined; + this.hiddenNodeset.delete(node); + }, + + showNode: function(node) { + var attr = node[this.hideNodeExpando]; + if ( attr === false ) { + node.removeAttribute('style'); + } else if ( typeof attr === 'string' ) { + node.setAttribute('style', attr); + } + }, + + unshowNode: function(node) { + this.hiddenNodesetToProcess.add(node); + }, + + toggle: function(state) { + this.userStylesheet.toggle(state); + var disabled = this.userStylesheet.disabled, + nodes = document.querySelectorAll('[' + this.hideNodeId + ']'); + for ( var node of nodes ) { + if ( disabled ) { + this.showNode(node); + } else { + this.unshowNode(node); + } + } + if ( disabled === false && this.hideNodeExpando !== undefined ) { + this.hideNodeBatchProcessTimer.start(); + } + }, + + getFilteredElementCount: function() { + return document.querySelectorAll( + this.userStylesheet.getAllSelectors().join(',\n') + ).length; + }, + + getAllDeclarativeSelectors: function() { + return [].concat( + Array.from(this.specificSimpleHide), + Array.from(this.specificComplexHide), + Array.from(this.genericSimpleHide), + Array.from(this.genericComplexHide) + ).join(',\n'); + } +}; + +/******************************************************************************/ +/******************************************************************************/ + +} // <<<<<<<< end of HUGE-IF-BLOCK diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 03ef0bebd..c99da7eeb 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -566,6 +566,7 @@ var contentObserver = { let sandbox = this.initContentScripts(win, true); try { lss(this.contentBaseURI + 'vapi-client.js', sandbox); + lss(this.contentBaseURI + 'vapi-usercss.js', sandbox); lss(this.contentBaseURI + 'contentscript.js', sandbox); } catch (ex) { //console.exception(ex.msg, ex.stack); diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index 22205299a..d30bbe631 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -119,6 +119,36 @@ vAPI.shutdown = { /******************************************************************************/ +var insertUserCSS = self.injectCSS || function(){}, + removeUserCSS = self.removeCSS || function(){}; + +var processUserCSS = function(details, callback) { + var cssText; + var aa = details.add; + if ( Array.isArray(aa) ) { + for ( cssText of aa ) { + insertUserCSS( + 'data:text/css;charset=utf-8,' + + encodeURIComponent(cssText) + ); + } + } + aa = details.remove; + if ( Array.isArray(aa) ) { + for ( cssText of aa ) { + removeUserCSS( + 'data:text/css;charset=utf-8,' + + encodeURIComponent(cssText) + ); + } + } + if ( typeof callback === 'function' ) { + callback(); + } +}; + +/******************************************************************************/ + vAPI.messaging = { channels: Object.create(null), channelCount: 0, @@ -276,6 +306,10 @@ vAPI.messaging = { }, send: function(channelName, message, callback) { + // User stylesheets are handled content-side on legacy Firefox. + if ( channelName === 'vapi-background' && message.what === 'userCSS' ) { + return processUserCSS(message, callback); + } this.sendTo(channelName, message, undefined, undefined, callback); }, @@ -358,15 +392,12 @@ vAPI.messaging = { sendToChannelListeners: function(channelName, msg) { var listeners = this.channels[channelName]; - if ( listeners === undefined ) { - return; - } + if ( listeners === undefined ) { return; } + listeners = listeners.slice(0); var response; - for ( var i = 0, n = listeners.length; i < n; i++ ) { - response = listeners[i](msg); - if ( response !== undefined ) { - break; - } + for ( var listener of listeners ) { + response = listener(msg); + if ( response !== undefined ) { break; } } return response; } @@ -378,46 +409,6 @@ vAPI.messaging.start(); /******************************************************************************/ -if ( self.injectCSS ) { - vAPI.userCSS = { - _userCSS: '', - _sheetURI: '', - _load: function() { - if ( this._userCSS === '' || this._sheetURI !== '' ) { return; } - this._sheetURI = 'data:text/css;charset=utf-8,' + encodeURIComponent(this._userCSS); - self.injectCSS(this._sheetURI); - }, - _unload: function() { - if ( this._sheetURI === '' ) { return; } - self.removeCSS(this._sheetURI); - this._sheetURI = ''; - }, - add: function(cssText) { - if ( cssText === '' ) { return; } - if ( this._userCSS !== '' ) { this._userCSS += '\n'; } - this._userCSS += cssText; - this._unload(); - this._load(); - }, - remove: function(cssText) { - if ( cssText === '' || this._userCSS === '' ) { return; } - this._userCSS = this._userCSS.replace(cssText, '').trim(); - this._unload(); - this._load(); - }, - toggle: function(state) { - if ( this._userCSS === '' ) { return; } - if ( state === undefined ) { - state = this._sheetURI === ''; - } - return state ? this._load() : this._unload(); - } - }; - vAPI.hideNode = vAPI.unhideNode = function(){}; -} - -/******************************************************************************/ - // https://bugzilla.mozilla.org/show_bug.cgi?id=444165 // https://github.com/gorhill/uBlock/issues/2256 // Not the prettiest solution, but that's the safest/simplest I can think diff --git a/platform/webext/vapi-usercss.js b/platform/webext/vapi-usercss.js index 609bb6d36..690ab0093 100644 --- a/platform/webext/vapi-usercss.js +++ b/platform/webext/vapi-usercss.js @@ -23,41 +23,222 @@ // For content pages +if ( typeof vAPI === 'object' ) { // >>>>>>>> start of HUGE-IF-BLOCK + +/******************************************************************************/ /******************************************************************************/ -(function() { - if ( typeof vAPI !== 'object' ) { return; } +vAPI.DOMFilterer = function() { + this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); + this.domIsReady = document.readyState !== 'loading'; + this.listeners = []; + this.hideNodeId = vAPI.randomToken(); + this.hideNodeStylesheet = false; + this.excludedNodeSet = new WeakSet(); + this.addedCSSRules = []; + this.removedCSSRules = []; + this.internalRules = new Set(); - vAPI.userCSS = { - _userCSS: new Set(), - _disabled: false, - _send: function(add, css) { - vAPI.messaging.send('vapi-background', { - what: 'userCSS', - add: add, - css: css - }); + this.userStylesheets = { + current: new Set(), + added: new Set(), + removed: new Set(), + disabled: false, + apply: function() { + for ( let cssText of this.added ) { + if ( this.current.has(cssText) || this.removed.has(cssText) ) { + this.added.delete(cssText); + } else { + this.current.add(cssText); + } + } + for ( let cssText of this.removed ) { + if ( this.current.has(cssText) === false ) { + this.removed.delete(cssText); + } else { + this.current.delete(cssText); + } + } + if ( this.added.size === 0 && this.removed.size === 0 ) { return; } + if ( this.disabled === false ) { + vAPI.messaging.send('vapi-background', { + what: 'userCSS', + add: Array.from(this.added), + remove: Array.from(this.removed) + }); + } + this.added.clear(); + this.removed.clear(); }, add: function(cssText) { - if ( cssText === '' || this._userCSS.has(cssText) ) { return; } - this._userCSS.add(cssText); - if ( this._disabled ) { return; } - this._send(true, cssText); + if ( cssText === '' ) { return; } + this.added.add(cssText); }, remove: function(cssText) { if ( cssText === '' ) { return; } - if ( this._userCSS.delete(cssText) && !this._disabled ) { - this._send(true, cssText); - this._send(false, cssText); - } + this.removed.add(cssText); }, toggle: function(state) { - if ( state === undefined ) { state = this._disabled; } - if ( state !== this._disabled ) { return; } - this._disabled = !state; - if ( this._userCSS.size === 0 ) { return; } - this._send(state, Array.from(this._userCSS)); + if ( state === undefined ) { state = this.disabled; } + if ( state !== this.disabled ) { return; } + this.disabled = !state; + if ( this.current.size === 0 ) { return; } + var all = Array.from(this.current); + var toAdd = [], toRemove = []; + if ( this.disabled ) { + toRemove = all; + } else { + toAdd = all; + } + vAPI.messaging.send('vapi-background', { + what: 'userCSS', + add: toAdd, + remove: toRemove + }); } }; - vAPI.hideNode = vAPI.unhideNode = function(){}; -})(); + + if ( this.domIsReady !== true ) { + document.addEventListener('DOMContentLoaded', () => { + this.domIsReady = true; + this.commit(); + }); + } +}; + +vAPI.DOMFilterer.prototype = { + reOnlySelectors: /\n\{[^\n]+/g, + commitNow: function() { + this.commitTimer.clear(); + var i, entry, ruleText; + i = this.addedCSSRules.length; + while ( i-- ) { + entry = this.addedCSSRules[i]; + if ( entry.lazy !== true || this.domIsReady ) { + ruleText = entry.selectors + '\n{ ' + entry.declarations + ' }'; + this.userStylesheets.add(ruleText); + this.addedCSSRules.splice(i, 1); + if ( entry.internal ) { + this.internalRules.add(ruleText); + } + } + } + i = this.removedCSSRules.length; + while ( i-- ) { + entry = this.removedCSSRules[i]; + ruleText = entry.selectors + '\n{ ' + entry.declarations + ' }'; + this.userStylesheets.remove(ruleText); + this.internalRules.delete(ruleText); + } + this.removedCSSRules = []; + this.userStylesheets.apply(); + }, + + commit: function(commitNow) { + if ( commitNow ) { + this.commitTimer.clear(); + this.commitNow(); + } else { + this.commitTimer.start(); + } + }, + + addCSSRule: function(selectors, declarations, details) { + if ( selectors === undefined ) { return; } + var selectorsStr = Array.isArray(selectors) + ? selectors.join(',\n') + : selectors; + if ( selectorsStr.length === 0 ) { return; } + this.addedCSSRules.push({ + selectors: selectorsStr, + declarations, + lazy: details && details.lazy === true, + internal: details && details.internal === true + }); + this.commit(); + this.triggerListeners('declarative', selectorsStr); + }, + + removeCSSRule: function(selectors, declarations) { + var selectorsStr = Array.isArray(selectors) + ? selectors.join(',\n') + : selectors; + if ( selectorsStr.length === 0 ) { return; } + this.removedCSSRules.push({ + selectors: selectorsStr, + declarations, + }); + this.commit(); + }, + + addListener: function(listener) { + if ( this.listeners.indexOf(listener) !== -1 ) { return; } + this.listeners.push(listener); + }, + + removeListener: function(listener) { + var pos = this.listeners.indexOf(listener); + if ( pos === -1 ) { return; } + this.listeners.splice(pos, 1); + }, + + triggerListeners: function(type, selectors) { + var i = this.listeners.length; + while ( i-- ) { + this.listeners[i].onFiltersetChanged(type, selectors); + } + }, + + excludeNode: function(node) { + this.excludedNodeSet.add(node); + this.unhideNode(node); + }, + + hideNode: function(node) { + if ( this.excludedNodeSet.has(node) ) { return; } + node.setAttribute(this.hideNodeId, ''); + if ( this.hideNodeStylesheet === false ) { + this.hideNodeStylesheet = true; + this.addCSSRule( + '[' + this.hideNodeId + ']', + 'display: none !important;', + { internal: true } + ); + } + }, + + unhideNode: function(node) { + node.removeAttribute(this.hideNodeId); + }, + + toggle: function(state) { + this.userStylesheets.toggle(state); + }, + + getAllDeclarativeSelectors_: function(all) { + let selectors = []; + for ( var sheet of this.userStylesheets.current ) { + if ( all === false && this.internalRules.has(sheet) ) { continue; } + selectors.push( + sheet.replace(this.reOnlySelectors, ',').trim().slice(0, -1) + ); + } + return selectors.join(',\n'); + }, + + getFilteredElementCount: function() { + let selectors = this.getAllDeclarativeSelectors_(true); + return selectors.length !== 0 + ? document.querySelectorAll(selectors).length + : 0; + }, + + getAllDeclarativeSelectors: function() { + return this.getAllDeclarativeSelectors_(false); + } +}; + +/******************************************************************************/ +/******************************************************************************/ + +} // <<<<<<<< end of HUGE-IF-BLOCK diff --git a/src/js/background.js b/src/js/background.js index c66b06ad1..a60e2ea05 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -120,8 +120,8 @@ var µBlock = (function() { // jshint ignore:line // read-only systemSettings: { - compiledMagic: 'dhmexnfqwlom', - selfieMagic: 'dhmexnfqwlom' + compiledMagic: 'vrgorlgelgws', + selfieMagic: 'vrgorlgelgws' }, restoreBackupSettings: { diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 5838b4dde..297504f65 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -23,9 +23,15 @@ /******************************************************************************* - +--> [[domSurveyor] --> domFilterer] - domWatcher--| - +--> [domCollapser] + +--> domCollapser + | + | + domWatcher--+ + | +-- domSurveyor + | | + +--> domFilterer --+-- domLogger + | + +-- domInspector domWatcher: Watches for changes in the DOM, and notify the other components about these @@ -41,11 +47,18 @@ domSurveyor: Surveys the DOM to find new cosmetic filters to apply to the current page. + domLogger: + Surveys the page to find and report the injected cosmetic filters blocking + actual elements on the current page. This component is dynamically loaded + IF AND ONLY IF uBO's logger is opened. + If page is whitelisted: - domWatcher: off - domCollapser: off - domFilterer: off - domSurveyor: off + - domLogger: off + I verified that the code in this file is completely flushed out of memory when a page is whitelisted. @@ -54,25 +67,36 @@ - domCollapser: on - domFilterer: off - domSurveyor: off + - domLogger: off If generic cosmetic filtering is disabled: - domWatcher: on - domCollapser: on - domFilterer: on - domSurveyor: off + - domLogger: on if uBO logger is opened + + If generic cosmetic filtering is enabled: + - domWatcher: on + - domCollapser: on + - domFilterer: on + - domSurveyor: on + - domLogger: on if uBO logger is opened Additionally, the domSurveyor can turn itself off once it decides that it has become pointless (repeatedly not finding new cosmetic filters). - The domFilterer makes use of platform-dependent user styles[1] code, or - provide a default generic implementation if none is present. + The domFilterer makes use of platform-dependent user stylesheets[1]. + At time of writing, only modern Firefox provides a custom implementation, which makes for solid, reliable and low overhead cosmetic filtering on Firefox. + The generic implementation[2] performs as best as can be, but won't ever be - as reliable as real user styles. - [1] "user styles" refer to local CSS rules which have priority over, and - can't be overriden by a web page's own CSS rules. + as reliable and accurate as real user stylesheets. + + [1] "user stylesheets" refer to local CSS rules which have priority over, + and can't be overriden by a web page's own CSS rules. [2] below, see platformUserCSS / platformHideNode / platformUnhideNode */ @@ -87,6 +111,190 @@ if ( typeof vAPI !== 'undefined' ) { // >>>>>>>> start of HUGE-IF-BLOCK /******************************************************************************/ /******************************************************************************/ +// https://github.com/gorhill/uBlock/issues/2147 + +vAPI.SafeAnimationFrame = function(callback) { + this.fid = this.tid = null; + this.callback = callback; +}; + +vAPI.SafeAnimationFrame.prototype = { + start: function(delay) { + if ( delay === undefined ) { + if ( this.fid === null ) { + this.fid = requestAnimationFrame(this.callback); + } + if ( this.tid === null ) { + this.tid = vAPI.setTimeout(this.callback, 1200000); + } + } else if ( this.fid === null && this.tid === null ) { + this.tid = vAPI.setTimeout(this.callback, delay); + } + }, + clear: function() { + if ( this.fid !== null ) { cancelAnimationFrame(this.fid); } + if ( this.tid !== null ) { clearTimeout(this.tid); } + this.fid = this.tid = null; + } +}; + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +vAPI.domWatcher = (function() { + + var addedNodeLists = [], + addedNodes = [], + domIsReady = false, + domLayoutObserver, + ignoreTags = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style' ]), + listeners = [], + listenerIterator = [], listenerIteratorDirty = false, + removedNodeLists = [], + removedNodes = false, + safeObserverHandlerTimer; + + var safeObserverHandler = function() { + console.time('dom watcher/safe observer handler'); + safeObserverHandlerTimer.clear(); + var i = addedNodeLists.length, + j = addedNodes.length, + nodeList, iNode, node; + while ( i-- ) { + nodeList = addedNodeLists[i]; + iNode = nodeList.length; + while ( iNode-- ) { + node = nodeList[iNode]; + if ( node.nodeType !== 1 ) { continue; } + if ( ignoreTags.has(node.localName) ) { continue; } + if ( node.parentElement === null ) { continue; } + addedNodes[j++] = node; + } + } + addedNodeLists.length = 0; + i = removedNodeLists.length; + while ( i-- && removedNodes === false ) { + nodeList = removedNodeLists[i]; + iNode = nodeList.length; + while ( iNode-- ) { + if ( nodeList[iNode].nodeType !== 1 ) { continue; } + removedNodes = true; + break; + } + } + removedNodeLists.length = 0; + console.timeEnd('dom watcher/safe observer handler'); + if ( addedNodes.length === 0 && removedNodes === false ) { return; } + for ( var listener of getListenerIterator() ) { + listener.onDOMChanged(addedNodes, removedNodes); + } + addedNodes.length = 0; + removedNodes = false; + }; + + // https://github.com/chrisaljoudi/uBlock/issues/205 + // Do not handle added node directly from within mutation observer. + var observerHandler = function(mutations) { + console.time('dom watcher/observer handler'); + var nodeList, mutation, + i = mutations.length; + while ( i-- ) { + mutation = mutations[i]; + nodeList = mutation.addedNodes; + if ( nodeList.length !== 0 ) { + addedNodeLists.push(nodeList); + } + if ( removedNodes ) { continue; } + nodeList = mutation.removedNodes; + if ( nodeList.length !== 0 ) { + removedNodeLists.push(nodeList); + } + } + if ( addedNodeLists.length !== 0 || removedNodes ) { + safeObserverHandlerTimer.start(); + } + console.timeEnd('dom watcher/observer handler'); + }; + + var startMutationObserver = function() { + if ( domLayoutObserver !== undefined || !domIsReady ) { return; } + domLayoutObserver = new MutationObserver(observerHandler); + domLayoutObserver.observe(document.documentElement, { + //attributeFilter: [ 'class', 'id' ], + //attributes: true, + childList: true, + subtree: true + }); + safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler); + vAPI.shutdown.add(cleanup); + }; + + var stopMutationObserver = function() { + if ( domLayoutObserver === undefined ) { return; } + cleanup(); + vAPI.shutdown.remove(cleanup); + }; + + var getListenerIterator = function() { + if ( listenerIteratorDirty ) { + listenerIterator = listeners.slice(); + listenerIteratorDirty = false; + } + return listenerIterator; + }; + + var addListener = function(listener) { + if ( listeners.indexOf(listener) !== -1 ) { return; } + listeners.push(listener); + listenerIteratorDirty = true; + if ( domIsReady ) { + listener.onDOMCreated(); + } else { + startMutationObserver(); + } + }; + + var removeListener = function(listener) { + var pos = listeners.indexOf(listener); + if ( pos === -1 ) { return; } + listeners.splice(pos, 1); + listenerIteratorDirty = true; + if ( listeners.length === 0 ) { + stopMutationObserver(); + } + }; + + var cleanup = function() { + if ( domLayoutObserver !== undefined ) { + domLayoutObserver.disconnect(); + domLayoutObserver = null; + } + if ( safeObserverHandlerTimer !== undefined ) { + safeObserverHandlerTimer.clear(); + safeObserverHandlerTimer = undefined; + } + }; + + var start = function() { + domIsReady = true; + for ( var listener of getListenerIterator() ) { + listener.onDOMCreated(); + } + startMutationObserver(); + }; + + return { + start: start, + addListener: addListener, + removeListener: removeListener + }; +})(); + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + vAPI.matchesProp = (function() { var docElem = document.documentElement; if ( typeof docElem.matches !== 'function' ) { @@ -105,30 +313,6 @@ vAPI.matchesProp = (function() { /******************************************************************************/ /******************************************************************************/ -// https://github.com/gorhill/uBlock/issues/2147 - -vAPI.SafeAnimationFrame = function(callback) { - this.fid = this.tid = null; - this.callback = callback; -}; - -vAPI.SafeAnimationFrame.prototype.start = function() { - if ( this.fid !== null ) { return; } - this.fid = requestAnimationFrame(this.callback); - this.tid = vAPI.setTimeout(this.callback, 1200000); -}; - -vAPI.SafeAnimationFrame.prototype.clear = function() { - if ( this.fid === null ) { return; } - cancelAnimationFrame(this.fid); - clearTimeout(this.tid); - this.fid = this.tid = null; -}; - -/******************************************************************************/ -/******************************************************************************/ -/******************************************************************************/ - vAPI.injectScriptlet = function(doc, text) { if ( !doc ) { return; } try { @@ -141,868 +325,360 @@ vAPI.injectScriptlet = function(doc, text) { /******************************************************************************/ /******************************************************************************/ -/******************************************************************************/ +/******************************************************************************* -// The DOM filterer is the heart of uBO's cosmetic filtering. + The DOM filterer is the heart of uBO's cosmetic filtering. -vAPI.domFilterer = (function() { + DOMBaseFilterer: platform-specific + | + | + +---- DOMFilterer: adds procedural cosmetic filtering -/******************************************************************************/ +*/ -var allExceptions = new Set(), - allSelectors = new Set(), - stagedNodes = []; +vAPI.DOMFilterer = (function() { -// Complex selectors, due to their nature may need to be "de-committed". A -// Set() is used to implement this functionality. + // 'P' stands for 'Procedural' -var complexSelectorsOldResultSet, - complexSelectorsCurrentResultSet = new Set(); - -/******************************************************************************/ - -var cosmeticFiltersActivatedTimer = null; - -var cosmeticFiltersActivated = function() { - cosmeticFiltersActivatedTimer = null; - vAPI.messaging.send( - 'contentscript', - { what: 'cosmeticFiltersActivated' } - ); -}; - -/******************************************************************************/ - -// If a platform does not support its own vAPI.userCSS (user styles), we -// provide a default (imperfect) implementation. - -// Probably no longer need to watch for style tags removal/tampering with fix -// to https://github.com/gorhill/uBlock/issues/963 - -// https://github.com/gorhill/uBlock/issues/2810 -// With Firefox Nightly, it may happens style tags are injected before the -// head element is present. - -var platformUserCSS = (function() { - if ( vAPI.userCSS instanceof Object ) { - return vAPI.userCSS; - } - - return { - enabled: true, - styles: [], - add: function(css) { - var style = document.createElement('style'); - style.setAttribute('type', 'text/css'); - style.textContent = css; - var parent = document.head || document.documentElement; - if ( parent !== null ) { - parent.appendChild(style); - } - this.styles.push(style); - if ( style.sheet ) { - style.sheet.disabled = !this.enabled; - } - }, - remove: function(css) { - var i = this.styles.length, - style, parent; - while ( i-- ) { - style = this.styles[i]; - if ( style.textContent !== css ) { continue; } - parent = style.parentNode; - if ( parent !== null ) { - parent.removeChild(style); - } - this.styles.splice(i, 1); - } - }, - toggle: function(state) { - if ( this.styles.length === '' ) { return; } - if ( state === undefined ) { - state = !this.enabled; - } - var i = this.styles.length, style; - while ( i-- ) { - style = this.styles[i]; - if ( style.sheet !== null ) { - style.sheet.disabled = !state; - } - } - this.enabled = state; - } + var PSelectorHasTask = function(task) { + this.selector = task[1]; }; -})(); - -// If a platform does not provide its own (improved) vAPI.hideNode, we assign -// a default one to try to override author styles as best as can be. - -var platformHideNode = vAPI.hideNode, - platformUnhideNode = vAPI.unhideNode; - -(function() { - if ( platformHideNode instanceof Function ) { - return; - } - - var uid, - timer, - observer, - changedNodes = new Set(), - observerOptions = { - attributes: true, - attributeFilter: [ 'style' ] - }; - - // https://jsperf.com/clientheight-and-clientwidth-vs-getcomputedstyle - // Avoid getComputedStyle(), detecting whether a node is visible can be - // achieved with clientWidth/clientHeight. - // https://gist.github.com/paulirish/5d52fb081b3570c81e3a - // Do not interleave read-from/write-to the DOM. Write-to DOM - // operations would cause the first read-from to be expensive, and - // interleaving means that potentially all single read-from operation - // would be expensive rather than just the 1st one. - // Benchmarking toggling off/on cosmetic filtering confirms quite an - // improvement when: - // - batching as much as possible handling of all nodes; - // - avoiding to interleave read-from/write-to operations. - // However, toggling off/on cosmetic filtering repeatedly is not - // a real use case, but this shows this will help performance - // on sites which try to use inline styles to bypass blockers. - var batchProcess = function() { - timer.clear(); - var uid_ = uid; - for ( var node of changedNodes ) { - if ( - node[uid_] === undefined || - node.clientHeight === 0 || node.clientWidth === 0 - ) { - continue; + PSelectorHasTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( input[i].querySelector(this.selector) !== null ) { + output.push(input[i]); } - var attr = node.getAttribute('style'); - if ( attr === null ) { - attr = ''; - } else if ( - attr.length !== 0 && - attr.charCodeAt(attr.length - 1) !== 0x3B /* ';' */ - ) { - attr += '; '; - } - node.setAttribute('style', attr + 'display: none !important;'); } - changedNodes.clear(); + return output; }; - var observerHandler = function(mutations) { - var i = mutations.length, - changedNodes_ = changedNodes; - while ( i-- ) { - changedNodes_.add(mutations[i].target); + var PSelectorHasTextTask = function(task) { + this.needle = new RegExp(task[1]); + }; + PSelectorHasTextTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.needle.test(input[i].textContent) ) { + output.push(input[i]); + } } - timer.start(); + return output; }; - platformHideNode = function(node) { - if ( uid === undefined ) { - uid = vAPI.randomToken(); - timer = new vAPI.SafeAnimationFrame(batchProcess); + var PSelectorIfTask = function(task) { + this.pselector = new PSelector(task[1]); + }; + PSelectorIfTask.prototype.target = true; + PSelectorIfTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.pselector.test(input[i]) === this.target ) { + output.push(input[i]); + } } - if ( node[uid] === undefined ) { - node[uid] = node.hasAttribute('style') && (node.getAttribute('style') || ''); - } - // Performance: batch-process nodes to hide. - changedNodes.add(node); - timer.start(); - if ( observer === undefined ) { - observer = new MutationObserver(observerHandler); - } - observer.observe(node, observerOptions); + return output; }; - platformUnhideNode = function(node) { - if ( uid === undefined ) { return; } - var attr = node[uid]; - if ( attr === false ) { - node.removeAttribute('style'); - } else if ( typeof attr === 'string' ) { - node.setAttribute('style', attr); - } - delete node[uid]; + var PSelectorIfNotTask = function(task) { + PSelectorIfTask.call(this, task); + this.target = false; }; -})(); + PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); + PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; -/******************************************************************************/ - -// 'P' stands for 'Procedural' - -var PSelectorHasTask = function(task) { - this.selector = task[1]; -}; -PSelectorHasTask.prototype.exec = function(input) { - var output = []; - for ( var i = 0, n = input.length; i < n; i++ ) { - if ( input[i].querySelector(this.selector) !== null ) { - output.push(input[i]); - } - } - return output; -}; - -var PSelectorHasTextTask = function(task) { - this.needle = new RegExp(task[1]); -}; -PSelectorHasTextTask.prototype.exec = function(input) { - var output = []; - for ( var i = 0, n = input.length; i < n; i++ ) { - if ( this.needle.test(input[i].textContent) ) { - output.push(input[i]); - } - } - return output; -}; - -var PSelectorIfTask = function(task) { - this.pselector = new PSelector(task[1]); -}; -PSelectorIfTask.prototype.target = true; -PSelectorIfTask.prototype.exec = function(input) { - var output = []; - for ( var i = 0, n = input.length; i < n; i++ ) { - if ( this.pselector.test(input[i]) === this.target ) { - output.push(input[i]); - } - } - return output; -}; - -var PSelectorIfNotTask = function(task) { - PSelectorIfTask.call(this, task); - this.target = false; -}; -PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); -PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; - -var PSelectorMatchesCSSTask = function(task) { - this.name = task[1].name; - this.value = new RegExp(task[1].value); -}; -PSelectorMatchesCSSTask.prototype.pseudo = null; -PSelectorMatchesCSSTask.prototype.exec = function(input) { - var output = [], style; - for ( var i = 0, n = input.length; i < n; i++ ) { - style = window.getComputedStyle(input[i], this.pseudo); - if ( style === null ) { return null; } /* FF */ - if ( this.value.test(style[this.name]) ) { - output.push(input[i]); - } - } - return output; -}; - -var PSelectorMatchesCSSAfterTask = function(task) { - PSelectorMatchesCSSTask.call(this, task); - this.pseudo = ':after'; -}; -PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); -PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; - -var PSelectorMatchesCSSBeforeTask = function(task) { - PSelectorMatchesCSSTask.call(this, task); - this.pseudo = ':before'; -}; -PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); -PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; - -var PSelectorXpathTask = function(task) { - this.xpe = document.createExpression(task[1], null); - this.xpr = null; -}; -PSelectorXpathTask.prototype.exec = function(input) { - var output = [], j, node; - for ( var i = 0, n = input.length; i < n; i++ ) { - this.xpr = this.xpe.evaluate( - input[i], - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - this.xpr - ); - j = this.xpr.snapshotLength; - while ( j-- ) { - node = this.xpr.snapshotItem(j); - if ( node.nodeType === 1 ) { - output.push(node); + var PSelectorMatchesCSSTask = function(task) { + this.name = task[1].name; + this.value = new RegExp(task[1].value); + }; + PSelectorMatchesCSSTask.prototype.pseudo = null; + PSelectorMatchesCSSTask.prototype.exec = function(input) { + var output = [], style; + for ( var i = 0, n = input.length; i < n; i++ ) { + style = window.getComputedStyle(input[i], this.pseudo); + if ( style === null ) { return null; } /* FF */ + if ( this.value.test(style[this.name]) ) { + output.push(input[i]); } } - } - return output; -}; + return output; + }; -var PSelector = function(o) { - if ( PSelector.prototype.operatorToTaskMap === undefined ) { - PSelector.prototype.operatorToTaskMap = new Map([ - [ ':has', PSelectorHasTask ], - [ ':has-text', PSelectorHasTextTask ], - [ ':if', PSelectorIfTask ], - [ ':if-not', PSelectorIfNotTask ], - [ ':matches-css', PSelectorMatchesCSSTask ], - [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], - [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], - [ ':xpath', PSelectorXpathTask ] - ]); - } - this.raw = o.raw; - this.selector = o.selector; - this.tasks = []; - var tasks = o.tasks; - if ( !tasks ) { return; } - for ( var i = 0, task, ctor; i < tasks.length; i++ ) { - task = tasks[i]; - ctor = this.operatorToTaskMap.get(task[0]); - this.tasks.push(new ctor(task)); - } -}; -PSelector.prototype.operatorToTaskMap = undefined; -PSelector.prototype.prime = function(input) { - var root = input || document; - if ( this.selector !== '' ) { - return root.querySelectorAll(this.selector); - } - return [ root ]; -}; -PSelector.prototype.exec = function(input) { - //var t0 = window.performance.now(); - var tasks = this.tasks, nodes = this.prime(input); - for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) { - nodes = tasks[i].exec(nodes); - } - //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); - return nodes; -}; -PSelector.prototype.test = function(input) { - //var t0 = window.performance.now(); - var tasks = this.tasks, nodes = this.prime(input), AA = [ null ], aa; - for ( var i = 0, ni = nodes.length; i < ni; i++ ) { - AA[0] = nodes[i]; aa = AA; - for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { - aa = tasks[j].exec(aa); - } - if ( aa.length !== 0 ) { return true; } - } - //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); - return false; -}; + var PSelectorMatchesCSSAfterTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':after'; + }; + PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); + PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; -/******************************************************************************/ + var PSelectorMatchesCSSBeforeTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':before'; + }; + PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); + PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; -var domFilterer = { - addedNodesHandlerMissCount: 0, - commitTimer: null, - disabledId: vAPI.randomToken(), - enabled: true, - excludeId: undefined, - hiddenId: vAPI.randomToken(), - hiddenNodeCount: 0, - hiddenNodeEnforcer: false, - loggerEnabled: undefined, - - newHideSelectorBuffer: [], // Hide style filter buffer - newStyleRuleBuffer: [], // Non-hide style filter buffer - simpleHideSelectors: { // Hiding filters: simple selectors - entries: [], - matchesProp: vAPI.matchesProp, - selector: undefined, - add: function(selector) { - this.entries.push(selector); - this.selector = undefined; - }, - forEachNode: function(callback, root, extra) { - if ( this.selector === undefined ) { - this.selector = this.entries.join(extra + ',') + extra; - } - if ( root[this.matchesProp](this.selector) ) { - callback(root); - } - var nodes = root.querySelectorAll(this.selector), - i = nodes.length; - while ( i-- ) { - callback(nodes[i]); - } - } - }, - complexHideSelectors: { // Hiding filters: complex selectors - entries: [], - selector: undefined, - add: function(selector) { - this.entries.push(selector); - this.selector = undefined; - }, - forEachNode: function(callback) { - if ( this.selector === undefined ) { - this.selector = this.entries.join(','); - } - var nodes = document.querySelectorAll(this.selector), - i = nodes.length; - while ( i-- ) { - callback(nodes[i]); - } - } - }, - nqsSelectors: [], // Non-querySelector-able filters - proceduralSelectors: { // Hiding filters: procedural - entries: [], - add: function(o) { - this.entries.push(new PSelector(o)); - }, - forEachNode: function(callback) { - var pfilters = this.entries, i = pfilters.length, pfilter, nodes, j; - while ( i-- ) { - pfilter = pfilters[i]; - nodes = pfilter.exec(); - j = nodes.length; - while ( j-- ) { - callback(nodes[j], pfilter); - } - } - } - }, - - addExceptions: function(aa) { - for ( var i = 0, n = aa.length; i < n; i++ ) { - allExceptions.add(aa[i]); - } - }, - - addSelector: function(selector) { - if ( allSelectors.has(selector) || allExceptions.has(selector) ) { - return; - } - allSelectors.add(selector); - if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) { - this.newHideSelectorBuffer.push(selector); - if ( selector.indexOf(' ') === -1 ) { - this.simpleHideSelectors.add(selector); - } else { - this.complexHideSelectors.add(selector); - } - return; - } - var o = JSON.parse(selector); - if ( o.style ) { - this.newStyleRuleBuffer.push(o.style.join(' ')); - this.nqsSelectors.push(o.raw); - return; - } - if ( o.pseudoclass ) { - this.newHideSelectorBuffer.push(o.raw); - this.nqsSelectors.push(o.raw); - return; - } - if ( o.tasks ) { - this.proceduralSelectors.add(o); - return; - } - }, - - addSelectors: function(aa) { - for ( var i = 0, n = aa.length; i < n; i++ ) { - this.addSelector(aa[i]); - } - }, - - commit_: function() { - this.commitTimer.clear(); - - var beforeHiddenNodeCount = this.hiddenNodeCount, - styleText = ''; - - // CSS rules/hide - if ( this.newHideSelectorBuffer.length ) { - styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }'; - this.newHideSelectorBuffer.length = 0; - } - - // CSS rules/any css declaration - if ( this.newStyleRuleBuffer.length ) { - styleText += '\n' + this.newStyleRuleBuffer.join('\n'); - this.newStyleRuleBuffer.length = 0; - } - - // Simple selectors: incremental. - - // Simple css selectors/hide - if ( this.simpleHideSelectors.entries.length ) { - var i = stagedNodes.length; - while ( i-- ) { - this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId); - } - } - stagedNodes = []; - - // Complex selectors: non-incremental. - complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; - complexSelectorsCurrentResultSet = new Set(); - - // Complex css selectors/hide - // The handling of these can be considered optional, since they are - // also applied declaratively using a style tag. - if ( this.complexHideSelectors.entries.length ) { - this.complexHideSelectors.forEachNode(complexHideNode); - } - - // Procedural cosmetic filters - if ( this.proceduralSelectors.entries.length ) { - this.proceduralSelectors.forEachNode(complexHideNode); - } - - // https://github.com/gorhill/uBlock/issues/1912 - // If one or more nodes have been manually hidden, insert a style tag - // targeting these manually hidden nodes. For browsers supporting - // user styles, this allows uBO to win. - var commitHit = this.hiddenNodeCount !== beforeHiddenNodeCount; - if ( commitHit ) { - if ( this.hiddenNodeEnforcer === false ) { - styleText += '\n:root *[' + this.hiddenId + '][hidden] { display: none !important; }'; - this.hiddenNodeEnforcer = true; - } - this.addedNodesHandlerMissCount = 0; - } else { - this.addedNodesHandlerMissCount += 1; - } - - if ( styleText !== '' ) { - platformUserCSS.add(styleText); - } - - // Un-hide nodes previously hidden. - for ( var node of complexSelectorsOldResultSet ) { - this.unhideNode(node); - } - complexSelectorsOldResultSet.clear(); - - // If DOM nodes have been affected, lazily notify core process. - if ( - this.loggerEnabled !== false && - commitHit && - cosmeticFiltersActivatedTimer === null - ) { - cosmeticFiltersActivatedTimer = vAPI.setTimeout( - cosmeticFiltersActivated, - 503 + var PSelectorXpathTask = function(task) { + this.xpe = document.createExpression(task[1], null); + this.xpr = null; + }; + PSelectorXpathTask.prototype.exec = function(input) { + var output = [], j, node; + for ( var i = 0, n = input.length; i < n; i++ ) { + this.xpr = this.xpe.evaluate( + input[i], + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + this.xpr ); - } - }, - - commit: function(nodes, commitNow) { - if ( nodes === 'all' ) { - stagedNodes = [ document.documentElement ]; - } else if ( stagedNodes[0] !== document.documentElement ) { - stagedNodes = stagedNodes.concat(nodes); - } - if ( commitNow ) { - this.commitTimer.clear(); - this.commit_(); - return; - } - this.commitTimer.start(); - }, - - createProceduralFilter: function(o) { - return new PSelector(o); - }, - - getExcludeId: function() { - if ( this.excludeId === undefined ) { - this.excludeId = vAPI.randomToken(); - } - return this.excludeId; - }, - - hideNode: function(node) { - if ( node[this.hiddenId] !== undefined ) { return; } - if ( this.excludeId !== undefined && node[this.excludeId] ) { return; } - node.setAttribute(this.hiddenId, ''); - this.hiddenNodeCount += 1; - node.hidden = true; - node[this.hiddenId] = null; - platformHideNode(node); - }, - - init: function() { - this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this)); - }, - - showNode: function(node) { - node.hidden = false; - platformUnhideNode(node); - }, - - toggleLogging: function(state) { - this.loggerEnabled = state; - }, - - toggleOff: function() { - platformUserCSS.toggle(false); - this.enabled = false; - }, - - toggleOn: function() { - platformUserCSS.toggle(true); - this.enabled = true; - }, - - userCSS: platformUserCSS, - - unhideNode: function(node) { - if ( node[this.hiddenId] !== undefined ) { - this.hiddenNodeCount--; - } - node.removeAttribute(this.hiddenId); - node[this.hiddenId] = undefined; - node.hidden = false; - platformUnhideNode(node); - }, - - unshowNode: function(node) { - node.hidden = true; - platformHideNode(node); - }, - - domChangedHandler: function(addedNodes) { - this.commit(addedNodes); - }, - - start: function() { - var domChangedHandler = this.domChangedHandler.bind(this); - vAPI.domWatcher.addListener(domChangedHandler); - vAPI.shutdown.add(function() { - vAPI.domWatcher.removeListener(domChangedHandler); - }); - } -}; - -/******************************************************************************/ - -var hideNode = domFilterer.hideNode.bind(domFilterer); - -var complexHideNode = function(node) { - complexSelectorsCurrentResultSet.add(node); - if ( !complexSelectorsOldResultSet.delete(node) ) { - hideNode(node); - } -}; - -var cssNotHiddenId = ':not([' + domFilterer.hiddenId + '])'; - -domFilterer.init(); - -/******************************************************************************/ - -return domFilterer; - -/******************************************************************************/ - -})(); - -/******************************************************************************/ -/******************************************************************************/ -/******************************************************************************/ - -// This is executed once, and since no hooks are left behind once the response -// is received, I expect this code to be garbage collected by the browser. - -(function domIsLoading() { - - var responseHandler = function(response) { - // cosmetic filtering engine aka 'cfe' - var cfeDetails = response && response.specificCosmeticFilters; - if ( !cfeDetails || !cfeDetails.ready ) { - vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer = - vAPI.domSurveyor = vAPI.domIsLoaded = null; - return; - } - - if ( response.noCosmeticFiltering ) { - vAPI.domFilterer = null; - vAPI.domSurveyor = null; - } else { - var domFilterer = vAPI.domFilterer; - domFilterer.toggleLogging(response.loggerEnabled); - if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) { - vAPI.domSurveyor = null; - } - if ( cfeDetails.cosmeticHide.length !== 0 || cfeDetails.cosmeticDonthide.length !== 0 ) { - domFilterer.addExceptions(cfeDetails.cosmeticDonthide); - domFilterer.addSelectors(cfeDetails.cosmeticHide); - domFilterer.commit('all', true); - } - } - - var parent = document.head || document.documentElement; - if ( parent ) { - var elem, text; - if ( cfeDetails.netHide.length !== 0 ) { - elem = document.createElement('style'); - elem.setAttribute('type', 'text/css'); - text = cfeDetails.netHide.join(',\n'); - text += response.collapseBlocked ? - '\n{display:none !important;}' : - '\n{visibility:hidden !important;}'; - elem.appendChild(document.createTextNode(text)); - parent.appendChild(elem); - } - // Library of resources is located at: - // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt - if ( cfeDetails.scripts ) { - // Have the injected script tag remove itself when execution completes: - // to keep DOM as clean as possible. - text = cfeDetails.scripts + - "\n" + - "(function() {\n" + - " var c = document.currentScript,\n" + - " p = c && c.parentNode;\n" + - " if ( p ) {\n" + - " p.removeChild(c);\n" + - " }\n" + - "})();"; - vAPI.injectScriptlet(document, text); - vAPI.injectedScripts = text; - } - } - - // https://github.com/chrisaljoudi/uBlock/issues/587 - // If no filters were found, maybe the script was injected before - // uBlock's process was fully initialized. When this happens, pages - // won't be cleaned right after browser launch. - if ( document.readyState !== 'loading' ) { - (new vAPI.SafeAnimationFrame(vAPI.domIsLoaded)).start(); - } else { - document.addEventListener('DOMContentLoaded', vAPI.domIsLoaded); - } - }; - - var url = window.location.href; - vAPI.messaging.send( - 'contentscript', - { - what: 'retrieveContentScriptParameters', - pageURL: url, - locationURL: url - }, - responseHandler - ); - -})(); - -/******************************************************************************/ -/******************************************************************************/ -/******************************************************************************/ - -vAPI.domWatcher = (function() { - - var domLayoutObserver = null, - ignoreTags = new Set([ 'head', 'link', 'meta', 'script', 'style' ]), - addedNodeLists = [], - addedNodes = [], - removedNodes = false, - listeners = []; - - var safeObserverHandler = function() { - safeObserverHandlerTimer.clear(); - var i = addedNodeLists.length, - j = addedNodes.length, - nodeList, iNode, node; - while ( i-- ) { - nodeList = addedNodeLists[i]; - iNode = nodeList.length; - while ( iNode-- ) { - node = nodeList[iNode]; - if ( - node.nodeType === 1 && - ignoreTags.has(node.localName) === false && - node.parentElement !== null - ) { - addedNodes[j++] = node; + j = this.xpr.snapshotLength; + while ( j-- ) { + node = this.xpr.snapshotItem(j); + if ( node.nodeType === 1 ) { + output.push(node); } } } - addedNodeLists.length = 0; - if ( j === 0 && removedNodes === false ) { return; } - listeners[0](addedNodes); - if ( listeners[1] ) { - listeners[1](addedNodes); - } - addedNodes.length = 0; - removedNodes = false; + return output; }; - var safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler); - - // https://github.com/chrisaljoudi/uBlock/issues/205 - // Do not handle added node directly from within mutation observer. - var observerHandler = function(mutations) { - var nodeList, mutation, - i = mutations.length; - while ( i-- ) { - mutation = mutations[i]; - nodeList = mutation.addedNodes; - if ( nodeList.length !== 0 ) { - addedNodeLists.push(nodeList); + var PSelector = function(o) { + if ( PSelector.prototype.operatorToTaskMap === undefined ) { + PSelector.prototype.operatorToTaskMap = new Map([ + [ ':has', PSelectorHasTask ], + [ ':has-text', PSelectorHasTextTask ], + [ ':if', PSelectorIfTask ], + [ ':if-not', PSelectorIfNotTask ], + [ ':matches-css', PSelectorMatchesCSSTask ], + [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], + [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], + [ ':xpath', PSelectorXpathTask ] + ]); + } + this.raw = o.raw; + this.selector = o.selector; + this.tasks = []; + var tasks = o.tasks; + if ( !tasks ) { return; } + for ( var i = 0, task, ctor; i < tasks.length; i++ ) { + task = tasks[i]; + ctor = this.operatorToTaskMap.get(task[0]); + this.tasks.push(new ctor(task)); + } + }; + PSelector.prototype.operatorToTaskMap = undefined; + PSelector.prototype.prime = function(input) { + var root = input || document; + if ( this.selector !== '' ) { + return root.querySelectorAll(this.selector); + } + return [ root ]; + }; + PSelector.prototype.exec = function(input) { + var tasks = this.tasks, nodes = this.prime(input); + for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) { + nodes = tasks[i].exec(nodes); + } + return nodes; + }; + PSelector.prototype.test = function(input) { + var tasks = this.tasks, nodes = this.prime(input), AA = [ null ], aa; + for ( var i = 0, ni = nodes.length; i < ni; i++ ) { + AA[0] = nodes[i]; aa = AA; + for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { + aa = tasks[j].exec(aa); } - if ( mutation.removedNodes.length !== 0 ) { - removedNodes = true; + if ( aa.length !== 0 ) { return true; } + } + return false; + }; + + var DOMProceduralFilterer = function(domFilterer) { + this.domFilterer = domFilterer; + this.domIsReady = false; + this.domIsWatched = false; + this.addedSelectors = new Map(); + this.addedNodes = false; + this.removedNodes = false; + this.addedNodesHandlerMissCount = 0; + this.currentResultset = new Set(); + this.selectors = new Map(); + }; + + DOMProceduralFilterer.prototype = { + + addProceduralSelectors: function(aa) { + var raw, o, pselector, + mustCommit = this.domIsWatched; + for ( var i = 0, n = aa.length; i < n; i++ ) { + raw = aa[i]; + o = JSON.parse(raw); + if ( o.style ) { + this.domFilterer.addCSSRule(o.style[0], o.style[1]); + mustCommit = true; + continue; + } + if ( o.pseudoclass ) { + this.domFilterer.addCSSRule( + o.raw, + 'display: none !important;' + ); + mustCommit = true; + continue; + } + if ( o.tasks ) { + if ( this.selectors.has(raw) === false ) { + pselector = new PSelector(o); + this.selectors.set(raw, pselector); + this.addedSelectors.set(raw, pselector); + mustCommit = true; + } + continue; + } } - } - if ( addedNodeLists.length !== 0 || removedNodes ) { - safeObserverHandlerTimer.start(); - } - }; + if ( mustCommit === false ) { return; } + this.domFilterer.commit(); + this.domFilterer.triggerListeners( + 'procedural', + new Map(this.addedSelectors) + ); + }, - var addListener = function(listener) { - if ( listeners.indexOf(listener) !== -1 ) { - return; - } - listeners.push(listener); - if ( domLayoutObserver !== null ) { - return; - } - domLayoutObserver = new MutationObserver(observerHandler); - domLayoutObserver.observe(document.documentElement, { - //attributeFilter: [ 'class', 'id' ], - //attributes: true, - childList: true, - subtree: true - }); - }; - - var removeListener = function(listener) { - var pos = listeners.indexOf(listener); - if ( pos === -1 ) { - return; - } - listeners.splice(pos, 1); - if ( listeners.length !== 0 || domLayoutObserver === null ) { - return; - } - domLayoutObserver.disconnect(); - domLayoutObserver = null; - }; - - var start = function() { - vAPI.shutdown.add(function() { - if ( domLayoutObserver !== null ) { - domLayoutObserver.disconnect(); - domLayoutObserver = null; + commitNow: function() { + if ( this.selectors.size === 0 || this.domIsReady === false ) { + return; } - safeObserverHandlerTimer.clear(); - }); + + if ( this.addedNodes || this.removedNodes ) { + this.addedSelectors.clear(); + } + + var currentResultset = this.currentResultset, + entry, nodes, i, node; + + if ( this.addedSelectors.size !== 0 ) { + console.time('procedural filterset changed'); + for ( entry of this.addedSelectors ) { + nodes = entry[1].exec(); + i = nodes.length; + while ( i-- ) { + node = nodes[i]; + this.domFilterer.hideNode(node); + currentResultset.add(node); + } + } + this.addedSelectors.clear(); + console.timeEnd('procedural filterset changed'); + return; + } + + console.time('dom layout changed/procedural selectors'); + + this.addedNodes = this.removedNodes = false; + + var afterResultset = new Set(); + + for ( entry of this.selectors ) { + nodes = entry[1].exec(); + i = nodes.length; + while ( i-- ) { + node = nodes[i]; + this.domFilterer.hideNode(node); + afterResultset.add(node); + } + } + if ( afterResultset.size !== currentResultset.size ) { + this.addedNodesHandlerMissCount = 0; + } else { + this.addedNodesHandlerMissCount += 1; + } + for ( node of currentResultset ) { + if ( afterResultset.has(node) === false ) { + this.domFilterer.unhideNode(node); + } + } + + this.currentResultset = afterResultset; + + console.timeEnd('dom layout changed/procedural selectors'); + }, + + createProceduralFilter: function(o) { + return new PSelector(o); + }, + + onDOMCreated: function() { + this.domIsReady = true; + this.domFilterer.commit(); + }, + + onDOMChanged: function(addedNodes, removedNodes) { + if ( this.selectors.size === 0 ) { return; } + this.addedNodes = this.addedNodes || addedNodes.length !== 0; + this.removedNodes = this.removedNodes || removedNodes; + this.domFilterer.commit(); + } }; - return { - addListener: addListener, - removeListener: removeListener, - start: start + var DOMFiltererBase = vAPI.DOMFilterer; + + var domFilterer = function() { + DOMFiltererBase.call(this); + this.exceptions = []; + this.proceduralFilterer = new DOMProceduralFilterer(this); + + // May or may not exist: cache locally since this may be called often. + this.baseOnDOMChanged = DOMFiltererBase.prototype.onDOMChanged; + + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(this); + } }; + domFilterer.prototype = Object.create(DOMFiltererBase.prototype); + domFilterer.prototype.constructor = domFilterer; + + domFilterer.prototype.commitNow = function() { + DOMFiltererBase.prototype.commitNow.call(this); + this.proceduralFilterer.commitNow(); + }; + + domFilterer.prototype.addProceduralSelectors = function(aa) { + this.proceduralFilterer.addProceduralSelectors(aa); + }; + + domFilterer.prototype.createProceduralFilter = function(o) { + return this.proceduralFilterer.createProceduralFilter(o); + }; + + domFilterer.prototype.getAllProceduralSelectors = function() { + return new Map(this.proceduralFilterer.selectors); + }; + + domFilterer.prototype.getAllExceptionSelectors = function() { + return this.exceptions.join(',\n'); + }; + + domFilterer.prototype.onDOMCreated = function() { + if ( DOMFiltererBase.prototype.onDOMCreated !== undefined ) { + DOMFiltererBase.prototype.onDOMCreated.call(this); + } + this.proceduralFilterer.onDOMCreated(); + }; + + domFilterer.prototype.onDOMChanged = function() { + if ( this.baseOnDOMChanged !== undefined ) { + this.baseOnDOMChanged.apply(this, arguments); + } + this.proceduralFilterer.onDOMChanged.apply( + this.proceduralFilterer, + arguments + ); + }; + + return domFilterer; })(); +vAPI.domFilterer = new vAPI.DOMFilterer(); + /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ @@ -1098,6 +774,7 @@ vAPI.domCollapser = (function() { iframeLoadEventPatch(target); } } + /* if ( selectors.length !== 0 ) { messaging.send( 'contentscript', @@ -1109,6 +786,7 @@ vAPI.domCollapser = (function() { } ); } + */ }; var send = function() { @@ -1203,68 +881,77 @@ vAPI.domCollapser = (function() { var onResourceFailed = function(ev) { if ( tagToTypeMap[ev.target.localName] !== undefined ) { - vAPI.domCollapser.add(ev.target); - vAPI.domCollapser.process(); + add(ev.target); + process(); } }; - var domChangedHandler = function(nodes) { - var node; - for ( var i = 0, ni = nodes.length; i < ni; i++ ) { - node = nodes[i]; - if ( node.localName === 'iframe' ) { - addIFrame(node); + var domWatcherInterface = { + onDOMCreated: function() { + if ( vAPI instanceof Object === false ) { return; } + if ( vAPI.domCollapser instanceof Object === false ) { + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.removeListener(domWatcherInterface); + } + return; } - if ( node.childElementCount !== 0 ) { + // Listener to collapse blocked resources. + // - Future requests not blocked yet + // - Elements dynamically added to the page + // - Elements which resource URL changes + // https://github.com/chrisaljoudi/uBlock/issues/7 + // Preferring getElementsByTagName over querySelectorAll: + // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145 + var elems = document.images || document.getElementsByTagName('img'), + i = elems.length, elem; + while ( i-- ) { + elem = elems[i]; + if ( elem.complete ) { + add(elem); + } + } + addMany(document.embeds || document.getElementsByTagName('embed')); + addMany(document.getElementsByTagName('object')); + addIFrames(document.getElementsByTagName('iframe')); + process(0); + + document.addEventListener('error', onResourceFailed, true); + + vAPI.shutdown.add(function() { + document.removeEventListener('error', onResourceFailed, true); + if ( processTimer !== undefined ) { + clearTimeout(processTimer); + } + }); + }, + onDOMChanged: function(addedNodes) { + var ni = addedNodes.length; + if ( ni === 0 ) { return; } + for ( var i = 0, node; i < ni; i++ ) { + node = addedNodes[i]; + if ( node.localName === 'iframe' ) { + addIFrame(node); + } + if ( node.childElementCount === 0 ) { continue; } var iframes = node.getElementsByTagName('iframe'); if ( iframes.length !== 0 ) { addIFrames(iframes); } } + process(); } - process(); }; - var start = function() { - // Listener to collapse blocked resources. - // - Future requests not blocked yet - // - Elements dynamically added to the page - // - Elements which resource URL changes - // https://github.com/chrisaljoudi/uBlock/issues/7 - // Preferring getElementsByTagName over querySelectorAll: - // http://jsperf.com/queryselectorall-vs-getelementsbytagname/145 - var elems = document.images || document.getElementsByTagName('img'), - i = elems.length, elem; - while ( i-- ) { - elem = elems[i]; - if ( elem.complete ) { - add(elem); - } - } - addMany(document.embeds || document.getElementsByTagName('embed')); - addMany(document.getElementsByTagName('object')); - addIFrames(document.getElementsByTagName('iframe')); - process(0); - - document.addEventListener('error', onResourceFailed, true); - vAPI.domWatcher.addListener(domChangedHandler); - - vAPI.shutdown.add(function() { - document.removeEventListener('error', onResourceFailed, true); - vAPI.domWatcher.removeListener(domChangedHandler); - if ( processTimer !== undefined ) { - clearTimeout(processTimer); - } - }); - }; + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(domWatcherInterface); + } return { add: add, addMany: addMany, addIFrame: addIFrame, addIFrames: addIFrames, - process: process, - start: start + process: process }; })(); @@ -1273,250 +960,111 @@ vAPI.domCollapser = (function() { /******************************************************************************/ vAPI.domSurveyor = (function() { - var domFilterer = null, - messaging = vAPI.messaging, - surveyPhase3Nodes = [], - cosmeticSurveyingMissCount = 0, - highGenerics = null, - lowGenericSelectors = [], - queriedSelectors = new Set(), + var messaging = vAPI.messaging, + domFilterer, + queriedIds = new Set(), + queriedClasses = new Set(), + pendingIdNodes = { nodes: [], added: [] }, + pendingClassNodes = { nodes: [], added: [] }, surveyCost = 0; + // This is to shutdown the surveyor if result of surveying keeps being + // fruitless. This is useful on long-lived web page. + var surveyingMissCount = 0; + // Handle main process' response. var surveyPhase3 = function(response) { var result = response && response.result, - firstSurvey = highGenerics === null; + mustCommit = false; if ( result ) { - if ( result.hide.length ) { - processLowGenerics(result.hide); + var selectors = result.simple; + if ( Array.isArray(selectors) && selectors.length !== 0 ) { + domFilterer.addCSSRule( + selectors, + 'display: none !important;', + { type: 'simple' } + ); + mustCommit = true; } - if ( result.highGenerics ) { - highGenerics = result.highGenerics; + selectors = result.complex; + if ( Array.isArray(selectors) && selectors.length !== 0 ) { + domFilterer.addCSSRule( + selectors, + 'display: none !important;', + { type: 'complex' } + ); + mustCommit = true; } } - if ( highGenerics ) { - var t0 = window.performance.now(); - if ( highGenerics.hideLowCount ) { - processHighLowGenerics(highGenerics.hideLow); - } - if ( highGenerics.hideMediumCount ) { - processHighMediumGenerics(highGenerics.hideMedium); - } - if ( highGenerics.hideHighSimpleCount || highGenerics.hideHighComplexCount ) { - processHighHighGenerics(); - } - surveyCost += window.performance.now() - t0; + if ( hasChunk(pendingIdNodes) || hasChunk(pendingClassNodes) ) { + surveyTimer.start(1); } - // Need to do this before committing DOM filterer, as needed info - // will no longer be there after commit. - if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) { - messaging.send( - 'contentscript', - { - what: 'cosmeticFiltersInjected', - type: 'cosmetic', - hostname: window.location.hostname, - selectors: domFilterer.newHideSelectorBuffer, - first: firstSurvey, - cost: surveyCost - } - ); - } - - // Shutdown surveyor if too many consecutive empty resultsets. - if ( domFilterer.newHideSelectorBuffer.length === 0 ) { - cosmeticSurveyingMissCount += 1; - } else { - cosmeticSurveyingMissCount = 0; - } - - domFilterer.commit(surveyPhase3Nodes); - surveyPhase3Nodes = []; - }; - - // Query main process. - - var surveyPhase2 = function(addedNodes) { - surveyPhase3Nodes = surveyPhase3Nodes.concat(addedNodes); - if ( lowGenericSelectors.length !== 0 || highGenerics === null ) { - messaging.send( - 'contentscript', - { - what: 'retrieveGenericCosmeticSelectors', - pageURL: window.location.href, - selectors: lowGenericSelectors, - firstSurvey: highGenerics === null - }, - surveyPhase3 - ); - lowGenericSelectors = []; - } else { - surveyPhase3(null); - } - }; - - // Low generics: - // - [id] - // - [class] - - var processLowGenerics = function(generics) { - domFilterer.addSelectors(generics); - }; - - // High-low generics: - // - [alt="..."] - // - [title="..."] - - var processHighLowGenerics = function(generics) { - var attrs = ['title', 'alt']; - var attr, attrValue, nodeList, iNode, node; - var selector; - while ( (attr = attrs.pop()) ) { - nodeList = selectNodes('[' + attr + ']', surveyPhase3Nodes); - iNode = nodeList.length; - while ( iNode-- ) { - node = nodeList[iNode]; - attrValue = node.getAttribute(attr); - if ( !attrValue ) { continue; } - // Candidate 1 = generic form - // If generic form is injected, no need to process the - // specific form, as the generic will affect all related - // specific forms. - selector = '[' + attr + '="' + attrValue + '"]'; - if ( generics.hasOwnProperty(selector) ) { - domFilterer.addSelector(selector); - continue; - } - // Candidate 2 = specific form - selector = node.localName + selector; - if ( generics.hasOwnProperty(selector) ) { - domFilterer.addSelector(selector); - } - } - } - }; - - // High-medium generics: - // - [href^="http"] - - var processHighMediumGenerics = function(generics) { - var stagedNodes = surveyPhase3Nodes, - i = stagedNodes.length; - if ( i === 1 && stagedNodes[0] === document.documentElement ) { - processHighMediumGenericsForNodes(document.links, generics); + if ( mustCommit ) { + surveyingMissCount = 0; return; } - var aa = [ null ], - node, nodes; - while ( i-- ) { - node = stagedNodes[i]; - if ( node.localName === 'a' ) { - aa[0] = node; - processHighMediumGenericsForNodes(aa, generics); - } - nodes = node.getElementsByTagName('a'); - if ( nodes.length !== 0 ) { - processHighMediumGenericsForNodes(nodes, generics); - } - } + + surveyingMissCount += 1; + if ( surveyingMissCount < 256 ) { return; } + + console.log('dom surveyor shutting down: too many misses.'); + + vAPI.domWatcher.removeListener(domWatcherInterface); + vAPI.domSurveyor = null; }; - var processHighMediumGenericsForNodes = function(nodes, generics) { - var i = nodes.length, - node, href, pos, entry, j, selector; - while ( i-- ) { - node = nodes[i]; - href = node.getAttribute('href'); - if ( !href ) { continue; } - pos = href.indexOf('://'); - if ( pos === -1 ) { continue; } - entry = generics[href.slice(pos + 3, pos + 11)]; - if ( entry === undefined ) { continue; } - if ( typeof entry === 'string' ) { - if ( href.lastIndexOf(entry.slice(8, -2), 0) === 0 ) { - domFilterer.addSelector(entry); - } - continue; - } - j = entry.length; - while ( j-- ) { - selector = entry[j]; - if ( href.lastIndexOf(selector.slice(8, -2), 0) === 0 ) { - domFilterer.addSelector(selector); - } - } - } + var surveyTimer = new vAPI.SafeAnimationFrame(function() { + surveyPhase1(); + }); + + // The purpose of "chunkification" is to ensure the surveyor won't unduly + // block the main event loop. + + var hasChunk = function(pending) { + return pending.nodes.length !== 0 || + pending.added.length !== 0; }; - var highHighSimpleGenericsCost = 0, - highHighSimpleGenericsInjected = false, - highHighComplexGenericsCost = 0, - highHighComplexGenericsInjected = false; - - var processHighHighGenerics = function() { - var tstart; - // Simple selectors. + var addChunk = function(pending, added) { + if ( added.length === 0 ) { return; } if ( - highHighSimpleGenericsInjected === false && - highHighSimpleGenericsCost < 50 && - highGenerics.hideHighSimpleCount !== 0 + Array.isArray(added) === false || + pending.added.length === 0 || + Array.isArray(pending.added[0]) === false || + pending.added[0].length >= 1000 ) { - tstart = window.performance.now(); - var matchesProp = vAPI.matchesProp, - nodes = surveyPhase3Nodes, - i = nodes.length, node; - while ( i-- ) { - node = nodes[i]; - if ( - node[matchesProp](highGenerics.hideHighSimple) || - node.querySelector(highGenerics.hideHighSimple) !== null - ) { - highHighSimpleGenericsInjected = true; - domFilterer.addSelectors(highGenerics.hideHighSimple.split(',\n')); - break; - } - } - highHighSimpleGenericsCost += window.performance.now() - tstart; - } - // Complex selectors. - if ( - highHighComplexGenericsInjected === false && - highHighComplexGenericsCost < 50 && - highGenerics.hideHighComplexCount !== 0 - ) { - tstart = window.performance.now(); - if ( document.querySelector(highGenerics.hideHighComplex) !== null ) { - highHighComplexGenericsInjected = true; - domFilterer.addSelectors(highGenerics.hideHighComplex.split(',\n')); - } - highHighComplexGenericsCost += window.performance.now() - tstart; + pending.added.push(added); + } else { + pending.added = pending.added.concat(added); } }; - // Extract and return the staged nodes which (may) match the selectors. - - var selectNodes = function(selector, nodes) { - var stagedNodes = nodes, - i = stagedNodes.length; - if ( i === 1 && stagedNodes[0] === document.documentElement ) { - return document.querySelectorAll(selector); + var nextChunk = function(pending) { + var added = pending.added.length !== 0 ? pending.added.shift() : [], + nodes; + if ( pending.nodes.length === 0 ) { + if ( added.length <= 1000 ) { return added; } + nodes = Array.isArray(added) + ? added + : Array.prototype.slice.call(added); + pending.nodes = nodes.splice(1000); + return nodes; } - var targetNodes = [], - node, nodeList, j; - while ( i-- ) { - node = stagedNodes[i]; - targetNodes.push(node); - nodeList = node.querySelectorAll(selector); - j = nodeList.length; - while ( j-- ) { - targetNodes.push(nodeList[j]); - } + if ( Array.isArray(added) === false ) { + added = Array.prototype.slice.call(added); } - return targetNodes; + if ( pending.nodes.length < 1000 ) { + nodes = pending.nodes.concat(added.splice(0, 1000 - pending.nodes.length)); + pending.nodes = added; + } else { + nodes = pending.nodes.splice(0, 1000); + pending.nodes = pending.nodes.concat(added); + } + return nodes; }; // Extract all classes/ids: these will be passed to the cosmetic @@ -1527,145 +1075,282 @@ vAPI.domSurveyor = (function() { // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens // http://jsperf.com/enumerate-classes/6 - var surveyPhase1 = function(addedNodes) { - var t0 = window.performance.now(), - rews = reWhitespace, - qq = queriedSelectors, - ll = lowGenericSelectors, - lli = ll.length, - nodes, i, node, v, vv, j; - nodes = selectNodes('[id]', addedNodes); + var surveyPhase1 = function() { + console.time('dom surveyor/surveying'); + surveyTimer.clear(); + var t0 = window.performance.now(); + var rews = reWhitespace, + qq, iout, nodes, i, node, v, vv, j; + var ids = []; + iout = 0; + qq = queriedIds; + nodes = nextChunk(pendingIdNodes); i = nodes.length; while ( i-- ) { node = nodes[i]; v = node.id; if ( typeof v !== 'string' ) { continue; } - v = '#' + v.trim(); - if ( qq.has(v) === false && v.length !== 1 ) { - ll[lli] = v; lli++; qq.add(v); + v = v.trim(); + if ( qq.has(v) === false && v.length !== 0 ) { + ids[iout++] = v; qq.add(v); } } - nodes = selectNodes('[class]', addedNodes); + var classes = []; + iout = 0; + qq = queriedClasses; + nodes = nextChunk(pendingClassNodes); i = nodes.length; while ( i-- ) { node = nodes[i]; vv = node.className; if ( typeof vv !== 'string' ) { continue; } if ( rews.test(vv) === false ) { - v = '.' + vv; - if ( qq.has(v) === false && v.length !== 1 ) { - ll[lli] = v; lli++; qq.add(v); + if ( qq.has(vv) === false && vv.length !== 0 ) { + classes[iout++] = vv; qq.add(vv); } } else { vv = node.classList; j = vv.length; while ( j-- ) { - v = '.' + vv[j]; + v = vv[j]; if ( qq.has(v) === false ) { - ll[lli] = v; lli++; qq.add(v); + classes[iout++] = v; qq.add(v); } } } } surveyCost += window.performance.now() - t0; - surveyPhase2(addedNodes); + // Phase 2: Ask main process to lookup relevant cosmetic filters. + if ( ids.length !== 0 || classes.length !== 0 ) { + messaging.send( + 'contentscript', + { + what: 'retrieveGenericCosmeticSelectors', + frameURL: window.location.href, + ids: ids.join('\n'), + classes: classes.join('\n'), + exceptions: domFilterer.exceptions, + cost: surveyCost + }, + surveyPhase3 + ); + } else { + surveyPhase3(null); + } + console.timeEnd('dom surveyor/surveying'); }; var reWhitespace = /\s/; - var domChangedHandler = function(addedNodes) { - if ( cosmeticSurveyingMissCount > 255 ) { - vAPI.domWatcher.removeListener(domChangedHandler); - vAPI.domSurveyor = null; - domFilterer.domChangedHandler(addedNodes); - domFilterer.start(); - return; + var domWatcherInterface = { + onDOMCreated: function() { + if ( + vAPI instanceof Object === false || + vAPI.domSurveyor instanceof Object === false || + vAPI.domFilterer instanceof Object === false + ) { + if ( vAPI instanceof Object ) { + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.removeListener(domWatcherInterface); + } + vAPI.domSurveyor = null; + } + return; + } + console.time('dom surveyor/dom layout created'); + domFilterer = vAPI.domFilterer; + addChunk(pendingIdNodes, document.querySelectorAll('[id]')); + addChunk(pendingClassNodes, document.querySelectorAll('[class]')); + surveyTimer.start(); + console.timeEnd('dom surveyor/dom layout created'); + }, + onDOMChanged: function(addedNodes) { + if ( addedNodes.length === 0 ) { return; } + console.time('dom surveyor/dom layout changed'); + var idNodes = [], iid = 0, + classNodes = [], iclass = 0; + var i = addedNodes.length, + node, nodeList, j; + while ( i-- ) { + node = addedNodes[i]; + idNodes[iid++] = node; + classNodes[iclass++] = node; + if ( node.childElementCount === 0 ) { continue; } + nodeList = node.querySelectorAll('[id]'); + j = nodeList.length; + while ( j-- ) { + idNodes[iid++] = nodeList[j]; + } + nodeList = node.querySelectorAll('[class]'); + j = nodeList.length; + while ( j-- ) { + classNodes[iclass++] = nodeList[j]; + } + } + if ( idNodes.length !== 0 || classNodes.lengh !== 0 ) { + addChunk(pendingIdNodes, idNodes); + addChunk(pendingClassNodes, classNodes); + surveyTimer.start(1); + } + console.timeEnd('dom surveyor/dom layout changed'); } - - surveyPhase1(addedNodes); }; - var start = function() { - domFilterer = vAPI.domFilterer; - domChangedHandler([ document.documentElement ]); - vAPI.domWatcher.addListener(domChangedHandler); - vAPI.shutdown.add(function() { - vAPI.domWatcher.removeListener(domChangedHandler); - }); - }; + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(domWatcherInterface); + } - return { - start: start - }; + return {}; })(); /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -vAPI.domIsLoaded = function(ev) { - // This can happen on Firefox. For instance: - // https://github.com/gorhill/uBlock/issues/1893 - if ( window.location === null ) { return; } +// Bootstrapping allows all components of the content script to be launched +// if/when needed. - var slowLoad = ev instanceof Event; - if ( slowLoad ) { - document.removeEventListener('DOMContentLoaded', vAPI.domIsLoaded); - } - vAPI.domIsLoaded = null; +(function bootstrap() { - vAPI.domWatcher.start(); - vAPI.domCollapser.start(); + var bootstrapPhase2 = function(ev) { + // This can happen on Firefox. For instance: + // https://github.com/gorhill/uBlock/issues/1893 + if ( window.location === null ) { return; } - if ( vAPI.domFilterer ) { - // To avoid neddless CPU overhead, we commit existing cosmetic filters - // only if the page loaded "slowly", i.e. if the code here had to wait - // for a DOMContentLoaded event -- in which case the DOM may have - // changed a lot since last time the domFilterer acted on it. - if ( slowLoad ) { - vAPI.domFilterer.commit('all'); + if ( ev ) { + document.removeEventListener('DOMContentLoaded', bootstrapPhase2); } - if ( vAPI.domSurveyor ) { - vAPI.domSurveyor.start(); - } else { - vAPI.domFilterer.start(); - } - } - // To send mouse coordinates to main process, as the chrome API fails - // to provide the mouse position to context menu listeners. - // https://github.com/chrisaljoudi/uBlock/issues/1143 - // Also, find a link under the mouse, to try to avoid confusing new tabs - // as nuisance popups. - // Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu - - var onMouseClick = function(ev) { - var elem = ev.target; - while ( elem !== null && elem.localName !== 'a' ) { - elem = elem.parentElement; + if ( vAPI instanceof Object && vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.start(); } - vAPI.messaging.send( - 'contentscript', - { - what: 'mouseClick', - x: ev.clientX, - y: ev.clientY, - url: elem !== null && ev.isTrusted !== false ? elem.href : '' + + if ( window !== window.top || !vAPI.domFilterer ) { return; } + + // To send mouse coordinates to main process, as the chrome API fails + // to provide the mouse position to context menu listeners. + // https://github.com/chrisaljoudi/uBlock/issues/1143 + // Also, find a link under the mouse, to try to avoid confusing new tabs + // as nuisance popups. + // Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu + + var onMouseClick = function(ev) { + var elem = ev.target; + while ( elem !== null && elem.localName !== 'a' ) { + elem = elem.parentElement; } - ); - }; + vAPI.messaging.send( + 'contentscript', + { + what: 'mouseClick', + x: ev.clientX, + y: ev.clientY, + url: elem !== null && ev.isTrusted !== false ? elem.href : '' + } + ); + }; - (function() { - if ( window !== window.top || !vAPI.domFilterer ) { - return; - } document.addEventListener('mousedown', onMouseClick, true); // https://github.com/gorhill/uMatrix/issues/144 vAPI.shutdown.add(function() { document.removeEventListener('mousedown', onMouseClick, true); }); - })(); -}; + }; + + var bootstrapPhase1 = function(response) { + // cosmetic filtering engine aka 'cfe' + var cfeDetails = response && response.specificCosmeticFilters; + if ( !cfeDetails || !cfeDetails.ready ) { + vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer = + vAPI.domSurveyor = vAPI.domIsLoaded = null; + return; + } + + if ( response.noCosmeticFiltering ) { + vAPI.domFilterer = null; + vAPI.domSurveyor = null; + } else { + var domFilterer = vAPI.domFilterer; + if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) { + vAPI.domSurveyor = null; + } + domFilterer.exceptions = cfeDetails.exceptionFilters; + domFilterer.addCSSRule( + cfeDetails.declarativeFilters, + 'display: none !important;' + ); + domFilterer.addCSSRule( + cfeDetails.highGenericHideSimple, + 'display: none !important;', + { type: 'simple', lazy: true } + ); + domFilterer.addCSSRule( + cfeDetails.highGenericHideComplex, + 'display: none !important;', + { type: 'complex', lazy: true } + ); + domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters); + } + + var parent = document.head || document.documentElement; + if ( parent ) { + var elem, text; + if ( cfeDetails.netHide.length !== 0 ) { + elem = document.createElement('style'); + elem.setAttribute('type', 'text/css'); + text = cfeDetails.netHide.join(',\n'); + text += response.collapseBlocked ? + '\n{ display:none !important; }' : + '\n{ visibility:hidden !important; }'; + elem.appendChild(document.createTextNode(text)); + parent.appendChild(elem); + } + // Library of resources is located at: + // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt + if ( cfeDetails.scripts ) { + // Have the injected script tag remove itself when execution completes: + // to keep DOM as clean as possible. + text = cfeDetails.scripts + + "\n" + + "(function() {\n" + + " var c = document.currentScript,\n" + + " p = c && c.parentNode;\n" + + " if ( p ) {\n" + + " p.removeChild(c);\n" + + " }\n" + + "})();"; + vAPI.injectScriptlet(document, text); + vAPI.injectedScripts = text; + } + } + + // https://github.com/chrisaljoudi/uBlock/issues/587 + // If no filters were found, maybe the script was injected before + // uBlock's process was fully initialized. When this happens, pages + // won't be cleaned right after browser launch. + if ( + typeof document.readyState === 'string' && + document.readyState !== 'loading' + ) { + bootstrapPhase2(); + } else { + document.addEventListener('DOMContentLoaded', bootstrapPhase2); + } + }; + + // This starts bootstrap process. + var url = window.location.href; + vAPI.messaging.send( + 'contentscript', + { + what: 'retrieveContentScriptParameters', + pageURL: url, + locationURL: url, + isRootFrame: window === window.top + }, + bootstrapPhase1 + ); +})(); /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 6020466c6..c3d15e554 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -161,7 +161,7 @@ FilterHostname.prototype.fid = 8; FilterHostname.prototype.retrieve = function(hostname, out) { if ( hostname.endsWith(this.hostname) ) { - out.push(this.s); + out.add(this.s); } }; @@ -408,16 +408,15 @@ SelectorCacheEntry.factory = function() { /******************************************************************************/ -var netSelectorCacheLowWaterMark = 20; +// var netSelectorCacheLowWaterMark = 20; var netSelectorCacheHighWaterMark = 30; /******************************************************************************/ SelectorCacheEntry.prototype.reset = function() { - this.cosmetic = {}; + this.cosmetic = new Set(); this.cosmeticSurveyingMissCount = 0; - this.net = {}; - this.netCount = 0; + this.net = new Map(); this.lastAccessTime = Date.now(); return this; }; @@ -445,15 +444,15 @@ SelectorCacheEntry.prototype.addCosmetic = function(details) { return; } this.cosmeticSurveyingMissCount = 0; - var dict = this.cosmetic; while ( i-- ) { - dict[selectors[i]] = true; + this.cosmetic.add(selectors[i]); } }; /******************************************************************************/ -SelectorCacheEntry.prototype.addNet = function(selectors) { +SelectorCacheEntry.prototype.addNet = function(/* selectors */) { +/* if ( typeof selectors === 'string' ) { this.addNetOne(selectors, Date.now()); } else { @@ -462,41 +461,30 @@ SelectorCacheEntry.prototype.addNet = function(selectors) { // Net request-derived selectors: I limit the number of cached selectors, // as I expect cases where the blocked net-requests are never the // exact same URL. - if ( this.netCount < netSelectorCacheHighWaterMark ) { - return; - } + if ( this.net.size < netSelectorCacheHighWaterMark ) { return; } var dict = this.net; - var keys = Object.keys(dict).sort(function(a, b) { - return dict[b] - dict[a]; + var keys = µb.arrayFrom(dict.keys()).sort(function(a, b) { + return dict.get(b) - dict.get(a); }).slice(netSelectorCacheLowWaterMark); var i = keys.length; while ( i-- ) { - delete dict[keys[i]]; + dict.delete(keys[i]); } +*/ }; /******************************************************************************/ SelectorCacheEntry.prototype.addNetOne = function(selector, now) { - var dict = this.net; - if ( dict[selector] === undefined ) { - this.netCount += 1; - } - dict[selector] = now; + this.net.set(selector, now); }; /******************************************************************************/ SelectorCacheEntry.prototype.addNetMany = function(selectors, now) { - var dict = this.net; var i = selectors.length || 0; - var selector; while ( i-- ) { - selector = selectors[i]; - if ( dict[selector] === undefined ) { - this.netCount += 1; - } - dict[selector] = now; + this.net.set(selectors[i], now); } }; @@ -517,12 +505,11 @@ SelectorCacheEntry.prototype.add = function(details) { SelectorCacheEntry.prototype.remove = function(type) { this.lastAccessTime = Date.now(); if ( type === undefined || type === 'cosmetic' ) { - this.cosmetic = {}; + this.cosmetic.clear(); this.cosmeticSurveyingMissCount = 0; } if ( type === undefined || type === 'net' ) { - this.net = {}; - this.netCount = 0; + this.net.clear(); } }; @@ -531,10 +518,8 @@ SelectorCacheEntry.prototype.remove = function(type) { SelectorCacheEntry.prototype.retrieve = function(type, out) { this.lastAccessTime = Date.now(); var dict = type === 'cosmetic' ? this.cosmetic : this.net; - for ( var selector in dict ) { - if ( dict.hasOwnProperty(selector) ) { - out.push(selector); - } + for ( var selector of dict ) { + out.add(selector); } }; @@ -611,20 +596,60 @@ var makeHash = function(token) { var FilterContainer = function() { this.noDomainHash = '-'; this.parser = new FilterParser(); - this.selectorCachePruneDelay = 10 * 60 * 1000; // 15 minutes + this.reHasUnicode = /[^\x00-\x7F]/; + this.rePlainSelector = /^[#.][\w\\-]+/; + this.rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/; + this.rePlainSelectorEx = /^[^#.\[(]+([#.][\w-]+)|([#.][\w-]+)$/; + this.reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g; + this.reSimpleHighGeneric1 = /^[a-z]*\[[^[]+]$/; + this.reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/; + this.reScriptSelector = /^script:(contains|inject)\((.+)\)$/; + this.punycode = punycode; + + this.selectorCache = new Map(); + this.selectorCachePruneDelay = 10 * 60 * 1000; // 10 minutes this.selectorCacheAgeMax = 120 * 60 * 1000; // 120 minutes this.selectorCacheCountMin = 25; this.netSelectorCacheCountMax = netSelectorCacheHighWaterMark; this.selectorCacheTimer = null; - this.reHasUnicode = /[^\x00-\x7F]/; - this.rePlainSelector = /^[#.][\w\\-]+/; - this.rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/; - this.rePlainSelectorEx = /^[^#.\[(]+([#.][\w-]+)/; - this.reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g; - this.reHighLow = /^[a-z]*\[(?:alt|title)="[^"]+"\]$/; - this.reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/; - this.reScriptSelector = /^script:(contains|inject)\((.+)\)$/; - this.punycode = punycode; + + // generic exception filters + this.genericDonthideSet = new Set(); + + // hostname, entity-based filters + this.specificFilters = new Map(); + this.proceduralFilters = new Map(); + + // low generic cosmetic filters, organized by id/class then simple/complex. + this.lowlyGeneric = Object.create(null); + this.lowlyGeneric.id = { + canonical: 'ids', + prefix: '#', + simple: new Set(), + complex: new Map() + }; + this.lowlyGeneric.cl = { + canonical: 'classes', + prefix: '.', + simple: new Set(), + complex: new Map() + }; + + // highly generic selectors sets + this.highlyGenericSimpleHideSet = new Set(); + this.highlyGenericComplexHideSet = new Set(); + this.mruHighlyGenericHideStrings = new µb.MRUCache(8); + + this.userScripts = new Map(); + + // Short-lived: content is valid only during one function call. These + // is to prevent repeated allocation/deallocation overheads -- the + // constructors/destructors of javascript Set/Map is assumed to be costlier + // than just calling clear() on these. + this.setRegister0 = new Set(); + this.setRegister1 = new Set(); + this.setRegister2 = new Set(); + this.reset(); }; @@ -640,8 +665,7 @@ FilterContainer.prototype.reset = function() { this.discardedCount = 0; this.duplicateBuster = new Set(); - this.selectorCache = {}; - this.selectorCacheCount = 0; + this.selectorCache.clear(); if ( this.selectorCacheTimer !== null ) { clearTimeout(this.selectorCacheTimer); this.selectorCacheTimer = null; @@ -650,37 +674,27 @@ FilterContainer.prototype.reset = function() { // generic filters this.hasGenericHide = false; - // [class], [id] - this.lowGenericHide = new Set(); - this.lowGenericHideEx = new Map(); - this.lowGenericHideCount = 0; - - // [alt="..."], [title="..."] - this.highLowGenericHide = {}; - this.highLowGenericHideCount = 0; - - // a[href^="http..."] - this.highMediumGenericHide = {}; - this.highMediumGenericHideCount = 0; - - // high-high-simple selectors - this.highHighSimpleGenericHideArray = []; - this.highHighSimpleGenericHide = ''; - this.highHighSimpleGenericHideCount = 0; - - // high-high-complex selectors - this.highHighComplexGenericHideArray = []; - this.highHighComplexGenericHide = ''; - this.highHighComplexGenericHideCount = 0; - // generic exception filters - this.genericDonthide = []; + this.genericDonthideSet.clear(); // hostname, entity-based filters - this.specificFilters = new Map(); + this.specificFilters.clear(); + this.proceduralFilters.clear(); + + // low generic cosmetic filters, organized by id/class then simple/complex. + this.lowlyGeneric.id.simple.clear(); + this.lowlyGeneric.id.complex.clear(); + this.lowlyGeneric.cl.simple.clear(); + this.lowlyGeneric.cl.complex.clear(); + + // highly generic selectors sets + this.highlyGenericSimpleHideSet.clear(); + this.highlyGenericComplexHideSet.clear(); + this.mruHighlyGenericHideStrings.reset(); + this.scriptTagFilters = {}; this.scriptTagFilterCount = 0; - this.userScripts = new Map(); + this.userScripts.clear(); this.userScriptCount = 0; }; @@ -689,23 +703,29 @@ FilterContainer.prototype.reset = function() { FilterContainer.prototype.freeze = function() { this.duplicateBuster = new Set(); - if ( this.highHighSimpleGenericHide !== '' ) { - this.highHighSimpleGenericHideArray.unshift(this.highHighSimpleGenericHide); - } - this.highHighSimpleGenericHide = this.highHighSimpleGenericHideArray.join(',\n'); - this.highHighSimpleGenericHideArray = []; + this.hasGenericHide = + this.lowlyGeneric.id.simple.size !== 0 || + this.lowlyGeneric.id.complex.size !== 0 || + this.lowlyGeneric.cl.simple.size !== 0 || + this.lowlyGeneric.cl.complex.size !== 0 || + this.highlyGenericSimpleHideSet.size !== 0 || + this.highlyGenericComplexHideSet.size !== 0; - if ( this.highHighComplexGenericHide !== '' ) { - this.highHighComplexGenericHideArray.unshift(this.highHighComplexGenericHide); + if ( this.genericDonthideSet.size !== 0 ) { + for ( var selector of this.genericDonthideSet ) { + var type = selector.charCodeAt(0); + if ( type === 0x23 /* '#' */ ) { + this.lowlyGeneric.id.simple.delete(selector.slice(1)); + } else if ( type === 0x2E /* '.' */ ) { + this.lowlyGeneric.cl.simple.delete(selector.slice(1)); + } + // TODO: + // this.lowlyGeneric.id.complex.delete(selector); + // this.lowlyGeneric.cl.complex.delete(selector); + this.highlyGenericSimpleHideSet.delete(selector); + this.highlyGenericComplexHideSet.delete(selector); + } } - this.highHighComplexGenericHide = this.highHighComplexGenericHideArray.join(',\n'); - this.highHighComplexGenericHideArray = []; - - this.hasGenericHide = this.lowGenericHideCount !== 0 || - this.highLowGenericHideCount !== 0 || - this.highMediumGenericHideCount !== 0 || - this.highHighSimpleGenericHideCount !== 0 || - this.highHighComplexGenericHideCount !== 0; this.parser.reset(); this.compileSelector.reset(); @@ -803,12 +823,10 @@ FilterContainer.prototype.compileSelector = (function() { selector += pseudoclass; } if ( style !== undefined ) { - if ( isValidStyleProperty(style) === false ) { - return; - } + if ( isValidStyleProperty(style) === false ) { return; } return JSON.stringify({ raw: raw, - style: [ selector, '{' + style + '}' ] + style: [ selector, style ] }); } return JSON.stringify({ @@ -1135,22 +1153,39 @@ FilterContainer.prototype.compileGenericSelector = function(parsed, writer) { FilterContainer.prototype.compileGenericHideSelector = function(parsed, writer) { var selector = parsed.suffix, - type = selector.charAt(0), - key, matches; + type = selector.charCodeAt(0), + key; - if ( type === '#' || type === '.' ) { + if ( type === 0x23 /* '#' */ ) { key = this.keyFromSelector(selector); if ( key === undefined ) { return; } - // Single-CSS rule: no need to test for whether the selector - // is valid, the regex took care of this. Most generic selector falls - // into that category. + // Simple selector-based CSS rule: no need to test for whether the + // selector is valid, the regex took care of this. Most generic + // selector falls into that category. if ( key === selector ) { - writer.push([ 0 /* lg */, key ]); + writer.push([ 0 /* lg */, key.slice(1) ]); return; } - // Composite CSS rule. + // Complex selector-based CSS rule. if ( this.compileSelector(selector) !== undefined ) { - writer.push([ 1 /* lg+ */, key, selector ]); + writer.push([ 1 /* lg+ */, key.slice(1), selector ]); + } + return; + } + + if ( type === 0x2E /* '.' */ ) { + key = this.keyFromSelector(selector); + if ( key === undefined ) { return; } + // Simple selector-based CSS rule: no need to test for whether the + // selector is valid, the regex took care of this. Most generic + // selector falls into that category. + if ( key === selector ) { + writer.push([ 2 /* lg */, key.slice(1) ]); + return; + } + // Complex selector-based CSS rule. + if ( this.compileSelector(selector) !== undefined ) { + writer.push([ 3 /* lg+ */, key.slice(1), selector ]); } return; } @@ -1159,34 +1194,35 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, writer) if ( compiled === undefined ) { return; } // TODO: Detect and error on procedural cosmetic filters. - // ["title"] and ["alt"] will go in high-low generic bin. - if ( this.reHighLow.test(selector) ) { - writer.push([ 2 /* hlg0 */, selector ]); - return; - } - - // [href^="..."] will go in high-medium generic bin. - matches = this.reHighMedium.exec(selector); - if ( matches && matches.length === 2 ) { - writer.push([ 3 /* hmg0 */, matches[1], selector ]); - return; - } - // https://github.com/gorhill/uBlock/issues/909 - // Anything which contains a plain id/class selector can be classified - // as a low generic cosmetic filter. - matches = this.rePlainSelectorEx.exec(selector); - if ( matches && matches.length === 2 ) { - writer.push([ 1 /* lg+ */, matches[1], selector ]); + // Anything which contains a plain id/class selector can be classified + // as a low generic cosmetic filter. + var matches = this.rePlainSelectorEx.exec(selector); + if ( matches !== null ) { + key = matches[1] || matches[2]; + type = key.charCodeAt(0); + writer.push([ + type === 0x23 ? 1 : 3 /* lg+ */, + key.slice(1), + selector + ]); + return; + } + + // Pass this point, we are dealing with highly-generic cosmetic filters. + // + // For efficiency purpose, we will distinguish between simple and complex + // selectors. + + if ( this.reSimpleHighGeneric1.test(selector) ) { + writer.push([ 4 /* simple */, selector ]); return; } - // All else: high-high generics. - // Distinguish simple vs complex selectors. if ( selector.indexOf(' ') === -1 ) { - writer.push([ 4 /* hhsg0 */, selector ]); + writer.push([ 4 /* simple */, selector ]); } else { - writer.push([ 5 /* hhcg0 */, selector ]); + writer.push([ 5 /* complex */, selector ]); } }; @@ -1258,7 +1294,13 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, w // h, hash, example.com, .promoted-tweet // h, hash, example.*, .promoted-tweet - writer.push([ 8 /* h */, hash, hostname, compiled ]); + // 8 = declarative, 9 = procedural + writer.push([ + compiled.charCodeAt(0) !== 0x7B /* '{' */ ? 8 : 9, + hash, + hostname, + compiled + ]); }; /******************************************************************************/ @@ -1277,7 +1319,7 @@ FilterContainer.prototype.fromCompiledContent = function( return; } - var fingerprint, args, filter, bucket; + var fingerprint, args, db, filter, bucket; while ( reader.next() === true ) { this.acceptedCount += 1; @@ -1292,69 +1334,49 @@ FilterContainer.prototype.fromCompiledContent = function( switch ( args[0] ) { - // .largeAd - case 0: - bucket = this.lowGenericHideEx.get(args[1]); + // low generic, simple + case 0: // #AdBanner + case 2: // .largeAd + db = args[0] === 0 ? this.lowlyGeneric.id : this.lowlyGeneric.cl; + bucket = db.complex.get(args[1]); if ( bucket === undefined ) { - this.lowGenericHide.add(args[1]); + db.simple.add(args[1]); } else if ( Array.isArray(bucket) ) { bucket.push(args[1]); } else { - this.lowGenericHideEx.set(args[1], [ bucket, args[1] ]); + db.complex.set(args[1], [ bucket, args[1] ]); } - this.lowGenericHideCount += 1; break; - // .Mpopup, .Mpopup + #Mad > #MadZone - case 1: - bucket = this.lowGenericHideEx.get(args[1]); + // low generic, complex + case 1: // #tads + div + .c + case 3: // .Mpopup + #Mad > #MadZone + db = args[0] === 0 ? this.lowlyGeneric.id : this.lowlyGeneric.cl; + bucket = db.complex.get(args[1]); if ( bucket === undefined ) { - if ( this.lowGenericHide.has(args[1]) ) { - this.lowGenericHideEx.set(args[1], [ args[1], args[2] ]); + if ( db.simple.has(args[1]) ) { + db.complex.set(args[1], [ args[1], args[2] ]); } else { - this.lowGenericHideEx.set(args[1], args[2]); - this.lowGenericHide.add(args[1]); + db.complex.set(args[1], args[2]); + db.simple.add(args[1]); } } else if ( Array.isArray(bucket) ) { bucket.push(args[2]); } else { - this.lowGenericHideEx.set(args[1], [ bucket, args[2] ]); + db.complex.set(args[1], [ bucket, args[2] ]); } - this.lowGenericHideCount += 1; - break; - - // ["title"] - // ["alt"] - case 2: - this.highLowGenericHide[args[1]] = true; - this.highLowGenericHideCount += 1; - break; - - // [href^="..."] - case 3: - bucket = this.highMediumGenericHide[args[1]]; - if ( bucket === undefined ) { - this.highMediumGenericHide[args[1]] = args[2]; - } else if ( Array.isArray(bucket) ) { - bucket.push(args[2]); - } else { - this.highMediumGenericHide[args[1]] = [bucket, args[2]]; - } - this.highMediumGenericHideCount += 1; break; // High-high generic hide/simple selectors // div[id^="allo"] case 4: - this.highHighSimpleGenericHideArray.push(args[1]); - this.highHighSimpleGenericHideCount += 1; + this.highlyGenericSimpleHideSet.add(args[1]); break; // High-high generic hide/complex selectors // div[id^="allo"] > span case 5: - this.highHighComplexGenericHideArray.push(args[1]); - this.highHighComplexGenericHideCount += 1; + this.highlyGenericComplexHideSet.add(args[1]); break; // js, hash, example.com, script:contains(...) @@ -1367,20 +1389,22 @@ FilterContainer.prototype.fromCompiledContent = function( // Generic exception filters: expected to be a rare occurrence. // #@#.tweet case 7: - this.genericDonthide.push(args[1]); + this.genericDonthideSet.add(args[1]); break; // h, hash, example.com, .promoted-tweet // h, hash, example.*, .promoted-tweet case 8: + case 9: + db = args[0] === 8 ? this.specificFilters : this.proceduralFilters; filter = new FilterHostname(args[3], args[2]); - bucket = this.specificFilters.get(args[1]); + bucket = db.get(args[1]); if ( bucket === undefined ) { - this.specificFilters.set(args[1], filter); + db.set(args[1], filter); } else if ( bucket instanceof FilterBucket ) { bucket.add(filter); } else { - this.specificFilters.set(args[1], new FilterBucket(bucket, filter)); + db.set(args[1], new FilterBucket(bucket, filter)); } break; @@ -1394,7 +1418,7 @@ FilterContainer.prototype.fromCompiledContent = function( /******************************************************************************/ FilterContainer.prototype.skipGenericCompiledContent = function(reader) { - var fingerprint, args, filter, bucket; + var fingerprint, args, db, filter, bucket; while ( reader.next() === true ) { this.acceptedCount += 1; @@ -1419,21 +1443,23 @@ FilterContainer.prototype.skipGenericCompiledContent = function(reader) { // Generic exception filters: expected to be a rare occurrence. case 7: this.duplicateBuster.add(fingerprint); - this.genericDonthide.push(args[1]); + this.genericDonthideSet.add(args[1]); break; // h, hash, example.com, .promoted-tweet // h, hash, example.*, .promoted-tweet case 8: + case 9: + db = args[0] === 8 ? this.specificFilters : this.proceduralFilters; this.duplicateBuster.add(fingerprint); filter = new FilterHostname(args[3], args[2]); - bucket = this.specificFilters.get(args[1]); + bucket = db.get(args[1]); if ( bucket === undefined ) { - this.specificFilters.set(args[1], filter); + db.set(args[1], filter); } else if ( bucket instanceof FilterBucket ) { bucket.add(filter); } else { - this.specificFilters.set(args[1], new FilterBucket(bucket, filter)); + db.set(args[1], new FilterBucket(bucket, filter)); } break; @@ -1596,16 +1622,16 @@ FilterContainer.prototype.retrieveUserScripts = function(domain, hostname) { } // Explicit (hash is domain). - var selectors = [], bucket; + var selectors = new Set(), + bucket; if ( (bucket = this.userScripts.get(domain)) ) { bucket.retrieve(hostname, selectors); } if ( entity !== '' && (bucket = this.userScripts.get(entity)) ) { bucket.retrieve(entity, selectors); } - var i = selectors.length; - while ( i-- ) { - this._lookupUserScript(scripts, selectors[i].slice(14, -1).trim(), reng, out); + for ( var selector of selectors ) { + this._lookupUserScript(scripts, selector.slice(14, -1).trim(), reng, out); } if ( out.length === 0 ) { @@ -1623,16 +1649,16 @@ FilterContainer.prototype.retrieveUserScripts = function(domain, hostname) { // Exceptions should be rare, so we check for exception only if there are // scriptlets returned. - var exceptions = [], j, token; + var exceptions = new Set(), + j, token; if ( (bucket = this.userScripts.get('!' + domain)) ) { bucket.retrieve(hostname, exceptions); } if ( entity !== '' && (bucket = this.userScripts.get('!' + entity)) ) { bucket.retrieve(hostname, exceptions); } - i = exceptions.length; - while ( i-- ) { - token = exceptions[i].slice(14, -1); + for ( var exception of exceptions ) { + token = exception.slice(14, -1); if ( (j = scripts.get(token)) !== undefined ) { out[j] = '// User script "' + token + '" excepted.\n'; } @@ -1696,19 +1722,15 @@ FilterContainer.prototype.toSelfie = function() { acceptedCount: this.acceptedCount, discardedCount: this.discardedCount, specificFilters: selfieFromMap(this.specificFilters), + proceduralFilters: selfieFromMap(this.proceduralFilters), hasGenericHide: this.hasGenericHide, - lowGenericHide: µb.setToArray(this.lowGenericHide), - lowGenericHideEx: µb.mapToArray(this.lowGenericHideEx), - lowGenericHideCount: this.lowGenericHideCount, - highLowGenericHide: this.highLowGenericHide, - highLowGenericHideCount: this.highLowGenericHideCount, - highMediumGenericHide: this.highMediumGenericHide, - highMediumGenericHideCount: this.highMediumGenericHideCount, - highHighSimpleGenericHide: this.highHighSimpleGenericHide, - highHighSimpleGenericHideCount: this.highHighSimpleGenericHideCount, - highHighComplexGenericHide: this.highHighComplexGenericHide, - highHighComplexGenericHideCount: this.highHighComplexGenericHideCount, - genericDonthide: this.genericDonthide, + lowlyGenericSID: µb.arrayFrom(this.lowlyGeneric.id.simple), + lowlyGenericCID: µb.arrayFrom(this.lowlyGeneric.id.complex), + lowlyGenericSCL: µb.arrayFrom(this.lowlyGeneric.cl.simple), + lowlyGenericCCL: µb.arrayFrom(this.lowlyGeneric.cl.complex), + highSimpleGenericHideArray: µb.arrayFrom(this.highlyGenericSimpleHideSet), + highComplexGenericHideArray: µb.arrayFrom(this.highlyGenericComplexHideSet), + genericDonthideArray: µb.arrayFrom(this.genericDonthideSet), scriptTagFilters: this.scriptTagFilters, scriptTagFilterCount: this.scriptTagFilterCount, userScripts: selfieFromMap(this.userScripts), @@ -1733,19 +1755,15 @@ FilterContainer.prototype.fromSelfie = function(selfie) { this.acceptedCount = selfie.acceptedCount; this.discardedCount = selfie.discardedCount; this.specificFilters = mapFromSelfie(selfie.specificFilters); + this.proceduralFilters = mapFromSelfie(selfie.proceduralFilters); this.hasGenericHide = selfie.hasGenericHide; - this.lowGenericHide = µb.setFromArray(selfie.lowGenericHide); - this.lowGenericHideEx = µb.mapFromArray(selfie.lowGenericHideEx); - this.lowGenericHideCount = selfie.lowGenericHideCount; - this.highLowGenericHide = selfie.highLowGenericHide; - this.highLowGenericHideCount = selfie.highLowGenericHideCount; - this.highMediumGenericHide = selfie.highMediumGenericHide; - this.highMediumGenericHideCount = selfie.highMediumGenericHideCount; - this.highHighSimpleGenericHide = selfie.highHighSimpleGenericHide; - this.highHighSimpleGenericHideCount = selfie.highHighSimpleGenericHideCount; - this.highHighComplexGenericHide = selfie.highHighComplexGenericHide; - this.highHighComplexGenericHideCount = selfie.highHighComplexGenericHideCount; - this.genericDonthide = selfie.genericDonthide; + this.lowlyGeneric.id.simple = new Set(selfie.lowlyGenericSID); + this.lowlyGeneric.id.complex = new Map(selfie.lowlyGenericCID); + this.lowlyGeneric.cl.simple = new Set(selfie.lowlyGenericSCL); + this.lowlyGeneric.cl.complex = new Map(selfie.lowlyGenericCCL); + this.highlyGenericSimpleHideSet = new Set(selfie.highSimpleGenericHideArray); + this.highlyGenericComplexHideSet = new Set(selfie.highComplexGenericHideArray); + this.genericDonthideSet = new Set(selfie.genericDonthideArray); this.scriptTagFilters = selfie.scriptTagFilters; this.scriptTagFilterCount = selfie.scriptTagFilterCount; this.userScripts = mapFromSelfie(selfie.userScripts); @@ -1756,170 +1774,197 @@ FilterContainer.prototype.fromSelfie = function(selfie) { /******************************************************************************/ FilterContainer.prototype.triggerSelectorCachePruner = function() { - if ( this.selectorCacheTimer !== null ) { - return; - } - if ( this.selectorCacheCount <= this.selectorCacheCountMin ) { - return; - } // Of interest: http://fitzgeraldnick.com/weblog/40/ // http://googlecode.blogspot.ca/2009/07/gmail-for-mobile-html5-series-using.html - this.selectorCacheTimer = vAPI.setTimeout( - this.pruneSelectorCacheAsync.bind(this), - this.selectorCachePruneDelay - ); + if ( this.selectorCacheTimer === null ) { + this.selectorCacheTimer = vAPI.setTimeout( + this.pruneSelectorCacheAsync.bind(this), + this.selectorCachePruneDelay + ); + } }; /******************************************************************************/ FilterContainer.prototype.addToSelectorCache = function(details) { var hostname = details.hostname; - if ( typeof hostname !== 'string' || hostname === '' ) { - return; - } + if ( typeof hostname !== 'string' || hostname === '' ) { return; } var selectors = details.selectors; - if ( !selectors ) { - return; - } - var entry = this.selectorCache[hostname]; + if ( Array.isArray(selectors) === false ) { return; } + var entry = this.selectorCache.get(hostname); if ( entry === undefined ) { - entry = this.selectorCache[hostname] = SelectorCacheEntry.factory(); - this.selectorCacheCount += 1; - this.triggerSelectorCachePruner(); + entry = SelectorCacheEntry.factory(); + this.selectorCache.set(hostname, entry); + if ( this.selectorCache.size > this.selectorCacheCountMin ) { + this.triggerSelectorCachePruner(); + } } entry.add(details); }; /******************************************************************************/ -FilterContainer.prototype.removeFromSelectorCache = function(targetHostname, type) { - var targetHostnameLength = targetHostname.length; - for ( var hostname in this.selectorCache ) { - if ( this.selectorCache.hasOwnProperty(hostname) === false ) { - continue; - } +FilterContainer.prototype.removeFromSelectorCache = function( + targetHostname, + type +) { + var targetHostnameLength = targetHostname.length, + hostname, item; + for ( var entry of this.selectorCache ) { + hostname = entry[0]; + item = entry[1]; if ( targetHostname !== '*' ) { - if ( hostname.endsWith(targetHostname) === false ) { - continue; - } - if ( hostname.length !== targetHostnameLength && - hostname.charAt(hostname.length - targetHostnameLength - 1) !== '.' ) { + if ( hostname.endsWith(targetHostname) === false ) { continue; } + if ( + hostname.length !== targetHostnameLength && + hostname.charAt(hostname.length - targetHostnameLength - 1) !== '.' + ) { continue; } } - this.selectorCache[hostname].remove(type); + item.remove(type); } }; /******************************************************************************/ -FilterContainer.prototype.retrieveFromSelectorCache = function(hostname, type, out) { - var entry = this.selectorCache[hostname]; - if ( entry === undefined ) { - return; +FilterContainer.prototype.retrieveFromSelectorCache = function( + hostname, + type, + out +) { + var entry = this.selectorCache.get(hostname); + if ( entry !== undefined ) { + entry.retrieve(type, out); } - entry.retrieve(type, out); }; /******************************************************************************/ FilterContainer.prototype.pruneSelectorCacheAsync = function() { this.selectorCacheTimer = null; - if ( this.selectorCacheCount <= this.selectorCacheCountMin ) { - return; - } + if ( this.selectorCache.size <= this.selectorCacheCountMin ) { return; } var cache = this.selectorCache; // Sorted from most-recently-used to least-recently-used, because // we loop beginning at the end below. // We can't avoid sorting because we have to keep a minimum number of // entries, and these entries should always be the most-recently-used. - var hostnames = Object.keys(cache) - .sort(function(a, b) { return cache[b].lastAccessTime - cache[a].lastAccessTime; }) - .slice(this.selectorCacheCountMin); - var obsolete = Date.now() - this.selectorCacheAgeMax; - var hostname, entry; - var i = hostnames.length; + var hostnames = µb.arrayFrom(cache.keys()) + .sort(function(a, b) { + return cache.get(b).lastAccessTime - + cache.get(a).lastAccessTime; + }) + .slice(this.selectorCacheCountMin); + var obsolete = Date.now() - this.selectorCacheAgeMax, + hostname, entry, + i = hostnames.length; while ( i-- ) { hostname = hostnames[i]; - entry = cache[hostname]; - if ( entry.lastAccessTime > obsolete ) { - break; - } + entry = cache.get(hostname); + if ( entry.lastAccessTime > obsolete ) { break; } // console.debug('pruneSelectorCacheAsync: flushing "%s"', hostname); entry.dispose(); - delete cache[hostname]; - this.selectorCacheCount -= 1; + cache.delete(hostname); + } + if ( cache.size > this.selectorCacheCountMin ) { + this.triggerSelectorCachePruner(); } - this.triggerSelectorCachePruner(); }; /******************************************************************************/ FilterContainer.prototype.retrieveGenericSelectors = function(request) { - if ( this.acceptedCount === 0 ) { - return; - } - if ( !request.selectors ) { - return; - } + if ( this.acceptedCount === 0 ) { return; } + if ( !request.ids && !request.classes ) { return; } - //quickProfiler.start('FilterContainer.retrieve()'); + console.time('cosmeticFilteringEngine.retrieveGenericSelectors'); - var r = { - hide: [] - }; + var simpleSelectors = this.setRegister0, + complexSelectors = this.setRegister1; + var entry, selectors, + strEnd, sliceBeg, sliceEnd, + selector, bucket, item; - if ( request.firstSurvey ) { - r.highGenerics = { - hideLow: this.highLowGenericHide, - hideLowCount: this.highLowGenericHideCount, - hideMedium: this.highMediumGenericHide, - hideMediumCount: this.highMediumGenericHideCount, - hideHighSimple: this.highHighSimpleGenericHide, - hideHighSimpleCount: this.highHighSimpleGenericHideCount, - hideHighComplex: this.highHighComplexGenericHide, - hideHighComplexCount: this.highHighComplexGenericHideCount - }; - } - - var hideSelectors = r.hide, - selectors = request.selectors, - i = selectors.length, - selector, bucket; - while ( i-- ) { - selector = selectors[i]; - if ( this.lowGenericHide.has(selector) === false ) { continue; } - if ( (bucket = this.lowGenericHideEx.get(selector)) !== undefined ) { - if ( Array.isArray(bucket) ) { - hideSelectors = hideSelectors.concat(bucket); + for ( var type in this.lowlyGeneric ) { + entry = this.lowlyGeneric[type]; + selectors = request[entry.canonical]; + if ( typeof selectors !== 'string' ) { continue; } + strEnd = selectors.length; + sliceBeg = 0; + do { + sliceEnd = selectors.indexOf('\n', sliceBeg); + if ( sliceEnd === -1 ) { sliceEnd = strEnd; } + selector = selectors.slice(sliceBeg, sliceEnd); + sliceBeg = sliceEnd + 1; + if ( entry.simple.has(selector) === false ) { continue; } + if ( (bucket = entry.complex.get(selector)) !== undefined ) { + if ( Array.isArray(bucket) ) { + for ( item of bucket ) { + complexSelectors.add(item); + } + } else { + complexSelectors.add(bucket); + } } else { - hideSelectors.push(bucket); + simpleSelectors.add(entry.prefix + selector); } - } else { - hideSelectors.push(selector); + } while ( sliceBeg < strEnd ); + } + + // Apply exceptions: it is the responsibility of the caller to provide + // the exceptions to be applied. + if ( Array.isArray(request.exceptions) ) { + for ( var exception of request.exceptions ) { + simpleSelectors.delete(exception); + complexSelectors.delete(exception); } } - r.hide = hideSelectors; - //quickProfiler.stop(); + var out = { + simple: µb.arrayFrom(simpleSelectors), + complex: µb.arrayFrom(complexSelectors) + }; - return r; + // Cache looked-up low generic cosmetic filters. + if ( + (simpleSelectors.size !== 0 || complexSelectors.size !== 0) && + (typeof request.frameURL === 'string') + ) { + var hostname = µb.URI.hostnameFromURI(request.frameURL); + if ( hostname !== '' ) { + this.addToSelectorCache({ + selectors: out.simple.concat(out.complex), + type: 'cosmetic', + hostname: hostname, + cost: request.surveyCost || 0, + }); + } + } + + // Important: always clear used registers before leaving. + this.setRegister0.clear(); + this.setRegister1.clear(); + + console.timeEnd('cosmeticFilteringEngine.retrieveGenericSelectors'); + + return out; }; /******************************************************************************/ -FilterContainer.prototype.retrieveDomainSelectors = function(request, noCosmeticFiltering) { - if ( !request.locationURL ) { - return; - } +FilterContainer.prototype.retrieveDomainSelectors = function( + request, + sender, + options +) { + if ( !request.locationURL ) { return; } - //quickProfiler.start('FilterContainer.retrieve()'); + console.time('cosmeticFilteringEngine.retrieveDomainSelectors'); var hostname = this.µburi.hostnameFromURI(request.locationURL), domain = this.µburi.domainFromHostname(hostname) || hostname, pos = domain.indexOf('.'), - entity = pos === -1 ? '' : domain.slice(0, pos - domain.length) + '.*', - cacheEntry = this.selectorCache[hostname]; + entity = pos === -1 ? '' : domain.slice(0, pos - domain.length) + '.*'; // https://github.com/chrisaljoudi/uBlock/issues/587 // r.ready will tell the content script the cosmetic filtering engine is @@ -1933,68 +1978,144 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request, noCosmetic domain: domain, entity: entity, noDOMSurveying: this.hasGenericHide === false, - cosmeticHide: [], - cosmeticDonthide: [], + declarativeFilters: [], + exceptionFilters: [], + highGenericSimple: '', + highGenericComplex: '', netHide: [], + proceduralFilters: [], scripts: undefined }; - if ( !noCosmeticFiltering ) { - var hash, bucket; + if ( options.noCosmeticFiltering !== true ) { + var domainHash = makeHash(domain), + entityHash = entity !== '' ? makeHash(entity) : undefined, + exception, bucket; - // Generic exception cosmetic filters. - r.cosmeticDonthide = this.genericDonthide.slice(); - - // Specific cosmetic filters. - hash = makeHash(domain); - if ( (bucket = this.specificFilters.get(hash)) ) { - bucket.retrieve(hostname, r.cosmeticHide); + // Exception cosmetic filters: prime with generic exception filters. + var exceptionSet = this.setRegister0; + // Genetic exceptions (should be extremely rare). + for ( exception of this.genericDonthideSet ) { + exceptionSet.add(exception); } // Specific exception cosmetic filters. - if ( (bucket = this.specificFilters.get('!' + hash)) ) { - bucket.retrieve(hostname, r.cosmeticDonthide); + if ( (bucket = this.specificFilters.get('!' + domainHash)) ) { + bucket.retrieve(hostname, exceptionSet); } - - // Specific entity-based cosmetic filters. - if ( entity !== '' ) { - // Specific entity-based cosmetic filters. - hash = makeHash(entity); - if ( (bucket = this.specificFilters.get(hash)) ) { - bucket.retrieve(entity, r.cosmeticHide); + // Specific entity-based exception cosmetic filters. + if ( entityHash !== undefined ) { + if ( (bucket = this.specificFilters.get('!' + entityHash)) ) { + bucket.retrieve(entity, exceptionSet); } - // Specific entity-based exception cosmetic filters. - //if ( (bucket = this.specificFilters.get('!' + hash)) ) { - // bucket.retrieve(entity, r.cosmeticHide); - //} + } + // Special bucket for those filters without a valid + // domain name as per PSL. + if ( (bucket = this.specificFilters.get('!' + this.noDomainHash)) ) { + bucket.retrieve(hostname, exceptionSet); + } + if ( exceptionSet.size !== 0 ) { + r.exceptionFilters = µb.arrayFrom(exceptionSet); } + // Declarative cosmetic filters. + // TODO: Should I go one step further and store specific simple and + // specific complex in different collections? This could simplify + // slightly content script code. + var specificSet = this.setRegister1; + // Specific cosmetic filters. + if ( (bucket = this.specificFilters.get(domainHash)) ) { + bucket.retrieve(hostname, specificSet); + } + // Specific entity-based cosmetic filters. + if ( entityHash !== undefined ) { + if ( (bucket = this.specificFilters.get(entityHash)) ) { + bucket.retrieve(entity, specificSet); + } + } // https://github.com/chrisaljoudi/uBlock/issues/188 // Special bucket for those filters without a valid domain name as per PSL if ( (bucket = this.specificFilters.get(this.noDomainHash)) ) { - bucket.retrieve(hostname, r.cosmeticHide); + bucket.retrieve(hostname, specificSet); } - if ( (bucket = this.specificFilters.get('!' + this.noDomainHash)) ) { - bucket.retrieve(hostname, r.cosmeticDonthide); - } - - // cached cosmetic filters. - if ( cacheEntry ) { - cacheEntry.retrieve('cosmetic', r.cosmeticHide); + // Cached cosmetic filters: these are always declarative. + var cacheEntry = this.selectorCache.get(hostname); + if ( cacheEntry !== undefined ) { + cacheEntry.retrieve('cosmetic', specificSet); if ( r.noDOMSurveying === false ) { - r.noDOMSurveying = cacheEntry.cosmeticSurveyingMissCount > cosmeticSurveyingMissCountMax; + r.noDOMSurveying = cacheEntry.cosmeticSurveyingMissCount > + cosmeticSurveyingMissCountMax; } } + + // Procedural cosmetic filters. + var proceduralSet = this.setRegister2; + // Specific cosmetic filters. + if ( (bucket = this.proceduralFilters.get(domainHash)) ) { + bucket.retrieve(hostname, proceduralSet); + } + // Specific entity-based cosmetic filters. + if ( entityHash !== undefined ) { + if ( (bucket = this.proceduralFilters.get(entityHash)) ) { + bucket.retrieve(entity, proceduralSet); + } + } + // https://github.com/chrisaljoudi/uBlock/issues/188 + // Special bucket for those filters without a valid domain name as per PSL + if ( (bucket = this.proceduralFilters.get(this.noDomainHash)) ) { + bucket.retrieve(hostname, proceduralSet); + } + + // Apply exceptions. + for ( exception of exceptionSet ) { + specificSet.delete(exception); + proceduralSet.delete(exception); + } + if ( specificSet.size !== 0 ) { + r.declarativeFilters = µb.arrayFrom(specificSet); + } + if ( proceduralSet.size !== 0 ) { + r.proceduralFilters = µb.arrayFrom(proceduralSet); + } + + // Highly generic cosmetic filters: sent once along with specific ones. + if ( options.noGenericCosmeticFiltering !== true ) { + var exceptionHash = r.exceptionFilters.join(), + entry = this.mruHighlyGenericHideStrings.lookup(exceptionHash); + if ( entry === undefined ) { + var simpleSet = new Set(this.highlyGenericSimpleHideSet), + complexSet = new Set(this.highlyGenericComplexHideSet); + for ( exception of exceptionSet ) { + simpleSet.delete(exception); + complexSet.delete(exception); + } + entry = { + simple: µb.arrayFrom(simpleSet).join(',\n'), + complex: µb.arrayFrom(complexSet).join(',\n') + }; + this.mruHighlyGenericHideStrings.add(exceptionHash, entry); + } + r.highGenericHideSimple = entry.simple; + r.highGenericHideComplex = entry.complex; + } + + // Important: always clear used registers before leaving. + this.setRegister0.clear(); + this.setRegister1.clear(); + this.setRegister2.clear(); } // Scriptlet injection. r.scripts = this.retrieveUserScripts(domain, hostname); + // TODO: Is it *really* worth to cache selectors of collapsed resources? + // This adds code complexity and I am having doubts about the + // benefits. Investigate. // Collapsible blocked resources. - if ( cacheEntry ) { - cacheEntry.retrieve('net', r.netHide); - } + //if ( cacheEntry ) { + // cacheEntry.retrieve('net', r.netHide); + //} - //quickProfiler.stop(); + console.timeEnd('cosmeticFilteringEngine.retrieveDomainSelectors'); return r; }; diff --git a/src/js/logger.js b/src/js/logger.js index 700521ab5..5c96df4b5 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -137,6 +137,7 @@ var janitor = function() { ) { api.writeOne = writeOneNoop; logBuffer = null; + vAPI.messaging.broadcast({ what: 'loggerDisabled' }); } if ( logBuffer !== null ) { vAPI.setTimeout(janitor, logBufferObsoleteAfter); diff --git a/src/js/messaging.js b/src/js/messaging.js index a5bde04bf..29ac6cb2f 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -107,8 +107,6 @@ var onMessage = function(request, sender, callback) { case 'cosmeticFiltersInjected': µb.cosmeticFilteringEngine.addToSelectorCache(request); - /* falls through */ - case 'cosmeticFiltersActivated': // Net-based cosmetic filters are of no interest for logging purpose. if ( µb.logger.isEnabled() && request.type !== 'net' ) { µb.logCosmeticFilters(tabId); @@ -466,9 +464,12 @@ var onMessage = function(request, sender, callback) { // Sync var µb = µBlock, response, + tabId, pageStore; + if ( sender && sender.tab ) { - pageStore = µb.pageStoreFromTabId(sender.tab.id); + tabId = sender.tab.id; + pageStore = µb.pageStoreFromTabId(tabId); } switch ( request.what ) { @@ -476,7 +477,8 @@ var onMessage = function(request, sender, callback) { response = { id: request.id, hash: request.hash, - netSelectorCacheCountMax: µb.cosmeticFilteringEngine.netSelectorCacheCountMax + netSelectorCacheCountMax: + µb.cosmeticFilteringEngine.netSelectorCacheCountMax }; if ( µb.userSettings.collapseBlocked && @@ -490,22 +492,27 @@ var onMessage = function(request, sender, callback) { case 'retrieveContentScriptParameters': if ( pageStore && pageStore.getNetFilteringSwitch() ) { response = { - loggerEnabled: µb.logger.isEnabled(), collapseBlocked: µb.userSettings.collapseBlocked, - noCosmeticFiltering: µb.cosmeticFilteringEngine.acceptedCount === 0 || pageStore.noCosmeticFiltering === true, - noGenericCosmeticFiltering: pageStore.noGenericCosmeticFiltering === true + noCosmeticFiltering: + µb.cosmeticFilteringEngine.acceptedCount === 0 || + pageStore.noCosmeticFiltering === true, + noGenericCosmeticFiltering: + pageStore.noGenericCosmeticFiltering === true }; - response.specificCosmeticFilters = µb.cosmeticFilteringEngine.retrieveDomainSelectors( - request, - response.noCosmeticFiltering - ); + response.specificCosmeticFilters = + µb.cosmeticFilteringEngine + .retrieveDomainSelectors(request, sender, response); + if ( request.isRootFrame && µb.logger.isEnabled() ) { + µb.logCosmeticFilters(tabId); + } } break; case 'retrieveGenericCosmeticSelectors': if ( pageStore && pageStore.getGenericCosmeticFilteringSwitch() ) { response = { - result: µb.cosmeticFilteringEngine.retrieveGenericSelectors(request) + result: µb.cosmeticFilteringEngine + .retrieveGenericSelectors(request) }; } break; diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index c7e0edff8..70912195a 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -338,11 +338,11 @@ RedirectEngine.prototype.toSelfie = function() { } var µb = µBlock; return { - resources: µb.mapToArray(this.resources), + resources: µb.arrayFrom(this.resources), rules: rules, - ruleTypes: µb.setToArray(this.ruleTypes), - ruleSources: µb.setToArray(this.ruleSources), - ruleDestinations: µb.setToArray(this.ruleDestinations) + ruleTypes: µb.arrayFrom(this.ruleTypes), + ruleSources: µb.arrayFrom(this.ruleSources), + ruleDestinations: µb.arrayFrom(this.ruleDestinations) }; }; @@ -359,11 +359,10 @@ RedirectEngine.prototype.fromSelfie = function(selfie) { } // Rules. - var µb = µBlock; - this.rules = µb.mapFromArray(selfie.rules); - this.ruleTypes = µb.setFromArray(selfie.ruleTypes); - this.ruleSources = µb.setFromArray(selfie.ruleSources); - this.ruleDestinations = µb.setFromArray(selfie.ruleDestinations); + this.rules = new Map(selfie.rules); + this.ruleTypes = new Set(selfie.ruleTypes); + this.ruleSources = new Set(selfie.ruleSources); + this.ruleDestinations = new Set(selfie.ruleDestinations); return true; }; diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index 8d04148fc..aea180856 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -100,10 +100,27 @@ var fromCosmeticFilter = function(details) { prefix = match[0], filter = details.rawFilter.slice(prefix.length); + // With low generic simple cosmetic filters, the class or id prefix + // character is not part of the compiled data. So we must be ready to + // look-up version of the selector without the prefix character. + var idOrClassPrefix = filter.charAt(0), + cssPrefixMatcher; + if ( idOrClassPrefix === '#' ) { + cssPrefixMatcher = '#?'; + filter = filter.slice(1); + } else if ( idOrClassPrefix === '.' ) { + cssPrefixMatcher = '\\.?'; + filter = filter.slice(1); + } else { + idOrClassPrefix = ''; + cssPrefixMatcher = ''; + } + // https://github.com/gorhill/uBlock/issues/3101 // Use `m` flag for efficient regex execution. var reFilter = new RegExp( '^\\[\\d,[^\\n]*\\\\*"' + + cssPrefixMatcher + reEscapeCosmetic(filter) + '\\\\*"[^\\n]*\\]$', 'gm' @@ -154,8 +171,10 @@ var fromCosmeticFilter = function(details) { while ( (match = reFilter.exec(content)) !== null ) { fargs = JSON.parse(match[0]); switch ( fargs[0] ) { - case 0: - case 2: + case 0: // id-based + case 2: // class-based + found = prefix + idOrClassPrefix + filter; + break; case 4: case 5: case 7: @@ -169,6 +188,7 @@ var fromCosmeticFilter = function(details) { break; case 6: case 8: + case 9: if ( fargs[2] === '' || reHostname.test(fargs[2]) === true || diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index bbbd637f7..7cb4a8b17 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -27,62 +27,220 @@ /******************************************************************************/ -if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { +if ( + typeof vAPI !== 'object' || + vAPI.domFilterer instanceof Object === false || + vAPI.domWatcher instanceof Object === false +) { return; } -var loggedSelectors = vAPI.loggedSelectors || {}, - matchedSelectors = []; +var reHasCSSCombinators = /[ >+~]/, + reHasPseudoClass = /:+(?:after|before)$/, + sanitizedSelectors = new Map(), + matchProp = vAPI.matchesProp, + simple = { dict: new Set(), str: undefined }, + complex = { dict: new Set(), str: undefined }, + procedural = { dict: new Map() }, + jobQueue = []; - -var evaluateSelector = function(selector) { +var DeclarativeSimpleJob = function(node) { + this.node = node; +}; +DeclarativeSimpleJob.create = function(node) { + return new DeclarativeSimpleJob(node); +}; +DeclarativeSimpleJob.prototype.lookup = function(out) { + if ( simple.dict.size === 0 ) { return; } + if ( simple.str === undefined ) { + simple.str = Array.from(simple.dict).join(',\n'); + } if ( - loggedSelectors.hasOwnProperty(selector) === false && - document.querySelector(selector) !== null + (this.node === document || this.node[matchProp](simple.str) === false) && + (this.node.querySelector(simple.str) === null) ) { - loggedSelectors[selector] = true; - matchedSelectors.push(selector); + return; + } + for ( var selector of simple.dict ) { + if ( + this.node !== document && this.node[matchProp](selector) || + this.node.querySelector(selector) !== null + ) { + out.push(sanitizedSelectors.get(selector) || selector); + simple.dict.delete(selector); + simple.str = undefined; + if ( simple.dict.size === 0 ) { return; } + } } }; -// Simple CSS selector-based cosmetic filters. -vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector); - -// Complex CSS selector-based cosmetic filters. -vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector); - -// Non-querySelector-able filters. -vAPI.domFilterer.nqsSelectors.forEach(function(filter) { - if ( loggedSelectors.hasOwnProperty(filter) === false ) { - loggedSelectors[filter] = true; - matchedSelectors.push(filter); +var DeclarativeComplexJob = function() { +}; +DeclarativeComplexJob.instance = null; +DeclarativeComplexJob.create = function() { + if ( DeclarativeComplexJob.instance === null ) { + DeclarativeComplexJob.instance = new DeclarativeComplexJob(); } -}); - -// Procedural cosmetic filters. -vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) { - if ( - loggedSelectors.hasOwnProperty(pfilter.raw) === false && - pfilter.exec().length !== 0 - ) { - loggedSelectors[pfilter.raw] = true; - matchedSelectors.push(pfilter.raw); + return DeclarativeComplexJob.instance; +}; +DeclarativeComplexJob.prototype.lookup = function(out) { + if ( complex.dict.size === 0 ) { return; } + if ( complex.str === undefined ) { + complex.str = Array.from(complex.dict).join(',\n'); } -}); - -vAPI.loggedSelectors = loggedSelectors; - -if ( matchedSelectors.length ) { - vAPI.messaging.send( - 'scriptlets', - { - what: 'logCosmeticFilteringData', - frameURL: window.location.href, - frameHostname: window.location.hostname, - matchedSelectors: matchedSelectors + if ( document.querySelector(complex.str) === null ) { return; } + for ( var selector of complex.dict ) { + if ( document.querySelector(selector) !== null ) { + out.push(sanitizedSelectors.get(selector) || selector); + complex.dict.delete(selector); + complex.str = undefined; + if ( complex.dict.size === 0 ) { return; } } - ); -} + } +}; + +var ProceduralJob = function() { +}; +ProceduralJob.instance = null; +ProceduralJob.create = function() { + if ( ProceduralJob.instance === null ) { + ProceduralJob.instance = new ProceduralJob(); + } + return ProceduralJob.instance; +}; +ProceduralJob.prototype.lookup = function(out) { + for ( var entry of procedural.dict ) { + if ( entry[1].test() ) { + procedural.dict.delete(entry[0]); + out.push(entry[1].raw); + if ( procedural.dict.size === 0 ) { return; } + } + } +}; + +var jobQueueTimer = new vAPI.SafeAnimationFrame(function processJobQueue() { + console.time('dom logger/scanning for matches'); + jobQueueTimer.clear(); + var toLog = [], + t0 = Date.now(), + job; + while ( (job = jobQueue.shift()) ) { + job.lookup(toLog); + if ( (Date.now() - t0) > 10 ) { break; } + } + if ( toLog.length !== 0 ) { + vAPI.messaging.send( + 'scriptlets', + { + what: 'logCosmeticFilteringData', + frameURL: window.location.href, + frameHostname: window.location.hostname, + matchedSelectors: toLog + } + ); + } + if ( simple.dict.size === 0 && complex.dict.size === 0 ) { + jobQueue = []; + } + if ( jobQueue.length !== 0 ) { + jobQueueTimer.start(100); + } + console.timeEnd('dom logger/scanning for matches'); +}); + +var handlers = { + + onFiltersetChanged: function(type, selectors) { + console.time('dom logger/filterset changed'); + var selector, + sanitized; + if ( type === 'declarative' ) { + var simpleSizeBefore = simple.dict.size, + complexSizeBefore = complex.dict.size; + for ( selector of selectors.split(',\n') ) { + if ( reHasPseudoClass.test(selector) ) { + sanitized = selector.replace(reHasPseudoClass, ''); + sanitizedSelectors.set(sanitized, selector); + selector = sanitized; + } + if ( reHasCSSCombinators.test(selector) ) { + complex.dict.add(selector); + complex.str = undefined; + } else { + simple.dict.add(selector); + simple.str = undefined; + } + } + if ( simple.dict.size !== simpleSizeBefore ) { + jobQueue.push(DeclarativeSimpleJob.create(document)); + } + if ( complex.dict.size !== complexSizeBefore ) { + complex.str = Array.from(complex.dict).join(',\n'); + jobQueue.push(DeclarativeComplexJob.create()); + } + } else if ( type === 'procedural' ) { + for ( selector of selectors ) { + procedural.dict.set(selector[0], selector[1]); + } + if ( selectors.size !== 0 ) { + jobQueue.push(ProceduralJob.create()); + } + } + if ( jobQueue.length !== 0 ) { + jobQueueTimer.start(1); + } + console.timeEnd('dom logger/filterset changed'); + }, + + onDOMCreated: function() { + handlers.onFiltersetChanged( + 'declarative', + vAPI.domFilterer.getAllDeclarativeSelectors() + ); + handlers.onFiltersetChanged( + 'procedural', + vAPI.domFilterer.getAllProceduralSelectors() + ); + vAPI.domFilterer.addListener(handlers); + }, + + onDOMChanged: function(addedNodes) { + if ( simple.dict.size === 0 && complex.dict.size === 0 ) { return; } + // This is to guard against runaway job queue. I suspect this could + // occur on slower devices. + if ( jobQueue.length <= 300 ) { + if ( simple.dict.size !== 0 ) { + for ( var node of addedNodes ) { + jobQueue.push(DeclarativeSimpleJob.create(node)); + } + } + if ( complex.dict.size !== 0 ) { + jobQueue.push(DeclarativeComplexJob.create()); + } + if ( procedural.dict.size !== 0 ) { + jobQueue.push(ProceduralJob.create()); + } + } + if ( jobQueue.length !== 0 ) { + jobQueueTimer.start(100); + } + } + +}; + +/******************************************************************************/ + +var onMessage = function(msg) { + if ( msg.what === 'loggerDisabled' ) { + jobQueueTimer.clear(); + vAPI.domFilterer.removeListener(handlers); + vAPI.domWatcher.removeListener(handlers); + vAPI.messaging.removeChannelListener('domLogger', onMessage); + } +}; +vAPI.messaging.addChannelListener('domLogger', onMessage); + +vAPI.domWatcher.addListener(handlers); /******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-off.js b/src/js/scriptlets/cosmetic-off.js index cd92eb780..a3ab578bc 100644 --- a/src/js/scriptlets/cosmetic-off.js +++ b/src/js/scriptlets/cosmetic-off.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2016 Raymond Hill + Copyright (C) 2015-2017 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 @@ -23,20 +23,6 @@ /******************************************************************************/ -(function() { - if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { - return; - } - - var elems = []; - try { - elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); - } catch (e) { - } - var i = elems.length; - while ( i-- ) { - vAPI.domFilterer.showNode(elems[i]); - } - - vAPI.domFilterer.toggleOff(); -})(); +if ( typeof vAPI === 'object' && vAPI.domFilterer ) { + vAPI.domFilterer.toggle(false); +} diff --git a/src/js/scriptlets/cosmetic-on.js b/src/js/scriptlets/cosmetic-on.js index d3a6de830..c7136a6f9 100644 --- a/src/js/scriptlets/cosmetic-on.js +++ b/src/js/scriptlets/cosmetic-on.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2016 Raymond Hill + Copyright (C) 2015-2017 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 @@ -23,20 +23,6 @@ /******************************************************************************/ -(function() { - if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { - return; - } - - var elems = []; - try { - elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); - } catch (e) { - } - var i = elems.length; - while ( i-- ) { - vAPI.domFilterer.unshowNode(elems[i]); - } - - vAPI.domFilterer.toggleOn(); -})(); +if ( typeof vAPI === 'object' && vAPI.domFilterer ) { + vAPI.domFilterer.toggle(true); +} diff --git a/src/js/scriptlets/cosmetic-survey.js b/src/js/scriptlets/cosmetic-survey.js index d3956ca70..c94099dbb 100644 --- a/src/js/scriptlets/cosmetic-survey.js +++ b/src/js/scriptlets/cosmetic-survey.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2016 Raymond Hill + Copyright (C) 2015-2017 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 @@ -24,24 +24,14 @@ /******************************************************************************/ (function() { - if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { - return; - } - - var xpr = document.evaluate( - 'count(//*[@' + vAPI.domFilterer.hiddenId + '])', - document, - null, - XPathResult.NUMBER_TYPE, - null - ); + if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { return; } vAPI.messaging.send( 'scriptlets', { what: 'cosmeticallyFilteredElementCount', pageURL: window.location.href, - filteredElementCount: xpr && xpr.numberValue || 0 + filteredElementCount: vAPI.domFilterer.getFilteredElementCount() } ); })(); diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index ce8a4632d..d0b7e1388 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2016 Raymond Hill + Copyright (C) 2015-2017 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 @@ -140,11 +140,16 @@ var cssEscape = (function(/*root*/) { // Highlighter-related var svgRoot = null; var pickerRoot = null; -var highlightedElementLists = [ [], [], [] ]; var nodeToIdMap = new WeakMap(); // No need to iterate -var nodeToCosmeticFilterMap = new WeakMap(); -var toggledNodes = new Map(); + +var blueNodes = []; +var roRedNodes = new Map(); // node => current cosmetic filter +var rwRedNodes = new Set(); // node => new cosmetic filter (toggle node) +//var roGreenNodes = new Map(); // node => current exception cosmetic filter (can't toggle) +var rwGreenNodes = new Set(); // node => new exception cosmetic filter (toggle filter) + +var reHasCSSCombinators = /[ >+~]/; /******************************************************************************/ @@ -224,7 +229,7 @@ var domLayout = (function() { this.lvl = 0; this.sel = 'body'; this.cnt = 0; - this.filter = nodeToCosmeticFilterMap.get(document.body); + this.filter = roRedNodes.get(document.body); }; var DomNode = function(node, level) { @@ -232,7 +237,7 @@ var domLayout = (function() { this.lvl = level; this.sel = selectorFromNode(node); this.cnt = 0; - this.filter = nodeToCosmeticFilterMap.get(node); + this.filter = roRedNodes.get(node); }; var domNodeFactory = function(level, node) { @@ -687,20 +692,27 @@ var cosmeticFilterMapper = (function() { } var nodesFromStyleTag = function(rootNode) { - var filterMap = nodeToCosmeticFilterMap, + var filterMap = roRedNodes, selectors, selector, nodes, node, i, j; - // CSS-based selectors: simple one. - selectors = vAPI.domFilterer.simpleHideSelectors.entries; + // Declarative selectors. + selectors = vAPI.domFilterer.getAllDeclarativeSelectors().split(',\n'); i = selectors.length; while ( i-- ) { selector = selectors[i]; - if ( filterMap.has(rootNode) === false && rootNode[matchesFnName](selector) ) { - filterMap.set(rootNode, selector); + if ( reHasCSSCombinators.test(selector) ) { + nodes = document.querySelectorAll(selector); + } else { + if ( + filterMap.has(rootNode) === false && + rootNode[matchesFnName](selector) + ) { + filterMap.set(rootNode, selector); + } + nodes = rootNode.querySelectorAll(selector); } - nodes = rootNode.querySelectorAll(selector); j = nodes.length; while ( j-- ) { node = nodes[j]; @@ -710,42 +722,31 @@ var cosmeticFilterMapper = (function() { } } - // CSS-based selectors: complex one (must query from doc root). - selectors = vAPI.domFilterer.complexHideSelectors.entries; - i = selectors.length; - while ( i-- ) { - selector = selectors[i]; - nodes = document.querySelectorAll(selector); + // Procedural selectors. + selectors = vAPI.domFilterer.getAllProceduralSelectors(); + for ( var entry of selectors ) { + nodes = entry[1].exec(); j = nodes.length; while ( j-- ) { - node = nodes[j]; if ( filterMap.has(node) === false ) { - filterMap.set(node, selector); + filterMap.set(node, entry[0]); } } } - - // Non-CSS selectors. - var runJobCallback = function(node, pfilter) { - if ( filterMap.has(node) === false ) { - filterMap.set(node, pfilter.raw); - } - }; - vAPI.domFilterer.proceduralSelectors.forEachNode(runJobCallback); }; var incremental = function(rootNode) { - vAPI.domFilterer.userCSS.toggle(false); + vAPI.domFilterer.toggle(false); nodesFromStyleTag(rootNode); }; var reset = function() { - nodeToCosmeticFilterMap = new WeakMap(); + roRedNodes = new Map(); incremental(document.documentElement); }; var shutdown = function() { - vAPI.domFilterer.userCSS.toggle(true); + vAPI.domFilterer.toggle(true); }; return { @@ -824,55 +825,96 @@ var getSvgRootChildren = function() { var highlightElements = function(scrollTo) { var wv = pickerRoot.contentWindow.innerWidth; var hv = pickerRoot.contentWindow.innerHeight; - var ocean = ['M0 0h' + wv + 'v' + hv + 'h-' + wv, 'z'], islands; - var elems, elem, rect, poly; + var islands; + var elem, rect, poly; var xl, xr, yt, yb, w, h, ws; var xlu = Number.MAX_VALUE, xru = 0, ytu = Number.MAX_VALUE, ybu = 0; - var lists = highlightedElementLists; var svgRootChildren = getSvgRootChildren(); - for ( var i = 0; i < lists.length; i++ ) { - elems = lists[i]; - islands = []; - for ( var j = 0; j < elems.length; j++ ) { - elem = elems[j]; - if ( elem === pickerRoot ) { - continue; - } - if ( typeof elem.getBoundingClientRect !== 'function' ) { - continue; - } - - rect = elem.getBoundingClientRect(); - xl = rect.left; - xr = rect.right; - w = rect.width; - yt = rect.top; - yb = rect.bottom; - h = rect.height; - - ws = w.toFixed(1); - poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + - 'h' + ws + - 'v' + h.toFixed(1) + - 'h-' + ws + - 'z'; - ocean.push(poly); - islands.push(poly); - - if ( !scrollTo ) { - continue; - } - - if ( xl < xlu ) { xlu = xl; } - if ( xr > xru ) { xru = xr; } - if ( yt < ytu ) { ytu = yt; } - if ( yb > ybu ) { ybu = yb; } - } - svgRootChildren[i+1].setAttribute('d', islands.join('') || 'M0 0'); + islands = []; + for ( elem of rwRedNodes.keys() ) { + if ( elem === pickerRoot ) { continue; } + if ( rwGreenNodes.has(elem) ) { continue; } + if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + islands.push(poly); } + svgRootChildren[0].setAttribute('d', islands.join('') || 'M0 0'); - svgRoot.firstElementChild.setAttribute('d', ocean.join('')); + islands = []; + for ( elem of rwGreenNodes ) { + if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + islands.push(poly); + } + svgRootChildren[1].setAttribute('d', islands.join('') || 'M0 0'); + + islands = []; + for ( elem of roRedNodes.keys() ) { + if ( elem === pickerRoot ) { continue; } + if ( rwGreenNodes.has(elem) ) { continue; } + if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + islands.push(poly); + } + svgRootChildren[2].setAttribute('d', islands.join('') || 'M0 0'); + + islands = []; + for ( elem of blueNodes ) { + if ( elem === pickerRoot ) { continue; } + if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } + rect = elem.getBoundingClientRect(); + xl = rect.left; + xr = rect.right; + w = rect.width; + yt = rect.top; + yb = rect.bottom; + h = rect.height; + ws = w.toFixed(1); + poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) + + 'h' + ws + + 'v' + h.toFixed(1) + + 'h-' + ws + + 'z'; + islands.push(poly); + } + svgRootChildren[3].setAttribute('d', islands.join('') || 'M0 0'); if ( !scrollTo ) { return; @@ -913,34 +955,12 @@ var onScrolled = function() { /******************************************************************************/ -var resetToggledNodes = function() { - for ( var entry of toggledNodes ) { - if ( entry[1].show ) { - showNode(entry[0], entry[1].v1, entry[1].v2); - } else { - hideNode(entry[0]); - } - } - toggledNodes.clear(); -}; - -/******************************************************************************/ - -var forgetToggledNodes = function() { - toggledNodes.clear(); -}; - -/******************************************************************************/ - var selectNodes = function(selector, nid) { var nodes = elementsFromSelector(selector); - if ( nid === '' ) { - return nodes; - } - var i = nodes.length; - while ( i-- ) { - if ( nodeToIdMap.get(nodes[i]) === nid ) { - return [nodes[i]]; + if ( nid === '' ) { return nodes; } + for ( var node of nodes ) { + if ( nodeToIdMap.get(node) === nid ) { + return [ node ]; } } return []; @@ -950,84 +970,37 @@ var selectNodes = function(selector, nid) { var shutdown = function() { cosmeticFilterMapper.shutdown(); - resetToggledNodes(); domLayout.shutdown(); vAPI.messaging.removeAllChannelListeners('domInspector'); window.removeEventListener('scroll', onScrolled, true); document.documentElement.removeChild(pickerRoot); pickerRoot = svgRoot = null; - highlightedElementLists = [ [], [], [] ]; }; /******************************************************************************/ -// original, target = what to do -// any, any = restore saved display property -// any, hidden = set display to `none`, remember original state -// hidden, any = remove display property, don't remember original state -// hidden, hidden = set display to `none` - -var toggleNodes = function(nodes, originalState, targetState) { - var i = nodes.length; - if ( i === 0 ) { - return; - } - var node, details; - while ( i-- ) { - node = nodes[i]; - // originally visible node - if ( originalState ) { - // unhide visible node - if ( targetState ) { - details = toggledNodes.get(node) || {}; - showNode(node, details.v1, details.v2); - toggledNodes.delete(node); - } - // hide visible node - else { - toggledNodes.set(node, { - show: true, - v1: node.style.getPropertyValue('display') || '', - v2: node.style.getPropertyPriority('display') || '' - }); - hideNode(node); - } +var toggleExceptions = function(nodes, targetState) { + for ( var node of nodes ) { + if ( targetState ) { + rwGreenNodes.add(node); + } else { + rwGreenNodes.delete(node); } - // originally hidden node - else { - // show hidden node - if ( targetState ) { - toggledNodes.set(node, { show: false }); - showNode(node, 'initial', 'important'); - } - // hide hidden node - else { - hideNode(node); - toggledNodes.delete(node); - } + } +}; + +var toggleFilter = function(nodes, targetState) { + for ( var node of nodes ) { + if ( targetState ) { + rwRedNodes.delete(node); + } else { + rwRedNodes.add(node); } } }; // https://www.youtube.com/watch?v=L5jRewnxSBY -/******************************************************************************/ - -var showNode = function(node, v1, v2) { - vAPI.domFilterer.showNode(node); - if ( !v1 ) { - node.style.removeProperty('display'); - } else { - node.style.setProperty('display', v1, v2); - } -}; - -/******************************************************************************/ - -var hideNode = function(node) { - vAPI.domFilterer.unshowNode(node); -}; - /******************************************************************************/ /******************************************************************************/ @@ -1036,11 +1009,6 @@ var onMessage = function(request) { switch ( request.what ) { case 'commitFilters': - resetToggledNodes(); - toggleNodes(selectNodes(request.hide, ''), true, false); - toggleNodes(selectNodes(request.unhide, ''), false, true); - forgetToggledNodes(); - highlightedElementLists = [ [], [], [] ]; highlightElements(); break; @@ -1053,44 +1021,38 @@ var onMessage = function(request) { break; case 'highlightMode': - svgRoot.classList.toggle('invert', request.invert); + //svgRoot.classList.toggle('invert', request.invert); break; case 'highlightOne': - highlightedElementLists[0] = selectNodes(request.selector, request.nid); + blueNodes = selectNodes(request.selector, request.nid); highlightElements(request.scrollTo); break; - case 'resetToggledNodes': - resetToggledNodes(); - break; - case 'showCommitted': - resetToggledNodes(); - highlightedElementLists[0] = []; - highlightedElementLists[1] = selectNodes(request.hide, ''); - highlightedElementLists[2] = selectNodes(request.unhide, ''); - toggleNodes(highlightedElementLists[2], false, true); + blueNodes = []; + // TODO: show only the new filters and exceptions. highlightElements(true); break; case 'showInteractive': - resetToggledNodes(); - toggleNodes(selectNodes(request.hide, ''), true, false); - toggleNodes(selectNodes(request.unhide, ''), false, true); - highlightedElementLists = [ [], [], [] ]; + blueNodes = []; highlightElements(); break; case 'toggleFilter': - highlightedElementLists[0] = selectNodes(request.filter, request.nid); - toggleNodes(highlightedElementLists[0], request.original, request.target); + toggleExceptions( + selectNodes(request.filter, request.nid), + request.target + ); highlightElements(true); break; case 'toggleNodes': - highlightedElementLists[0] = selectNodes(request.selector, request.nid); - toggleNodes(highlightedElementLists[0], request.original, request.target); + toggleFilter( + selectNodes(request.selector, request.nid), + request.target + ); highlightElements(true); break; @@ -1149,22 +1111,22 @@ pickerRoot.onload = function() { 'top: 0;', 'width: 100%;', '}', - 'svg > path:first-child {', - 'fill: rgba(0,0,0,0.75);', - 'fill-rule: evenodd;', + 'svg > path:nth-of-type(1) {', + 'fill: rgba(255,0,0,0.2);', + 'stroke: #F00;', '}', 'svg > path:nth-of-type(2) {', - 'fill: rgba(0,0,255,0.1);', - 'stroke: #FFF;', - 'stroke-width: 0.5px;', + 'fill: rgba(0,255,0,0.2);', + 'stroke: #0F0;', '}', 'svg > path:nth-of-type(3) {', 'fill: rgba(255,0,0,0.2);', 'stroke: #F00;', '}', 'svg > path:nth-of-type(4) {', - 'fill: rgba(0,255,0,0.2);', - 'stroke: #0F0;', + 'fill: rgba(0,0,255,0.1);', + 'stroke: #FFF;', + 'stroke-width: 0.5px;', '}', '' ].join('\n'); @@ -1179,8 +1141,8 @@ pickerRoot.onload = function() { window.addEventListener('scroll', onScrolled, true); - highlightElements(); cosmeticFilterMapper.reset(); + highlightElements(); vAPI.messaging.addChannelListener('domInspector', onMessage); }; diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index 82d5ebc1b..dd15d019e 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -118,12 +118,11 @@ /******************************************************************************/ -if ( typeof vAPI !== 'object' ) { - return; -} - -// don't run in frames -if ( window.top !== window ) { +if ( + window.top !== window || + typeof vAPI !== 'object' || + vAPI.domFilterer instanceof Object === false +) { return; } @@ -131,8 +130,8 @@ var pickerRoot = document.getElementById(vAPI.sessionId); if ( pickerRoot ) { return; } + var pickerBody = null; -var pickerStyle = null; var svgOcean = null; var svgIslands = null; var svgRoot = null; @@ -1397,13 +1396,12 @@ var stopPicker = function() { candidateElements = []; bestCandidateFilter = null; - if ( pickerRoot === null ) { - return; - } + if ( pickerRoot === null ) { return; } // https://github.com/gorhill/uBlock/issues/2060 - if ( vAPI.userCSS ) { - vAPI.userCSS.remove(pickerStyle.textContent); + if ( vAPI.domFilterer instanceof Object ) { + vAPI.domFilterer.removeCSSRule(pickerCSSSelector1, pickerCSSDeclaration1); + vAPI.domFilterer.removeCSSRule(pickerCSSSelector2, pickerCSSDeclaration2); } window.removeEventListener('scroll', onScrolled, true); @@ -1414,7 +1412,6 @@ var stopPicker = function() { svgRoot.removeEventListener('click', onSvgClicked); svgRoot.removeEventListener('touchstart', onSvgTouchStartStop); svgRoot.removeEventListener('touchend', onSvgTouchStartStop); - pickerStyle.parentNode.removeChild(pickerStyle); pickerRoot.parentNode.removeChild(pickerRoot); pickerRoot.removeEventListener('load', stopPicker); pickerRoot = @@ -1548,50 +1545,51 @@ var bootstrapPicker = function() { pickerRoot = document.createElement('iframe'); pickerRoot.id = vAPI.sessionId; -pickerRoot.style.cssText = [ - 'background: transparent', - 'border: 0', - 'border-radius: 0', - 'box-shadow: none', - 'display: block', - 'height: 100%', - 'left: 0', - 'margin: 0', - 'max-height: none', - 'max-width: none', - 'opacity: 1', - 'outline: 0', - 'padding: 0', - 'position: fixed', - 'top: 0', - 'visibility: visible', - 'width: 100%', - 'z-index: 2147483647', - '' -].join(' !important;'); + +var pickerCSSSelector1 = '#' + pickerRoot.id; +var pickerCSSDeclaration1 = [ + 'background: transparent', + 'border: 0', + 'border-radius: 0', + 'box-shadow: none', + 'display: block', + 'height: 100%', + 'left: 0', + 'margin: 0', + 'max-height: none', + 'max-width: none', + 'opacity: 1', + 'outline: 0', + 'padding: 0', + 'position: fixed', + 'top: 0', + 'visibility: visible', + 'width: 100%', + 'z-index: 2147483647', + '' + ].join(' !important;'); +var pickerCSSSelector2 = '[' + pickerRoot.id + '-clickblind]'; +var pickerCSSDeclaration2 = 'pointer-events: none !important;'; + + +pickerRoot.style.cssText = pickerCSSDeclaration1; // https://github.com/gorhill/uBlock/issues/1529 -// In addition to inline styles, harden the element picker styles by using -// a dedicated style tag. -pickerStyle = document.createElement('style'); -pickerStyle.textContent = [ - '#' + pickerRoot.id + ' {', - pickerRoot.style.cssText, - '}', - '[' + pickerRoot.id + '-clickblind] {', - 'pointer-events: none !important;', - '}', - '' -].join('\n'); -document.documentElement.appendChild(pickerStyle); +// In addition to inline styles, harden the element picker styles by using +// dedicated CSS rules. +vAPI.domFilterer.addCSSRule( + pickerCSSSelector1, + pickerCSSDeclaration1, + { internal: true } +); +vAPI.domFilterer.addCSSRule( + pickerCSSSelector2, + pickerCSSDeclaration2, + { internal: true } +); // https://github.com/gorhill/uBlock/issues/2060 -if ( vAPI.domFilterer ) { - pickerRoot[vAPI.domFilterer.getExcludeId()] = true; -} -if ( vAPI.userCSS ) { - vAPI.userCSS.add(pickerStyle.textContent); -} +vAPI.domFilterer.excludeNode(pickerRoot); pickerRoot.addEventListener('load', bootstrapPicker); document.documentElement.appendChild(pickerRoot); diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 2bee348bc..6205f8ef1 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1137,12 +1137,12 @@ FilterHostnameDict.prototype.logData = function() { }; FilterHostnameDict.prototype.compile = function() { - return [ this.fid, µb.setToArray(this.dict) ]; + return [ this.fid, µb.arrayFrom(this.dict) ]; }; FilterHostnameDict.load = function(args) { var f = new FilterHostnameDict(); - f.dict = µb.setFromArray(args[1]); + f.dict = new Set(args[1]); return f; }; @@ -2006,8 +2006,8 @@ FilterContainer.prototype.freeze = function() { this.fdataLast = null; this.filterLast = null; this.frozen = true; - //console.log(JSON.stringify(Array.from(filterClassHistogram))); - //this.tokenHistogram = new Map(Array.from(this.tokenHistogram).sort(function(a, b) { + //console.log(JSON.stringify(µb.arrayFrom(filterClassHistogram))); + //this.tokenHistogram = new Map(µb.arrayFrom(this.tokenHistogram).sort(function(a, b) { // return a[0].localeCompare(b[0]) || (b[1] - a[1]); //})); }; diff --git a/src/js/storage.js b/src/js/storage.js index 9d32ac351..d14f6a131 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -211,7 +211,7 @@ this.removeFilterList(oldKeys[i]); } } - newKeys = this.setToArray(newSet); + newKeys = this.arrayFrom(newSet); var bin = { selectedFilterLists: newKeys, remoteBlacklists: this.oldDataFromNewListKeys(newKeys) @@ -342,10 +342,10 @@ } selectedListKeySet.add(assetKey); } - externalLists = this.setToArray(importedSet).sort().join('\n'); + externalLists = this.arrayFrom(importedSet).sort().join('\n'); } - var result = this.setToArray(selectedListKeySet); + var result = this.arrayFrom(selectedListKeySet); if ( externalLists !== this.userSettings.externalLists ) { this.userSettings.externalLists = externalLists; vAPI.storage.set({ externalLists: externalLists }); @@ -371,7 +371,7 @@ } out.add(location); } - return this.setToArray(out); + return this.arrayFrom(out); }; /******************************************************************************/ diff --git a/src/js/utils.js b/src/js/utils.js index a8146e1b8..b802835d8 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -320,34 +320,16 @@ /******************************************************************************/ -µBlock.mapToArray = typeof Array.from === 'function' +µBlock.arrayFrom = typeof Array.from === 'function' ? Array.from - : function(map) { - var out = []; - for ( var entry of map ) { - out.push(entry); + : function(iterable) { + var out = [], i = 0; + for ( var value of iterable ) { + out[i++] = value; } return out; }; -µBlock.mapFromArray = function(arr) { - return new Map(arr); -}; - -µBlock.setToArray = typeof Array.from === 'function' - ? Array.from - : function(dict) { - var out = []; - for ( var value of dict ) { - out.push(value); - } - return out; - }; - -µBlock.setFromArray = function(arr) { - return new Set(arr); -}; - /******************************************************************************/ µBlock.openNewTab = function(details) { @@ -376,3 +358,41 @@ }; /******************************************************************************/ + +µBlock.MRUCache = function(size) { + this.size = size; + this.array = []; + this.map = new Map(); +}; + +µBlock.MRUCache.prototype = { + add: function(key, value) { + var found = this.map.has(key); + this.map.set(key, value); + if ( !found ) { + if ( this.array.length === this.size ) { + this.map.delete(this.array.pop()); + } + this.array.unshift(key); + } + }, + remove: function(key) { + if ( this.map.has(key) ) { + this.array.splice(this.array.indexOf(key), 1); + } + }, + lookup: function(key) { + var value = this.map.get(key); + if ( value !== undefined && this.array[0] !== key ) { + this.array.splice(this.array.indexOf(key), 1); + this.array.unshift(key); + } + return value; + }, + reset: function() { + this.array = []; + this.map.clear(); + } +}; + +/******************************************************************************/ diff --git a/tools/make-firefox.sh b/tools/make-firefox.sh index c393e1738..32dddba7a 100755 --- a/tools/make-firefox.sh +++ b/tools/make-firefox.sh @@ -21,6 +21,7 @@ mv $DES/img/icon_128.png $DES/icon.png cp platform/firefox/css/* $DES/css/ cp platform/firefox/polyfill.js $DES/js/ cp platform/firefox/vapi-*.js $DES/js/ +cp platform/webext/vapi-usercss.js $DES/js/ cp platform/firefox/bootstrap.js $DES/ cp platform/firefox/processScript.js $DES/ cp platform/firefox/frame*.js $DES/