From 3573b6b32c3f5721eb606b1e0698ed66497ab080 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 16 May 2019 13:44:49 -0400 Subject: [PATCH] Add ability to report exception cosmetic filters in the logger Related issue: - https://github.com/gorhill/uBlock/issues/127 Additionally, the extended exception filters in the logger will be rendered with a line-through to more easily distinguish them from non-exception ones. Also, opportunistically converted revisited code to ES6 syntax. --- platform/chromium/vapi-usercss.js | 2 +- platform/chromium/vapi-usercss.pseudo.js | 268 ++++++++++++----------- platform/chromium/vapi-usercss.real.js | 154 ++++++------- src/css/logger-ui.css | 7 +- src/js/contentscript.js | 107 +++++---- src/js/cosmetic-filtering.js | 69 ++++-- src/js/logger-ui.js | 4 +- src/js/messaging.js | 2 +- src/js/reverselookup-worker.js | 4 +- src/js/scriptlets/cosmetic-logger.js | 112 ++++++---- 10 files changed, 400 insertions(+), 329 deletions(-) diff --git a/platform/chromium/vapi-usercss.js b/platform/chromium/vapi-usercss.js index 72b9c23c9..0cf97b7aa 100644 --- a/platform/chromium/vapi-usercss.js +++ b/platform/chromium/vapi-usercss.js @@ -30,7 +30,7 @@ if ( typeof vAPI === 'object' ) { vAPI.supportsUserStylesheets = - /\bChrom(?:e|ium)\/(?:6[6789]|[789]|1\d\d)/.test(navigator.userAgent); + /\bChrom(?:e|ium)\/(?:5\d|6[012345])\b/.test(navigator.userAgent) === false; } diff --git a/platform/chromium/vapi-usercss.pseudo.js b/platform/chromium/vapi-usercss.pseudo.js index 106370b6a..308811629 100644 --- a/platform/chromium/vapi-usercss.pseudo.js +++ b/platform/chromium/vapi-usercss.pseudo.js @@ -40,10 +40,10 @@ vAPI.userStylesheet = { inject: function() { this.style = document.createElement('style'); this.style.disabled = this.disabled; - var parent = document.head || document.documentElement; + const parent = document.head || document.documentElement; if ( parent === null ) { return; } parent.appendChild(this.style); - var observer = new MutationObserver(function() { + const observer = new MutationObserver(function() { if ( this.style === null ) { return; } if ( this.style.sheet !== null ) { return; } this.styleFixCount += 1; @@ -58,31 +58,31 @@ vAPI.userStylesheet = { add: function(cssText) { if ( cssText === '' || this.css.has(cssText) ) { return; } if ( this.style === null ) { this.inject(); } - var sheet = this.style.sheet; + const sheet = this.style.sheet; if ( !sheet ) { return; } - var i = sheet.cssRules.length; + const i = sheet.cssRules.length; sheet.insertRule(cssText, i); this.css.set(cssText, sheet.cssRules[i]); }, remove: function(cssText) { if ( cssText === '' ) { return; } - var cssRule = this.css.get(cssText); + const cssRule = this.css.get(cssText); if ( cssRule === undefined ) { return; } this.css.delete(cssText); if ( this.style === null ) { return; } - var sheet = this.style.sheet; + const sheet = this.style.sheet; if ( !sheet ) { return; } - var rules = sheet.cssRules, - i = rules.length; + const rules = sheet.cssRules; + let i = rules.length; while ( i-- ) { if ( rules[i] !== cssRule ) { continue; } sheet.deleteRule(i); break; } if ( rules.length !== 0 ) { return; } - var style = this.style; + const style = this.style; this.style = null; - var parent = style.parentNode; + const parent = style.parentNode; if ( parent !== null ) { parent.removeChild(style); } @@ -99,55 +99,54 @@ vAPI.userStylesheet = { /******************************************************************************/ -vAPI.DOMFilterer = function() { - this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); - this.domIsReady = document.readyState !== 'loading'; - this.listeners = []; - this.excludedNodeSet = new WeakSet(); - this.addedNodes = new Set(); - this.removedNodes = false; +vAPI.DOMFilterer = class { + constructor() { + this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); + this.domIsReady = document.readyState !== 'loading'; + this.listeners = []; + 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.specificOthers = []; - this.genericSimpleHide = new Set(); - this.genericComplexHide = new Set(); + this.specificSimpleHide = new Set(); + this.specificSimpleHideAggregated = undefined; + this.addedSpecificSimpleHide = []; + this.specificComplexHide = new Set(); + this.specificComplexHideAggregated = undefined; + this.addedSpecificComplexHide = []; + this.specificOthers = []; + this.genericSimpleHide = new Set(); + this.genericComplexHide = new Set(); + this.exceptedCSSRules = []; - this.hideNodeExpando = undefined; - this.hideNodeBatchProcessTimer = undefined; - this.hiddenNodeObserver = undefined; - this.hiddenNodesetToProcess = new Set(); - this.hiddenNodeset = new WeakSet(); + 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); + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(this); + } + + // https://www.w3.org/community/webed/wiki/CSS/Selectors#Combinators + this.reCSSCombinators = /[ >+~]/; } -}; -vAPI.DOMFilterer.prototype = { - // https://www.w3.org/community/webed/wiki/CSS/Selectors#Combinators - reCSSCombinators: /[ >+~]/, - - commitNow: function() { + commitNow() { this.commitTimer.clear(); if ( this.domIsReady !== true || vAPI.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 ) { + const nodes = document.querySelectorAll(this.addedSpecificSimpleHide.join(',')); + for ( const node of nodes ) { this.hideNode(node); } this.addedSpecificSimpleHide = []; @@ -158,8 +157,8 @@ vAPI.DOMFilterer.prototype = { 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 ) { + const nodes = document.querySelectorAll(this.addedSpecificComplexHide.join(',')); + for ( const node of nodes ) { this.hideNode(node); } this.addedSpecificComplexHide = []; @@ -169,8 +168,8 @@ vAPI.DOMFilterer.prototype = { // DOM layout changed. - var domNodesAdded = this.addedNodes.size !== 0, - domLayoutChanged = domNodesAdded || this.removedNodes; + const domNodesAdded = this.addedNodes.size !== 0; + const domLayoutChanged = domNodesAdded || this.removedNodes; if ( domNodesAdded === false || domLayoutChanged === false ) { return; @@ -184,12 +183,12 @@ vAPI.DOMFilterer.prototype = { this.specificSimpleHideAggregated = Array.from(this.specificSimpleHide).join(',\n'); } - for ( node of this.addedNodes ) { + for ( const node of this.addedNodes ) { if ( node[vAPI.matchesProp](this.specificSimpleHideAggregated) ) { this.hideNode(node); } - nodes = node.querySelectorAll(this.specificSimpleHideAggregated); - for ( node of nodes ) { + const nodes = node.querySelectorAll(this.specificSimpleHideAggregated); + for ( const node of nodes ) { this.hideNode(node); } } @@ -202,8 +201,8 @@ vAPI.DOMFilterer.prototype = { this.specificComplexHideAggregated = Array.from(this.specificComplexHide).join(',\n'); } - nodes = document.querySelectorAll(this.specificComplexHideAggregated); - for ( node of nodes ) { + const nodes = document.querySelectorAll(this.specificComplexHideAggregated); + for ( const node of nodes ) { this.hideNode(node); } //console.timeEnd('dom layout changed/specific complex selectors'); @@ -211,23 +210,23 @@ vAPI.DOMFilterer.prototype = { this.addedNodes.clear(); this.removedNodes = false; - }, + } - commit: function(now) { + commit(now) { if ( now ) { this.commitTimer.clear(); this.commitNow(); } else { this.commitTimer.start(); } - }, + } - addCSSRule: function(selectors, declarations, details) { + addCSSRule(selectors, declarations, details) { if ( selectors === undefined ) { return; } if ( details === undefined ) { details = {}; } - var selectorsStr = Array.isArray(selectors) ? + const selectorsStr = Array.isArray(selectors) ? selectors.join(',\n') : selectors; if ( selectorsStr.length === 0 ) { return; } @@ -251,10 +250,9 @@ vAPI.DOMFilterer.prototype = { // Do not strongly enforce internal CSS rules. if ( details.internal ) { return; } - var isGeneric= details.lazy === true, - isSimple = details.type === 'simple', - isComplex = details.type === 'complex', - selector; + const isGeneric= details.lazy === true; + const isSimple = details.type === 'simple'; + const isComplex = details.type === 'complex'; if ( isGeneric ) { if ( isSimple ) { @@ -267,12 +265,12 @@ vAPI.DOMFilterer.prototype = { } } - var selectorsArr = Array.isArray(selectors) ? + const selectorsArr = Array.isArray(selectors) ? selectors : selectors.split(',\n'); if ( isGeneric ) { - for ( selector of selectorsArr ) { + for ( const selector of selectorsArr ) { if ( this.reCSSCombinators.test(selector) ) { this.genericComplexHide.add(selector); } else { @@ -283,7 +281,7 @@ vAPI.DOMFilterer.prototype = { } // Specific cosmetic filters. - for ( selector of selectorsArr ) { + for ( const selector of selectorsArr ) { if ( isComplex || isSimple === false && this.reCSSCombinators.test(selector) @@ -297,44 +295,51 @@ vAPI.DOMFilterer.prototype = { this.addedSpecificSimpleHide.push(selector); } } - }, + } - onDOMCreated: function() { + exceptCSSRules(exceptions) { + if ( exceptions.length === 0 ) { return; } + this.exceptedCSSRules.push(...exceptions); + if ( this.hasListeners() ) { + this.triggerListeners({ exceptions }); + } + } + + onDOMCreated() { this.domIsReady = true; this.addedNodes.clear(); this.removedNodes = false; this.commit(); - }, + } - onDOMChanged: function(addedNodes, removedNodes) { - for ( var node of addedNodes ) { + onDOMChanged(addedNodes, removedNodes) { + for ( const node of addedNodes ) { this.addedNodes.add(node); } this.removedNodes = this.removedNodes || removedNodes; this.commit(); - }, + } - addListener: function(listener) { + addListener(listener) { if ( this.listeners.indexOf(listener) !== -1 ) { return; } this.listeners.push(listener); - }, + } - removeListener: function(listener) { - var pos = this.listeners.indexOf(listener); + removeListener(listener) { + const pos = this.listeners.indexOf(listener); if ( pos === -1 ) { return; } this.listeners.splice(pos, 1); - }, + } - hasListeners: function() { + hasListeners() { return this.listeners.length !== 0; - }, + } - triggerListeners: function(changes) { - var i = this.listeners.length; - while ( i-- ) { - this.listeners[i].onFiltersetChanged(changes); + triggerListeners(changes) { + for ( const listener of this.listeners ) { + listener.onFiltersetChanged(changes); } - }, + } // https://jsperf.com/clientheight-and-clientwidth-vs-getcomputedstyle // Avoid getComputedStyle(), detecting whether a node is visible can be @@ -351,10 +356,10 @@ vAPI.DOMFilterer.prototype = { // 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() { + hideNodeBatchProcess() { this.hideNodeBatchProcessTimer.clear(); - var expando = this.hideNodeExpando; - for ( var node of this.hiddenNodesetToProcess ) { + const expando = this.hideNodeExpando; + for ( const node of this.hiddenNodesetToProcess ) { if ( this.hiddenNodeset.has(node) === false || node[expando] === undefined || @@ -362,7 +367,7 @@ vAPI.DOMFilterer.prototype = { ) { continue; } - var attr = node.getAttribute('style'); + let attr = node.getAttribute('style'); if ( attr === null ) { attr = ''; } else if ( @@ -374,24 +379,18 @@ vAPI.DOMFilterer.prototype = { node.setAttribute('style', attr + 'display:none!important;'); } this.hiddenNodesetToProcess.clear(); - }, + } - hideNodeObserverHandler: function(mutations) { + hideNodeObserverHandler(mutations) { if ( vAPI.userStylesheet.disabled ) { return; } - var i = mutations.length, - stagedNodes = this.hiddenNodesetToProcess; - while ( i-- ) { - stagedNodes.add(mutations[i].target); + const stagedNodes = this.hiddenNodesetToProcess; + for ( const mutation of mutations ) { + stagedNodes.add(mutation.target); } this.hideNodeBatchProcessTimer.start(); - }, + } - hiddenNodeObserverOptions: { - attributes: true, - attributeFilter: [ 'style' ] - }, - - hideNodeInit: function() { + hideNodeInit() { this.hideNodeExpando = vAPI.randomToken(); this.hideNodeBatchProcessTimer = new vAPI.SafeAnimationFrame(this.hideNodeBatchProcess.bind(this)); @@ -400,21 +399,21 @@ vAPI.DOMFilterer.prototype = { if ( this.hideNodeStyleSheetInjected === false ) { this.hideNodeStyleSheetInjected = true; vAPI.userStylesheet.add( - '[' + this.hideNodeAttr + ']\n{display:none!important;}' + `[${this.hideNodeAttr}]\n{display:none!important;}` ); } - }, + } - excludeNode: function(node) { + excludeNode(node) { this.excludedNodeSet.add(node); this.unhideNode(node); - }, + } - unexcludeNode: function(node) { + unexcludeNode(node) { this.excludedNodeSet.delete(node); - }, + } - hideNode: function(node) { + hideNode(node) { if ( this.excludedNodeSet.has(node) ) { return; } if ( this.hideNodeAttr === undefined ) { return; } if ( this.hiddenNodeset.has(node) ) { return; } @@ -430,15 +429,15 @@ vAPI.DOMFilterer.prototype = { this.hiddenNodesetToProcess.add(node); this.hideNodeBatchProcessTimer.start(); this.hiddenNodeObserver.observe(node, this.hiddenNodeObserverOptions); - }, + } - unhideNode: function(node) { + unhideNode(node) { if ( this.hiddenNodeset.has(node) === false ) { return; } node.hidden = false; node.removeAttribute(this.hideNodeAttr); this.hiddenNodesetToProcess.delete(node); if ( this.hideNodeExpando === undefined ) { return; } - var attr = node[this.hideNodeExpando]; + const attr = node[this.hideNodeExpando]; if ( attr === false ) { node.removeAttribute('style'); } else if ( typeof attr === 'string' ) { @@ -446,28 +445,28 @@ vAPI.DOMFilterer.prototype = { } node[this.hideNodeExpando] = undefined; this.hiddenNodeset.delete(node); - }, + } - showNode: function(node) { + showNode(node) { node.hidden = false; - var attr = node[this.hideNodeExpando]; + const attr = node[this.hideNodeExpando]; if ( attr === false ) { node.removeAttribute('style'); } else if ( typeof attr === 'string' ) { node.setAttribute('style', attr); } - }, + } - unshowNode: function(node) { + unshowNode(node) { node.hidden = true; this.hiddenNodesetToProcess.add(node); - }, + } - toggle: function(state, callback) { + toggle(state, callback) { vAPI.userStylesheet.toggle(state); - var disabled = vAPI.userStylesheet.disabled, - nodes = document.querySelectorAll('[' + this.hideNodeAttr + ']'); - for ( var node of nodes ) { + const disabled = vAPI.userStylesheet.disabled; + const nodes = document.querySelectorAll(`[${this.hideNodeAttr}]`); + for ( const node of nodes ) { if ( disabled ) { this.showNode(node); } else { @@ -480,11 +479,12 @@ vAPI.DOMFilterer.prototype = { if ( typeof callback === 'function' ) { callback(); } - }, + } - getAllSelectors_: function(all) { - var out = { - declarative: [] + getAllSelectors_(all) { + const out = { + declarative: [], + exceptions: this.exceptedCSSRules, }; if ( this.specificSimpleHide.size !== 0 ) { out.declarative.push([ @@ -516,28 +516,30 @@ vAPI.DOMFilterer.prototype = { 'display:none!important;' ]); } - for ( var entry of this.specificOthers ) { + for ( const entry of this.specificOthers ) { out.declarative.push([ entry.selectors, entry.declarations ]); } return out; - }, + } - getFilteredElementCount: function() { - var details = this.getAllSelectors_(true); + getFilteredElementCount() { + const details = this.getAllSelectors_(true); if ( Array.isArray(details.declarative) === false ) { return 0; } - var selectors = details.declarative.reduce(function(acc, entry) { - acc.push(entry[0]); - return acc; - }, []); + const selectors = details.declarative.map(entry => entry[0]); if ( selectors.length === 0 ) { return 0; } return document.querySelectorAll(selectors.join(',\n')).length; - }, + } - getAllSelectors: function() { + getAllSelectors() { return this.getAllSelectors_(false); } }; +vAPI.DOMFilterer.prototype.hiddenNodeObserverOptions = { + attributes: true, + attributeFilter: [ 'style' ] +}; + /******************************************************************************/ /******************************************************************************/ diff --git a/platform/chromium/vapi-usercss.real.js b/platform/chromium/vapi-usercss.real.js index 89d12eb35..49a6061d7 100644 --- a/platform/chromium/vapi-usercss.real.js +++ b/platform/chromium/vapi-usercss.real.js @@ -57,31 +57,31 @@ vAPI.userStylesheet = { /******************************************************************************/ -vAPI.DOMFilterer = function() { - this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); - this.domIsReady = document.readyState !== 'loading'; - this.disabled = false; - this.listeners = []; - this.filterset = new Set(); - this.excludedNodeSet = new WeakSet(); - this.addedCSSRules = new Set(); +vAPI.DOMFilterer = class { + constructor() { + this.commitTimer = new vAPI.SafeAnimationFrame(( ) => this.commitNow); + this.domIsReady = document.readyState !== 'loading'; + this.disabled = false; + this.listeners = []; + this.filterset = new Set(); + this.excludedNodeSet = new WeakSet(); + this.addedCSSRules = new Set(); + this.exceptedCSSRules = []; + this.reOnlySelectors = /\n\{[^\n]+/g; - // https://github.com/uBlockOrigin/uBlock-issues/issues/167 - // By the time the DOMContentLoaded is fired, the content script might - // have been disconnected from the background page. Unclear why this - // would happen, so far seems to be a Chromium-specific behavior at - // launch time. - if ( this.domIsReady !== true ) { - document.addEventListener('DOMContentLoaded', ( ) => { - if ( vAPI instanceof Object === false ) { return; } - this.domIsReady = true; - this.commit(); - }); + // https://github.com/uBlockOrigin/uBlock-issues/issues/167 + // By the time the DOMContentLoaded is fired, the content script might + // have been disconnected from the background page. Unclear why this + // would happen, so far seems to be a Chromium-specific behavior at + // launch time. + if ( this.domIsReady !== true ) { + document.addEventListener('DOMContentLoaded', ( ) => { + if ( vAPI instanceof Object === false ) { return; } + this.domIsReady = true; + this.commit(); + }); + } } -}; - -vAPI.DOMFilterer.prototype = { - reOnlySelectors: /\n\{[^\n]+/g, // Here we will deal with: // - Injecting low priority user styles; @@ -92,11 +92,11 @@ vAPI.DOMFilterer.prototype = { // process. Another approach would be to have vAPI.SafeAnimationFrame // register a shutdown job: to evaluate. For now I will keep the fix // trivial. - commitNow: function() { + commitNow() { this.commitTimer.clear(); if ( vAPI instanceof Object === false ) { return; } - let userStylesheet = vAPI.userStylesheet; - for ( let entry of this.addedCSSRules ) { + const userStylesheet = vAPI.userStylesheet; + for ( const entry of this.addedCSSRules ) { if ( this.disabled === false && entry.lazy && @@ -109,25 +109,25 @@ vAPI.DOMFilterer.prototype = { } this.addedCSSRules.clear(); userStylesheet.apply(); - }, + } - commit: function(commitNow) { + commit(commitNow) { if ( commitNow ) { this.commitTimer.clear(); this.commitNow(); } else { this.commitTimer.start(); } - }, + } - addCSSRule: function(selectors, declarations, details) { + addCSSRule(selectors, declarations, details) { if ( selectors === undefined ) { return; } - var selectorsStr = Array.isArray(selectors) + const selectorsStr = Array.isArray(selectors) ? selectors.join(',\n') : selectors; if ( selectorsStr.length === 0 ) { return; } if ( details === undefined ) { details = {}; } - var entry = { + const entry = { selectors: selectorsStr, declarations, lazy: details.lazy === true, @@ -148,64 +148,71 @@ vAPI.DOMFilterer.prototype = { declarative: [ [ selectorsStr, declarations ] ] }); } - }, + } - addListener: function(listener) { + exceptCSSRules(exceptions) { + if ( exceptions.length === 0 ) { return; } + this.exceptedCSSRules.push(...exceptions); + if ( this.hasListeners() ) { + this.triggerListeners({ exceptions }); + } + } + + addListener(listener) { if ( this.listeners.indexOf(listener) !== -1 ) { return; } this.listeners.push(listener); - }, + } - removeListener: function(listener) { - var pos = this.listeners.indexOf(listener); + removeListener(listener) { + const pos = this.listeners.indexOf(listener); if ( pos === -1 ) { return; } this.listeners.splice(pos, 1); - }, + } - hasListeners: function() { + hasListeners() { return this.listeners.length !== 0; - }, + } - triggerListeners: function(changes) { - var i = this.listeners.length; - while ( i-- ) { - this.listeners[i].onFiltersetChanged(changes); + triggerListeners(changes) { + for ( const listener of this.listeners ) { + listener.onFiltersetChanged(changes); } - }, + } - excludeNode: function(node) { + excludeNode(node) { this.excludedNodeSet.add(node); this.unhideNode(node); - }, + } - unexcludeNode: function(node) { + unexcludeNode(node) { this.excludedNodeSet.delete(node); - }, + } - hideNode: function(node) { + hideNode(node) { if ( this.excludedNodeSet.has(node) ) { return; } if ( this.hideNodeAttr === undefined ) { return; } node.setAttribute(this.hideNodeAttr, ''); if ( this.hideNodeStyleSheetInjected === false ) { this.hideNodeStyleSheetInjected = true; this.addCSSRule( - '[' + this.hideNodeAttr + ']', + `[${this.hideNodeAttr}]`, 'display:none!important;' ); } - }, + } - unhideNode: function(node) { + unhideNode(node) { if ( this.hideNodeAttr === undefined ) { return; } node.removeAttribute(this.hideNodeAttr); - }, + } - toggle: function(state, callback) { + toggle(state, callback) { if ( state === undefined ) { state = this.disabled; } if ( state !== this.disabled ) { return; } this.disabled = !state; - var userStylesheet = vAPI.userStylesheet; - for ( var entry of this.filterset ) { - var rule = entry.selectors + '\n{' + entry.declarations + '}'; + const userStylesheet = vAPI.userStylesheet; + for ( const entry of this.filterset ) { + const rule = `${entry.selectors}\n{${entry.declarations}}`; if ( this.disabled ) { userStylesheet.remove(rule); } else { @@ -213,38 +220,35 @@ vAPI.DOMFilterer.prototype = { } } userStylesheet.apply(callback); - }, + } - getAllSelectors_: function(all) { - var out = { - declarative: [] + getAllSelectors_(all) { + const out = { + declarative: [], + exceptions: this.exceptedCSSRules, }; - var selectors; - for ( var entry of this.filterset ) { - selectors = entry.selectors; + for ( const entry of this.filterset ) { + let selectors = entry.selectors; if ( all !== true && this.hideNodeAttr !== undefined ) { selectors = selectors - .replace('[' + this.hideNodeAttr + ']', '') + .replace(`[${this.hideNodeAttr}]`, '') .replace(/^,\n|,\n$/gm, ''); if ( selectors === '' ) { continue; } } out.declarative.push([ selectors, entry.declarations ]); } return out; - }, + } - getFilteredElementCount: function() { - let details = this.getAllSelectors_(true); + getFilteredElementCount() { + const details = this.getAllSelectors_(true); if ( Array.isArray(details.declarative) === false ) { return 0; } - let selectors = details.declarative.reduce(function(acc, entry) { - acc.push(entry[0]); - return acc; - }, []); + const selectors = details.declarative.map(entry => entry[0]); if ( selectors.length === 0 ) { return 0; } return document.querySelectorAll(selectors.join(',\n')).length; - }, + } - getAllSelectors: function() { + getAllSelectors() { return this.getAllSelectors_(false); } }; diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index aef76fa1d..f253b4aaa 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -265,11 +265,11 @@ body.colorBlind #netFilteringDialog > .panes > .details > div[data-status="2"] { #vwRenderer .logEntry > div[data-tabid="-1"] { text-shadow: 0 0.2em 0.4em #aaa; } -#vwRenderer .logEntry > div.cosmetic, +#vwRenderer .logEntry > div.cosmeticRealm, #vwRenderer .logEntry > div.redirect { background-color: rgba(255, 255, 0, 0.1); } -body.colorBlind #vwRenderer .logEntry > div.cosmetic, +body.colorBlind #vwRenderer .logEntry > div.cosmeticRealm, body.colorBlind #vwRenderer .logEntry > div.redirect { background-color: rgba(0, 19, 110, 0.1); } @@ -326,6 +326,9 @@ body[dir="rtl"] #vwRenderer .logEntry > div > span:first-child { #vwRenderer .logEntry > div.messageRealm[data-type="tabLoad"] > span:nth-of-type(2) { text-align: center; } +#vwRenderer .logEntry > div.cosmeticRealm.isException > span:nth-of-type(2) { + text-decoration: line-through; + } #vwRenderer .logEntry > div > span:nth-of-type(3) { font: 12px monospace; padding-left: 0.3em; diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 6aa7ad90a..115362f55 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -825,66 +825,60 @@ vAPI.DOMFilterer = (function() { } }; - const DOMFiltererBase = vAPI.DOMFilterer; + const DOMFilterer = class extends vAPI.DOMFilterer { + constructor() { + super(); + this.exceptions = []; + this.proceduralFilterer = new DOMProceduralFilterer(this); + this.hideNodeAttr = undefined; + this.hideNodeStyleSheetInjected = false; + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(this); + } + } - const domFilterer = function() { - DOMFiltererBase.call(this); - this.exceptions = []; - this.proceduralFilterer = new DOMProceduralFilterer(this); - this.hideNodeAttr = undefined; - this.hideNodeStyleSheetInjected = false; + commitNow() { + super.commitNow(); + this.proceduralFilterer.commitNow(); + } - // May or may not exist: cache locally since this may be called often. - this.baseOnDOMChanged = DOMFiltererBase.prototype.onDOMChanged; + addProceduralSelectors(aa) { + this.proceduralFilterer.addProceduralSelectors(aa); + } - if ( vAPI.domWatcher instanceof Object ) { - vAPI.domWatcher.addListener(this); + createProceduralFilter(o) { + return this.proceduralFilterer.createProceduralFilter(o); + } + + getAllSelectors() { + const out = super.getAllSelectors(); + out.procedural = Array.from(this.proceduralFilterer.selectors.values()); + return out; + } + + getAllExceptionSelectors() { + return this.exceptions.join(',\n'); + } + + onDOMCreated() { + if ( super.onDOMCreated instanceof Function ) { + super.onDOMCreated(); + } + this.proceduralFilterer.onDOMCreated(); + } + + onDOMChanged() { + if ( super.onDOMChanged instanceof Function ) { + super.onDOMChanged(arguments); + } + this.proceduralFilterer.onDOMChanged.apply( + this.proceduralFilterer, + arguments + ); } }; - 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.getAllSelectors = function() { - const out = DOMFiltererBase.prototype.getAllSelectors.call(this); - out.procedural = Array.from(this.proceduralFilterer.selectors.values()); - return out; - }; - - 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; + return DOMFilterer; })(); vAPI.domFilterer = new vAPI.DOMFilterer(); @@ -1340,6 +1334,10 @@ vAPI.domSurveyor = (function() { ); mustCommit = true; } + selectors = result.excepted; + if ( Array.isArray(selectors) && selectors.length !== 0 ) { + domFilterer.exceptCSSRules(selectors); + } } if ( pendingNodes.stopped === false ) { @@ -1524,6 +1522,7 @@ vAPI.bootstrap = (function() { { injected: true } ); domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters); + domFilterer.exceptCSSRules(cfeDetails.exceptedFilters); } if ( cfeDetails.networkFilters.length !== 0 ) { diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 3cc890fbc..c5731a772 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -843,22 +843,22 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { //console.time('cosmeticFilteringEngine.retrieveGenericSelectors'); - let simpleSelectors = this.setRegister0, - complexSelectors = this.setRegister1; + const simpleSelectors = this.setRegister0; + const complexSelectors = this.setRegister1; - let cacheEntry = this.selectorCache.get(request.hostname), - previousHits = cacheEntry && cacheEntry.cosmetic || this.setRegister2; + const cacheEntry = this.selectorCache.get(request.hostname); + const previousHits = cacheEntry && cacheEntry.cosmetic || this.setRegister2; - for ( let type in this.lowlyGeneric ) { - let entry = this.lowlyGeneric[type]; - let selectors = request[entry.canonical]; + for ( const type in this.lowlyGeneric ) { + const entry = this.lowlyGeneric[type]; + const selectors = request[entry.canonical]; if ( Array.isArray(selectors) === false ) { continue; } for ( let selector of selectors ) { if ( entry.simple.has(selector) === false ) { continue; } - let bucket = entry.complex.get(selector); + const bucket = entry.complex.get(selector); if ( bucket !== undefined ) { if ( Array.isArray(bucket) ) { - for ( selector of bucket ) { + for ( const selector of bucket ) { if ( previousHits.has(selector) === false ) { complexSelectors.add(selector); } @@ -877,26 +877,39 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { // Apply exceptions: it is the responsibility of the caller to provide // the exceptions to be applied. + const excepted = []; if ( Array.isArray(request.exceptions) ) { - for ( let exception of request.exceptions ) { - simpleSelectors.delete(exception); - complexSelectors.delete(exception); + for ( const exception of request.exceptions ) { + if ( + simpleSelectors.delete(exception) || + complexSelectors.delete(exception) + ) { + excepted.push(exception); + } } } - if ( simpleSelectors.size === 0 && complexSelectors.size === 0 ) { + if ( + simpleSelectors.size === 0 && + complexSelectors.size === 0 && + excepted.length === 0 + ) { return; } - let out = { + const out = { simple: Array.from(simpleSelectors), complex: Array.from(complexSelectors), - injected: '' + injected: '', + excepted, }; // Cache and inject (if user stylesheets supported) looked-up low generic // cosmetic filters. - if ( typeof request.hostname === 'string' && request.hostname !== '' ) { + if ( + (typeof request.hostname === 'string' && request.hostname !== '') && + (out.simple.length !== 0 || out.complex.length !== 0) + ) { this.addToSelectorCache({ cost: request.surveyCost || 0, hostname: request.hostname, @@ -913,7 +926,7 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { request.tabId !== undefined && request.frameId !== undefined ) { - let injected = []; + const injected = []; if ( out.simple.length !== 0 ) { injected.push(out.simple.join(',\n')); out.simple = []; @@ -964,6 +977,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( domain: request.domain, declarativeFilters: [], exceptionFilters: [], + exceptedFilters: [], hideNodeAttr: this.randomAlphaToken(), hideNodeStyleSheetInjected: false, highGenericHideSimple: '', @@ -1008,8 +1022,13 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( if ( exceptionSet.size !== 0 ) { out.exceptionFilters = Array.from(exceptionSet); for ( const exception of exceptionSet ) { - specificSet.delete(exception); - proceduralSet.delete(exception); + if ( + specificSet.delete(exception) || + proceduralSet.delete(exception) + ) { + out.exceptedFilters.push(exception); + } + } } @@ -1034,7 +1053,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( const entry = this.highlyGeneric[type]; let str = entry.mru.lookup(exceptionHash); if ( str === undefined ) { - str = { s: entry.str }; + str = { s: entry.str, excepted: [] }; let genericSet = entry.dict; let hit = false; for ( const exception of exceptionSet ) { @@ -1042,14 +1061,20 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( } if ( hit ) { genericSet = new Set(entry.dict); - for ( let exception of exceptionSet ) { - genericSet.delete(exception); + for ( const exception of exceptionSet ) { + if ( genericSet.delete(exception) ) { + str.excepted.push(exception); + } } str.s = Array.from(genericSet).join(',\n'); } entry.mru.add(exceptionHash, str); } out[entry.canonical] = str.s; + if ( str.excepted.length !== 0 ) { + out.exceptedFilters.push(...str.excepted); + } + } } diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index b2884779a..8e705717b 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -622,13 +622,13 @@ const viewPort = (function() { if ( filter !== undefined ) { if ( typeof filter.source === 'string' ) { filteringType = filter.source; - divcl.add(filteringType); } if ( filteringType === 'static' ) { divcl.add('canLookup'); div.setAttribute('data-filter', filter.compiled); } else if ( filteringType === 'cosmetic' ) { divcl.add('canLookup'); + divcl.toggle('isException', filter.raw.startsWith('#@#')); } } span = div.children[1]; @@ -1503,7 +1503,7 @@ const reloadTab = function(ev) { text = trch[1].textContent; if ( (text !== '') && - (trcl.contains('cosmetic') || trcl.contains('static')) + (trcl.contains('cosmeticRealm') || trcl.contains('networkRealm')) ) { rows[0].children[1].textContent = text; } else { diff --git a/src/js/messaging.js b/src/js/messaging.js index ab3945c6a..643906f05 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1352,7 +1352,7 @@ const logCosmeticFilters = function(tabId, details) { .setDocOriginFromURL(details.frameURL) .setFilter(filter); for ( const selector of details.matchedSelectors.sort() ) { - filter.raw = '##' + selector; + filter.raw = selector; fctxt.toLogger(); } }; diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index fb3dbda52..1689f838e 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -186,6 +186,7 @@ const fromCosmeticFilter = function(details) { // Lowly generic cosmetic filters case 0: // simple id-based if ( + exception === false && fargs[1] === selector.slice(1) && selector.charAt(0) === '#' ) { @@ -194,6 +195,7 @@ const fromCosmeticFilter = function(details) { break; case 2: // simple class-based if ( + exception === false && fargs[1] === selector.slice(1) && selector.charAt(0) === '.' ) { @@ -202,7 +204,7 @@ const fromCosmeticFilter = function(details) { break; case 1: // complex id-based case 3: // complex class-based - if ( fargs[2] === selector ) { + if ( exception === false && fargs[2] === selector ) { found = prefix + selector; } break; diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 46186ac0a..077772941 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -35,30 +35,34 @@ if ( return; } -let reHasCSSCombinators = /[ >+~]/, - reHasPseudoClass = /:+(?:after|before)$/, - sanitizedSelectors = new Map(), - simpleDeclarativeSet = new Set(), - simpleDeclarativeStr, - complexDeclarativeSet = new Set(), - complexDeclarativeStr, - proceduralDict = new Map(), - nodesToProcess = new Set(), - shouldProcessDeclarativeComplex = false, - shouldProcessProcedural = false, - loggedSelectors = new Set(); +const reHasCSSCombinators = /[ >+~]/; +const reHasPseudoClass = /:+(?:after|before)$/; +const sanitizedSelectors = new Map(); +const simpleDeclarativeSet = new Set(); +let simpleDeclarativeStr; +const complexDeclarativeSet = new Set(); +let complexDeclarativeStr; +const proceduralDict = new Map(); +const exceptionSet = new Set(); +let exceptionStr; +const nodesToProcess = new Set(); +let shouldProcessDeclarativeComplex = false; +let shouldProcessProcedural = false; +let shouldProcessExceptions = false; +const loggedSelectors = new Set(); /******************************************************************************/ -let shouldProcess = function() { +const shouldProcess = function() { return nodesToProcess.size !== 0 || shouldProcessDeclarativeComplex || - shouldProcessProcedural; + shouldProcessProcedural || + shouldProcessExceptions; }; /******************************************************************************/ -let processDeclarativeSimple = function(node, out) { +const processDeclarativeSimple = function(node, out) { if ( simpleDeclarativeSet.size === 0 ) { return; } if ( simpleDeclarativeStr === undefined ) { simpleDeclarativeStr = Array.from(simpleDeclarativeSet).join(',\n'); @@ -69,14 +73,14 @@ let processDeclarativeSimple = function(node, out) { ) { return; } - for ( let selector of simpleDeclarativeSet ) { + for ( const selector of simpleDeclarativeSet ) { if ( (node === document || node.matches(selector) === false) && (node.querySelector(selector) === null) ) { continue; } - out.push(sanitizedSelectors.get(selector) || selector); + out.push(`##${sanitizedSelectors.get(selector) || selector}`); simpleDeclarativeSet.delete(selector); simpleDeclarativeStr = undefined; loggedSelectors.add(selector); @@ -86,15 +90,15 @@ let processDeclarativeSimple = function(node, out) { /******************************************************************************/ -let processDeclarativeComplex = function(out) { +const processDeclarativeComplex = function(out) { if ( complexDeclarativeSet.size === 0 ) { return; } if ( complexDeclarativeStr === undefined ) { complexDeclarativeStr = Array.from(complexDeclarativeSet).join(',\n'); } if ( document.querySelector(complexDeclarativeStr) === null ) { return; } - for ( let selector of complexDeclarativeSet ) { + for ( const selector of complexDeclarativeSet ) { if ( document.querySelector(selector) === null ) { continue; } - out.push(sanitizedSelectors.get(selector) || selector); + out.push(`##${sanitizedSelectors.get(selector) || selector}`); complexDeclarativeSet.delete(selector); complexDeclarativeStr = undefined; loggedSelectors.add(selector); @@ -104,11 +108,11 @@ let processDeclarativeComplex = function(out) { /******************************************************************************/ -let processProcedural = function(out) { +const processProcedural = function(out) { if ( proceduralDict.size === 0 ) { return; } - for ( let entry of proceduralDict ) { + for ( const entry of proceduralDict ) { if ( entry[1].test() === false ) { continue; } - out.push(entry[1].raw); + out.push(`##${entry[1].raw}`); proceduralDict.delete(entry[0]); if ( proceduralDict.size === 0 ) { break; } } @@ -116,16 +120,33 @@ let processProcedural = function(out) { /******************************************************************************/ -let processTimer = new vAPI.SafeAnimationFrame(() => { +const processExceptions = function(out) { + if ( exceptionSet.size === 0 ) { return; } + if ( exceptionStr === undefined ) { + exceptionStr = Array.from(exceptionSet).join(',\n'); + } + if ( document.querySelector(exceptionStr) === null ) { return; } + for ( const selector of exceptionSet ) { + if ( document.querySelector(selector) === null ) { continue; } + out.push(`#@#${selector}`); + exceptionSet.delete(selector); + exceptionStr = undefined; + loggedSelectors.add(selector); + } +}; + +/******************************************************************************/ + +const processTimer = new vAPI.SafeAnimationFrame(() => { //console.time('dom logger/scanning for matches'); processTimer.clear(); - let toLog = []; + const toLog = []; if ( nodesToProcess.size !== 0 && simpleDeclarativeSet.size !== 0 ) { if ( nodesToProcess.size !== 1 && nodesToProcess.has(document) ) { nodesToProcess.clear(); nodesToProcess.add(document); } - for ( let node of nodesToProcess ) { + for ( const node of nodesToProcess ) { processDeclarativeSimple(node, toLog); } nodesToProcess.clear(); @@ -138,6 +159,10 @@ let processTimer = new vAPI.SafeAnimationFrame(() => { processProcedural(toLog); shouldProcessProcedural = false; } + if ( shouldProcessExceptions ) { + processExceptions(toLog); + shouldProcessExceptions = false; + } if ( toLog.length === 0 ) { return; } vAPI.messaging.send( 'scriptlets', @@ -145,7 +170,7 @@ let processTimer = new vAPI.SafeAnimationFrame(() => { what: 'logCosmeticFilteringData', frameURL: window.location.href, frameHostname: window.location.hostname, - matchedSelectors: toLog + matchedSelectors: toLog, } ); //console.timeEnd('dom logger/scanning for matches'); @@ -153,10 +178,10 @@ let processTimer = new vAPI.SafeAnimationFrame(() => { /******************************************************************************/ -let attributeObserver = new MutationObserver(mutations => { +const attributeObserver = new MutationObserver(mutations => { if ( simpleDeclarativeSet.size !== 0 ) { - for ( let mutation of mutations ) { - let node = mutation.target; + for ( const mutation of mutations ) { + const node = mutation.target; if ( node.nodeType !== 1 ) { continue; } nodesToProcess.add(node); } @@ -174,20 +199,20 @@ let attributeObserver = new MutationObserver(mutations => { /******************************************************************************/ -let handlers = { +const handlers = { onFiltersetChanged: function(changes) { //console.time('dom logger/filterset changed'); - let simpleSizeBefore = simpleDeclarativeSet.size, + const simpleSizeBefore = simpleDeclarativeSet.size, complexSizeBefore = complexDeclarativeSet.size, logNow = []; - for ( let entry of (changes.declarative || []) ) { + for ( const entry of (changes.declarative || []) ) { for ( let selector of entry[0].split(',\n') ) { if ( entry[1] !== 'display:none!important;' ) { - logNow.push(selector + ':style(' + entry[1] + ')'); + logNow.push(`##${selector}:style(${entry[1]})`); continue; } if ( reHasPseudoClass.test(selector) ) { - let sanitized = selector.replace(reHasPseudoClass, ''); + const sanitized = selector.replace(reHasPseudoClass, ''); sanitizedSelectors.set(sanitized, selector); selector = sanitized; } @@ -222,11 +247,19 @@ let handlers = { Array.isArray(changes.procedural) && changes.procedural.length !== 0 ) { - for ( let selector of changes.procedural ) { + for ( const selector of changes.procedural ) { proceduralDict.set(selector.raw, selector); } shouldProcessProcedural = true; } + if ( Array.isArray(changes.exceptions) ) { + for ( const selector of changes.exceptions ) { + if ( loggedSelectors.has(selector) ) { continue; } + exceptionSet.add(selector); + } + exceptionStr = undefined; + shouldProcessExceptions = true; + } if ( shouldProcess() ) { processTimer.start(1); } @@ -246,7 +279,7 @@ let handlers = { // This is to guard against runaway job queue. I suspect this could // occur on slower devices. if ( simpleDeclarativeSet.size !== 0 ) { - for ( let node of addedNodes ) { + for ( const node of addedNodes ) { if ( node.parentNode === null ) { continue; } nodesToProcess.add(node); } @@ -257,6 +290,9 @@ let handlers = { if ( proceduralDict.size !== 0 ) { shouldProcessProcedural = true; } + if ( exceptionSet.size !== 0 ) { + shouldProcessExceptions = true; + } if ( shouldProcess() ) { processTimer.start(100); } @@ -265,7 +301,7 @@ let handlers = { /******************************************************************************/ -let onMessage = function(msg) { +const onMessage = function(msg) { if ( msg.what === 'loggerDisabled' ) { processTimer.clear(); attributeObserver.disconnect();