From ef289bc4da3d97ae4a391925b64be1a320c5542c Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 13 Aug 2014 20:03:55 -0400 Subject: [PATCH] this fixes #131 --- js/abp-hide-filters.js | 164 ++++++++++++++++++-------------------- js/contentscript-end.js | 125 ++++++++++++++++++++--------- js/contentscript-start.js | 17 ++-- js/messaging-handlers.js | 7 ++ 4 files changed, 179 insertions(+), 134 deletions(-) diff --git a/js/abp-hide-filters.js b/js/abp-hide-filters.js index 36090cdd6..47febe143 100644 --- a/js/abp-hide-filters.js +++ b/js/abp-hide-filters.js @@ -127,23 +127,6 @@ FilterHostname.prototype.retrieve = function(hostname, out) { } }; -/******************************************************************************/ - -// Any selector specific to an entity -// Examples: -// google.*###cnt #center_col > #res > #topstuff > .ts - -var FilterEntity = function(s, entity) { - this.s = s; - this.entity = entity; -}; - -FilterEntity.prototype.retrieve = function(entity, out) { - if ( entity.slice(-this.entity.length) === this.entity ) { - out.push(this.s); - } -}; - /******************************************************************************/ /******************************************************************************/ @@ -240,6 +223,27 @@ FilterParser.prototype.parse = function(s) { /******************************************************************************/ /******************************************************************************/ +var SelectorCacheEntry = function() { + this.selectors = []; + this.lastAccessTime = Date.now(); +}; + +SelectorCacheEntry.prototype.add = function(selectors) { + this.lastAccessTime = Date.now(); + this.selectors.push(selectors); +}; + +SelectorCacheEntry.prototype.retrieve = function(out) { + this.lastAccessTime = Date.now(); + var i = this.selectors.length; + while ( i-- ) { + out.push(this.selectors[i]); + } +}; + +/******************************************************************************/ +/******************************************************************************/ + // Two Unicode characters: // T0HHHHHHH HHHHHHHHH // | | | @@ -306,7 +310,6 @@ var makeHash = function(unhide, token, mask) { // High-high generic: everything else // Specific // Specfic hostname -// Specific entity // // Generic filters can only be enforced once the main document is loaded. // Specific filers can be enforced before the main document is loaded. @@ -328,6 +331,11 @@ FilterContainer.prototype.reset = function() { this.domainHashMask = (1 << 10) - 1; this.genericHashMask = (1 << 15) - 1; + this.selectorCache = {}; + this.selectorCacheCount = 0; + this.selectorCacheLowWaterMark = 75; + this.selectorCacheHighWaterMark = 100; + // temporary (at parse time) this.lowGenericHide = {}; this.lowGenericDonthide = {}; @@ -335,8 +343,6 @@ FilterContainer.prototype.reset = function() { this.highGenericDonthide = {}; this.hostnameHide = {}; this.hostnameDonthide = {}; - this.entityHide = {}; - this.entityDonthide = {}; // permanent // [class], [id] @@ -361,7 +367,6 @@ FilterContainer.prototype.reset = function() { this.highHighGenericDonthideCount = 0; this.hostnameFilters = {}; - this.entityFilters = {}; }; /******************************************************************************/ @@ -389,7 +394,7 @@ FilterContainer.prototype.add = function(s) { if ( hostname.charAt(0) !== '~' ) { applyGlobally = false; } - this.addSpecificSelector(hostname, parsed); + this.addHostnameSelector(hostname, parsed); } if ( applyGlobally ) { this.addGenericSelector(parsed); @@ -422,17 +427,6 @@ FilterContainer.prototype.addGenericSelector = function(parsed) { /******************************************************************************/ -FilterContainer.prototype.addSpecificSelector = function(hostname, parsed) { - // rhill 2014-07-13: new filter class: entity. - if ( hostname.slice(-2) === '.*' ) { - this.addEntitySelector(hostname, parsed); - } else { - this.addHostnameSelector(hostname, parsed); - } -}; - -/******************************************************************************/ - FilterContainer.prototype.addHostnameSelector = function(hostname, parsed) { // https://github.com/gorhill/uBlock/issues/145 var unhide = parsed.unhide; @@ -458,26 +452,6 @@ FilterContainer.prototype.addHostnameSelector = function(hostname, parsed) { /******************************************************************************/ -FilterContainer.prototype.addEntitySelector = function(hostname, parsed) { - var entries = parsed.unhide === 0 ? - this.entityHide : - this.entityDonthide; - var entity = hostname.slice(0, -2); - var entry = entries[entity]; - if ( entry === undefined ) { - entry = entries[entity] = {}; - entry[parsed.suffix] = true; - this.acceptedCount += 1; - } else if ( entry[parsed.suffix] === undefined ) { - entry[parsed.suffix] = true; - this.acceptedCount += 1; - } else { - this.duplicateCount += 1; - } -}; - -/******************************************************************************/ - FilterContainer.prototype.freezeLowGenerics = function(what, type) { var selectors = this[what]; var matches, selectorPrefix, f, hash, bucket; @@ -519,7 +493,7 @@ FilterContainer.prototype.freezeHostnameSpecifics = function(what, type) { if ( entries.hasOwnProperty(hostname) === false ) { continue; } - f = new FilterHostname(Object.keys(entries[hostname]).join(','), hostname); + f = new FilterHostname(Object.keys(entries[hostname]).join(',\n'), hostname); hash = makeHash(type, µburi.domainFromHostname(hostname), this.domainHashMask); bucket = filters[hash]; if ( bucket === undefined ) { @@ -535,30 +509,6 @@ FilterContainer.prototype.freezeHostnameSpecifics = function(what, type) { /******************************************************************************/ -FilterContainer.prototype.freezeEntitySpecifics = function(what, type) { - var entries = this[what]; - var filters = this.entityFilters; - var f, hash, bucket; - for ( var entity in entries ) { - if ( entries.hasOwnProperty(entity) === false ) { - continue; - } - f = new FilterEntity(Object.keys(entries[entity]).join(','), entity); - hash = makeHash(type, entity, this.domainHashMask); - bucket = filters[hash]; - if ( bucket === undefined ) { - filters[hash] = f; - } else if ( bucket instanceof FilterBucket ) { - bucket.add(f); - } else { - filters[hash] = new FilterBucket(bucket, f); - } - } - this[what] = {}; -}; - -/******************************************************************************/ - FilterContainer.prototype.freezeHighGenerics = function(what) { var selectors = this['highGeneric' + what]; @@ -614,8 +564,6 @@ FilterContainer.prototype.freeze = function() { this.freezeHighGenerics('Donthide'); this.freezeHostnameSpecifics('hostnameHide', 0); this.freezeHostnameSpecifics('hostnameDonthide', 1); - this.freezeEntitySpecifics('entityHide', 0); - this.freezeEntitySpecifics('entityDonthide', 1); this.filterParser.reset(); this.frozen = true; @@ -625,6 +573,51 @@ FilterContainer.prototype.freeze = function() { /******************************************************************************/ +FilterContainer.prototype.addToSelectorCache = function(hostname, selectors) { + if ( typeof hostname !== 'string' || hostname === '' ) { + return; + } + if ( typeof selectors !== 'string' || selectors === '' ) { + return; + } + var entry = this.selectorCache[hostname]; + if ( entry === undefined ) { + entry = this.selectorCache[hostname] = new SelectorCacheEntry(); + this.selectorCacheCount += 1; + if ( this.selectorCacheCount > this.selectorCacheHighWaterMark ) { + this.pruneSelectorCache(); + } + } + entry.add(selectors); +}; + +/******************************************************************************/ + +FilterContainer.prototype.retrieveFromSelectorCache = function(hostname, out) { + var entry = this.selectorCache[hostname]; + if ( entry === undefined ) { + return; + } + entry.retrieve(out); +}; + +/******************************************************************************/ + +FilterContainer.prototype.pruneSelectorCache = function() { + var cache = this.selectorCache; + var hostnames = Object.keys(cache).sort(function(a ,b) { + return cache[b].lastAccessTime - cache[a].lastAccessTime; + }); + var toRemove = hostnames.slice(this.selectorCacheLowWaterMark); + var i = toRemove.length; + while ( i-- ) { + delete cache[toRemove[i]]; + } + this.selectorCacheCount -= toRemove.length; +}; + +/******************************************************************************/ + FilterContainer.prototype.retrieveGenericSelectors = function(request) { if ( µb.userSettings.parseAllABPHideFilters !== true ) { return; @@ -698,11 +691,8 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { //quickProfiler.start('FilterContainer.retrieve()'); var hostname = µb.URI.hostnameFromURI(request.locationURL); - var domain = µb.URI.domainFromHostname(hostname); - var pos = domain.indexOf('.'); var r = { - domain: domain, - entity: pos === -1 ? domain : domain.slice(0, pos - domain.length), + domain: µb.URI.domainFromHostname(hostname), hide: [], donthide: [] }; @@ -712,15 +702,13 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { if ( bucket = this.hostnameFilters[hash] ) { bucket.retrieve(hostname, r.hide); } - hash = makeHash(0, r.entity, this.domainHashMask); - if ( bucket = this.entityFilters[hash] ) { - bucket.retrieve(pos === -1 ? domain : hostname.slice(0, pos - domain.length), r.hide); - } hash = makeHash(1, r.domain, this.domainHashMask); if ( bucket = this.hostnameFilters[hash] ) { bucket.retrieve(hostname, r.donthide); } + this.retrieveFromSelectorCache(hostname, r.hide); + //quickProfiler.stop(); //console.log( diff --git a/js/contentscript-end.js b/js/contentscript-end.js index 6956a8dd4..f258d7810 100644 --- a/js/contentscript-end.js +++ b/js/contentscript-end.js @@ -132,16 +132,25 @@ var uBlockMessaging = (function(name){ var contextNodes = [document]; var domLoaded = function() { - // https://github.com/gorhill/uBlock/issues/14 - // Treat any existing domain-specific exception selectors as if they had - // been injected already. - var style = document.getElementById('uBlock1ae7a5f130fc79b4fdb8a4272d9426b5'); - var exceptions = style && style.getAttribute('uBlock1ae7a5f130fc79b4fdb8a4272d9426b5'); - if ( exceptions ) { - exceptions = JSON.parse(exceptions); - var i = exceptions.length; + var style = document.getElementById('uBlockPreload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + if ( style ) { + // https://github.com/gorhill/uBlock/issues/14 + // Treat any existing domain-specific exception selectors as if + // they had been injected already. + var selectors, i; + var exceptions = style.getAttribute('uBlockExceptions'); + if ( exceptions ) { + selectors = JSON.parse(exceptions); + i = selectors.length; + while ( i-- ) { + injectedSelectors[selectors[i]] = true; + } + } + // Avoid re-injecting already injected CSS rules. + selectors = selectorsFromStyles(style); + i = selectors.length; while ( i-- ) { - injectedSelectors[exceptions[i]] = true; + injectedSelectors[selectors[i]] = true; } } idsFromNodeList(document.querySelectorAll('[id]')); @@ -149,6 +158,28 @@ var uBlockMessaging = (function(name){ retrieveGenericSelectors(); }; + var selectorsFromStyles = function(styleRef) { + var selectors = []; + var styles = typeof styleRef === 'string' ? + document.querySelectorAll(styleRef): + [styleRef]; + var i = styles.length; + var style, subset, lastSelector, pos; + while ( i-- ) { + style = styles[i]; + subset = style.textContent.split(',\n'); + lastSelector = subset.pop(); + if ( lastSelector ) { + pos = lastSelector.indexOf('\n'); + if ( pos !== -1 ) { + subset.push(lastSelector.slice(0, pos)); + } + } + selectors = selectors.concat(subset); + } + return selectors; + }; + var retrieveGenericSelectors = function() { var selectors = classSelectors !== null ? Object.keys(classSelectors) : []; if ( idSelectors !== null ) { @@ -209,12 +240,19 @@ var uBlockMessaging = (function(name){ if ( hideSelectors.length ) { applyCSS(hideSelectors, 'display', 'none'); var style = document.createElement('style'); - var text = hideSelectors.join(',\n') + ' {display:none !important;}'; - style.appendChild(document.createTextNode(text)); + style.setAttribute('class', 'uBlockPostload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + // The linefeed before the style block is very important: do no remove! + var text = hideSelectors.join(',\n'); + style.appendChild(document.createTextNode(text + '\n{display:none !important;}')); var parent = document.body || document.documentElement; if ( parent ) { parent.appendChild(style); } + messaging.tell({ + what: 'injectedGenericCosmeticSelectors', + hostname: window.location.hostname, + selectors: text + }); //console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text); } contextNodes.length = 0; @@ -268,7 +306,8 @@ var uBlockMessaging = (function(name){ var processHighLowGenerics = function(generics, out) { var attrs = ['title', 'alt']; - var attr, attrValue, nodeList, iNode, node, selector; + var attr, attrValue, nodeList, iNode, node; + var selector; while ( attr = attrs.pop() ) { nodeList = selectNodes('[' + attr + ']'); iNode = nodeList.length; @@ -277,17 +316,21 @@ var uBlockMessaging = (function(name){ attrValue = node.getAttribute(attr); if ( !attrValue ) { continue; } selector = '[' + attr + '="' + attrValue + '"]'; - if ( injectedSelectors[selector] === undefined && generics[selector] ) { - injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); + if ( generics[selector] ) { + if ( injectedSelectors[selector] === undefined ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { + out.push(selector); + } } } selector = node.tagName.toLowerCase() + selector; - if ( injectedSelectors[selector] === undefined && generics[selector] ) { - injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); + if ( generics[selector] ) { + if ( injectedSelectors[selector] === undefined ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { + out.push(selector); + } } } } @@ -297,7 +340,7 @@ var uBlockMessaging = (function(name){ var processHighMediumGenerics = function(generics, out) { var nodeList = selectNodes('a[href^="http"]'); var iNode = nodeList.length; - var node, href, pos, hash, selector; + var node, href, pos, hash, selectors, selector, iSelector; while ( iNode-- ) { node = nodeList[iNode]; href = node.getAttribute('href'); @@ -305,29 +348,37 @@ var uBlockMessaging = (function(name){ pos = href.indexOf('://'); if ( pos === -1 ) { continue; } hash = href.slice(pos + 3, pos + 11); - selector = generics[hash]; - if ( selector === undefined ) { continue; } - if ( injectedSelectors[selector] !== undefined ) { continue; } - injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); + selectors = generics[hash]; + if ( selectors === undefined ) { continue; } + selectors = selectors.split(',\n'); + iSelector = selectors.length; + while ( iSelector-- ) { + selector = selectors[iSelector]; + if ( injectedSelectors[selector] === undefined ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { + out.push(selector); + } + } } } }; var processHighHighGenerics = function(generics, out) { - if ( injectedSelectors[generics] !== undefined ) { return; } + if ( injectedSelectors['{{highHighGenerics}}'] !== undefined ) { return; } if ( document.querySelector(generics) === null ) { return; } - injectedSelectors[generics] = true; - if ( out !== undefined ) { - var selectors = generics.split(',\n'); - var i = selectors.length; - while ( i-- ) { - if ( injectedSelectors[selectors[i]] !== undefined ) { - selectors.splice(i, 1); + injectedSelectors['{{highHighGenerics}}'] = true; + var selectors = generics.split(',\n'); + var iSelector = selectors.length; + var selector; + while ( iSelector-- ) { + selector = selectors[iSelector]; + if ( injectedSelectors[selector] === undefined ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { + out.push(selector); } } - out.push(selectors.join(',\n')); } }; @@ -347,7 +398,6 @@ var uBlockMessaging = (function(name){ if ( node.nodeType !== 1 ) { continue; } // id v = nodes[i].id; - // quite unlikely, so no need to be fancy if ( typeof v !== 'string' ) { continue; } v = v.trim(); if ( v === '' ) { continue; } @@ -418,7 +468,6 @@ var uBlockMessaging = (function(name){ var mutationObservedHandler = function(mutations) { var iMutation = mutations.length; - var nodes = []; var nodeList, iNode, node; while ( iMutation-- ) { nodeList = mutations[iMutation].addedNodes; diff --git a/js/contentscript-start.js b/js/contentscript-start.js index 77376f058..bcdf9a6a3 100644 --- a/js/contentscript-start.js +++ b/js/contentscript-start.js @@ -141,21 +141,21 @@ var domainCosmeticFilteringHandler = function(selectors) { return; } var style = document.createElement('style'); + style.setAttribute('id', 'uBlockPreload-1ae7a5f130fc79b4fdb8a4272d9426b5'); var donthide = selectors.donthide; var hide = selectors.hide; if ( donthide.length !== 0 ) { - donthide = donthide.length !== 1 ? donthide.join(',') : donthide[0]; - donthide = donthide.split(','); - style.setAttribute('id', 'uBlock1ae7a5f130fc79b4fdb8a4272d9426b5'); - style.setAttribute('uBlock1ae7a5f130fc79b4fdb8a4272d9426b5', JSON.stringify(donthide)); + donthide = donthide.length !== 1 ? donthide.join(',\n') : donthide[0]; + donthide = donthide.split(',\n'); + style.setAttribute('uBlockExceptions', JSON.stringify(donthide)); // https://github.com/gorhill/uBlock/issues/143 if ( hide.length !== 0 ) { // I chose to use Array.indexOf() instead of converting the array to // a map, then deleting whitelisted selectors, and then converting // back the map into an array, because there are typically very few // exception filters, if any. - hide = hide.length !== 1 ? hide.join(',') : hide[0]; - hide = hide.split(','); + hide = hide.length !== 1 ? hide.join(',\n') : hide[0]; + hide = hide.split(',\n'); var i = donthide.length, j; while ( i-- ) { j = hide.indexOf(donthide[i]); @@ -166,9 +166,10 @@ var domainCosmeticFilteringHandler = function(selectors) { } } if ( hide.length !== 0 ) { - var text = hide.join(','); + var text = hide.join(',\n'); domainCosmeticFilteringApplyCSS(text, 'display', 'none'); - style.appendChild(document.createTextNode(text + ' {display:none !important;}')); + // The linefeed before the style block is very important: do no remove! + style.appendChild(document.createTextNode(text + '\n{display:none !important;}')); //console.debug('µBlock> "%s" cosmetic filters: injecting %d CSS rules:', selectors.domain, selectors.hide.length, hideStyleText); } var parent = document.head || document.documentElement; diff --git a/js/messaging-handlers.js b/js/messaging-handlers.js index c31efd151..cc1902766 100644 --- a/js/messaging-handlers.js +++ b/js/messaging-handlers.js @@ -166,6 +166,13 @@ var onMessage = function(request, sender, callback) { } break; + case 'injectedGenericCosmeticSelectors': + µb.abpHideFilters.addToSelectorCache( + request.hostname, + request.selectors + ); + break; + case 'blockedRequests': response = { collapse: µb.userSettings.collapseBlocked,