From c6b7dfabb17105ffb4ebf6fdcc459753c7b8325a Mon Sep 17 00:00:00 2001 From: gorhill Date: Tue, 12 Aug 2014 12:19:54 -0400 Subject: [PATCH] this fixes #145 + refactoring/performance --- js/abp-hide-filters.js | 567 +++++++++++++++++++++------------------- js/contentscript-end.js | 281 +++++++++++--------- 2 files changed, 460 insertions(+), 388 deletions(-) diff --git a/js/abp-hide-filters.js b/js/abp-hide-filters.js index 746f961d2..25a350428 100644 --- a/js/abp-hide-filters.js +++ b/js/abp-hide-filters.js @@ -176,14 +176,11 @@ var FilterParser = function() { this.prefix = ''; this.suffix = ''; this.anchor = 0; - this.filterType = '#'; + this.unhide = 0; this.hostnames = []; this.invalid = false; this.unsupported = false; this.reParser = /^\s*([^#]*)(##|#@#)(.+)\s*$/; - this.rePlain = /^([#.][\w-]+)/; - this.rePlainMore = /^[#.][\w-]+[^\w-]/; - this.reElement = /^[a-z]/i; }; /******************************************************************************/ @@ -193,8 +190,8 @@ FilterParser.prototype.reset = function() { this.prefix = ''; this.suffix = ''; this.anchor = ''; - this.filterType = '#'; - this.hostnames = []; + this.unhide = 0; + this.hostnames.length = 0; this.invalid = false; return this; }; @@ -221,12 +218,19 @@ FilterParser.prototype.parse = function(s) { // https://github.com/gorhill/httpswitchboard/issues/260 // Any sequence of `#` longer than one means the line is not a valid // cosmetic filter. - if ( this.suffix.indexOf('##') >= 0 ) { + if ( this.suffix.indexOf('##') !== -1 ) { this.invalid = true; return this; } - this.filterType = this.anchor.charAt(1); + // Normalize high-medium selectors: `href` is assumed to imply `a` tag. We + // need to do this here in order to correctly avoid duplicates. The test + // is designed to minimize overhead -- this is a low occurrence filter. + if ( this.suffix.charAt(1) === '[' && this.suffix.slice(2, 9) === 'href^="' ) { + this.suffix = this.suffix.slice(1); + } + + this.unhide = this.anchor.charAt(1) === '@' ? 1 : 0; if ( this.prefix !== '' ) { this.hostnames = this.prefix.split(/\s*,\s*/); } @@ -234,30 +238,79 @@ FilterParser.prototype.parse = function(s) { }; /******************************************************************************/ - -FilterParser.prototype.isPlainMore = function() { - return this.rePlainMore.test(this.suffix); -}; - /******************************************************************************/ -FilterParser.prototype.isElement = function() { - return this.reElement.test(this.suffix); -}; +// Two Unicode characters: +// T0HHHHHHH HHHHHHHHH +// | | | +// | | | +// | | | +// | | +-- bit 8-0 of FNV +// | | +// | +-- bit 15-9 of FNV +// | +// +-- filter type (0=hide 1=unhide) +// -/******************************************************************************/ - -FilterParser.prototype.extractPlain = function() { - var matches = this.rePlain.exec(this.suffix); - if ( matches && matches.length === 2 ) { - return matches[1]; - } - return ''; +var makeHash = function(unhide, token, mask) { + // Ref: Given a URL, returns a unique 4-character long hash string + // Based on: FNV32a + // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source + // The rest is custom, suited for µBlock. + var i1 = token.length; + var i2 = i1 >> 1; + var i4 = i1 >> 2; + var i8 = i1 >> 3; + var hval = (0x811c9dc5 ^ token.charCodeAt(0)) >>> 0; + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i8); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i4); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i4+i8); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i2); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i2+i8); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i2+i4); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval ^= token.charCodeAt(i1-1); + hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); + hval >>>= 0; + hval &= mask; + if ( unhide !== 0 ) { + hval |= 0x20000; + } + return String.fromCharCode(hval >>> 9, hval & 0x1FF); }; /******************************************************************************/ /******************************************************************************/ +// Cosmetic filter family tree: +// +// Generic +// Low generic simple: class or id only +// Low generic complex: class or id + extra stuff after +// High generic: +// High-low generic: [alt="..."],[title="..."] +// High-medium generic: [href^="..."] +// 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. + var FilterContainer = function() { this.filterParser = new FilterParser(); this.reset(); @@ -271,25 +324,44 @@ FilterContainer.prototype.reset = function() { this.filterParser.reset(); this.frozen = false; this.acceptedCount = 0; - this.processedCount = 0; + this.duplicateCount = 0; this.domainHashMask = (1 << 10) - 1; this.genericHashMask = (1 << 15) - 1; - this.genericFilters = {}; + + // temporary (at parse time) + this.lowGenericHide = {}; + this.lowGenericDonthide = {}; + this.highGenericHide = {}; + this.highGenericDonthide = {}; this.hostnameHide = {}; this.hostnameDonthide = {}; - this.hostnameFilters = {}; this.entityHide = {}; this.entityDonthide = {}; + + // permanent + // [class], [id] + this.lowGenericFilters = {}; + + // [alt="..."], [title="..."] + this.highLowGenericHide = {}; + this.highLowGenericDonthide = {}; + this.highLowGenericHideCount = 0; + this.highLowGenericDonthideCount = 0; + + // a[href^="http..."] + this.highMediumGenericHide = {}; + this.highMediumGenericDonthide = {}; + this.highMediumGenericHideCount = 0; + this.highMediumGenericDonthideCount = 0; + + // everything else + this.highHighGenericHide = []; + this.highHighGenericDonthide = []; + this.highHighGenericHideCount = 0; + this.highHighGenericDonthideCount = 0; + + this.hostnameFilters = {}; this.entityFilters = {}; - this.hideUnfiltered = []; - this.hideLowGenerics = {}; - this.hideHighGenerics = []; - this.donthideUnfiltered = []; - this.donthideLowGenerics = {}; - this.donthideHighGenerics = []; - this.rejected = []; - this.duplicates = {}; - this.duplicateCount = 0; }; /******************************************************************************/ @@ -301,43 +373,132 @@ FilterContainer.prototype.add = function(s) { return false; } - this.processedCount += 1; - - //if ( s === 'mail.google.com##.nH.adC > .nH > .nH > .u5 > .azN' ) { - // debugger; - //} - - // hostname-based filters: with a hostname, narrowing is good enough, no - // need to further narrow. - if ( parsed.hostnames.length ) { - return this.addSpecificFilter(parsed); - } - - if ( this.duplicates[s] ) { - this.duplicateCount++; - return false; - } - this.duplicates[s] = true; - - // no specific hostname, narrow using class or id. - var selectorType = parsed.suffix.charAt(0); - if ( selectorType === '#' || selectorType === '.' ) { - return this.addPlainFilter(parsed); - } - - // no specific hostname, no class, no id. - if ( parsed.filterType === '#' ) { - this.hideUnfiltered.push(parsed.suffix); + var hostnames = parsed.hostnames; + var i = hostnames.length; + if ( i === 0 ) { + this.addGenericSelector(parsed); } else { - this.donthideUnfiltered.push(parsed.suffix); + while ( i-- ) { + this.addSpecificSelector(hostnames[i], parsed); + } } - this.acceptedCount += 1; - return true; }; /******************************************************************************/ +FilterContainer.prototype.addGenericSelector = function(parsed) { + var entries; + var selectorType = parsed.suffix.charAt(0); + if ( selectorType === '#' || selectorType === '.' ) { + entries = parsed.unhide === 0 ? + this.lowGenericHide : + this.lowGenericDonthide; + } else { + entries = parsed.unhide === 0 ? + this.highGenericHide : + this.highGenericDonthide; + } + if ( entries[parsed.suffix] === undefined ) { + entries[parsed.suffix] = true; + this.acceptedCount += 1; + } else { + this.duplicateCount += 1; + } + return true; +}; + +/******************************************************************************/ + +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; + if ( hostname.charAt(0) === '~' ) { + this.addGenericSelector(parsed); + hostname = hostname.slice(1); + unhide ^= 1; + } + var entries = unhide === 0 ? + this.hostnameHide : + this.hostnameDonthide; + var entry = entries[hostname]; + if ( entry === undefined ) { + entry = entries[hostname] = {}; + 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.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; + for ( var selector in selectors ) { + if ( selectors.hasOwnProperty(selector) === false ) { + continue; + } + matches = this.rePlainSelector.exec(selector); + if ( !matches ) { + continue; + } + selectorPrefix = matches[1]; + f = selectorPrefix === selector ? + new FilterPlain(selector) : + new FilterPlainMore(selector); + hash = makeHash(type, selectorPrefix, this.genericHashMask); + bucket = this.lowGenericFilters[hash]; + if ( bucket === undefined ) { + this.lowGenericFilters[hash] = f; + } else if ( bucket instanceof FilterBucket ) { + bucket.add(f); + } else { + this.lowGenericFilters[hash] = new FilterBucket(bucket, f); + } + } + this[what] = {}; +}; + +FilterContainer.prototype.rePlainSelector = /^([#.][\w-]+)/; + +/******************************************************************************/ + FilterContainer.prototype.freezeHostnameSpecifics = function(what, type) { var µburi = µb.URI; var entries = this[what]; @@ -387,215 +548,72 @@ FilterContainer.prototype.freezeEntitySpecifics = function(what, type) { /******************************************************************************/ -FilterContainer.prototype.freezeGenerics = function(what) { - var selectors = this[what + 'Unfiltered']; - //console.log('%d highly generic selectors:\n', selectors.length, selectors.sort().join('\n')); +FilterContainer.prototype.freezeHighGenerics = function(what) { + var selectors = this['highGeneric' + what]; - // ["title"] and ["alt"] will be sorted out manually, these are the most - // common generic selectors, aka "low generics". The rest will be put in - // the high genericity bin. - var lowGenerics = {}; - var lowGenericCount = 0; - var re = /^(([a-z]*)\[(alt|title)="([^"]+)"\])$/; - var i = selectors.length; - var selector, matches; - while ( i-- ) { - selector = selectors[i]; - matches = re.exec(selector); - if ( !matches ) { + // ["title"] and ["alt"] will go in high-low generic bin. + // [href^="..."] wil go in high-mdium generic bin. + // The rest will be put in the high-high generic bin. + var highLowGeneric = {}; + var highLowGenericCount = 0; + var highMediumGeneric = {}; + var highMediumGenericCount = 0; + var highHighGeneric = []; + var reHighLow = /^[a-z]*(\[(?:alt|title)="[^"]+"\])$/; + var reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/; + var matches, hash; + for ( var selector in selectors ) { + if ( selectors.hasOwnProperty(selector) === false ) { continue; } - lowGenerics[matches[1]] = true; - lowGenericCount++; - selectors.splice(i, 1); - } - - // Chunksize is a compromise between number of selectors per chunk (the - // number of selectors querySelector() will have to deal with), and the - // number of chunks (the number of times querySelector() will have to - // be called.) - // Benchmarking shows this is a hot spot performance-wise for "heavy" - // sites (like say, Sports Illustrated, good test case). Not clear what - // better can be done at this point, I doubt javascript-side code can beat - // querySelector(). - var chunkSize = Math.max(selectors.length >>> 3, 8); - var chunkified = [], chunk; - for (;;) { - chunk = selectors.splice(0, chunkSize); - if ( chunk.length === 0 ) { - break; + matches = reHighLow.exec(selector); + if ( matches && matches.length === 2 ) { + highLowGeneric[matches[1]] = true; + highLowGenericCount += 1; + continue; } - chunkified.push(chunk.join(',')); + matches = reHighMedium.exec(selector); + if ( matches && matches.length === 2 ) { + hash = matches[1]; + if ( highMediumGeneric[hash] === undefined ) { + highMediumGeneric[hash] = matches[0]; + } else { + highMediumGeneric[hash] += ',\n' + matches[0]; + } + highMediumGenericCount += 1; + continue; + } + highHighGeneric.push(selector); } - - this[what + 'LowGenerics'] = lowGenerics; - this[what + 'LowGenericCount'] = lowGenericCount; - this[what + 'HighGenerics'] = chunkified; - this[what + 'Unfiltered'] = []; + this['highLowGeneric' + what] = highLowGeneric; + this['highLowGeneric' + what + 'Count'] = highLowGenericCount; + this['highMediumGeneric' + what] = highMediumGeneric; + this['highMediumGeneric' + what + 'Count'] = highMediumGenericCount; + this['highHighGeneric' + what] = highHighGeneric.join(',\n'); + this['highHighGeneric' + what + 'Count'] = highHighGeneric.length; + this['highGeneric' + what] = {}; }; /******************************************************************************/ FilterContainer.prototype.freeze = function() { - this.freezeHostnameSpecifics('hostnameHide', '#'); - this.freezeHostnameSpecifics('hostnameDonthide', '@'); - this.freezeEntitySpecifics('entityHide', '#'); - this.freezeEntitySpecifics('entityDonthide', '@'); - this.freezeGenerics('hide'); - this.freezeGenerics('donthide'); - + this.freezeLowGenerics('lowGenericHide', 0); + this.freezeLowGenerics('lowGenericDonthide', 1); + this.freezeHighGenerics('Hide'); + this.freezeHighGenerics('Donthide'); + this.freezeHostnameSpecifics('hostnameHide', 0); + this.freezeHostnameSpecifics('hostnameDonthide', 1); + this.freezeEntitySpecifics('entityHide', 0); + this.freezeEntitySpecifics('entityDonthide', 1); this.filterParser.reset(); - - // console.debug('Number of duplicate cosmetic filters skipped:', this.duplicateCount); - this.duplicates = {}; this.frozen = true; - //histogram('genericFilters', this.genericFilters); + //histogram('lowGenericFilters', this.lowGenericFilters); //histogram('hostnameFilters', this.hostnameFilters); }; /******************************************************************************/ -// Two Unicode characters: -// T0HHHHHHH HHHHHHHHH -// | | | -// | | | -// | | | -// | | +-- bit 8-0 of FNV -// | | -// | +-- bit 15-9 of FNV -// | -// +-- filter type (0=hide 1=unhide) -// - -var makeHash = function(type, token, mask) { - // Ref: Given a URL, returns a unique 4-character long hash string - // Based on: FNV32a - // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source - // The rest is custom, suited for µBlock. - var i1 = token.length; - var i2 = i1 >> 1; - var i4 = i1 >> 2; - var i8 = i1 >> 3; - var hval = (0x811c9dc5 ^ token.charCodeAt(0)) >>> 0; - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i8); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i4); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i4+i8); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i2); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i2+i8); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i2+i4); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval ^= token.charCodeAt(i1-1); - hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24); - hval >>>= 0; - hval &= mask; - if ( type === '@' ) { - hval |= 0x20000; - } - return String.fromCharCode(hval >>> 9, hval & 0x1FF); -}; - -/******************************************************************************/ - -FilterContainer.prototype.addPlainFilter = function(parsed) { - // Verify whether the plain selector is followed by extra selector stuff - if ( parsed.isPlainMore() ) { - return this.addPlainMoreFilter(parsed); - } - var f = new FilterPlain(parsed.suffix); - var hash = makeHash(parsed.filterType, parsed.suffix, this.genericHashMask); - this.addFilterEntry(this.genericFilters, hash, f); - this.acceptedCount += 1; -}; - -/******************************************************************************/ - -FilterContainer.prototype.addPlainMoreFilter = function(parsed) { - var selectorSuffix = parsed.extractPlain(); - if ( selectorSuffix === '' ) { - return; - } - var f = new FilterPlainMore(parsed.suffix); - var hash = makeHash(parsed.filterType, selectorSuffix, this.genericHashMask); - this.addFilterEntry(this.genericFilters, hash, f); - this.acceptedCount += 1; -}; - -/******************************************************************************/ - -FilterContainer.prototype.addHostnameFilter = function(hostname, parsed) { - var entries = parsed.filterType === '#' ? - this.hostnameHide : - this.hostnameDonthide; - var entry = entries[hostname]; - if ( entry === undefined ) { - entry = entries[hostname] = {}; - } - entry[parsed.suffix] = true; -}; - -/******************************************************************************/ - -FilterContainer.prototype.addEntityFilter = function(hostname, parsed) { - var entries = parsed.filterType === '#' ? - this.entityHide : - this.entityDonthide; - var entity = hostname.slice(0, -2); - var entry = entries[entity]; - if ( entry === undefined ) { - entry = entries[entity] = {}; - } - entry[parsed.suffix] = true; -}; - -/******************************************************************************/ - -FilterContainer.prototype.addSpecificFilter = function(parsed) { - var hostnames = parsed.hostnames; - var i = hostnames.length, hostname; - while ( i-- ) { - hostname = hostnames[i]; - if ( !hostname ) { - continue; - } - // rhill 2014-07-13: new filter class: entity. - if ( hostname.slice(-2) === '.*' ) { - this.addEntityFilter(hostname, parsed); - } else { - this.addHostnameFilter(hostname, parsed); - } - } - this.acceptedCount += 1; -}; - -/******************************************************************************/ - -FilterContainer.prototype.addFilterEntry = function(filterDict, hash, f) { - var bucket = filterDict[hash]; - if ( bucket === undefined ) { - filterDict[hash] = f; - } else if ( bucket instanceof FilterBucket ) { - bucket.add(f); - } else { - filterDict[hash] = new FilterBucket(bucket, f); - } -}; - -/******************************************************************************/ - FilterContainer.prototype.retrieveGenericSelectors = function(request) { if ( µb.userSettings.parseAllABPHideFilters !== true ) { return; @@ -608,15 +626,26 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { var r = { hide: [], - donthide: [], - hideLowGenerics: this.hideLowGenerics, - hideLowGenericCount: this.hideLowGenericCount, - hideHighGenerics: this.hideHighGenerics, - donthideLowGenerics: this.donthideLowGenerics, - donthideLowGenericCount: this.donthideLowGenericCount, - donthideHighGenerics: this.donthideHighGenerics + donthide: [] }; + if ( request.highGenerics ) { + r.highGenerics = { + hideLow: this.highLowGenericHide, + hideLowCount: this.highLowGenericHideCount, + hideMedium: this.highMediumGenericHide, + hideMediumCount: this.highMediumGenericHideCount, + hideHigh: this.highHighGenericHide, + hideHighCount: this.highHighGenericHideCount, + donthideLow: this.highLowGenericDonthide, + donthideLowCount: this.highLowGenericDonthideCount, + donthideMedium: this.highMediumGenericDonthide, + donthideMediumCount: this.highMediumGenericDonthideCount, + donthideHigh: this.highHighGenericDonthide, + donthideHighCount: this.highHighGenericDonthideCount + }; + } + var hash, bucket; var hashMask = this.genericHashMask; var hideSelectors = r.hide; @@ -628,8 +657,8 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { if ( !selector ) { continue; } - hash = makeHash('#', selector, hashMask); - if ( bucket = this.genericFilters[hash] ) { + hash = makeHash(0, selector, hashMask); + if ( bucket = this.lowGenericFilters[hash] ) { bucket.retrieve(selector, hideSelectors); } } @@ -668,15 +697,15 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { }; var hash, bucket; - hash = makeHash('#', r.domain, this.domainHashMask); + hash = makeHash(0, r.domain, this.domainHashMask); if ( bucket = this.hostnameFilters[hash] ) { bucket.retrieve(hostname, r.hide); } - hash = makeHash('#', r.entity, this.domainHashMask); + 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('@', r.domain, this.domainHashMask); + hash = makeHash(1, r.domain, this.domainHashMask); if ( bucket = this.hostnameFilters[hash] ) { bucket.retrieve(hostname, r.donthide); } diff --git a/js/contentscript-end.js b/js/contentscript-end.js index c30c66827..3c5eaad9f 100644 --- a/js/contentscript-end.js +++ b/js/contentscript-end.js @@ -26,13 +26,9 @@ /******************************************************************************/ /******************************************************************************/ -(function() { - -/******************************************************************************/ - // https://github.com/gorhill/httpswitchboard/issues/345 -var messaging = (function(name){ +var uBlockMessaging = (function(name){ var port = null; var dangling = false; var requestId = 1; @@ -127,10 +123,13 @@ var messaging = (function(name){ // ABP cosmetic filters (function() { + var messaging = uBlockMessaging; var queriedSelectors = {}; var injectedSelectors = {}; var classSelectors = null; var idSelectors = null; + var highGenerics = null; + var contextNodes = [document]; var domLoaded = function() { // https://github.com/gorhill/uBlock/issues/14 @@ -155,46 +154,70 @@ var messaging = (function(name){ if ( idSelectors !== null ) { selectors = selectors.concat(idSelectors); } - if ( selectors.length > 0 ) { + if ( selectors.length > 0 || highGenerics === null ) { //console.log('µBlock> ABP cosmetic filters: retrieving CSS rules using %d selectors', selectors.length); messaging.ask({ what: 'retrieveGenericCosmeticSelectors', pageURL: window.location.href, - selectors: selectors + selectors: selectors, + highGenerics: highGenerics === null }, retrieveHandler ); + } else { + retrieveHandler(null); } idSelectors = null; classSelectors = null; }; var retrieveHandler = function(selectors) { - if ( !selectors ) { - return; + //console.debug('µBlock> contextNodes = %o', contextNodes); + if ( selectors && selectors.highGenerics ) { + highGenerics = selectors.highGenerics; } - filterLowGenerics(selectors, 'donthide'); - filterHighGenerics(selectors, 'donthide'); - if ( selectors.donthide.length ) { - var i = selectors.donthide.length; - while ( i-- ) { - injectedSelectors[selectors.donthide[i]] = true; + if ( selectors && selectors.donthide.length ) { + processLowGenerics(selectors.donthide); + } + if ( highGenerics ) { + if ( highGenerics.donthideLowCount ) { + processHighLowGenerics(highGenerics.donthideLow); + } + if ( highGenerics.donthideMediumCount ) { + processHighMediumGenerics(highGenerics.donthideMedium); } } - filterLowGenerics(selectors, 'hide'); - filterHighGenerics(selectors, 'hide'); - reduce(selectors.hide, injectedSelectors); - if ( selectors.hide.length ) { - applyCSS(selectors.hide, 'display', 'none'); + // No such thing as high-high generic exceptions + //if ( highGenerics.donthideHighCount ) { + // processHighHighGenerics(document, highGenerics.donthideHigh); + //} + var hideSelectors = []; + if ( selectors && selectors.hide.length ) { + processLowGenerics(selectors.hide, hideSelectors); + } + if ( highGenerics ) { + if ( highGenerics.hideLowCount ) { + processHighLowGenerics(highGenerics.hideLow, hideSelectors); + } + if ( highGenerics.hideMediumCount ) { + processHighMediumGenerics(highGenerics.hideMedium, hideSelectors); + } + if ( highGenerics.hideHighCount ) { + processHighHighGenerics(highGenerics.hideHigh, hideSelectors); + } + } + if ( hideSelectors.length ) { + applyCSS(hideSelectors, 'display', 'none'); var style = document.createElement('style'); - var text = selectors.hide.join(',\n') + ' {display:none !important;}'; + var text = hideSelectors.join(',\n') + ' {display:none !important;}'; style.appendChild(document.createTextNode(text)); var parent = document.body || document.documentElement; if ( parent ) { parent.appendChild(style); } - //console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', selectors.hide.length, hideStyleText); + //console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text); } + contextNodes.length = 0; }; var applyCSS = function(selectors, prop, value) { @@ -208,88 +231,103 @@ var messaging = (function(name){ } }; - var filterTitleGeneric = function(generics, root, out) { - if ( !root.title.length ) { - return; - } - var selector = '[title="' + root.title + '"]'; - if ( generics[selector] && !injectedSelectors[selector] ) { - out.push(selector); - } - selector = root.tagName + selector; - if ( generics[selector] && !injectedSelectors[selector] ) { - out.push(selector); + var selectNodes = function(selector) { + var targetNodes = []; + var i = contextNodes.length; + var node, nodeList, j; + var doc = document; + while ( i-- ) { + node = contextNodes[i]; + if ( node === doc ) { + return doc.querySelectorAll(selector); + } + targetNodes.push(node); + nodeList = node.querySelectorAll(selector); + j = nodeList.length; + while ( j-- ) { + targetNodes.push(nodeList[j]); + } } + return targetNodes; }; - var filterAltGeneric = function(generics, root, out) { - var alt = root.getAttribute('alt'); - if ( !alt || !alt.length ) { - return; - } - var selector = '[alt="' + root.title + '"]'; - if ( generics[selector] && !injectedSelectors[selector] ) { - out.push(selector); - } - selector = root.tagName + selector; - if ( generics[selector] && !injectedSelectors[selector] ) { - out.push(selector); - } - }; - - var filterLowGenerics = function(selectors, what) { - if ( selectors[what + 'LowGenericCount'] === 0 ) { - return; - } - var out = selectors[what]; - var generics = selectors[what + 'LowGenerics']; - var nodeList, iNode; - // Low generics: ["title"] - nodeList = document.querySelectorAll('[title]'); - iNode = nodeList.length; - while ( iNode-- ) { - filterTitleGeneric(generics, nodeList[iNode], out); - } - // Low generics: ["alt"] - nodeList = document.querySelectorAll('[alt]'); - iNode = nodeList.length; - while ( iNode-- ) { - filterAltGeneric(generics, nodeList[iNode], out); - } - }; - - var filterHighGenerics = function(selectors, what) { - var out = selectors[what]; - var generics = selectors[what + 'HighGenerics']; - var iGeneric = generics.length; + var processLowGenerics = function(generics, out) { + var i = generics.length; var selector; - while ( iGeneric-- ) { - selector = generics[iGeneric]; - if ( injectedSelectors[selector] ) { + while ( i-- ) { + selector = generics[i]; + if ( injectedSelectors[selector] !== undefined ) { continue; } - if ( document.querySelector(selector) !== null ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { out.push(selector); } } }; - var reduce = function(selectors, dict) { - var i = selectors.length, selector, end; - while ( i-- ) { - selector = selectors[i]; - if ( !dict[selector] ) { - if ( end !== undefined ) { - selectors.splice(i+1, end-i); - end = undefined; + var processHighLowGenerics = function(generics, out) { + var attrs = ['title', 'alt']; + var attr, attrValue, nodeList, iNode, node, selector; + while ( attr = attrs.pop() ) { + nodeList = selectNodes('[' + attr + ']'); + iNode = nodeList.length; + while ( iNode-- ) { + node = nodeList[iNode]; + 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); + } + } + selector = node.tagName.toLowerCase() + selector; + if ( injectedSelectors[selector] === undefined && generics[selector] ) { + injectedSelectors[selector] = true; + if ( out !== undefined ) { + out.push(selector); + } } - dict[selector] = true; - } else if ( end === undefined ) { - end = i; } } - if ( end !== undefined ) { - selectors.splice(0, end+1); + }; + + var processHighMediumGenerics = function(generics, out) { + var nodeList = selectNodes('a[href^="http"]'); + var iNode = nodeList.length; + var node, href, pos, hash, selector; + while ( iNode-- ) { + node = nodeList[iNode]; + href = node.getAttribute('href'); + if ( !href ) { continue; } + 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); + } + } + }; + + var processHighHighGenerics = function(generics, out) { + if ( injectedSelectors[generics] !== undefined ) { return; } + if ( document.querySelectorAll(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); + } + } + out.push(selectors.join(',\n')); } }; @@ -362,25 +400,6 @@ var messaging = (function(name){ } }; - var processNodeLists = function(nodeLists) { - var i = nodeLists.length; - var nodeList, j, node; - while ( i-- ) { - nodeList = nodeLists[i]; - idsFromNodeList(nodeList); - classesFromNodeList(nodeList); - j = nodeList.length; - while ( j-- ) { - node = nodeList[j]; - if ( typeof node.querySelectorAll === 'function' ) { - idsFromNodeList(node.querySelectorAll('[id]')); - classesFromNodeList(node.querySelectorAll('[class]')); - } - } - } - retrieveGenericSelectors(); - }; - domLoaded(); // Observe changes in the DOM only if... @@ -390,19 +409,41 @@ var messaging = (function(name){ return; } + var ignoreTags = { + 'style': true, + 'STYLE': true, + 'script': true, + 'SCRIPT': true + }; + var mutationObservedHandler = function(mutations) { var iMutation = mutations.length; - var nodeLists = [], nodeList; + var nodes = []; + var nodeList, iNode, node; while ( iMutation-- ) { nodeList = mutations[iMutation].addedNodes; - if ( nodeList && nodeList.length ) { - nodeLists.push(nodeList); + if ( !nodeList ) { + continue; + } + iNode = nodeList.length; + while ( iNode-- ) { + node = nodeList[iNode]; + if ( typeof node.querySelectorAll !== 'function' ) { + continue; + } + if ( ignoreTags[node.tagName] ) { + continue; + } + contextNodes.push(node); } } - if ( nodeLists.length ) { - processNodeLists(nodeLists); + if ( contextNodes.length !== 0 ) { + idsFromNodeList(selectNodes('[id]')); + classesFromNodeList(selectNodes('[class]')); + retrieveGenericSelectors(); } }; + // https://github.com/gorhill/httpswitchboard/issues/176 var observer = new MutationObserver(mutationObservedHandler); observer.observe(document.body, { @@ -419,6 +460,8 @@ var messaging = (function(name){ // https://github.com/gorhill/uBlock/issues/7 (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. @@ -430,7 +473,7 @@ var messaging = (function(name){ // First pass messaging.ask({ what: 'blockedRequests' }, function(details) { - var elems = document.querySelectorAll('img,iframe'); + var elems = document.querySelectorAll('img,iframe,embed'); var blockedRequests = details.blockedRequests; var collapse = details.collapse; var i = elems.length; @@ -453,8 +496,9 @@ var messaging = (function(name){ // - Elements which resource URL changes var onResourceLoaded = function(ev) { var target = ev.target; - if ( target.tagName.toLowerCase() !== 'iframe' ) { return; } + //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); @@ -464,8 +508,9 @@ var messaging = (function(name){ }; var onResourceFailed = function(ev) { var target = ev.target; - if ( target.tagName.toLowerCase() !== 'img' ) { return; } + //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); @@ -478,5 +523,3 @@ var messaging = (function(name){ })(); /******************************************************************************/ - -})();