diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index e7158af6b..fdafc496b 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -60,6 +60,7 @@ vAPI.supportsUserStylesheets = chrome.extensionTypes instanceof Object && chrome.extensionTypes.CSSOrigin instanceof Object && 'USER' in chrome.extensionTypes.CSSOrigin; +vAPI.insertCSS = chrome.tabs.insertCSS; var noopFunc = function(){}; diff --git a/platform/chromium/vapi-usercss.js b/platform/chromium/vapi-usercss.js index e1da31e54..a02b92882 100644 --- a/platform/chromium/vapi-usercss.js +++ b/platform/chromium/vapi-usercss.js @@ -119,7 +119,7 @@ vAPI.DOMFilterer = function() { }; vAPI.DOMFilterer.prototype = { - reHideStyle: /^display: none !important;$/, + reHideStyle: /^display:none!important;$/, // https://www.w3.org/community/webed/wiki/CSS/Selectors#Combinators reCSSCombinators: /[ >+~]/, @@ -224,7 +224,7 @@ vAPI.DOMFilterer.prototype = { selectors; if ( selectorsStr.length === 0 ) { return; } - vAPI.userStylesheet.add(selectorsStr + '\n{ ' + declarations + ' }'); + vAPI.userStylesheet.add(selectorsStr + '\n{' + declarations + '}'); this.commit(); this.triggerListeners('declarative', selectorsStr); @@ -353,9 +353,9 @@ vAPI.DOMFilterer.prototype = { attr.length !== 0 && attr.charCodeAt(attr.length - 1) !== 0x3B /* ';' */ ) { - attr += '; '; + attr += ';'; } - node.setAttribute('style', attr + 'display: none !important;'); + node.setAttribute('style', attr + 'display:none!important;'); } this.hiddenNodesetToProcess.clear(); }, @@ -384,7 +384,7 @@ vAPI.DOMFilterer.prototype = { if ( this.hideNodeStylesheet === false ) { this.hideNodeStylesheet = true; vAPI.userStylesheet.add( - '[' + this.hideNodeAttr + ']\n{ display: none !important; }' + '[' + this.hideNodeAttr + ']\n{display:none!important;}' ); } }, diff --git a/platform/webext/vapi-usercss.js b/platform/webext/vapi-usercss.js index 46a9b64c2..35c7a0370 100644 --- a/platform/webext/vapi-usercss.js +++ b/platform/webext/vapi-usercss.js @@ -85,7 +85,11 @@ vAPI.DOMFilterer.prototype = { var userStylesheet = vAPI.userStylesheet, addedSelectors = []; for ( var entry of this.addedCSSRules ) { - if ( this.disabled === false && entry.lazy ) { + if ( + this.disabled === false && + entry.lazy && + entry.injected === false + ) { userStylesheet.add( entry.selectors + '\n{' + entry.declarations + '}' ); @@ -114,15 +118,21 @@ vAPI.DOMFilterer.prototype = { ? selectors.join(',\n') : selectors; if ( selectorsStr.length === 0 ) { return; } + if ( details === undefined ) { details = {}; } var entry = { selectors: selectorsStr, declarations, - lazy: details !== undefined && details.lazy === true, - internal: details && details.internal === true + lazy: details.lazy === true, + internal: details.internal === true, + injected: details.injected === true }; this.addedCSSRules.add(entry); this.filterset.add(entry); - if ( this.disabled === false && entry.lazy !== true ) { + if ( + this.disabled === false && + entry.lazy !== true && + entry.injected !== true + ) { vAPI.userStylesheet.add(selectorsStr + '\n{' + declarations + '}'); } this.commit(); @@ -162,7 +172,7 @@ vAPI.DOMFilterer.prototype = { this.hideNodeStylesheet = true; this.addCSSRule( '[' + this.hideNodeId + ']', - 'display: none !important;', + 'display:none!important;', { internal: true } ); } diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 54e61205e..96643ffc8 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -455,6 +455,7 @@ vAPI.DOMFilterer = (function() { ]); } this.raw = o.raw; + this.cost = 0; this.selector = o.selector; this.tasks = []; var tasks = o.tasks; @@ -520,7 +521,7 @@ vAPI.DOMFilterer = (function() { if ( o.pseudoclass ) { this.domFilterer.addCSSRule( o.raw, - 'display: none !important;' + 'display:none!important;' ); mustCommit = true; continue; @@ -575,16 +576,24 @@ vAPI.DOMFilterer = (function() { this.addedNodes = this.removedNodes = false; - var afterResultset = new Set(); + var afterResultset = new Set(), + t0 = Date.now(), t1, pselector; for ( entry of this.selectors ) { - nodes = entry[1].exec(); + pselector = entry[1]; + if ( pselector.cost > 100 ) { continue; } + nodes = pselector.exec(); i = nodes.length; while ( i-- ) { node = nodes[i]; this.domFilterer.hideNode(node); afterResultset.add(node); } + t1 = Date.now(); + pselector.cost += t1 - t0; + t0 = t1; + // TODO: Consider adding logging ability to report disabled + // precedural filter in the logger. } if ( afterResultset.size !== currentResultset.size ) { this.addedNodesHandlerMissCount = 0; @@ -982,7 +991,7 @@ vAPI.domSurveyor = (function() { if ( Array.isArray(selectors) && selectors.length !== 0 ) { domFilterer.addCSSRule( selectors, - 'display: none !important;', + 'display:none!important;', { type: 'simple' } ); mustCommit = true; @@ -991,7 +1000,7 @@ vAPI.domSurveyor = (function() { if ( Array.isArray(selectors) && selectors.length !== 0 ) { domFilterer.addCSSRule( selectors, - 'display: none !important;', + 'display:none!important;', { type: 'complex' } ); mustCommit = true; @@ -1265,6 +1274,8 @@ vAPI.domSurveyor = (function() { return; } + var injected = cfeDetails.rulesInjected === true; + if ( response.noCosmeticFiltering ) { vAPI.domFilterer = null; vAPI.domSurveyor = null; @@ -1276,47 +1287,45 @@ vAPI.domSurveyor = (function() { domFilterer.exceptions = cfeDetails.exceptionFilters; domFilterer.addCSSRule( cfeDetails.declarativeFilters, - 'display: none !important;' + 'display:none!important;', + { injected: injected } ); domFilterer.addCSSRule( cfeDetails.highGenericHideSimple, - 'display: none !important;', - { type: 'simple', lazy: true } + 'display:none!important;', + { type: 'simple', lazy: true, injected: injected } ); domFilterer.addCSSRule( cfeDetails.highGenericHideComplex, - 'display: none !important;', - { type: 'complex', lazy: true } + 'display:none!important;', + { type: 'complex', lazy: true, injected: injected } ); domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters); } - if ( cfeDetails.netFilters.length !== 0 ) { + if ( cfeDetails.netFilters.length !== 0 && injected !== true ) { vAPI.userStylesheet.add( - cfeDetails.netFilters + '\n{ display: none !important; }'); + cfeDetails.netFilters + '\n{display:none!important;}'); } vAPI.userStylesheet.apply(); - var parent = document.head || document.documentElement; - if ( parent ) { - // Library of resources is located at: - // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt - if ( cfeDetails.scripts ) { - // Have the injected script tag remove itself when execution completes: - // to keep DOM as clean as possible. - var text = cfeDetails.scripts + - "\n" + - "(function() {\n" + - " var c = document.currentScript,\n" + - " p = c && c.parentNode;\n" + - " if ( p ) {\n" + - " p.removeChild(c);\n" + - " }\n" + - "})();"; - vAPI.injectScriptlet(document, text); - vAPI.injectedScripts = text; - } + // Library of resources is located at: + // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt + if ( cfeDetails.scripts ) { + // Have the injected script tag remove itself when execution completes: + // to keep DOM as clean as possible. + var text = cfeDetails.scripts + + "\n" + + "(function() {\n" + + " var c = document.currentScript,\n" + + " p = c && c.parentNode;\n" + + " if ( p ) {\n" + + " p.removeChild(c);\n" + + " }\n" + + "})();"; + vAPI.injectScriptlet(document, text); + vAPI.injectedScripts = text; } // https://github.com/chrisaljoudi/uBlock/issues/587 diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 70de97e2d..4d6ef1b8a 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -625,6 +625,8 @@ var FilterContainer = function() { this.netSelectorCacheCountMax = netSelectorCacheHighWaterMark; this.selectorCacheTimer = null; + this.supportsUserStylesheets = vAPI.supportsUserStylesheets; + // generic exception filters this.genericDonthideSet = new Set(); @@ -1255,8 +1257,8 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, writer if ( compiled === undefined ) { return; } // https://github.com/chrisaljoudi/uBlock/issues/497 - // All generic exception filters are put in the same bucket: they are - // expected to be very rare. + // All generic exception filters are put in the same bucket: they are + // expected to be very rare. writer.push([ 7 /* g1 */, compiled ]); }; @@ -1964,6 +1966,26 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { /******************************************************************************/ +FilterContainer.prototype.injectHideRules = function( + tabId, + frameId, + selectors, + runAt +) { + var details = { + code: '', + cssOrigin: 'user', + frameId: frameId, + runAt: runAt + }; + for ( var selector of selectors ) { + details.code = selector + '\n{display:none!important;}'; + vAPI.insertCSS(tabId, details); + } +}; + +/******************************************************************************/ + FilterContainer.prototype.retrieveDomainSelectors = function( request, sender, @@ -1973,6 +1995,9 @@ FilterContainer.prototype.retrieveDomainSelectors = function( console.time('cosmeticFilteringEngine.retrieveDomainSelectors'); + // TODO: consider using MRUCache to quickly lookup all the previously + // looked-up data. + var hostname = this.µburi.hostnameFromURI(request.locationURL), domain = this.µburi.domainFromHostname(hostname) || hostname, pos = domain.indexOf('.'), @@ -1998,7 +2023,7 @@ FilterContainer.prototype.retrieveDomainSelectors = function( netFilters: '', proceduralFilters: [], scripts: undefined, - cssRulesInjected: false + rulesInjected: false }; if ( options.noCosmeticFiltering !== true ) { @@ -2027,6 +2052,22 @@ FilterContainer.prototype.retrieveDomainSelectors = function( if ( (bucket = this.specificFilters.get('!' + this.noDomainHash)) ) { bucket.retrieve(hostname, exceptionSet); } + + // Specific exception procedural cosmetic filters. + if ( (bucket = this.proceduralFilters.get('!' + domainHash)) ) { + bucket.retrieve(hostname, exceptionSet); + } + // Specific entity-based exception procedural cosmetic filters. + if ( entityHash !== undefined ) { + if ( (bucket = this.proceduralFilters.get('!' + entityHash)) ) { + bucket.retrieve(entity, exceptionSet); + } + } + // Special bucket for those filters without a valid + // domain name as per PSL. + if ( (bucket = this.proceduralFilters.get('!' + this.noDomainHash)) ) { + bucket.retrieve(hostname, exceptionSet); + } if ( exceptionSet.size !== 0 ) { r.exceptionFilters = µb.arrayFrom(exceptionSet); } @@ -2126,6 +2167,27 @@ FilterContainer.prototype.retrieveDomainSelectors = function( r.netFilters = netFilters.join(',\n'); } + if ( + this.supportsUserStylesheets && + sender instanceof Object && + sender.tab instanceof Object && + typeof sender.frameId === 'number' + ) { + this.injectHideRules( + sender.tab.id, + sender.frameId, + [ r.declarativeFilters.join(',\n'), r.netFilters ], + 'document_start' + ); + this.injectHideRules( + sender.tab.id, + sender.frameId, + [ r.highGenericHideSimple, r.highGenericHideComplex ], + 'document_end' + ); + r.rulesInjected = true; + } + console.timeEnd('cosmeticFilteringEngine.retrieveDomainSelectors'); return r; diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index 569212868..bb3a01a3b 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -98,34 +98,13 @@ var fromNetFilter = function(details) { var fromCosmeticFilter = function(details) { var match = /^#@?#/.exec(details.rawFilter), prefix = match[0], - needle = details.rawFilter.slice(prefix.length), - selector = needle; + selector = details.rawFilter.slice(prefix.length); - // With low generic simple cosmetic filters, the class or id prefix - // character is not part of the compiled data. So we must be ready to - // look-up version of the selector without the prefix character. - var idOrClassPrefix = needle.charAt(0), - cssPrefixMatcher; - if ( idOrClassPrefix === '#' ) { - cssPrefixMatcher = '#?'; - needle = needle.slice(1); - } else if ( idOrClassPrefix === '.' ) { - cssPrefixMatcher = '\\.?'; - needle = needle.slice(1); - } else { - idOrClassPrefix = ''; - cssPrefixMatcher = ''; - } - - // https://github.com/gorhill/uBlock/issues/3101 - // Use `m` flag for efficient regex execution. - var reFilter = new RegExp( - '^\\[\\d,[^\\n]*\\\\*"' + - cssPrefixMatcher + - reEscapeCosmetic(needle) + - '\\\\*"[^\\n]*\\]$', - 'gm' - ); + // The longer the needle, the lower the number of false positives. + var needles = selector.match(/\w+/g).sort(function(a, b) { + return b.length - a.length; + }); + var reNeedle = new RegExp(needles[0], 'g'); var reHostname = new RegExp( '^' + @@ -159,7 +138,7 @@ var fromCosmeticFilter = function(details) { } var response = Object.create(null), - assetKey, entry, content, found, fargs; + assetKey, entry, content, found, beg, end, fargs; for ( assetKey in listEntries ) { entry = listEntries[assetKey]; @@ -169,28 +148,61 @@ var fromCosmeticFilter = function(details) { filterClassSeparator.length ); found = undefined; - while ( (match = reFilter.exec(content)) !== null ) { - fargs = JSON.parse(match[0]); + while ( (match = reNeedle.exec(content)) !== null ) { + beg = content.lastIndexOf('\n', match.index); + if ( beg === -1 ) { beg = 0; } + end = content.indexOf('\n', reNeedle.lastIndex); + if ( end === -1 ) { end = content.length; } + fargs = JSON.parse(content.slice(beg, end)); switch ( fargs[0] ) { case 0: // id-based + if ( + fargs[1] === selector.slice(1) && + selector.charAt(0) === '#' + ) { + found = prefix + selector; + } + break; + case 1: // id-based + if ( + fargs[2] === selector.slice(1) && + selector.charAt(0) === '#' + ) { + found = prefix + selector; + } + break; case 2: // class-based - found = prefix + selector; + if ( + fargs[1] === selector.slice(1) && + selector.charAt(0) === '.' + ) { + found = prefix + selector; + } + break; + case 3: + if ( + fargs[2] === selector.slice(1) && + selector.charAt(0) === '.' + ) { + found = prefix + selector; + } break; case 4: case 5: case 7: - found = prefix + selector; - break; - case 1: - case 3: - if ( fargs[2] === selector ) { + if ( fargs[1] === selector ) { found = prefix + selector; } break; case 6: case 8: case 9: - if ( fargs[3] !== selector ) { break; } + if ( + fargs[0] === 8 && fargs[3] !== selector || + fargs[0] === 9 && JSON.parse(fargs[3]).raw !== selector + ) { + break; + } if ( fargs[2] === '' || reHostname.test(fargs[2]) === true || @@ -219,15 +231,6 @@ var fromCosmeticFilter = function(details) { }); }; -// https://github.com/gorhill/uBlock/issues/2666 -// Raw filters in compiled filter lists may have been JSON-stringified one or -// multiple times. - -var reEscapeCosmetic = function(s) { - return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/"/g, '\\\\*"'); -}; - /******************************************************************************/ onmessage = function(e) { // jshint ignore:line