diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index b17d147bb..9c6379625 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -189,7 +189,7 @@ body:not(.popupOn) #content tr.canMtx td:nth-of-type(2) { body:not(.popupOn) #content tr.canMtx td:nth-of-type(2):hover { background: #ccc; } -#content tr.cat_net[data-filter] td:nth-of-type(3) { +#content tr.canLookup td:nth-of-type(3) { cursor: zoom-in; } #content tr.cat_net td:nth-of-type(4), diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 98abdd891..3d0781f51 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -117,7 +117,7 @@ var FilterPlainMore = function(s) { }; FilterPlainMore.prototype.retrieve = function(s, out) { - if ( s === this.s.slice(0, s.length) ) { + if ( this.s.lastIndexOf(s, 0) === 0 ) { out.push(this.s); } }; diff --git a/src/js/document-blocked.js b/src/js/document-blocked.js index dc0b145be..306bcd9b2 100644 --- a/src/js/document-blocked.js +++ b/src/js/document-blocked.js @@ -44,7 +44,18 @@ var details = {}; (function() { var onReponseReady = function(response) { - var lists = response.matches; + if ( typeof response !== 'object' ) { + return; + } + var lists; + for ( var rawFilter in response ) { + if ( response.hasOwnProperty(rawFilter) === false ) { + continue; + } + lists = response[rawFilter]; + break; + } + if ( Array.isArray(lists) === false || lists.length === 0 ) { return; } @@ -72,8 +83,9 @@ var details = {}; }; messager.send({ - what: 'reverseLookupFilter', - filter: details.fc + what: 'listsFromNetFilter', + compiledFilter: details.fc, + rawFilter: details.fs }, onReponseReady); })(); diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index dd239eb27..d2ed9c8c3 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -389,10 +389,7 @@ var createHiddenTextNode = function(text) { var createGap = function(tabId, url) { var tr = createRow('1'); - tr.classList.add('tab'); - tr.classList.add('canMtx'); - tr.classList.add('tab_' + tabId); - tr.classList.add('maindoc'); + tr.classList.add('tab', 'canMtx', 'tab_' + tabId, 'maindoc'); tr.cells[firstVarDataCol].textContent = url; tbody.insertBefore(tr, tbody.firstChild); }; @@ -400,12 +397,13 @@ var createGap = function(tabId, url) { /******************************************************************************/ var renderNetLogEntry = function(tr, entry) { + var trcl = tr.classList; var filter = entry.d0; var type = entry.d1; var url = entry.d2; var td; - tr.classList.add('canMtx'); + trcl.add('canMtx'); // If the request is that of a root frame, insert a gap in the table // in order to visually separate entries for different documents. @@ -423,7 +421,7 @@ var renderNetLogEntry = function(tr, entry) { var filterCat = filter.slice(0, 3); if ( filterCat.charAt(2) === ':' ) { - tr.classList.add(filterCat.slice(0, 2)); + trcl.add(filterCat.slice(0, 2)); } var filteringType = filterCat.charAt(0); @@ -432,7 +430,11 @@ var renderNetLogEntry = function(tr, entry) { filter = filter.slice(3); if ( filteringType === 's' ) { td.textContent = filterDecompiler.toString(filter); + trcl.add('canLookup'); tr.setAttribute('data-filter', filter); + } else if ( filteringType === 'c' ) { + td.textContent = filter; + trcl.add('canLookup'); } else { td.textContent = filter; } @@ -441,13 +443,13 @@ var renderNetLogEntry = function(tr, entry) { td = tr.cells[3]; var filteringOp = filterCat.charAt(1); if ( filteringOp === 'b' ) { - tr.classList.add('blocked'); + trcl.add('blocked'); td.textContent = '--'; } else if ( filteringOp === 'a' ) { - tr.classList.add('allowed'); + trcl.add('allowed'); td.textContent = '++'; } else if ( filteringOp === 'n' ) { - tr.classList.add('nooped'); + trcl.add('nooped'); td.textContent = '**'; } else { td.textContent = ''; @@ -1262,7 +1264,6 @@ var netFilteringManager = (function() { /******************************************************************************/ var reverseLookupManager = (function() { - var rawFilter = ''; var reSentence1 = /\{\{filter\}\}/g; var sentence1Template = vAPI.i18n('loggerStaticFilteringFinderSentence1'); @@ -1284,16 +1285,12 @@ var reverseLookupManager = (function() { ev.stopPropagation(); }; - var reverseLookupDone = function(response) { - var lists = response.matches; - if ( Array.isArray(lists) === false ) { - return; + var nodeFromFilter = function(filter, lists) { + if ( Array.isArray(lists) === false || lists.length === 0 ) { + return null; } - - var dialog = filterFinderDialog.querySelector('.dialog'); - var p = dialog.querySelector('p'); - removeAllChildren(p); var node; + var p = document.createElement('p'); reSentence1.lastIndex = 0; var matches = reSentence1.exec(sentence1Template); @@ -1302,13 +1299,12 @@ var reverseLookupManager = (function() { } else { node = uDom.nodeFromSelector('#filterFinderDialogSentence1 > span').cloneNode(true); node.childNodes[0].textContent = sentence1Template.slice(0, matches.index); - node.childNodes[1].textContent = rawFilter; + node.childNodes[1].textContent = filter; node.childNodes[2].textContent = sentence1Template.slice(reSentence1.lastIndex); } p.appendChild(node); - var ul = dialog.querySelector('ul'); - removeAllChildren(ul); + var ul = document.createElement('ul'); var list, li; for ( var i = 0; i < lists.length; i++ ) { list = lists[i]; @@ -1324,6 +1320,26 @@ var reverseLookupManager = (function() { li.appendChild(node); ul.appendChild(li); } + p.appendChild(ul); + + return p; + }; + + var reverseLookupDone = function(response) { + if ( typeof response !== 'object' ) { + return; + } + + var dialog = filterFinderDialog.querySelector('.dialog'); + removeAllChildren(dialog); + + for ( var filter in response ) { + var p = nodeFromFilter(filter, response[filter]); + if ( p === null ) { + continue; + } + dialog.appendChild(p); + } document.body.appendChild(filterFinderDialog); filterFinderDialog.addEventListener('click', onClick, true); @@ -1331,15 +1347,24 @@ var reverseLookupManager = (function() { var toggleOn = function(ev) { var row = ev.target.parentElement; - var filter = row.getAttribute('data-filter') || ''; - if ( filter === '' ) { + var rawFilter = row.cells[2].textContent; + if ( rawFilter === '' ) { return; } - rawFilter = row.cells[2].textContent; - messager.send({ - what: 'reverseLookupFilter', - filter: filter - }, reverseLookupDone); + + if ( row.classList.contains('cat_net') ) { + messager.send({ + what: 'listsFromNetFilter', + compiledFilter: row.getAttribute('data-filter') || '', + rawFilter: rawFilter + }, reverseLookupDone); + } else if ( row.classList.contains('cat_cosmetic') ) { + messager.send({ + what: 'listsFromCosmeticFilter', + hostname: row.getAttribute('data-hn-frame') || '', + rawFilter: rawFilter, + }, reverseLookupDone); + } }; var toggleOff = function() { @@ -1656,7 +1681,7 @@ uDom.onLoad(function() { uDom('#maxEntries').on('change', onMaxEntriesChanged); uDom('#content table').on('click', 'tr.canMtx > td:nth-of-type(2)', popupManager.toggleOn); uDom('#content').on('click', 'tr.cat_net > td:nth-of-type(4)', netFilteringManager.toggleOn); - uDom('#content').on('click', 'tr[data-filter] > td:nth-of-type(3)', reverseLookupManager.toggleOn); + uDom('#content').on('click', 'tr.canLookup > td:nth-of-type(3)', reverseLookupManager.toggleOn); }); /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 2c0d9649c..215fbd389 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -66,8 +66,20 @@ var onMessage = function(request, sender, callback) { µb.reloadAllFilters(callback); return; - case 'reverseLookupFilter': - µb.staticFilteringReverseLookup.lookup(request.filter, callback); + case 'listsFromNetFilter': + µb.staticFilteringReverseLookup.fromNetFilter( + request.compiledFilter, + request.rawFilter, + callback + ); + return; + + case 'listsFromCosmeticFilter': + µb.staticFilteringReverseLookup.fromCosmeticFilter( + request.hostname, + request.rawFilter, + callback + ); return; default: @@ -1351,7 +1363,9 @@ var logCosmeticFilters = function(tabId, details) { 'cosmetic', 'cb:##' + selectors[i], 'dom', - details.pageURL + details.frameURL, + null, + details.frameHostname ); } }; diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index f484633f9..7a5fb145a 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -29,35 +29,164 @@ var listEntries = Object.create(null); /******************************************************************************/ -var lookup = function(details) { - var matches = []; +// Helpers + +var rescape = function(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; + +/******************************************************************************/ + +var fromNetFilter = function(details) { + var lists = []; + var entry, pos; for ( var path in listEntries ) { entry = listEntries[path]; if ( entry === undefined ) { continue; } - pos = entry.content.indexOf(details.filter); + pos = entry.content.indexOf(details.compiledFilter); if ( pos === -1 ) { continue; } - matches.push({ + lists.push({ title: entry.title, supportURL: entry.supportURL }); } + var response = {}; + response[details.rawFilter] = lists; + postMessage({ id: details.id, - response: { - filter: details.filter, - matches: matches - } + response: response }); }; /******************************************************************************/ +// Looking up filter lists from a cosmetic filter is a bit more complicated +// than with network filters: +// +// The filter is its raw representation, not its compiled version. This is +// because the cosmetic filtering engine can't translate a live cosmetic +// filter into its compiled version. Reason is I do not want to burden +// cosmetic filtering with the resource overhead of being able to re-compile +// live cosmetic filters. I want the cosmetic filtering code to be left +// completely unaffected by reverse lookup requirements. +// +// Mainly, given a CSS selector and a hostname as context, we will derive +// various versions of compiled filters and see if there are matches. This way +// the whole CPU cost is incurred by the reverse lookup code -- in a worker +// thread, and the cosmetic filtering engine incurred zero cost. +// +// For this though, the reverse lookup code here needs some knowledge of +// the inners of the cosmetic filtering engine. +// FilterContainer.fromCompiledContent() is our reference code to create +// the various compiled versions. + +var fromCosmeticFilter = function(details) { + var filter = details.rawFilter; + var exception = filter.lastIndexOf('#@#', 0) === 0; + + filter = exception ? filter.slice(3) : filter.slice(2); + + var candidates = Object.create(null); + var response = Object.create(null); + + // First step: assuming the filter is generic, find out its compiled + // representation. + // Reference: FilterContainer.compileGenericSelector(). + var reStr = ''; + var matches = rePlainSelector.exec(filter); + if ( matches ) { + if ( matches[0] === filter ) { // simple CSS selector + reStr = rescape('c\vlg\v') + '\\w+' + rescape('\v' + filter); + } else { // complex CSS selector + reStr = rescape('c\vlg+\v') + '\\w+' + rescape('\v' + filter); + } + } else if ( reHighLow.test(filter) ) { // [alt] or [title] + reStr = rescape('c\vhlg0\v' + filter) + '(?:\\n|$)'; + } else if ( reHighMedium.test(filter) ) { // [href^="..."] + reStr = rescape('c\vhmg0\v') + '\\w+' + rescape('\v' + filter); + } else { // all else + reStr = rescape('c\vhhg0\v' + filter); + } + candidates[details.rawFilter] = new RegExp(reStr + '(?:\\n|$)'); + + var pos; + var domain = details.domain; + var hostname = details.hostname; + + if ( hostname !== '' ) { + for ( ;; ) { + candidates[hostname + '##' + filter] = new RegExp( + rescape('c\vh\v') + + '\\w+' + + rescape('\v' + hostname + '\v' + filter) + + '(?:\\n|$)' + ); + // If there is no valid domain, there won't be any other + // version of this hostname-based filter. + if ( domain === '' ) { + break; + } + if ( hostname === domain ) { + break; + } + pos = hostname.indexOf('.'); + if ( pos === -1 ) { + break; + } + hostname = hostname.slice(pos + 1); + } + } + + // Entity-based + pos = domain.indexOf('.'); + if ( pos !== -1 ) { + var entity = domain.slice(0, pos); + candidates[entity + '.*##' + filter] = new RegExp( + rescape('c\ve\v' + entity + '\v' + filter) + + '(?:\\n|$)' + ); + } + + var re, path, entry; + for ( var candidate in candidates ) { + re = candidates[candidate]; + for ( path in listEntries ) { + entry = listEntries[path]; + if ( entry === undefined ) { + continue; + } + if ( re.test(entry.content) === false ) { + continue; + } + if ( response[candidate] === undefined ) { + response[candidate] = []; + } + response[candidate].push({ + title: entry.title, + supportURL: entry.supportURL + }); + } + } + + postMessage({ + id: details.id, + response: response + }); +}; + +var rePlainSelector = /^([#.][\w-]+)/; +var reHighLow = /^[a-z]*\[(?:alt|title)="[^"]+"\]$/; +var reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/; + +/******************************************************************************/ + onmessage = function(e) { var msg = e.data; @@ -70,8 +199,12 @@ onmessage = function(e) { listEntries[msg.details.path] = msg.details; break; - case 'reverseLookup': - lookup(msg); + case 'fromNetFilter': + fromNetFilter(msg); + break; + + case 'fromCosmeticFilter': + fromCosmeticFilter(msg); break; } }; diff --git a/src/js/reverselookup.js b/src/js/reverselookup.js index e680fcdba..9e1449070 100644 --- a/src/js/reverselookup.js +++ b/src/js/reverselookup.js @@ -123,11 +123,16 @@ var initWorker = function(callback) { /******************************************************************************/ -var lookup = function(compiledFilter, callback) { +var fromNetFilter = function(compiledFilter, rawFilter, callback) { if ( typeof callback !== 'function' ) { return; } + if ( compiledFilter === '' || rawFilter === '' ) { + callback(); + return; + } + if ( workerTTLTimer !== null ) { clearTimeout(workerTTLTimer); workerTTLTimer = null; @@ -136,9 +141,46 @@ var lookup = function(compiledFilter, callback) { var onWorkerReady = function() { var id = messageId++; var message = { - what: 'reverseLookup', + what: 'fromNetFilter', id: id, - filter: compiledFilter + compiledFilter: compiledFilter, + rawFilter: rawFilter + }; + pendingResponses[id] = callback; + worker.postMessage(message); + + // The worker will be shutdown after n minutes without being used. + workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); + }; + + initWorker(onWorkerReady); +}; + +/******************************************************************************/ + +var fromCosmeticFilter = function(hostname, rawFilter, callback) { + if ( typeof callback !== 'function' ) { + return; + } + + if ( rawFilter === '' ) { + callback(); + return; + } + + if ( workerTTLTimer !== null ) { + clearTimeout(workerTTLTimer); + workerTTLTimer = null; + } + + var onWorkerReady = function() { + var id = messageId++; + var message = { + what: 'fromCosmeticFilter', + id: id, + domain: µBlock.URI.domainFromHostname(hostname), + hostname: hostname, + rawFilter: rawFilter }; pendingResponses[id] = callback; worker.postMessage(message); @@ -165,7 +207,8 @@ var resetLists = function() { /******************************************************************************/ return { - lookup: lookup, + fromNetFilter: fromNetFilter, + fromCosmeticFilter: fromCosmeticFilter, resetLists: resetLists, shutdown: stopWorker }; diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 62f6b29a5..8ef5d987d 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -83,7 +83,8 @@ var localMessager = vAPI.messaging.channel('scriptlets'); localMessager.send({ what: 'logCosmeticFilteringData', - pageURL: window.location.href, + frameURL: window.location.href, + frameHostname: window.location.hostname, matchedSelectors: matchedSelectors }, function() { localMessager.close(); diff --git a/src/logger-ui.html b/src/logger-ui.html index a32392628..0ed161d06 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -80,10 +80,7 @@
-
-

- -
+