From 3e47b8a9624140ff317a2f0d0d49ede9ed6c8a79 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 14 Aug 2014 13:59:37 -0400 Subject: [PATCH] code review + testing after fix to #131 --- js/abp-hide-filters.js | 181 +++++++++++++++++++++++++++++++++----- js/contentscript-end.js | 154 ++++++++++++++++---------------- js/contentscript-start.js | 51 +++++++---- js/messaging-handlers.js | 14 +-- 4 files changed, 271 insertions(+), 129 deletions(-) diff --git a/js/abp-hide-filters.js b/js/abp-hide-filters.js index 47febe143..3fffe4999 100644 --- a/js/abp-hide-filters.js +++ b/js/abp-hide-filters.js @@ -127,6 +127,23 @@ 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); + } +}; + /******************************************************************************/ /******************************************************************************/ @@ -224,20 +241,64 @@ FilterParser.prototype.parse = function(s) { /******************************************************************************/ var SelectorCacheEntry = function() { - this.selectors = []; + this.cosmetic = {}; + this.net = {}; + this.netCount = 0; this.lastAccessTime = Date.now(); }; -SelectorCacheEntry.prototype.add = function(selectors) { - this.lastAccessTime = Date.now(); - this.selectors.push(selectors); -}; +SelectorCacheEntry.prototype.netLowWaterMark = 20; +SelectorCacheEntry.prototype.netHighWaterMark = 30; -SelectorCacheEntry.prototype.retrieve = function(out) { - this.lastAccessTime = Date.now(); - var i = this.selectors.length; +SelectorCacheEntry.prototype.addCosmetic = function(selectors) { + var dict = this.cosmetic; + var i = selectors.length || 0; while ( i-- ) { - out.push(this.selectors[i]); + dict[selectors[i]] = true; + } +}; + +SelectorCacheEntry.prototype.addNet = function(selector) { + if ( typeof selector !== 'string' || selector === '' ) { + return; + } + // Net request-derived selectors: I limit the number of cached selectors, + // as I expect cases where the blocked net-requests are never the + // exact same URL. + var dict = this.net; + if ( dict[selector] !== undefined ) { + dict[selector] = Date.now(); + return; + } + if ( this.netCount >= this.netHighWaterMark ) { + var keys = Object.keys(dict).sort(function(a, b) { + return dict[b] - dict[a]; + }).slice(this.netLowWaterMark); + var i = keys.length; + while ( i-- ) { + delete dict[keys[i]]; + } + } + dict[selector] = Date.now(); + this.netCount += 1; +}; + +SelectorCacheEntry.prototype.add = function(selectors, type) { + this.lastAccessTime = Date.now(); + if ( type === 'cosmetic' ) { + this.addCosmetic(selectors); + } else { + this.addNet(selectors); + } +}; + +SelectorCacheEntry.prototype.retrieve = function(type, out) { + this.lastAccessTime = Date.now(); + var dict = type === 'cosmetic' ? this.cosmetic : this.net; + for ( var selector in dict ) { + if ( dict.hasOwnProperty(selector) ) { + out.push(selector); + } } }; @@ -310,7 +371,7 @@ 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. @@ -343,7 +404,8 @@ FilterContainer.prototype.reset = function() { this.highGenericDonthide = {}; this.hostnameHide = {}; this.hostnameDonthide = {}; - + this.entityHide = {}; + this.entityDonthide = {}; // permanent // [class], [id] this.lowGenericFilters = {}; @@ -367,6 +429,7 @@ FilterContainer.prototype.reset = function() { this.highHighGenericDonthideCount = 0; this.hostnameFilters = {}; + this.entityFilters = {}; }; /******************************************************************************/ @@ -394,7 +457,7 @@ FilterContainer.prototype.add = function(s) { if ( hostname.charAt(0) !== '~' ) { applyGlobally = false; } - this.addHostnameSelector(hostname, parsed); + this.addSpecificSelector(hostname, parsed); } if ( applyGlobally ) { this.addGenericSelector(parsed); @@ -427,6 +490,17 @@ 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; @@ -452,6 +526,26 @@ 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; @@ -509,6 +603,30 @@ 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(',\n'), 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]; @@ -564,6 +682,8 @@ 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; @@ -573,11 +693,13 @@ FilterContainer.prototype.freeze = function() { /******************************************************************************/ -FilterContainer.prototype.addToSelectorCache = function(hostname, selectors) { +FilterContainer.prototype.addToSelectorCache = function(details) { + var hostname = details.hostname; if ( typeof hostname !== 'string' || hostname === '' ) { return; } - if ( typeof selectors !== 'string' || selectors === '' ) { + var selectors = details.selectors; + if ( !selectors ) { return; } var entry = this.selectorCache[hostname]; @@ -588,17 +710,17 @@ FilterContainer.prototype.addToSelectorCache = function(hostname, selectors) { this.pruneSelectorCache(); } } - entry.add(selectors); + entry.add(selectors, details.type); }; /******************************************************************************/ -FilterContainer.prototype.retrieveFromSelectorCache = function(hostname, out) { +FilterContainer.prototype.retrieveFromSelectorCache = function(hostname, type, out) { var entry = this.selectorCache[hostname]; if ( entry === undefined ) { return; } - entry.retrieve(out); + entry.retrieve(type, out); }; /******************************************************************************/ @@ -691,30 +813,41 @@ 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: µb.URI.domainFromHostname(hostname), - hide: [], - donthide: [] + domain: domain, + entity: pos === -1 ? domain : domain.slice(0, pos - domain.length), + cosmeticHide: [], + cosmeticDonthide: [], + netHide: [], + netCollapse: µb.userSettings.collapseBlocked }; var hash, bucket; hash = makeHash(0, r.domain, this.domainHashMask); if ( bucket = this.hostnameFilters[hash] ) { - bucket.retrieve(hostname, r.hide); + bucket.retrieve(hostname, r.cosmeticHide); + } + hash = makeHash(0, r.entity, this.domainHashMask); + if ( bucket = this.entityFilters[hash] ) { + bucket.retrieve(pos === -1 ? domain : hostname.slice(0, pos - domain.length), r.cosmeticHide); } hash = makeHash(1, r.domain, this.domainHashMask); if ( bucket = this.hostnameFilters[hash] ) { - bucket.retrieve(hostname, r.donthide); + bucket.retrieve(hostname, r.cosmeticDonthide); } - this.retrieveFromSelectorCache(hostname, r.hide); + this.retrieveFromSelectorCache(hostname, 'cosmetic', r.cosmeticHide); + this.retrieveFromSelectorCache(hostname, 'net', r.netHide); //quickProfiler.stop(); //console.log( // 'µBlock> abp-hide-filters.js: "%s" => %d selectors out', // request.locationURL, - // r.hide.length + r.donthide.length + // r.cosmeticHide.length + r.cosmeticDonthide.length //); return r; diff --git a/js/contentscript-end.js b/js/contentscript-end.js index f258d7810..f10095e27 100644 --- a/js/contentscript-end.js +++ b/js/contentscript-end.js @@ -130,15 +130,16 @@ var uBlockMessaging = (function(name){ var idSelectors = null; var highGenerics = null; var contextNodes = [document]; + var nullArray = { push: function(){} }; var domLoaded = function() { - var style = document.getElementById('uBlockPreload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + var style = document.getElementById('ublock-preload-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'); + var exceptions = style.getAttribute('data-ublock-exceptions'); if ( exceptions ) { selectors = JSON.parse(exceptions); i = selectors.length; @@ -208,19 +209,19 @@ var uBlockMessaging = (function(name){ highGenerics = selectors.highGenerics; } if ( selectors && selectors.donthide.length ) { - processLowGenerics(selectors.donthide); + processLowGenerics(selectors.donthide, nullArray); } if ( highGenerics ) { if ( highGenerics.donthideLowCount ) { - processHighLowGenerics(highGenerics.donthideLow); + processHighLowGenerics(highGenerics.donthideLow, nullArray); } if ( highGenerics.donthideMediumCount ) { - processHighMediumGenerics(highGenerics.donthideMedium); + processHighMediumGenerics(highGenerics.donthideMedium, nullArray); } } - // No such thing as high-high generic exceptions + // No such thing as high-high generic exceptions. //if ( highGenerics.donthideHighCount ) { - // processHighHighGenerics(document, highGenerics.donthideHigh); + // processHighHighGenerics(document, highGenerics.donthideHigh, nullArray); //} var hideSelectors = []; if ( selectors && selectors.hide.length ) { @@ -240,18 +241,18 @@ var uBlockMessaging = (function(name){ if ( hideSelectors.length ) { applyCSS(hideSelectors, 'display', 'none'); var style = document.createElement('style'); - style.setAttribute('class', 'uBlockPostload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + style.setAttribute('class', 'ublock-postload-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;}')); + style.appendChild(document.createTextNode(hideSelectors.join(',\n') + '\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 + what: 'injectedSelectors', + type: 'cosmetic', + hostname: window.location.hostname, + selectors: hideSelectors }); //console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text); } @@ -298,9 +299,7 @@ var uBlockMessaging = (function(name){ continue; } injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); - } + out.push(selector); } }; @@ -319,18 +318,14 @@ var uBlockMessaging = (function(name){ if ( generics[selector] ) { if ( injectedSelectors[selector] === undefined ) { injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); - } + out.push(selector); } } selector = node.tagName.toLowerCase() + selector; if ( generics[selector] ) { if ( injectedSelectors[selector] === undefined ) { injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); - } + out.push(selector); } } } @@ -356,9 +351,7 @@ var uBlockMessaging = (function(name){ selector = selectors[iSelector]; if ( injectedSelectors[selector] === undefined ) { injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); - } + out.push(selector); } } } @@ -375,9 +368,7 @@ var uBlockMessaging = (function(name){ selector = selectors[iSelector]; if ( injectedSelectors[selector] === undefined ) { injectedSelectors[selector] = true; - if ( out !== undefined ) { - out.push(selector); - } + out.push(selector); } } }; @@ -460,10 +451,12 @@ var uBlockMessaging = (function(name){ } var ignoreTags = { - 'style': true, - 'STYLE': true, + 'link': true, + 'LINK': true, 'script': true, - 'SCRIPT': true + 'SCRIPT': true, + 'style': true, + 'STYLE': true }; var mutationObservedHandler = function(mutations) { @@ -511,62 +504,67 @@ var uBlockMessaging = (function(name){ (function() { var messaging = uBlockMessaging; - var hideOne = function(elem, collapse) { - // If `!important` is not there, going back using history will likely - // cause the hidden element to re-appear. - elem.style.visibility = 'hidden !important'; - if ( collapse && elem.parentNode ) { - elem.parentNode.removeChild(elem); - } - }; - - // First pass - messaging.ask({ what: 'blockedRequests' }, function(details) { - var elems = document.querySelectorAll('img,iframe,embed'); - var blockedRequests = details.blockedRequests; - var collapse = details.collapse; - var i = elems.length; - var elem, src; - while ( i-- ) { - elem = elems[i]; - src = elem.src; - if ( typeof src !== 'string' || src === '' ) { - continue; - } - if ( blockedRequests[src] ) { - hideOne(elem, collapse); - } - } - }); - // Listeners to mop up whatever is otherwise missed: // - Future requests not blocked yet // - Elements dynamically added to the page // - Elements which resource URL changes + + var loadedElements = { + 'iframe': 'src' + }; + + var failedElements = { + 'img': 'src', + 'object': 'data' + }; + + var onResource = function(target, dict) { + if ( !target ) { + return; + } + var tagName = target.tagName.toLowerCase(); + var prop = dict[tagName]; + if ( prop === undefined ) { + return; + } + var src = target[prop]; + if ( !src ) { + return; + } + var pos = src.indexOf('#'); + if ( pos !== -1 ) { + src = src.slice(0, pos); + } + var onAnswerReceived = function(details) { + if ( !details.blocked ) { + return; + } + // If `!important` is not there, going back using history will + // likely cause the hidden element to re-appear. + target.style.visibility = 'hidden !important'; + if ( details.collapse ) { + target.parentNode.removeChild(target); + } + messaging.tell({ + what: 'injectedSelectors', + type: 'net', + hostname: window.location.hostname, + selectors: tagName + '[' + prop + '="' + src + '"]' + }); + }; + messaging.ask({ what: 'blockedRequest', url: src }, onAnswerReceived); + }; + var onResourceLoaded = function(ev) { - var target = ev.target; //console.debug('Loaded %s[src="%s"]', target.tagName, target.src); - if ( !target || !target.src ) { return; } - if ( target.tagName.toLowerCase() !== 'iframe' ) { return; } - var onAnswerReceived = function(details) { - if ( details.blocked ) { - hideOne(target, details.collapse); - } - }; - messaging.ask({ what: 'blockedRequest', url: target.src }, onAnswerReceived); + onResource(ev.target, loadedElements); }; + var onResourceFailed = function(ev) { - var target = ev.target; - //console.debug('Failed to load %s[src="%s"]', target.tagName, target.src); - if ( !target || !target.src ) { return; } - if ( target.tagName.toLowerCase() !== 'img' ) { return; } - var onAnswerReceived = function(details) { - if ( details.blocked ) { - hideOne(target, details.collapse); - } - }; - messaging.ask({ what: 'blockedRequest', url: target.src }, onAnswerReceived); + //console.debug('Failed to load %o[src="%s"]', target, target.src); + onResource(ev.target, failedElements); }; + document.addEventListener('load', onResourceLoaded, true); document.addEventListener('error', onResourceFailed, true); })(); diff --git a/js/contentscript-start.js b/js/contentscript-start.js index bcdf9a6a3..0c2bcb4ee 100644 --- a/js/contentscript-start.js +++ b/js/contentscript-start.js @@ -133,21 +133,15 @@ var messaging = (function(name){ // Domain-based ABP cosmetic filters. // These can be inserted before the DOM is loaded. -var domainCosmeticFilteringHandler = function(selectors) { - if ( !selectors ) { - return; - } - if ( selectors.hide.length === 0 && selectors.donthide.length === 0 ) { - return; - } +var cosmeticFilters = function(details) { var style = document.createElement('style'); - style.setAttribute('id', 'uBlockPreload-1ae7a5f130fc79b4fdb8a4272d9426b5'); - var donthide = selectors.donthide; - var hide = selectors.hide; + style.setAttribute('id', 'ublock-preload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + var donthide = details.cosmeticDonthide; + var hide = details.cosmeticHide; if ( donthide.length !== 0 ) { donthide = donthide.length !== 1 ? donthide.join(',\n') : donthide[0]; donthide = donthide.split(',\n'); - style.setAttribute('uBlockExceptions', JSON.stringify(donthide)); + style.setAttribute('data-ublock-exceptions', 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 @@ -167,10 +161,10 @@ var domainCosmeticFilteringHandler = function(selectors) { } if ( hide.length !== 0 ) { var text = hide.join(',\n'); - domainCosmeticFilteringApplyCSS(text, 'display', 'none'); + applyCSS(text, 'display', 'none'); // 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); + //console.debug('µBlock> "%s" cosmetic filters: injecting %d CSS rules:', details.domain, details.hide.length, hideStyleText); } var parent = document.head || document.documentElement; if ( parent ) { @@ -178,7 +172,34 @@ var domainCosmeticFilteringHandler = function(selectors) { } }; -var domainCosmeticFilteringApplyCSS = function(selectors, prop, value) { +var netFilters = function(details) { + var parent = document.head || document.documentElement; + if ( !parent ) { + return; + } + var style = document.createElement('style'); + style.setAttribute('class', 'ublock-preload-1ae7a5f130fc79b4fdb8a4272d9426b5'); + var text = details.netHide.join(',\n'); + var css = details.netCollapse ? + '\n{display:none !important;}' : + '\n{visibility:hidden !important;}'; + style.appendChild(document.createTextNode(text + css)); + parent.appendChild(style); +}; + +var filteringHandler = function(details) { + if ( !details ) { + return; + } + if ( details.cosmeticHide.length !== 0 || details.cosmeticDonthide.length !== 0 ) { + cosmeticFilters(details); + } + if ( details.netHide.length !== 0 ) { + netFilters(details); + } +}; + +var applyCSS = function(selectors, prop, value) { if ( document.body === null ) { return; } @@ -195,7 +216,7 @@ messaging.ask( pageURL: window.location.href, locationURL: window.location.href }, - domainCosmeticFilteringHandler + filteringHandler ); /******************************************************************************/ diff --git a/js/messaging-handlers.js b/js/messaging-handlers.js index cc1902766..190557c60 100644 --- a/js/messaging-handlers.js +++ b/js/messaging-handlers.js @@ -166,18 +166,8 @@ var onMessage = function(request, sender, callback) { } break; - case 'injectedGenericCosmeticSelectors': - µb.abpHideFilters.addToSelectorCache( - request.hostname, - request.selectors - ); - break; - - case 'blockedRequests': - response = { - collapse: µb.userSettings.collapseBlocked, - blockedRequests: pageStore ? pageStore.blockedRequests : {} - }; + case 'injectedSelectors': + µb.abpHideFilters.addToSelectorCache(request); break; // Check a single request