code review + testing after fix to #131

This commit is contained in:
gorhill 2014-08-14 13:59:37 -04:00
parent 1c219ef718
commit 3e47b8a962
4 changed files with 271 additions and 129 deletions

View file

@ -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;

View file

@ -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',
what: 'injectedSelectors',
type: 'cosmetic',
hostname: window.location.hostname,
selectors: text
selectors: hideSelectors
});
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text);
}
@ -298,10 +299,8 @@ var uBlockMessaging = (function(name){
continue;
}
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
};
var processHighLowGenerics = function(generics, out) {
@ -319,22 +318,18 @@ var uBlockMessaging = (function(name){
if ( generics[selector] ) {
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
}
selector = node.tagName.toLowerCase() + selector;
if ( generics[selector] ) {
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
}
}
}
};
var processHighMediumGenerics = function(generics, out) {
@ -356,12 +351,10 @@ var uBlockMessaging = (function(name){
selector = selectors[iSelector];
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
}
}
};
var processHighHighGenerics = function(generics, out) {
@ -375,11 +368,9 @@ var uBlockMessaging = (function(name){
selector = selectors[iSelector];
if ( injectedSelectors[selector] === undefined ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
}
};
var idsFromNodeList = function(nodes) {
@ -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);
})();

View file

@ -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
);
/******************************************************************************/

View file

@ -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