diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 9a4a34d38..e6b9c5249 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -111,8 +111,12 @@ vAPI.executionCost.start(); /******************************************************************************/ -vAPI.sessionId = String.fromCharCode(Date.now() % 26 + 97) + - Math.random().toString(36).slice(2); +vAPI.randomToken = function() { + return String.fromCharCode(Date.now() % 26 + 97) + + Math.floor(Math.random() * 982451653 + 982451653).toString(36); +}; + +vAPI.sessionId = vAPI.randomToken(); vAPI.chrome = true; vAPI.setTimeout = vAPI.setTimeout || self.setTimeout.bind(self); diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index 160bf07a7..88843ea5a 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -102,8 +102,13 @@ vAPI.executionCost.start(); /******************************************************************************/ vAPI.firefox = true; -vAPI.sessionId = String.fromCharCode(Date.now() % 26 + 97) + - Math.random().toString(36).slice(2); + +vAPI.randomToken = function() { + return String.fromCharCode(Date.now() % 26 + 97) + + Math.floor(Math.random() * 982451653 + 982451653).toString(36); +}; + +vAPI.sessionId = vAPI.randomToken(); /******************************************************************************/ diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 924b27b29..b960dbb83 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -33,7 +33,7 @@ // - https://github.com/chrisaljoudi/uBlock/issues/456 if ( typeof vAPI !== 'object' || vAPI.contentscriptInjected ) { - throw new Error('Unexpected condition: aborting.'); + throw new Error('uBlock Origin: aborting content scripts for ' + window.location); } vAPI.executionCost.start(); @@ -58,91 +58,174 @@ vAPI.matchesProp = (function() { // The DOM filterer is the heart of uBO's cosmetic filtering. -vAPI.domFilterer = { - allExceptions: Object.create(null), - allSelectors: Object.create(null), - cosmeticFiltersActivatedTimer: null, - cssNotHiddenId: '', - disabledId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), - enabled: true, - hiddenId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), - hiddenNodeCount: 0, - matchesProp: vAPI.matchesProp, - newCSSRules: [], - newDeclarativeSelectors: [], - shadowId: String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 982451653 + 982451653).toString(36), - styleTags: [], +vAPI.domFilterer = (function() { - simpleGroupSelector: null, - simpleSelectors: [], +/******************************************************************************/ - complexGroupSelector: null, - complexSelectors: [], - complexSelectorsCost: 0, - complexSelectorsNodeSet: null, - - complexHasSelectors: [], - complexHasSelectorsCost: 0, - simpleHasSelectors: [], - - xpathExpression: null, - xpathResult: null, - xpathSelectors: [], - xpathSelectorsCost: 0, - - addExceptions: function(aa) { - for ( var i = 0, n = aa.length; i < n; i++ ) { - this.allExceptions[aa[i]] = true; +var SSet = (function() { + if ( typeof self.Set === 'function' ) { + return self.Set; + } + var Set = function() { + this._set = []; + this._i = 0; + this.value = undefined; + }; + Set.prototype.polyfill = true; + Set.prototype.clear = function() { + this._set = []; + }; + Set.prototype.add = function(k) { + if ( this._set.indexOf(k) === -1 ) { + this._set.push(k); } - }, - - addSelector: function(s) { - if ( this.allSelectors[s] || this.allExceptions[s] ) { - return this; - } - this.allSelectors[s] = true; - if ( s.charCodeAt(s.length-1) === 0x29 && this.addSelectorEx(s) ) { - return this; - } - if ( s.indexOf(' ') === -1 ) { - this.simpleSelectors.push(s); - this.simpleGroupSelector = null; - } else { - this.complexSelectors.push(s); - this.complexGroupSelector = null; - this.complexSelectorsCost = 0; - } - this.newDeclarativeSelectors.push(s); - return this; - }, - - addSelectorEx: function(s) { - var pos = s.indexOf(':has('); + }; + Set.prototype.delete = function(k) { + var pos = this._set.indexOf(k); if ( pos !== -1 ) { - var entry = { - a: s.slice(0, pos), - b: s.slice(pos + 5, -1) - }; - if ( entry.a.indexOf(' ') === -1 ) { - this.simpleHasSelectors.push(entry); - } else { - this.complexHasSelectors.push(entry); - this.complexHasSelectorsCost = 0; - } - return true; - } - pos = s.indexOf(':style('); - if ( pos !== -1 ) { - this.newCSSRules.push(s.slice(0, pos) + ' {' + s.slice(pos + 7, -1) + '}'); - return true; - } - if ( s.lastIndexOf(':xpath(', 0) === 0 ) { - this.xpathExpression = null; - this.xpathSelectorsCost = 0; - this.xpathSelectors.push(s.slice(7, -1)); + this._set.splice(pos, 1); return true; } return false; + }; + Set.prototype.has = function(k) { + return this._set.indexOf(k) !== -1; + }; + Set.prototype.values = function() { + this._i = 0; + return this; + }; + Set.prototype.next = function() { + this.value = this._set[this._i]; + this._i += 1; + return this; + }; + Object.defineProperty(self.Set.prototype, 'size', { + get: function() { return this._set.length; } + }); + return Set; +})(); + +/******************************************************************************/ + +var shadowId = document.documentElement.shadowRoot !== undefined ? + vAPI.randomToken(): + undefined; + +var jobQueue = [ + { t: 'css-hide', _0: [] }, // to inject in style tag + { t: 'css-style', _0: [] }, // to inject in style tag + { t: 'css-ssel', _0: [] }, // to manually hide (incremental) + { t: 'css-csel', _0: [] } // to manually hide (not incremental) +]; + +var reParserEx = /^(.*?(:has\(.+?\)|:xpath\(.+?\))?)(:style\(.+?\))?$/; + +var allExceptions = Object.create(null); +var allSelectors = Object.create(null); + +// Complex selectors, due to their nature may need to be "de-committed". A +// Set() is used to implement this functionality. For browser with no +// support of Set(), uBO will skip committing complex selectors. + +var complexSelectorsOldResultSet; +var complexSelectorsCurrentResultSet = new SSet(); + +/******************************************************************************/ + +var cosmeticFiltersActivatedTimer = null; + +var cosmeticFiltersActivated = function() { + cosmeticFiltersActivatedTimer = null; + vAPI.messaging.send( + 'contentscript', + { what: 'cosmeticFiltersActivated' } + ); +}; + +/******************************************************************************/ + +var domFilterer = { + disabledId: vAPI.randomToken(), + enabled: true, + hiddenId: vAPI.randomToken(), + hiddenNodeCount: 0, + matchesProp: vAPI.matchesProp, + styleTags: [], + + jobQueue: jobQueue, + // Stock jobs. + job0: jobQueue[0], + job1: jobQueue[1], + job2: jobQueue[2], + job3: jobQueue[3], + + addExceptions: function(aa) { + for ( var i = 0, n = aa.length; i < n; i++ ) { + allExceptions[aa[i]] = true; + } + }, + + // Job: + // Stock jobs in job queue: + // 0 = css rules/css declaration to remove visibility + // 1 = css rules/any css declaration + // 2 = simple css selectors/hide + // 3 = complex css selectors/hide + // Custom jobs: + // has/hide + // xpath/hide + // has/inline css declaration (not supported yet) + // xpath/inline css declaration (not supported yet) + + addSelector: function(s) { + if ( allSelectors[s] || allExceptions[s] ) { + return this; + } + allSelectors[s] = true; + var parts = reParserEx.exec(s); + if ( parts === null ) { return this; } + var sel0 = parts[1], sel1 = parts[2], style = parts[3]; + + // Hide + if ( style === undefined ) { + if ( sel1 === undefined ) { + this.job0._0.push(sel0); + if ( sel0.indexOf(' ') === -1 ) { + this.job2._0.push(sel0); + this.job2._1 = undefined; + } else { + this.job3._0.push(sel0); + this.job3._1 = undefined; + } + return this; + } + if ( sel1.lastIndexOf(':has', 0) === 0 ) { + this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0.slice(0, sel0.length - sel1.length), _1: sel1.slice(5, -1) }); + return this; + } + if ( sel1.lastIndexOf(':xpath',0) === 0 ) { + if ( sel0 !== sel1 ) { return this; } + this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) }); + return this; + } + // ignore unknown selector + return this; + } + + // Modify style + if ( sel1 === undefined ) { + this.job1._0.push(sel0); + this.job1._1 = undefined; + return this; + } + if ( sel1.lastIndexOf(':has', 0) === 0 ) { + return this; + } + if ( sel1.lastIndexOf(':xpath',0) === 0 ) { + if ( sel0 !== sel1 ) { return this; } + return this; + } }, addSelectors: function(aa) { @@ -194,231 +277,186 @@ vAPI.domFilterer = { stagedNodes = [ document.documentElement ]; } - // Inject new declarative selectors. - var styleTag; - if ( this.newDeclarativeSelectors.length ) { - styleTag = document.createElement('style'); + var styleText = '', i, n; + + // Stock job 0 = css rules/hide + if ( this.job0._0.length ) { + styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }'; + this.job0._0.length = 0; + } + + // Stock job 1 = css rules/any css declaration + if ( this.job1._0.length ) { + styleText += '\n:root ' + this.job1._0.join('\n:root '); + this.job1._0.length = 0; + } + + if ( styleText !== '' ) { + var styleTag = document.createElement('style'); styleTag.setAttribute('type', 'text/css'); - styleTag.textContent = - ':root ' + - this.newDeclarativeSelectors.join(',\n:root ') + - '\n{ display: none !important; }'; + styleTag.textContent = styleText; document.head.appendChild(styleTag); this.styleTags.push(styleTag); - this.newDeclarativeSelectors.length = 0; - } - // Inject new CSS rules. - if ( this.newCSSRules.length ) { - styleTag = document.createElement('style'); - styleTag.setAttribute('type', 'text/css'); - styleTag.textContent = ':root ' + this.newCSSRules.join('\n:root '); - document.head.appendChild(styleTag); - this.styleTags.push(styleTag); - this.newCSSRules.length = 0; } - // Simple `:has()` selectors. - if ( this.simpleHasSelectors.length ) { - this.commitSimpleHasSelectors(stagedNodes); + // Simple selectors: incremental. + + // Stock job 2 = simple css selectors/hide + if ( this.job2._0.length ) { + i = stagedNodes.length; + while ( i-- ) { + this.runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode); + } } - // Complex `:has()` selectors. - if ( this.complexHasSelectorsCost < 10 && this.complexHasSelectors.length ) { - this.commitComplexHasSelectors(); + // Complex selectors: non-incremental. + complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; + complexSelectorsCurrentResultSet = new SSet(); + + // Stock job 3 = complex css selectors/hide + // The handling of these can be considered optional, since they are + // also applied declaratively using a style tag. + if ( this.job3._0.length ) { + this.runComplexSelectorJob(this.job3, complexHideNode); } - // `:xpath()` selectors. - if ( this.xpathSelectorsCost < 10 && this.xpathSelectors.length ) { - this.commitXpathSelectors(); + // Custom jobs. No optional since they can't be applied in a + // declarative way. + for ( i = 4, n = this.jobQueue.length; i < n; i++ ) { + this.runJob(this.jobQueue[i], complexHideNode); } - // Committing declarative selectors is entirely optional, but it helps - // harden uBO against sites which try to bypass uBO's injected styles. - - // Simple selectors. - if ( this.simpleSelectors.length ) { - this.commitSimpleSelectors(stagedNodes); - } - - // Complex selectors. - if ( this.complexSelectorsCost < 10 && this.complexSelectors.length ) { - this.commitComplexSelectors(); + // Un-hide nodes previously hidden. + i = complexSelectorsOldResultSet.size; + if ( i !== 0 ) { + var iter = complexSelectorsOldResultSet.values(); + while ( i-- ) { + this.unhideNode(iter.next().value); + } + complexSelectorsOldResultSet.clear(); } // If DOM nodes have been affected, lazily notify core process. if ( this.hiddenNodeCount !== beforeHiddenNodeCount && - this.cosmeticFiltersActivatedTimer === null + cosmeticFiltersActivatedTimer === null ) { - this.cosmeticFiltersActivatedTimer = vAPI.setTimeout( - this.cosmeticFiltersActivated.bind(this), + cosmeticFiltersActivatedTimer = vAPI.setTimeout( + cosmeticFiltersActivated, 503 ); } }, - commitComplexHasSelectors: function() { - var tstart = window.performance.now(), - entry, nodes, j, node, - i = this.complexHasSelectors.length; - while ( i-- ) { - entry = this.complexHasSelectors[i]; - nodes = document.querySelectorAll(entry.a); - j = nodes.length; - while ( j-- ) { - node = nodes[j]; - if ( node.querySelector(entry.b) !== null ) { - this.hideNode(node); - } - } - } - this.complexHasSelectorsCost = window.performance.now() - tstart; - }, - - commitComplexSelectors: function() { - if ( this.complexSelectorsNodeSet === null ) { + hideNode: function(node) { + if ( node[this.hiddenId] ) { return; } - var tstart = window.performance.now(), - newNodeSet = new Set(); - if ( this.complexGroupSelector === null ) { - this.complexGroupSelector = this.complexSelectors.join(','); + node.setAttribute(this.hiddenId, ''); + node[this.hiddenId] = true; + this.hiddenNodeCount += 1; + node.hidden = true; + // TODO: conditionally make use of `display: none !important;` style. + // - is node.style.display !== ''? + // - if yes, is node.hasAttribute('style') true? + // - if yes: node.setAttribute('style', node.getAttribute('style') + + // ';display: none !important' + // - remember this was done + if ( shadowId === undefined ) { + return; } - var nodes = document.querySelectorAll(this.complexGroupSelector), + var shadow = node.shadowRoot; + if ( shadow ) { + if ( shadow[shadowId] && shadow.firstElementChild !== null ) { + shadow.removeChild(shadow.firstElementChild); + } + return; + } + // https://github.com/gorhill/uBlock/pull/555 + // Not all nodes can be shadowed: + // https://github.com/w3c/webcomponents/issues/102 + try { + shadow = node.createShadowRoot(); + shadow[shadowId] = true; + } catch (ex) { + } + }, + + runSimpleSelectorJob: function(job, root, fn) { + if ( job._1 === undefined ) { + job._1 = job._0.join(cssNotHiddenId + ','); + } + if ( root[this.matchesProp](job._1) ) { + fn(root); + } + var nodes = root.querySelectorAll(job._1), + i = nodes.length; + while ( i-- ) { + fn(nodes[i], job); + } + }, + + runComplexSelectorJob: function(job, fn) { + if ( job._1 === undefined ) { + job._1 = job._0.join(','); + } + var nodes = document.querySelectorAll(job._1), + i = nodes.length; + while ( i-- ) { + fn(nodes[i], job); + } + }, + + runHasJob: function(job, fn) { + var nodes = document.querySelectorAll(job._0), i = nodes.length, node; while ( i-- ) { node = nodes[i]; - newNodeSet.add(node); - if ( !this.complexSelectorsNodeSet.delete(node) ) { - this.hideNode(node); - } - } - var iter = this.complexSelectorsNodeSet.values(); - while ( (node = iter.next().value) ) { - this.unhideNode(node); - } - this.complexSelectorsNodeSet = newNodeSet; - this.complexSelectorsCost = window.performance.now() - tstart; - }, - - commitSimpleHasSelectors: function(stagedNodes) { - var i = this.simpleHasSelectors.length, - entry, j, parent, nodes, k, node; - while ( i-- ) { - entry = this.simpleHasSelectors[i]; - j = stagedNodes.length; - while ( j-- ) { - parent = stagedNodes[j]; - if ( parent[this.matchesProp](entry.a) && parent.querySelector(entry.b) !== null ) { - this.hideNode(parent); - } - nodes = parent.querySelectorAll(entry.a); - k = nodes.length; - while ( k-- ) { - node = nodes[k]; - if ( node.querySelector(entry.b) !== null ) { - this.hideNode(node); - } - } + if ( node.querySelector(job._1) !== null ) { + fn(node, job); } } }, - commitSimpleSelectors: function(stagedNodes) { - if ( this.simpleGroupSelector === null ) { - this.simpleGroupSelector = - this.simpleSelectors.join(this.cssNotHiddenId + ',') + - this.cssNotHiddenId; + runXpathJob: function(job, fn) { + if ( job._1 === undefined ) { + job._1 = document.createExpression(job._0, null); } - var i = stagedNodes.length, stagedNode, nodes, j; - while ( i-- ) { - stagedNode = stagedNodes[i]; - if ( stagedNode[this.matchesProp](this.simpleGroupSelector) ) { - this.hideNode(stagedNode); - } - nodes = stagedNode.querySelectorAll(this.simpleGroupSelector); - j = nodes.length; - while ( j-- ) { - this.hideNode(nodes[j]); - } - } - }, - - commitXpathSelectors: function() { - var tstart = window.performance.now(); - if ( this.xpathExpression === null ) { - this.xpathExpression = document.createExpression( - this.xpathSelectors.join('|'), - null - ); - } - this.xpathResult = this.xpathExpression.evaluate( + var xpr = job._2 = job._1.evaluate( document, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - this.xpathResult + job._2 || null ); - var i = this.xpathResult.snapshotLength, node; + var i = xpr.snapshotLength, node; while ( i-- ) { - node = this.xpathResult.snapshotItem(i); + node = xpr.snapshotItem(i); if ( node.nodeType === 1 ) { - this.hideNode(node); + fn(node, job); } } - this.xpathSelectorsCost = window.performance.now() - tstart; }, - cosmeticFiltersActivated: function() { - this.cosmeticFiltersActivatedTimer = null; - vAPI.messaging.send( - 'contentscript', - { what: 'cosmeticFiltersActivated' } - ); - }, - - hideNode: (function() { - if ( document.documentElement.shadowRoot === undefined ) { - return function(node) { - this.hiddenNodeCount += 1; - node.setAttribute(this.hiddenId, ''); - if ( this.enabled ) { - node.style.setProperty('display', 'none', 'important'); - } - }; + runJob: function(job, fn) { + switch ( job.t ) { + case 'has-hide': + this.runHasJob(job, fn); + break; + case 'xpath-hide': + this.runXpathJob(job, fn); + break; + case 'has-style': + // not supported yet + break; + case 'xpath-style': + // not supported yet + break; } - return function(node) { - this.hiddenNodeCount += 1; - node.setAttribute(this.hiddenId, ''); - if ( this.enabled === false ) { - return; - } - // https://github.com/gorhill/uBlock/issues/762 - // https://github.com/gorhill/uBlock/issues/769#issuecomment-229873048 - // Always enforce `display: none`. - node.style.setProperty('display', 'none', 'important'); - // https://www.chromestatus.com/features/4668884095336448 - // "Multiple shadow roots is being deprecated." - var shadow = node.shadowRoot; - if ( shadow ) { - if ( shadow.className === this.shadowId && shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); - } - return; - } - // https://github.com/gorhill/uBlock/pull/555 - // Not all nodes can be shadowed: - // https://github.com/w3c/webcomponents/issues/102 - try { - shadow = node.createShadowRoot(); - shadow.className = this.shadowId; - } catch (ex) { - } - }; - })(), + }, showNode: function(node) { - node.style.removeProperty('display'); + node.hidden = false; var shadow = node.shadowRoot; - if ( shadow && shadow.className === this.shadowId ) { + if ( shadow && shadow[shadowId] ) { if ( shadow.firstElementChild !== null ) { shadow.removeChild(shadow.firstElementChild); } @@ -435,11 +473,14 @@ vAPI.domFilterer = { }, unhideNode: function(node) { - this.hiddenNodeCount--; + if ( node[this.hiddenId] ) { + this.hiddenNodeCount--; + } node.removeAttribute(this.hiddenId); - node.style.removeProperty('display'); + node[this.hiddenId] = undefined; + node.hidden = false; var shadow = node.shadowRoot; - if ( shadow && shadow.className === this.shadowId ) { + if ( shadow && shadow[shadowId] ) { if ( shadow.firstElementChild !== null ) { shadow.removeChild(shadow.firstElementChild); } @@ -448,36 +489,35 @@ vAPI.domFilterer = { }, unshowNode: function(node) { - node.style.setProperty('display', 'none', 'important'); + node.hidden = true; var shadow = node.shadowRoot; - if ( - shadow && - shadow.className === this.shadowId && - shadow.firstElementChild !== null - ) { + if ( shadow && shadow[shadowId] && shadow.firstElementChild !== null ) { shadow.removeChild(shadow.firstElementChild); } - }, + } }; +/******************************************************************************/ -// Not everything could be initialized at declaration time. -(function() { - var df = vAPI.domFilterer; - df.cssNotHiddenId = ':not([' + df.hiddenId + '])'; +var hideNode = domFilterer.hideNode.bind(domFilterer); - // Complex selectors, due to their nature may need to be "de-committed". A - // Set() is used to implement this functionality. For browser with no - // support of Set(), uBO will skip committing complex selectors. - if ( typeof window.Set === 'function' ) { - df.complexSelectorsNodeSet = new Set(); +var complexHideNode = function(node) { + complexSelectorsCurrentResultSet.add(node); + if ( !complexSelectorsOldResultSet.delete(node) ) { + hideNode(node); } +}; + +/******************************************************************************/ + +var cssNotHiddenId = ':not([' + domFilterer.hiddenId + '])'; + +/******************************************************************************/ + +return domFilterer; + +/******************************************************************************/ - // Theoretically, `:has`- and `:xpath`-based selectors may also need to - // be de-committed. But for performance purpose, this is not implemented, - // and anyways, the point of these selectors is to be very accurate, so - // I do not expect de-committing scenarios to occur with proper use of - // these selectors. })(); /******************************************************************************/ @@ -489,113 +529,103 @@ vAPI.domFilterer = { (function domIsLoading() { -/******************************************************************************/ + // Domain-based ABP cosmetic filters. + // These can be inserted before the DOM is loaded. -// Domain-based ABP cosmetic filters. -// These can be inserted before the DOM is loaded. + var cosmeticFilters = function(details) { + var domFilterer = vAPI.domFilterer; + domFilterer.addExceptions(details.cosmeticDonthide); + // https://github.com/chrisaljoudi/uBlock/issues/143 + domFilterer.addSelectors(details.cosmeticHide); + domFilterer.commit(); + }; -var cosmeticFilters = function(details) { - var domFilterer = vAPI.domFilterer; - domFilterer.addExceptions(details.cosmeticDonthide); - // https://github.com/chrisaljoudi/uBlock/issues/143 - domFilterer.addSelectors(details.cosmeticHide); - domFilterer.commit(); -}; - -/******************************************************************************/ - -var netFilters = function(details) { - var parent = document.head || document.documentElement; - if ( !parent ) { - return; - } - var styleTag = document.createElement('style'); - styleTag.setAttribute('type', 'text/css'); - var text = details.netHide.join(',\n'); - var css = details.netCollapse ? - '\n{display:none !important;}' : - '\n{visibility:hidden !important;}'; - styleTag.appendChild(document.createTextNode(text + css)); - parent.appendChild(styleTag); -}; - -/******************************************************************************/ - -// Create script tags and assign data URIs looked up from our library of -// redirection resources: Sometimes it is useful to use these resources as -// standalone scriptlets. These scriptlets are injected from within the -// content scripts because what must be injected, if anything, depends on the -// currently active filters, as selected by the user. -// Library of redirection resources is located at: -// https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt - -var injectScripts = function(scripts) { - var parent = document.head || document.documentElement; - if ( !parent ) { - return; - } - var scriptTag = document.createElement('script'); - // Have the injected script tag remove itself when execution completes: to - // keep DOM as clean as possible. - scripts += - "\n" + - "(function() {\n" + - " var c = document.currentScript,\n" + - " p = c && c.parentNode;\n" + - " if ( p ) {\n" + - " p.removeChild(c);\n" + - " }\n" + - "})();"; - scriptTag.appendChild(document.createTextNode(scripts)); - parent.appendChild(scriptTag); - vAPI.injectedScripts = scripts; -}; - -/******************************************************************************/ - -var responseHandler = function(details) { - vAPI.executionCost.start(); - - if ( details ) { - if ( - (vAPI.skipCosmeticFiltering = details.skipCosmeticFiltering) !== true && - (details.cosmeticHide.length !== 0 || details.cosmeticDonthide.length !== 0) - ) { - cosmeticFilters(details); + var netFilters = function(details) { + var parent = document.head || document.documentElement; + if ( !parent ) { + return; } - if ( details.netHide.length !== 0 ) { - netFilters(details); + var styleTag = document.createElement('style'); + styleTag.setAttribute('type', 'text/css'); + var text = details.netHide.join(',\n'); + var css = details.netCollapse ? + '\n{display:none !important;}' : + '\n{visibility:hidden !important;}'; + styleTag.appendChild(document.createTextNode(text + css)); + parent.appendChild(styleTag); + }; + + // Create script tags and assign data URIs looked up from our library of + // redirection resources: Sometimes it is useful to use these resources as + // standalone scriptlets. These scriptlets are injected from within the + // content scripts because what must be injected, if anything, depends on + // the currently active filters, as selected by the user. + // Library of redirection resources is located at: + // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt + + var injectScripts = function(scripts) { + var parent = document.head || document.documentElement; + if ( !parent ) { + return; } - if ( details.scripts ) { - injectScripts(details.scripts); + var scriptTag = document.createElement('script'); + // Have the injected script tag remove itself when execution completes: + // to keep DOM as clean as possible. + scripts += + "\n" + + "(function() {\n" + + " var c = document.currentScript,\n" + + " p = c && c.parentNode;\n" + + " if ( p ) {\n" + + " p.removeChild(c);\n" + + " }\n" + + "})();"; + scriptTag.appendChild(document.createTextNode(scripts)); + parent.appendChild(scriptTag); + vAPI.injectedScripts = scripts; + }; + + var responseHandler = function(details) { + vAPI.executionCost.start(); + + if ( details ) { + vAPI.skipCosmeticFiltering = details.skipCosmeticFiltering; + vAPI.skipCosmeticSurveying = details.skipCosmeticSurveying; + if ( + (details.skipCosmeticFiltering !== true) && + (details.cosmeticHide.length !== 0 || details.cosmeticDonthide.length !== 0) + ) { + cosmeticFilters(details); + } + if ( details.netHide.length !== 0 ) { + netFilters(details); + } + if ( details.scripts ) { + injectScripts(details.scripts); + } + // The port will never be used again at this point, disconnecting + // allows the browser to flush this script from memory. } - // The port will never be used again at this point, disconnecting allows - // the browser to flush this script from memory. - } - // 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. - vAPI.contentscriptInjected = details && details.ready; + // 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. + vAPI.contentscriptInjected = details && details.ready; - vAPI.executionCost.stop('domIsLoading/responseHandler'); -}; + vAPI.executionCost.stop('domIsLoading/responseHandler'); + }; -/******************************************************************************/ - -var url = window.location.href; -vAPI.messaging.send( - 'contentscript', - { - what: 'retrieveDomainCosmeticSelectors', - pageURL: url, - locationURL: url - }, - responseHandler -); - -/******************************************************************************/ + var url = window.location.href; + vAPI.messaging.send( + 'contentscript', + { + what: 'retrieveDomainCosmeticSelectors', + pageURL: url, + locationURL: url + }, + responseHandler + ); })(); @@ -603,8 +633,6 @@ vAPI.messaging.send( /******************************************************************************/ /******************************************************************************/ -// https://github.com/chrisaljoudi/uBlock/issues/7 - var domCollapser = (function() { var timer = null; var requestId = 1; @@ -676,6 +704,7 @@ var domCollapser = (function() { // https://github.com/chrisaljoudi/uBlock/issues/399 // Never remove elements from the DOM, just hide them target.style.setProperty('display', 'none', 'important'); + target.hidden = true; // https://github.com/chrisaljoudi/uBlock/issues/1048 // Use attribute to construct CSS rule @@ -864,7 +893,12 @@ if ( !vAPI.contentscriptInjected ) { vAPI.executionCost.start(); -/******************************************************************************/ +/******************************************************************************* + +skip-survey=false: survey-phase-1 => survey-phase-2 => survey-phase-3 => commit + skip-survey=true: commit + +*/ // Cosmetic filtering. @@ -876,291 +910,316 @@ vAPI.executionCost.start(); // https://github.com/chrisaljoudi/uBlock/issues/789 // https://github.com/gorhill/uBlock/issues/873 - // Be sure that our style tags used for cosmetic filtering are still applied. + // Be sure that our style tags used for cosmetic filtering are still + // applied. var domFilterer = vAPI.domFilterer; domFilterer.checkStyleTags(false); domFilterer.commit(); var contextNodes = [ document.documentElement ], - messaging = vAPI.messaging, - highGenerics = null, - lowGenericSelectors = [], - queriedSelectors = Object.create(null); + messaging = vAPI.messaging; - var responseHandler = function(response) { - // https://github.com/gorhill/uMatrix/issues/144 - if ( response && response.shutdown ) { - vAPI.shutdown.exec(); + var domSurveyor = (function() { + if ( vAPI.skipCosmeticSurveying === true ) { return; } - vAPI.executionCost.start(); - - var result = response && response.result; - - if ( result ) { - if ( result.hide.length ) { - processLowGenerics(result.hide); - } - if ( result.highGenerics ) { - highGenerics = result.highGenerics; - } - } - - if ( highGenerics ) { - if ( highGenerics.hideLowCount ) { - processHighLowGenerics(highGenerics.hideLow); - } - if ( highGenerics.hideMediumCount ) { - processHighMediumGenerics(highGenerics.hideMedium); - } - if ( highGenerics.hideHighSimpleCount || highGenerics.hideHighComplexCount ) { - processHighHighGenerics(); - } - } - - domFilterer.commit(contextNodes); - contextNodes = []; - - vAPI.executionCost.stop('domIsLoaded/responseHandler'); - }; - - var retrieveGenericSelectors = function() { - if ( lowGenericSelectors.length !== 0 || highGenerics === null ) { - messaging.send( - 'contentscript', - { - what: 'retrieveGenericCosmeticSelectors', - pageURL: window.location.href, - selectors: lowGenericSelectors, - firstSurvey: highGenerics === null - }, - responseHandler - ); + var queriedSelectors = Object.create(null), + highGenerics = null, lowGenericSelectors = []; - } else { - responseHandler(null); - } - }; - // Extract and return the staged nodes which (may) match the selectors. + // Handle main process' response. - var selectNodes = function(selector) { - var stagedNodes = contextNodes, - i = stagedNodes.length; - if ( i === 1 && stagedNodes[0] === document.documentElement ) { - return document.querySelectorAll(selector); - } - 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]); + var surveyPhase3 = function(response) { + // https://github.com/gorhill/uMatrix/issues/144 + if ( response && response.shutdown ) { + vAPI.shutdown.exec(); + return; } - } - return targetNodes; - }; - // Low generics: - // - [id] - // - [class] + vAPI.executionCost.start(); - var processLowGenerics = function(generics) { - domFilterer.addSelectors(generics); - }; + var result = response && response.result, + firstSurvey = highGenerics === null; - // 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 + ']'); - 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).hideNode(node); - continue; + if ( result ) { + if ( result.hide.length ) { + processLowGenerics(result.hide); } - // Candidate 2 = specific form - selector = node.localName + selector; - if ( generics.hasOwnProperty(selector) ) { - domFilterer.addSelector(selector).hideNode(node); + if ( result.highGenerics ) { + highGenerics = result.highGenerics; } } - } - }; - // High-medium generics: - // - [href^="http"] - - var processHighMediumGenerics = function(generics) { - var stagedNodes = contextNodes, - i = stagedNodes.length; - if ( i === 1 && stagedNodes[0] === document.documentElement ) { - processHighMediumGenericsForNodes(document.links, generics); - 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); - } - } - }; - - 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).hideNode(node); + if ( highGenerics ) { + if ( highGenerics.hideLowCount ) { + processHighLowGenerics(highGenerics.hideLow); } - continue; - } - j = entry.length; - while ( j-- ) { - selector = entry[j]; - if ( href.lastIndexOf(selector.slice(8, -2), 0) === 0 ) { - domFilterer.addSelector(selector).hideNode(node); + if ( highGenerics.hideMediumCount ) { + processHighMediumGenerics(highGenerics.hideMedium); + } + if ( highGenerics.hideHighSimpleCount || highGenerics.hideHighComplexCount ) { + processHighHighGenerics(); } } - } - }; - var highHighSimpleGenericsCost = 0, - highHighSimpleGenericsInjected = false, - highHighComplexGenericsCost = 0, - highHighComplexGenericsInjected = false; + // Need to do this before committing DOM filterer, as needed info + // will no longer be there after commit. + if ( firstSurvey || domFilterer.job0._0.length ) { + messaging.send( + 'contentscript', + { + what: 'cosmeticFiltersInjected', + type: 'cosmetic', + hostname: window.location.hostname, + selectors: domFilterer.job0._0 + } + ); + } - var processHighHighGenerics = function() { - var tstart; - // Simple selectors. - if ( - highHighSimpleGenericsInjected === false && - highHighSimpleGenericsCost < 50 && - highGenerics.hideHighSimpleCount !== 0 - ) { - tstart = window.performance.now(); - var matchesProp = vAPI.matchesProp, - nodes = contextNodes, - i = nodes.length, node; + domFilterer.commit(contextNodes); + contextNodes = []; + + vAPI.executionCost.stop('domIsLoaded/surveyPhase2'); + }; + + // Query main process. + + var surveyPhase2 = function() { + 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 + ']'); + 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).hideNode(node); + continue; + } + // Candidate 2 = specific form + selector = node.localName + selector; + if ( generics.hasOwnProperty(selector) ) { + domFilterer.addSelector(selector).hideNode(node); + } + } + } + }; + + // High-medium generics: + // - [href^="http"] + + var processHighMediumGenerics = function(generics) { + var stagedNodes = contextNodes, + i = stagedNodes.length; + if ( i === 1 && stagedNodes[0] === document.documentElement ) { + processHighMediumGenericsForNodes(document.links, generics); + 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); + } + } + }; + + var processHighMediumGenericsForNodes = function(nodes, generics) { + var i = nodes.length, + node, href, pos, entry, j, selector; while ( i-- ) { node = nodes[i]; - if ( - node[matchesProp](highGenerics.hideHighSimple) || - node.querySelector(highGenerics.hideHighSimple) !== null - ) { - highHighSimpleGenericsInjected = true; - domFilterer.addSelectors(highGenerics.hideHighSimple.split(',\n')); - break; + 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).hideNode(node); + } + continue; + } + j = entry.length; + while ( j-- ) { + selector = entry[j]; + if ( href.lastIndexOf(selector.slice(8, -2), 0) === 0 ) { + domFilterer.addSelector(selector).hideNode(node); + } } } - 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')); - domFilterer.commit(); - } - highHighComplexGenericsCost += window.performance.now() - tstart; - } - }; + }; - // Extract all classes/ids: these will be passed to the cosmetic filtering - // engine, and in return we will obtain only the relevant CSS selectors. + var highHighSimpleGenericsCost = 0, + highHighSimpleGenericsInjected = false, + highHighComplexGenericsCost = 0, + highHighComplexGenericsInjected = false; - // https://github.com/gorhill/uBlock/issues/672 - // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens - // http://jsperf.com/enumerate-classes/6 + var processHighHighGenerics = function() { + var tstart; + // Simple selectors. + if ( + highHighSimpleGenericsInjected === false && + highHighSimpleGenericsCost < 50 && + highGenerics.hideHighSimpleCount !== 0 + ) { + tstart = window.performance.now(); + var matchesProp = vAPI.matchesProp, + nodes = contextNodes, + 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')); + domFilterer.commit(); + } + highHighComplexGenericsCost += window.performance.now() - tstart; + } + }; - var classesAndIdsFromNodeList = function(nodes) { - if ( !nodes ) { return; } - var qq = queriedSelectors; - var ll = lowGenericSelectors; - var node, v, vv, len, c, beg, end; - var i = nodes.length; - while ( i-- ) { - node = nodes[i]; - if ( node.nodeType !== 1 ) { continue; } - v = node.id; - if ( v !== '' && typeof v === 'string' ) { - v = '#' + v.trim(); - if ( v !== '#' && qq[v] === undefined ) { - ll.push(v); - qq[v] = true; + // Extract and return the staged nodes which (may) match the selectors. + + var selectNodes = function(selector) { + var stagedNodes = contextNodes, + i = stagedNodes.length; + if ( i === 1 && stagedNodes[0] === document.documentElement ) { + return document.querySelectorAll(selector); + } + 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]); } } - vv = node.className; - if ( vv === '' || typeof vv !== 'string' ) { continue; } - len = vv.length; - beg = 0; - for (;;) { - // Skip whitespaces - while ( beg !== len ) { - c = vv.charCodeAt(beg); - if ( c > 0x20 ) { break; } - beg++; + return targetNodes; + }; + + // Extract all classes/ids: these will be passed to the cosmetic + // filtering engine, and in return we will obtain only the relevant + // CSS selectors. + + // https://github.com/gorhill/uBlock/issues/672 + // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens + // http://jsperf.com/enumerate-classes/6 + + var surveyPhase1 = function() { + var nodes = selectNodes('[class],[id]'); + var qq = queriedSelectors; + var ll = lowGenericSelectors; + var node, v, vv, j; + var i = nodes.length; + while ( i-- ) { + node = nodes[i]; + if ( node.nodeType !== 1 ) { continue; } + v = node.id; + if ( v !== '' && typeof v === 'string' ) { + v = '#' + v.trim(); + if ( v !== '#' && qq[v] === undefined ) { + ll.push(v); + qq[v] = true; + } } - if ( beg === len ) { break; } - end = beg + 1; - // Skip non-whitespaces - while ( end !== len ) { - c = vv.charCodeAt(end); - if ( c <= 0x20 ) { break; } - end++; + vv = node.className; + if ( vv === '' || typeof vv !== 'string' ) { continue; } + if ( /\s/.test(vv) === false ) { + v = '.' + vv; + if ( qq[v] === undefined ) { + ll.push(v); + qq[v] = true; + } + } else { + vv = node.classList; + j = vv.length; + while ( j-- ) { + v = '.' + vv[j]; + if ( qq[v] === undefined ) { + ll.push(v); + qq[v] = true; + } + } } - v = '.' + vv.slice(beg, end); - if ( qq[v] === undefined ) { - ll.push(v); - qq[v] = true; - } - if ( end === len ) { break; } - beg = end + 1; } - } - }; + surveyPhase2(); + }; + + return surveyPhase1; + })(); // Start cosmetic filtering. - classesAndIdsFromNodeList(document.querySelectorAll('[class],[id]')); - retrieveGenericSelectors(); + if ( domSurveyor ) { + domSurveyor(); + } //console.debug('%f: uBlock: survey time', timer.now() - tStart); @@ -1219,11 +1278,15 @@ vAPI.executionCost.start(); } addedNodeLists.length = 0; if ( contextNodes.length !== 0 ) { - classesAndIdsFromNodeList(selectNodes('[class],[id]')); - retrieveGenericSelectors(); + if ( domSurveyor ) { + domSurveyor(); + } else { + domFilterer.commit(contextNodes); + contextNodes = []; + } } - vAPI.executionCost.stop('domIsLoaded/responseHandler'); + vAPI.executionCost.stop('domIsLoaded/addedNodesHandler'); }; // https://github.com/gorhill/uBlock/issues/873 diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index c82fe7654..50a080f3a 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -49,6 +49,8 @@ var isBadRegex = function(s) { return false; }; +var cosmeticSurveyingMissCountMax = parseInt(vAPI.localStorage.getItem('cosmeticSurveyingMissCountMax'), 10) || 10; + /******************************************************************************/ /* var histogram = function(label, buckets) { @@ -463,6 +465,7 @@ SelectorCacheEntry.prototype.netHighWaterMark = 30; SelectorCacheEntry.prototype.reset = function() { this.cosmetic = {}; + this.cosmeticSurveyingMissCount = 0; this.net = {}; this.netCount = 0; this.lastAccessTime = Date.now(); @@ -481,8 +484,13 @@ SelectorCacheEntry.prototype.dispose = function() { /******************************************************************************/ SelectorCacheEntry.prototype.addCosmetic = function(selectors) { - var dict = this.cosmetic; var i = selectors.length || 0; + if ( i === 0 ) { + this.cosmeticSurveyingMissCount += 1; + return; + } + this.cosmeticSurveyingMissCount = 0; + var dict = this.cosmetic; while ( i-- ) { dict[selectors[i]] = true; } @@ -555,6 +563,7 @@ SelectorCacheEntry.prototype.remove = function(type) { this.lastAccessTime = Date.now(); if ( type === undefined || type === 'cosmetic' ) { this.cosmetic = {}; + this.cosmeticSurveyingMissCount = 0; } if ( type === undefined || type === 'net' ) { this.net = {}; @@ -652,9 +661,9 @@ var FilterContainer = function() { this.type0NoDomainHash = 'type0NoDomain'; this.type1NoDomainHash = 'type1NoDomain'; this.parser = new FilterParser(); - this.selectorCachePruneDelay = 5 * 60 * 1000; // 5 minutes - this.selectorCacheAgeMax = 20 * 60 * 1000; // 20 minutes - this.selectorCacheCountMin = 10; + this.selectorCachePruneDelay = 10 * 60 * 1000; // 15 minutes + this.selectorCacheAgeMax = 120 * 60 * 1000; // 120 minutes + this.selectorCacheCountMin = 25; this.selectorCacheTimer = null; this.reHasUnicode = /[^\x00-\x7F]/; this.punycode = punycode; @@ -1555,6 +1564,7 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { ready: this.frozen, domain: domain, entity: pos === -1 ? domain : domain.slice(0, pos - domain.length), + skipCosmeticSurveying: false, skipCosmeticFiltering: this.acceptedCount === 0, cosmeticHide: [], cosmeticDonthide: this.genericDonthide.slice(), @@ -1591,8 +1601,12 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { bucket.retrieve(hostname, r.cosmeticDonthide); } - this.retrieveFromSelectorCache(hostname, 'cosmetic', r.cosmeticHide); - this.retrieveFromSelectorCache(hostname, 'net', r.netHide); + var cacheEntry = this.selectorCache[hostname]; + if ( cacheEntry ) { + cacheEntry.retrieve('cosmetic', r.cosmeticHide); + cacheEntry.retrieve('net', r.netHide); + r.skipCosmeticSurveying = cacheEntry.cosmeticSurveyingMissCount > cosmeticSurveyingMissCountMax; + } //quickProfiler.stop(); diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 28d42fcb7..4bd561a53 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -31,12 +31,14 @@ if ( typeof vAPI !== 'object' || typeof vAPI.domFilterer !== 'object' ) { return; } -var loggedSelectors = vAPI.loggedSelectors || {}, +var df = vAPI.domFilterer, + loggedSelectors = vAPI.loggedSelectors || {}, matchedSelectors = [], - selectors, i, selector, entry, nodes, j; + selectors, i, selector; -// CSS-based selectors. -selectors = vAPI.domFilterer.simpleSelectors.concat(vAPI.domFilterer.complexSelectors); + +// CSS selectors. +selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0); i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -50,48 +52,16 @@ while ( i-- ) { matchedSelectors.push(selector); } -// `:has`-based selectors. -selectors = vAPI.domFilterer.simpleHasSelectors.concat(vAPI.domFilterer.complexHasSelectors); -i = selectors.length; -while ( i-- ) { - entry = selectors[i]; - selector = entry.a + ':has(' + entry.b + ')'; - if ( loggedSelectors.hasOwnProperty(selector) ) { - continue; - } - nodes = document.querySelectorAll(entry.a); - j = nodes.length; - while ( j-- ) { - if ( nodes[j].querySelector(entry.b) !== null ) { - loggedSelectors[selector] = true; - matchedSelectors.push(selector); - break; - } - } -} - -// `:xpath`-based selectors. -var xpr = null, - xpathexpr; -selectors = vAPI.domFilterer.xpathSelectors; -i = selectors.length; -while ( i-- ) { - xpathexpr = selectors[i]; - selector = ':xpath(' + xpathexpr + ')'; - if ( loggedSelectors.hasOwnProperty(selector) ) { - continue; - } - xpr = document.evaluate( - 'boolean(' + xpathexpr + ')', - document, - null, - XPathResult.BOOLEAN_TYPE, - xpr - ); - if ( xpr.booleanValue ) { - loggedSelectors[selector] = true; - matchedSelectors.push(selector); +// Non-CSS selectors. +var logHit = function(node, job) { + if ( !job.raw || loggedSelectors.hasOwnProperty(job.raw) ) { + return; } + loggedSelectors[job.raw] = true; + matchedSelectors.push(job.raw); +}; +for ( i = 4; i < df.jobQueue.length; i++ ) { + df.runJob(df.jobQueue[i], logHit); } vAPI.loggedSelectors = loggedSelectors; diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index b58a45fa6..1bb086dae 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -694,11 +694,10 @@ var cosmeticFilterMapper = (function() { var filterMap = nodeToCosmeticFilterMap, selectors, selector, nodes, node, - entries, entry, i, j; // CSS-based selectors: simple one. - selectors = vAPI.domFilterer.simpleSelectors; + selectors = vAPI.domFilterer.job2._0; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -716,7 +715,7 @@ var cosmeticFilterMapper = (function() { } // CSS-based selectors: complex one (must query from doc root). - selectors = vAPI.domFilterer.complexSelectors; + selectors = vAPI.domFilterer.job3._0; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -730,72 +729,14 @@ var cosmeticFilterMapper = (function() { } } - // `:has()`-based selectors: simple ones. - entries = vAPI.domFilterer.simpleHasSelectors; - i = entries.length; - while ( i-- ) { - entry = entries[i]; - selector = entry.a + ':has(' + entry.b + ')'; - if ( - filterMap.has(rootNode) === false && - rootNode[matchesFnName](entry.a) && - rootNode.querySelector(entry.b) !== null - ) { - filterMap.set(rootNode, selector); - } - nodes = rootNode.querySelectorAll(entry.a); - j = nodes.length; - while ( j-- ) { - node = nodes[j]; - if ( - filterMap.has(node) === false && - node.querySelector(entry.b) !== null - ) { - filterMap.set(node, selector); - } - } - } - - // `:has()`-based selectors: complex ones (must query from doc root). - entries = vAPI.domFilterer.complexHasSelectors; - i = entries.length; - while ( i-- ) { - entry = entries[i]; - selector = entry.a + ':has(' + entry.b + ')'; - nodes = document.querySelectorAll(entry.a); - j = nodes.length; - while ( j-- ) { - node = nodes[j]; - if ( - filterMap.has(node) === false && - node.querySelector(entry.b) !== null - ) { - filterMap.set(node, selector); - } - } - } - - // `:xpath()`-based selectors. - var xpr; - entries = vAPI.domFilterer.xpathSelectors; - i = entries.length; - while ( i-- ) { - entry = entries[i]; - selector = ':xpath(' + entry + ')'; - xpr = document.evaluate( - entry, - document, - null, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - xpr - ); - j = xpr.snapshotLength; - while ( j-- ) { - node = xpr.snapshotItem(j); - if ( filterMap.has(node) === false ) { - filterMap.set(node, selector); - } + // Non-CSS selectors. + var runJobCallback = function(node, job) { + if ( filterMap.has(node) === false ) { + filterMap.set(node, job.raw); } + }; + for ( i = 4; i < vAPI.domFilterer.jobQueue.length; i++ ) { + vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback); } };