diff --git a/src/css/popup.css b/src/css/popup.css index 8a2f2e665..6cf491d7e 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -4,7 +4,6 @@ body { float: left; font: 13px sans-serif; margin: 0; - overflow: hidden; padding: 0; white-space: nowrap; } @@ -32,11 +31,19 @@ a { body > div { background-color: transparent; display: inline-block; + position: relative; vertical-align: top; } +body > div:nth-of-type(1) { + direction: rtl; /* scroll bar to the left */ + overflow-y: hidden; + overflow-x: hidden; + } +body.dynamicFilteringEnabled > div:nth-of-type(1) { + overflow-y: auto; + } body > div:nth-of-type(2) { padding: 4px 12px 0 5px; - position: relative; } p { margin: 16px 0; @@ -131,39 +138,51 @@ body.dynamicFilteringEnabled #dynamicFilteringToggler::before { #dynamicFilteringContainer { border: 0; - direction: rtl; font-size: 12px; margin: 0; - overflow: hidden; padding: 0; text-align: right; width: 7px; } body.dynamicFilteringEnabled #dynamicFilteringContainer { display: block; - width: 200px; + width: 280px; } #dynamicFilteringContainer > div { + background-color: transparent; border: 0; direction: ltr; margin: 0; padding: 0; - width: 200px; + width: 280px; + } +body.dynamicFilteringEnabled #dynamicFilteringContainer > div { + background-color: #e6e6e6; + } +body.dynamicFilteringEnabled #dynamicFilteringContainer > div:hover { + background-color: #f0f0f0; + } +#dynamicFilteringContainer > div#privacyInfo { + background-color: white; + color: gray; + padding: 4px 0; + text-align: center; } #dynamicFilteringContainer > div > span { background-color: transparent; border: none; border-bottom: 1px solid white; box-sizing: border-box; - color: gray; + color: transparent; display: inline-block; - height: 2em; - line-height: 2em; + height: 24px; + line-height: 28px; pointer-events: none; vertical-align: top; } body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span { - background-color: #e6e6e6; + color: #000; + overflow: hidden; pointer-events: auto; } #dynamicFilteringContainer > div > span:nth-of-type(1) { @@ -178,9 +197,14 @@ body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span { #dynamicFilteringContainer > div > span:nth-of-type(3) { border-left: 1px solid white; cursor: pointer; + text-align: center; width: 16%; } +#dynamicFilteringContainer > div.isDomain > span:nth-of-type(1) { + font-weight: bold; + } body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span:nth-of-type(3) { + color: #444; pointer-events: auto; } #dynamicFilteringContainer span.blocked[data-src] { @@ -188,7 +212,9 @@ body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span:nth-of-type } #dynamicFilteringContainer span.ownFilter[data-src] { background-color: #bbb; + color: white; } #dynamicFilteringContainer span.blocked.ownFilter[data-src] { background-color: #f66; + color: white; } \ No newline at end of file diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index 6e137da55..d1d8bfe1a 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -264,6 +264,12 @@ Matrix.prototype.mustBlockOrAllow = function() { /******************************************************************************/ +Matrix.prototype.mustAbort = function() { + return this.r === 3; +}; + +/******************************************************************************/ + Matrix.prototype.toFilterString = function() { if ( this.type === '' ) { return ''; diff --git a/src/js/messaging.js b/src/js/messaging.js index db662c506..0cf8b7ee4 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -103,46 +103,96 @@ var µb = µBlock; /******************************************************************************/ -var getDynamicFilterResults = function(scope) { +var getHostnameDict = function(hostnameToCountMap) { var r = {}; - var dFiltering = µb.dynamicNetFilteringEngine; - r['image'] = dFiltering.evaluateCellZY(scope, '*', 'image').toFilterString(); - r['inline-script'] = dFiltering.evaluateCellZY(scope, '*', 'inline-script').toFilterString(); - r['1p-script'] = dFiltering.evaluateCellZY(scope, '*', '1p-script').toFilterString(); - r['3p-script'] = dFiltering.evaluateCellZY(scope, '*', '3p-script').toFilterString(); - r['3p-frame'] = dFiltering.evaluateCellZY(scope, '*', '3p-frame').toFilterString(); + var µburi = µb.URI; + var domain, counts; + for ( var hostname in hostnameToCountMap ) { + if ( hostnameToCountMap.hasOwnProperty(hostname) === false ) { + continue; + } + if ( r.hasOwnProperty(hostname) ) { + continue; + } + domain = µburi.domainFromHostname(hostname); + counts = hostnameToCountMap[domain] || 0; + r[domain] = { + domain: domain, + blockCount: counts & 0xFFFF, + allowCount: counts >>> 16 & 0xFFFF + }; + if ( hostname === domain ) { + continue; + } + counts = hostnameToCountMap[hostname] || 0; + r[hostname] = { + domain: domain, + blockCount: counts & 0xFFFF, + allowCount: counts >>> 16 & 0xFFFF + }; + } return r; }; /******************************************************************************/ -var getStats = function(tab) { +var getDynamicFilterRules = function(srcHostname, desHostnames) { + var r = {}; + var dFiltering = µb.dynamicNetFilteringEngine; + r['/ * image'] = dFiltering.evaluateCellZY('*', '*', 'image').toFilterString(); + r['/ * inline-script'] = dFiltering.evaluateCellZY('*', '*', 'inline-script').toFilterString(); + r['/ * 1p-script'] = dFiltering.evaluateCellZY('*', '*', '1p-script').toFilterString(); + r['/ * 3p-script'] = dFiltering.evaluateCellZY('*', '*', '3p-script').toFilterString(); + r['/ * 3p-frame'] = dFiltering.evaluateCellZY('*', '*', '3p-frame').toFilterString(); + if ( typeof srcHostname !== 'string' ) { + return r; + } + + r['. * image'] = dFiltering.evaluateCellZY(srcHostname, '*', 'image').toFilterString(); + r['. * inline-script'] = dFiltering.evaluateCellZY(srcHostname, '*', 'inline-script').toFilterString(); + r['. * 1p-script'] = dFiltering.evaluateCellZY(srcHostname, '*', '1p-script').toFilterString(); + r['. * 3p-script'] = dFiltering.evaluateCellZY(srcHostname, '*', '3p-script').toFilterString(); + r['. * 3p-frame'] = dFiltering.evaluateCellZY(srcHostname, '*', '3p-frame').toFilterString(); + + for ( var desHostname in desHostnames ) { + if ( desHostnames.hasOwnProperty(desHostname) ) { + r['/ ' + desHostname + ' *'] = dFiltering.evaluateCellZY('*', desHostname, '*').toFilterString(); + r['. ' + desHostname + ' *'] = dFiltering.evaluateCellZY(srcHostname, desHostname, '*').toFilterString(); + } + } + return r; +}; + +/******************************************************************************/ + +var getStats = function(tabId) { var r = { appName: vAPI.app.name, appVersion: vAPI.app.version, globalBlockedRequestCount: µb.localSettings.blockedRequestCount, globalAllowedRequestCount: µb.localSettings.allowedRequestCount, - tabId: 0, + tabId: tabId, pageURL: '', pageBlockedRequestCount: 0, pageAllowedRequestCount: 0, netFilteringSwitch: false, cosmeticFilteringSwitch: false, logRequests: µb.userSettings.logRequests, - dynamicFilteringEnabled: µb.userSettings.dynamicFilteringEnabled, - dynamicFilterResults: { - '*': getDynamicFilterResults('*') - } + dynamicFilteringEnabled: µb.userSettings.dynamicFilteringEnabled }; - var pageStore = tab && µb.pageStoreFromTabId(tab.id); + var pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore ) { - r.tabId = tab.id; r.pageURL = pageStore.pageURL; + r.pageDomain = pageStore.pageDomain; r.pageHostname = pageStore.pageHostname; r.pageBlockedRequestCount = pageStore.perLoadBlockedRequestCount; r.pageAllowedRequestCount = pageStore.perLoadAllowedRequestCount; r.netFilteringSwitch = pageStore.getNetFilteringSwitch(); - r.dynamicFilterResults['local'] = getDynamicFilterResults(r.pageHostname); + r.hostnameDict = getHostnameDict(pageStore.hostnameToCountMap); + r.dynamicFilterRules = getDynamicFilterRules(pageStore.pageHostname, r.hostnameDict); + } else { + r.hostnameDict = {}; + r.dynamicFilterRules = getDynamicFilterRules(); } return r; }; @@ -153,7 +203,10 @@ var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { case 'activeTabStats': - vAPI.tabs.get(null, function(tab) { callback(getStats(tab)); }); + vAPI.tabs.get(null, function(tab) { + var tabId = tab && tab.id; + callback(getStats(tabId)); + }); return; default: @@ -182,10 +235,7 @@ var onMessage = function(request, sender, callback) { case 'toggleDynamicFilter': µb.toggleDynamicFilter(request); - response = { '*': getDynamicFilterResults('*') }; - if ( request.pageHostname ) { - response['local'] = getDynamicFilterResults(request.pageHostname); - } + response = getStats(request.tabId); break; default: diff --git a/src/js/pagestore.js b/src/js/pagestore.js index acc158a23..33715d015 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -296,16 +296,6 @@ PageStore.factory = function(tabId, pageURL) { /******************************************************************************/ -PageStore.prototype.bitFromResult = { - '': 1, - 'sb': 2, - 'sa': 4, - 'db': 8, - 'da': 16 -}; - -/******************************************************************************/ - PageStore.prototype.init = function(tabId, pageURL) { this.tabId = tabId; this.previousPageURL = ''; @@ -321,7 +311,7 @@ PageStore.prototype.init = function(tabId, pageURL) { // This is part of the filtering evaluation context this.requestURL = this.requestHostname = this.requestType = ''; - this.requestHostnames = {}; + this.hostnameToCountMap = {}; this.frames = {}; this.netFiltering = true; this.netFilteringReadTime = 0; @@ -381,7 +371,7 @@ PageStore.prototype.dispose = function() { this.pageHostname = this.pageDomain = this.rootHostname = this.rootDomain = this.requestURL = this.requestHostname = this.requestType = ''; - this.requestHostnames = null; + this.hostnameToCountMap = null; this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { @@ -451,12 +441,15 @@ PageStore.prototype.filterRequest = function(context) { this.recordResult(context.requestType, requestURL, result); var requestHostname = context.requestHostname; - if ( this.requestHostnames.hasOwnProperty(requestHostname) ) { - this.requestHostnames[requestHostname] |= this.bitFromResult[result.slice(0, 2)]; - } else { - this.requestHostnames[requestHostname] = this.bitFromResult[result.slice(0, 2)]; + if ( this.hostnameToCountMap.hasOwnProperty(requestHostname) === false ) { + this.hostnameToCountMap[requestHostname] = 0; + } + var c = result.charAt(1); + if ( c === '' || c === 'a' ) { + this.hostnameToCountMap[requestHostname] += 0x00010000; + } else /* if ( c === 'b' ) */ { + this.hostnameToCountMap[requestHostname] += 0x00000001; } - return result; }; diff --git a/src/js/popup.js b/src/js/popup.js index fdb82a669..cc21baeb3 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -30,6 +30,24 @@ /******************************************************************************/ var stats; +var dynaTypes = [ + 'image', + 'inline-script', + '1p-script', + '3p-script', + '3p-frame' +]; +var popupHeight; +var reIP = /^\d+(?:\.\d+){1,3}$/; +var reSrcHostnameFromResult = /^d[abn]:([^ ]+) ([^ ]+)/; +var touchedDomains = {}; +var scopeToSrcHostnameMap = { + '/': '*', + '.': '' +}; +var threePlus = '+++'; +var threeMinus = '\u2012\u2012\u2012'; +var sixSpace = '\u2007\u2007\u2007\u2007\u2007\u2007'; /******************************************************************************/ @@ -37,6 +55,15 @@ var stats; var messager = vAPI.messaging.channel('popup.js'); +/******************************************************************************/ + +var cachePopupData = function(data) { + if ( data ) { + stats = data; + scopeToSrcHostnameMap['.'] = data.pageHostname || ''; + } + return data; +}; /******************************************************************************/ @@ -49,61 +76,126 @@ var formatNumber = function(count) { /******************************************************************************/ -var syncDynamicFilter = function(scope, des, type, result) { - var el = uDom('span[data-src="' + scope + '"][data-des="' + des + '"][data-type="' + type + '"]'); - var blocked = result.charAt(1) === 'b'; - el.toggleClass('blocked', blocked); - - // https://github.com/gorhill/uBlock/issues/340 - // Use dark shade visual cue if the filter is specific to the page hostname - // or one of the ancestor hostname. - var ownFilter = false; - var matches = /^d[abn]:([^ ]+)/.exec(result); - if ( matches !== null ) { - var thisSrc = scope === 'local' ? stats.pageHostname : '*'; - var otherSrc = matches[1]; - ownFilter = thisSrc.slice(0 - otherSrc.length) === thisSrc; - if ( ownFilter && thisSrc.length !== otherSrc.length ) { - var c = thisSrc.substr(0 - otherSrc.length - 1, 1); - ownFilter = c === '' || c === '.'; - } +var rulekeyCompare = function(a, b) { + var ha = a.slice(2, a.indexOf(' ', 2)); + if ( !reIP.test(ha) ) { + ha = ha.split('.').reverse().join('.').replace(reRulekeyCompareNoise, '~'); } - el.toggleClass('ownFilter', ownFilter); + var hb = b.slice(2, b.indexOf(' ', 2)); + if ( !reIP.test(hb) ) { + hb = hb.split('.').reverse().join('.').replace(reRulekeyCompareNoise, '~'); + } + return ha.localeCompare(hb); +}; + +var reRulekeyCompareNoise = /[^a-z0-9.]/g; + +/******************************************************************************/ + +var addDynamicFilterRow = function(des) { + var row = uDom('#templates > div:nth-of-type(1)').clone(); + row.descendants('[data-des]').attr('data-des', des); + row.descendants('div > span:nth-of-type(1)').text(des); + + var hnDetails = stats.hostnameDict[des] || {}; + var isDomain = des === hnDetails.domain; + row.toggleClass('isDomain', isDomain); + if ( hnDetails.allowCount !== 0 ) { + touchedDomains[hnDetails.domain] = true; + } + + row.appendTo('#dynamicFilteringContainer'); + + // Hacky? I couldn't figure a CSS recipe for this problem. + // I do not want the left pane -- optional and hidden by defaut -- to + // dictate the height of the popup. The right pane dictates the height + // of the popup, and the left pane will have a scrollbar if ever its + // height is larger than what is available. + if ( popupHeight === undefined ) { + popupHeight = uDom('body > div:nth-of-type(2)').nodeAt(0).offsetHeight; + uDom('body > div:nth-of-type(1)').css('height', popupHeight + 'px'); + } + return row; +}; + +/******************************************************************************/ + +var syncDynamicFilter = function(scope, des, type, result) { + var selector = '#dynamicFilteringContainer span[data-src="' + scope + '"][data-des="' + des + '"][data-type="' + type + '"]'; + var cell = uDom(selector); + + // Create the row? + if ( cell.length === 0 ) { + cell = addDynamicFilterRow(des).descendants(selector); + } + + var blocked = result.charAt(1) === 'b'; + cell.toggleClass('blocked', blocked); + + // Use dark shade visual cue if the filter is specific to the cell. + var ownFilter = false; + var matches = reSrcHostnameFromResult.exec(result); + if ( matches !== null ) { + ownFilter = matches[2] === des && + matches[1] === scopeToSrcHostnameMap[scope]; + } + cell.toggleClass('ownFilter', ownFilter); + + if ( scope !== '.' || type !== '*' ) { + return; + } + if ( stats.hostnameDict.hasOwnProperty(des) === false ) { + return; + } + var hnDetails = stats.hostnameDict[des]; + var aCount = Math.min(Math.ceil(Math.log10(hnDetails.allowCount + 1)), 3); + var bCount = Math.min(Math.ceil(Math.log10(hnDetails.blockCount + 1)), 3); + cell.text( + threePlus.slice(0, aCount) + + sixSpace.slice(aCount + bCount) + + threeMinus.slice(0, bCount) + ); }; /******************************************************************************/ var syncAllDynamicFilters = function() { var hasBlock = false; - var scopes = ['*', 'local']; - var scope, results, i, result; - while ( scope = scopes.pop() ) { - if ( stats.dynamicFilterResults.hasOwnProperty(scope) === false ) { + var rules = stats.dynamicFilterRules; + var type, result; + var types = dynaTypes; + var i = types.length; + while ( i-- ) { + type = types[i]; + syncDynamicFilter('/', '*', type, rules['/ * ' + type] || ''); + result = rules['. * ' + type] || ''; + if ( result.charAt(1) === 'b' ) { + hasBlock = true; + } + syncDynamicFilter('.', '*', type, result); + } + + // Sort hostnames. First-party hostnames must always appear at the top + // of the list. + var keys = Object.keys(rules).sort(rulekeyCompare); + var key; + for ( var i = 0; i < keys.length; i++ ) { + key = keys[i]; + // Specific-type rules -- they were processed above + if ( key.slice(-1) !== '*' ) { continue; } - results = stats.dynamicFilterResults[scope]; - for ( var type in results ) { - if ( results.hasOwnProperty(type) === false ) { - continue; - } - result = results[type]; - syncDynamicFilter(scope, '*', type, result); - if ( scope === 'local' && result.charAt(1) === 'b' ) { - hasBlock = true; - } - } + syncDynamicFilter(key.charAt(0), key.slice(2, key.indexOf(' ', 2)), '*', rules[key]); } + uDom('body').toggleClass('hasDynamicBlock', hasBlock); + uDom('#privacyInfo > b').text(Object.keys(touchedDomains).length); }; /******************************************************************************/ var renderPopup = function(details) { - if ( details ) { - stats = details; - } - - if ( !stats ) { + if ( !cachePopupData(details) ) { return; } @@ -246,19 +338,20 @@ var onDynamicFilterClicked = function(ev) { if ( typeof stats.pageHostname !== 'string' || stats.pageHostname === '' ) { return; } - var elFilter = uDom(ev.target); - var scope = elFilter.attr('data-src') === '*' ? '*' : stats.pageHostname; + var cell = uDom(ev.target); + var scope = cell.attr('data-src') === '/' ? '*' : stats.pageHostname; var onDynamicFilterChanged = function(details) { - stats.dynamicFilterResults = details; + cachePopupData(details); syncAllDynamicFilters(); }; messager.send({ what: 'toggleDynamicFilter', + tabId: stats.tabId, pageHostname: stats.pageHostname, srcHostname: scope, - desHostname: elFilter.attr('data-des'), - requestType: elFilter.attr('data-type'), - block: elFilter.hasClassName('blocked') === false + desHostname: cell.attr('data-des'), + requestType: cell.attr('data-type'), + block: cell.hasClass('blocked') === false }, onDynamicFilterChanged); }; @@ -270,7 +363,7 @@ var toggleDynamicFiltering = function(ev) { messager.send({ what: 'userSettings', name: 'dynamicFilteringEnabled', - value: el.hasClassName('dynamicFilteringEnabled') + value: el.hasClass('dynamicFilteringEnabled') }); }; diff --git a/src/js/ublock.js b/src/js/ublock.js index d1d4461fb..e34e6c621 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -366,22 +366,24 @@ var matchWhitelistDirective = function(url, hostname, directive) { // Dynamic filters: // 3. specific source, any destination, specific type, allow/block // 4. any source, any destination, specific type, allow/block - if ( requestType === 'script' ) { - df.evaluateCellZY(rootHostname, requestHostname, this.isFirstParty(rootHostname, requestHostname) ? '1p-script' : '3p-script'); - if ( df.mustBlockOrAllow() ) { - return df.toFilterString(); - } - } else if ( requestType === 'sub_frame' ) { - if ( this.isFirstParty(rootHostname, requestHostname) === false ) { - df.evaluateCellZY(rootHostname, requestHostname, '3p-frame'); + if ( df.mustAbort() === false ) { + if ( requestType === 'script' ) { + df.evaluateCellZY(rootHostname, requestHostname, this.isFirstParty(rootHostname, requestHostname) ? '1p-script' : '3p-script'); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + } else if ( requestType === 'sub_frame' ) { + if ( this.isFirstParty(rootHostname, requestHostname) === false ) { + df.evaluateCellZY(rootHostname, requestHostname, '3p-frame'); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + } + } else { + df.evaluateCellZY(rootHostname, requestHostname, requestType); if ( df.mustBlockOrAllow() ) { return df.toFilterString(); } - } - } else { - df.evaluateCellZY(rootHostname, requestHostname, requestType); - if ( df.mustBlockOrAllow() ) { - return df.toFilterString(); } } diff --git a/src/popup.html b/src/popup.html index 952232fd6..a902c607b 100644 --- a/src/popup.html +++ b/src/popup.html @@ -12,11 +12,12 @@
@@ -32,6 +33,10 @@?