From 435c91636fdbfe90742b3f7c68df07283b63a37d Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 15 Feb 2021 06:52:31 -0500 Subject: [PATCH] Count allowed/blocked requests for 3rd-party scripts/frames Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/210 Additionally, a small (experimental) widget has been added to emphasize/de-emphasize rows which have 3rd-party scripts/frames, so as to more easily identify which rows are "affected" by 3rd-party scripts and/or frames. Tooltip localization for the new widget is not available yet as I want wait for the feature to be fully settled. --- src/css/popup-fenix.css | 57 ++- src/js/dynamic-net-filtering.js | 877 +++++++++++++++----------------- src/js/messaging.js | 136 +++-- src/js/pagestore.js | 175 +++++-- src/js/popup-fenix.js | 399 ++++++++++----- src/js/popup.js | 261 ++++++---- src/js/tab.js | 15 +- src/js/traffic.js | 8 +- src/popup-fenix.html | 6 +- 9 files changed, 1095 insertions(+), 839 deletions(-) diff --git a/src/css/popup-fenix.css b/src/css/popup-fenix.css index 3c849872f..b6294666f 100644 --- a/src/css/popup-fenix.css +++ b/src/css/popup-fenix.css @@ -262,7 +262,6 @@ body[data-more=""] #lessButton { min-width: var(--popup-firewall-min-width); padding: 0; overflow-y: auto; - text-align: right; } :root.desktop body.vMin #firewall { max-height: 100vh; @@ -271,7 +270,6 @@ body[data-more=""] #lessButton { border: 0; direction: ltr; display: flex; - justify-content: flex-end; margin: 0; margin-top: 1px; padding: 0; @@ -305,10 +303,45 @@ body[data-more=""] #lessButton { flex-grow: 1; justify-content: flex-end; padding-right: 2px; + text-align: right; white-space: normal; width: calc(100% - var(--popup-rule-cell-width)); word-break: break-word; } +#firewall > div[data-des="*"] > span:first-of-type { + flex-direction: row; + } +#firewall > div[data-des="*"] > span:first-of-type > span.filter { + flex-grow: 1; + padding-inline-start: 2px; + -webkit-padding-start: 2px; + text-align: left; + } +#firewall:not(.has3pScript) > [data-type="3p-script"] .filter, +#firewall:not(.has3pFrame) > [data-type="3p-frame"] .filter { + display: none; + } +#firewall > [data-des="*"] .filter::after { + content: '\22EF'; + } +#firewall.show3pScript > [data-type="3p-script"] .filter::after, +#firewall.show3pFrame > [data-type="3p-frame"] .filter::after { + content: '\2191'; + } +#firewall.hide3pScript > [data-type="3p-script"] .filter::after, +#firewall.hide3pFrame > [data-type="3p-frame"] .filter::after { + content: '\2193'; + } +#firewall.show3pScript > div:not([data-des="*"]):not(.hasScript), +#firewall.show3pScript > div:not([data-des="*"]):not(.is3p), +#firewall.hide3pScript > div:not([data-des="*"]).is3p.hasScript, +#firewall.show3pFrame > div:not([data-des="*"]):not(.hasFrame), +#firewall.show3pFrame > div:not([data-des="*"]):not(.is3p), +#firewall.hide3pFrame > div:not([data-des="*"]).is3p.hasFrame, +#firewall.show3pScript.show3pFrame > div:not([data-des="*"]).hasScript:not(.hasFrame), +#firewall.show3pScript.show3pFrame > div:not([data-des="*"]).hasFrame:not(.hasScript) { + opacity: 0.5; + } #firewall > div.isCname > span:first-of-type { color: var(--fg-popup-cell-cname); } @@ -431,33 +464,33 @@ body.advancedUser #firewall > div > span:first-of-type ~ span { width: 7px; } #firewall > div.isRootContext > span:first-of-type::before { - background-color: var(--fg-0-50); + background: var(--fg-0-50); width: 14px !important; } #firewall > div.allowed > span:first-of-type::before, #firewall > div.isDomain.totalAllowed > span:first-of-type::before { - background-color: var(--bg-popup-cell-allow-own); + background: var(--bg-popup-cell-allow-own); } #firewall > div.blocked > span:first-of-type::before, #firewall > div.isDomain.totalBlocked > span:first-of-type::before { - background-color: var(--bg-popup-cell-block-own); + background: var(--bg-popup-cell-block-own); } #firewall > div.allowed.blocked > span:first-of-type::before, #firewall > div.isDomain.totalAllowed.totalBlocked > span:first-of-type::before { - background-color: var(--bg-popup-cell-label-mixed); + background: var(--bg-popup-cell-label-mixed); } /* Rule cells */ body.advancedUser #firewall > div > span.allowRule, #actionSelector > #dynaAllow { - background-color: var(--bg-popup-cell-allow); + background: var(--bg-popup-cell-allow); } body.advancedUser #firewall > div > span.blockRule, #actionSelector > #dynaBlock { - background-color: var(--bg-popup-cell-block); + background: var(--bg-popup-cell-block); } body.advancedUser #firewall > div > span.noopRule, #actionSelector > #dynaNoop { - background-color: var(--bg-popup-cell-noop); + background: var(--bg-popup-cell-noop); } body.advancedUser #firewall > div > span.ownRule, #firewall > div > span.ownRule { @@ -465,15 +498,15 @@ body.advancedUser #firewall > div > span.ownRule, } body.advancedUser #firewall > div > span.allowRule.ownRule, :root:not(.mobile) #actionSelector > #dynaAllow:hover { - background-color: var(--bg-popup-cell-allow-own); + background: var(--bg-popup-cell-allow-own); } body.advancedUser #firewall > div > span.blockRule.ownRule, :root:not(.mobile) #actionSelector > #dynaBlock:hover { - background-color: var(--bg-popup-cell-block-own); + background: var(--bg-popup-cell-block-own); } body.advancedUser #firewall > div > span.noopRule.ownRule, :root:not(.mobile) #actionSelector > #dynaNoop:hover { - background-color: var(--bg-popup-cell-noop-own); + background: var(--bg-popup-cell-noop-own); } #actionSelector { diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index a27e2c8e9..c0f322e03 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -26,17 +26,12 @@ /******************************************************************************/ -µBlock.Firewall = (function() { +{ +// >>>>> start of local scope /******************************************************************************/ -var Matrix = function() { - this.reset(); -}; - -/******************************************************************************/ - -var supportedDynamicTypes = { +const supportedDynamicTypes = { '3p': true, 'image': true, 'inline-script': true, @@ -45,7 +40,7 @@ var supportedDynamicTypes = { '3p-frame': true }; -var typeBitOffsets = { +const typeBitOffsets = { '*': 0, 'inline-script': 2, '1p-script': 4, @@ -55,13 +50,13 @@ var typeBitOffsets = { '3p': 12 }; -var actionToNameMap = { +const actionToNameMap = { '1': 'block', '2': 'allow', '3': 'noop' }; -var nameToActionMap = { +const nameToActionMap = { 'block': 1, 'allow': 2, 'noop': 3 @@ -70,187 +65,10 @@ var nameToActionMap = { /******************************************************************************/ // For performance purpose, as simple tests as possible -var reBadHostname = /[^0-9a-z_.\[\]:%-]/; -var reNotASCII = /[^\x20-\x7F]/; +const reBadHostname = /[^0-9a-z_.\[\]:%-]/; +const reNotASCII = /[^\x20-\x7F]/; -/******************************************************************************/ - -Matrix.prototype.reset = function() { - this.r = 0; - this.type = ''; - this.y = ''; - this.z = ''; - this.rules = new Map(); - this.changed = false; - this.decomposedSource = []; - this.decomposedDestination = []; -}; - -/******************************************************************************/ - -Matrix.prototype.assign = function(other) { - // Remove rules not in other - for ( var k of this.rules.keys() ) { - if ( other.rules.has(k) === false ) { - this.rules.delete(k); - this.changed = true; - } - } - // Add/change rules in other - for ( var entry of other.rules ) { - if ( this.rules.get(entry[0]) !== entry[1] ) { - this.rules.set(entry[0], entry[1]); - this.changed = true; - } - } -}; - -/******************************************************************************/ - -Matrix.prototype.copyRules = function(from, srcHostname, desHostnames) { - // Specific types - let thisBits = this.rules.get('* *'); - let fromBits = from.rules.get('* *'); - if ( fromBits !== thisBits ) { - if ( fromBits !== undefined ) { - this.rules.set('* *', fromBits); - } else { - this.rules.delete('* *'); - } - this.changed = true; - } - - let key = srcHostname + ' *'; - thisBits = this.rules.get(key); - fromBits = from.rules.get(key); - if ( fromBits !== thisBits ) { - if ( fromBits !== undefined ) { - this.rules.set(key, fromBits); - } else { - this.rules.delete(key); - } - this.changed = true; - } - - // Specific destinations - for ( let desHostname in desHostnames ) { - if ( desHostnames.hasOwnProperty(desHostname) === false ) { continue; } - key = '* ' + desHostname; - thisBits = this.rules.get(key); - fromBits = from.rules.get(key); - if ( fromBits !== thisBits ) { - if ( fromBits !== undefined ) { - this.rules.set(key, fromBits); - } else { - this.rules.delete(key); - } - this.changed = true; - } - key = srcHostname + ' ' + desHostname ; - thisBits = this.rules.get(key); - fromBits = from.rules.get(key); - if ( fromBits !== thisBits ) { - if ( fromBits !== undefined ) { - this.rules.set(key, fromBits); - } else { - this.rules.delete(key); - } - this.changed = true; - } - } - - return this.changed; -}; - -/******************************************************************************/ - -// - * * type -// - from * type -// - * to * -// - from to * - -Matrix.prototype.hasSameRules = function(other, srcHostname, desHostnames) { - - // Specific types - var key = '* *'; - if ( this.rules.get(key) !== other.rules.get(key) ) { - return false; - } - key = srcHostname + ' *'; - if ( this.rules.get(key) !== other.rules.get(key) ) { - return false; - } - - // Specific destinations - for ( var desHostname in desHostnames ) { - key = '* ' + desHostname; - if ( this.rules.get(key) !== other.rules.get(key) ) { - return false; - } - key = srcHostname + ' ' + desHostname ; - if ( this.rules.get(key) !== other.rules.get(key) ) { - return false; - } - } - - return true; -}; - -/******************************************************************************/ - -Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) { - var bitOffset = typeBitOffsets[type]; - var k = srcHostname + ' ' + desHostname; - var oldBitmap = this.rules.get(k) || 0; - var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); - if ( newBitmap === oldBitmap ) { - return false; - } - if ( newBitmap === 0 ) { - this.rules.delete(k); - } else { - this.rules.set(k, newBitmap); - } - this.changed = true; - return true; -}; - -/******************************************************************************/ - -Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) { - this.evaluateCellZY(srcHostname, desHostname, type); - if ( this.r === 0 ) { - return false; - } - this.setCell(srcHostname, desHostname, type, 0); - this.changed = true; - return true; -}; - -// https://www.youtube.com/watch?v=Csewb_eIStY - -/******************************************************************************/ - -Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) { - var key = srcHostname + ' ' + desHostname; - var bitmap = this.rules.get(key); - if ( bitmap === undefined ) { - return 0; - } - return bitmap >> typeBitOffsets[type] & 3; -}; - -/******************************************************************************/ - -Matrix.prototype.clearRegisters = function() { - this.r = 0; - this.type = this.y = this.z = ''; - return this; -}; - -/******************************************************************************/ - -var is3rdParty = function(srcHostname, desHostname) { +const is3rdParty = function(srcHostname, desHostname) { // If at least one is party-less, the relation can't be labelled // "3rd-party" if ( desHostname === '*' || srcHostname === '*' || srcHostname === '' ) { @@ -261,7 +79,7 @@ var is3rdParty = function(srcHostname, desHostname) { // - localhost // - file-scheme // etc. - var srcDomain = domainFromHostname(srcHostname) || srcHostname; + const srcDomain = domainFromHostname(srcHostname) || srcHostname; if ( desHostname.endsWith(srcDomain) === false ) { return true; @@ -271,145 +89,446 @@ var is3rdParty = function(srcHostname, desHostname) { desHostname.charAt(desHostname.length - srcDomain.length - 1) !== '.'; }; -var domainFromHostname = µBlock.URI.domainFromHostname; +const domainFromHostname = µBlock.URI.domainFromHostname; /******************************************************************************/ -Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) { - µBlock.decomposeHostname(srcHostname, this.decomposedSource); - this.type = type; - let bitOffset = typeBitOffsets[type]; - for ( let shn of this.decomposedSource ) { - this.z = shn; - let v = this.rules.get(shn + ' ' + desHostname); - if ( v !== undefined ) { - v = v >>> bitOffset & 3; - if ( v !== 0 ) { - this.r = v; - return v; +const Matrix = class { + + constructor() { + this.reset(); + } + + + reset() { + this.r = 0; + this.type = ''; + this.y = ''; + this.z = ''; + this.rules = new Map(); + this.changed = false; + this.decomposedSource = []; + this.decomposedDestination = []; + } + + + assign(other) { + // Remove rules not in other + for ( const k of this.rules.keys() ) { + if ( other.rules.has(k) === false ) { + this.rules.delete(k); + this.changed = true; + } + } + // Add/change rules in other + for ( const entry of other.rules ) { + if ( this.rules.get(entry[0]) !== entry[1] ) { + this.rules.set(entry[0], entry[1]); + this.changed = true; } } } - // srcHostname is '*' at this point - this.r = 0; - return 0; -}; -/******************************************************************************/ -Matrix.prototype.evaluateCellZY = function(srcHostname, desHostname, type) { - // Pathological cases. - if ( desHostname === '' ) { + copyRules(from, srcHostname, desHostnames) { + // Specific types + let thisBits = this.rules.get('* *'); + let fromBits = from.rules.get('* *'); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set('* *', fromBits); + } else { + this.rules.delete('* *'); + } + this.changed = true; + } + + let key = srcHostname + ' *'; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + + // Specific destinations + for ( const desHostname in desHostnames ) { + if ( desHostnames.hasOwnProperty(desHostname) === false ) { + continue; + } + key = '* ' + desHostname; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + key = srcHostname + ' ' + desHostname ; + thisBits = this.rules.get(key); + fromBits = from.rules.get(key); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.rules.set(key, fromBits); + } else { + this.rules.delete(key); + } + this.changed = true; + } + } + + return this.changed; + } + + + // - * * type + // - from * type + // - * to * + // - from to * + + hasSameRules(other, srcHostname, desHostnames) { + // Specific types + let key = '* *'; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + key = srcHostname + ' *'; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + // Specific destinations + for ( const desHostname in desHostnames ) { + key = '* ' + desHostname; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + key = srcHostname + ' ' + desHostname ; + if ( this.rules.get(key) !== other.rules.get(key) ) { + return false; + } + } + return true; + } + + + setCell(srcHostname, desHostname, type, state) { + const bitOffset = typeBitOffsets[type]; + const k = srcHostname + ' ' + desHostname; + const oldBitmap = this.rules.get(k) || 0; + const newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); + if ( newBitmap === oldBitmap ) { + return false; + } + if ( newBitmap === 0 ) { + this.rules.delete(k); + } else { + this.rules.set(k, newBitmap); + } + this.changed = true; + return true; + } + + + unsetCell(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 0 ) { + return false; + } + this.setCell(srcHostname, desHostname, type, 0); + this.changed = true; + return true; + } + + + evaluateCell(srcHostname, desHostname, type) { + const key = srcHostname + ' ' + desHostname; + const bitmap = this.rules.get(key); + if ( bitmap === undefined ) { return 0; } + return bitmap >> typeBitOffsets[type] & 3; + } + + + clearRegisters() { + this.r = 0; + this.type = this.y = this.z = ''; + return this; + } + + + evaluateCellZ(srcHostname, desHostname, type) { + µBlock.decomposeHostname(srcHostname, this.decomposedSource); + this.type = type; + const bitOffset = typeBitOffsets[type]; + for ( const shn of this.decomposedSource ) { + this.z = shn; + let v = this.rules.get(shn + ' ' + desHostname); + if ( v !== undefined ) { + v = v >>> bitOffset & 3; + if ( v !== 0 ) { + this.r = v; + return v; + } + } + } + // srcHostname is '*' at this point this.r = 0; return 0; } - // Precedence: from most specific to least specific - // Specific-destination, any party, any type - µBlock.decomposeHostname(desHostname, this.decomposedDestination); - for ( let dhn of this.decomposedDestination ) { - if ( dhn === '*' ) { break; } - this.y = dhn; - if ( this.evaluateCellZ(srcHostname, dhn, '*') !== 0 ) { - return this.r; + evaluateCellZY(srcHostname, desHostname, type) { + // Pathological cases. + if ( desHostname === '' ) { + this.r = 0; + return 0; } - } - let thirdParty = is3rdParty(srcHostname, desHostname); + // Precedence: from most specific to least specific - // Any destination - this.y = '*'; - - // Specific party - if ( thirdParty ) { - // 3rd-party, specific type - if ( type === 'script' ) { - if ( this.evaluateCellZ(srcHostname, '*', '3p-script') !== 0 ) { - return this.r; - } - } else if ( type === 'sub_frame' ) { - if ( this.evaluateCellZ(srcHostname, '*', '3p-frame') !== 0 ) { + // Specific-destination, any party, any type + µBlock.decomposeHostname(desHostname, this.decomposedDestination); + for ( const dhn of this.decomposedDestination ) { + if ( dhn === '*' ) { break; } + this.y = dhn; + if ( this.evaluateCellZ(srcHostname, dhn, '*') !== 0 ) { return this.r; } } - // 3rd-party, any type - if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) { + + const thirdParty = is3rdParty(srcHostname, desHostname); + + // Any destination + this.y = '*'; + + // Specific party + // TODO: equate `object` as `sub_frame` + if ( thirdParty ) { + // 3rd-party, specific type + if ( type === 'script' ) { + if ( this.evaluateCellZ(srcHostname, '*', '3p-script') !== 0 ) { + return this.r; + } + } else if ( type === 'sub_frame' ) { + if ( this.evaluateCellZ(srcHostname, '*', '3p-frame') !== 0 ) { + return this.r; + } + } + // 3rd-party, any type + if ( this.evaluateCellZ(srcHostname, '*', '3p') !== 0 ) { + return this.r; + } + } else if ( type === 'script' ) { + // 1st party, specific type + if ( this.evaluateCellZ(srcHostname, '*', '1p-script') !== 0 ) { + return this.r; + } + } + + // Any destination, any party, specific type + if ( supportedDynamicTypes.hasOwnProperty(type) ) { + if ( this.evaluateCellZ(srcHostname, '*', type) !== 0 ) { + return this.r; + } + } + + // Any destination, any party, any type + if ( this.evaluateCellZ(srcHostname, '*', '*') !== 0 ) { return this.r; } - } else if ( type === 'script' ) { - // 1st party, specific type - if ( this.evaluateCellZ(srcHostname, '*', '1p-script') !== 0 ) { - return this.r; + + this.type = ''; + return 0; + } + + + mustAllowCellZY(srcHostname, desHostname, type) { + return this.evaluateCellZY(srcHostname, desHostname, type) === 2; + } + + + mustBlockOrAllow() { + return this.r === 1 || this.r === 2; + } + + + mustBlock() { + return this.r === 1; + } + + + mustAbort() { + return this.r === 3; + } + + + lookupRuleData(src, des, type) { + const r = this.evaluateCellZY(src, des, type); + if ( r === 0 ) { return; } + return `${this.z} ${this.y} ${this.type} ${r}`; + } + + + toLogData() { + if ( this.r === 0 || this.type === '' ) { return; } + return { + source: 'dynamicHost', + result: this.r, + raw: `${this.z} ${this.y} ${this.type} ${this.intToActionMap.get(this.r)}` + }; + } + + + srcHostnameFromRule(rule) { + return rule.slice(0, rule.indexOf(' ')); + } + + + desHostnameFromRule(rule) { + return rule.slice(rule.indexOf(' ') + 1); + } + + + toArray() { + const out = [], + toUnicode = punycode.toUnicode; + for ( const key of this.rules.keys() ) { + let srcHostname = this.srcHostnameFromRule(key); + let desHostname = this.desHostnameFromRule(key); + for ( const type in typeBitOffsets ) { + if ( typeBitOffsets.hasOwnProperty(type) === false ) { continue; } + const val = this.evaluateCell(srcHostname, desHostname, type); + if ( val === 0 ) { continue; } + if ( srcHostname.indexOf('xn--') !== -1 ) { + srcHostname = toUnicode(srcHostname); + } + if ( desHostname.indexOf('xn--') !== -1 ) { + desHostname = toUnicode(desHostname); + } + out.push( + srcHostname + ' ' + + desHostname + ' ' + + type + ' ' + + actionToNameMap[val] + ); + } + } + return out; + } + + + toString() { + return this.toArray().join('\n'); + } + + + fromString(text, append) { + const lineIter = new µBlock.LineIterator(text); + if ( append !== true ) { this.reset(); } + while ( lineIter.eot() === false ) { + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); } } - // Any destination, any party, specific type - if ( supportedDynamicTypes.hasOwnProperty(type) ) { - if ( this.evaluateCellZ(srcHostname, '*', type) !== 0 ) { - return this.r; + + validateRuleParts(parts) { + if ( parts.length < 4 ) { return; } + + // Ignore hostname-based switch rules + if ( parts[0].endsWith(':') ) { return; } + + // Ignore URL-based rules + if ( parts[1].indexOf('/') !== -1 ) { return; } + + if ( typeBitOffsets.hasOwnProperty(parts[2]) === false ) { return; } + + if ( nameToActionMap.hasOwnProperty(parts[3]) === false ) { return; } + + // https://github.com/chrisaljoudi/uBlock/issues/840 + // Discard invalid rules + if ( parts[1] !== '*' && parts[2] !== '*' ) { return; } + + // Performance: avoid punycoding if hostnames are made only of ASCII chars. + if ( reNotASCII.test(parts[0]) ) { parts[0] = punycode.toASCII(parts[0]); } + if ( reNotASCII.test(parts[1]) ) { parts[1] = punycode.toASCII(parts[1]); } + + // https://github.com/chrisaljoudi/uBlock/issues/1082 + // Discard rules with invalid hostnames + if ( + (parts[0] !== '*' && reBadHostname.test(parts[0])) || + (parts[1] !== '*' && reBadHostname.test(parts[1])) + ) { + return; } + + return parts; } - // Any destination, any party, any type - if ( this.evaluateCellZ(srcHostname, '*', '*') !== 0 ) { - return this.r; + + addFromRuleParts(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]); + return true; + } + return false; } - this.type = ''; - return 0; -}; -// http://youtu.be/gSGk1bQ9rcU?t=25m6s - -/******************************************************************************/ - -Matrix.prototype.mustAllowCellZY = function(srcHostname, desHostname, type) { - return this.evaluateCellZY(srcHostname, desHostname, type) === 2; -}; - -/******************************************************************************/ - -Matrix.prototype.mustBlockOrAllow = function() { - return this.r === 1 || this.r === 2; -}; - -/******************************************************************************/ - -Matrix.prototype.mustBlock = function() { - return this.r === 1; -}; - -/******************************************************************************/ - -Matrix.prototype.mustAbort = function() { - return this.r === 3; -}; - -/******************************************************************************/ - -Matrix.prototype.lookupRuleData = function(src, des, type) { - var r = this.evaluateCellZY(src, des, type); - if ( r === 0 ) { - return null; + removeFromRuleParts(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.setCell(parts[0], parts[1], parts[2], 0); + return true; + } + return false; } - return { - src: this.z, - des: this.y, - type: this.type, - action: r === 1 ? 'block' : (r === 2 ? 'allow' : 'noop') - }; -}; -/******************************************************************************/ -Matrix.prototype.toLogData = function() { - if ( this.r === 0 || this.type === '' ) { return; } - return { - source: 'dynamicHost', - result: this.r, - raw: `${this.z} ${this.y} ${this.type} ${this.intToActionMap.get(this.r)}` - }; + toSelfie() { + return { + magicId: this.magicId, + rules: Array.from(this.rules) + }; + } + + + fromSelfie(selfie) { + if ( selfie.magicId !== this.magicId ) { return false; } + this.rules = new Map(selfie.rules); + this.changed = true; + return true; + } + + + async benchmark() { + const requests = await µBlock.loadBenchmarkDataset(); + if ( Array.isArray(requests) === false || requests.length === 0 ) { + log.print('No requests found to benchmark'); + return; + } + log.print(`Benchmarking sessionFirewall.evaluateCellZY()...`); + const fctxt = µBlock.filteringContext.duplicate(); + const t0 = self.performance.now(); + for ( const request of requests ) { + fctxt.setURL(request.url); + fctxt.setTabOriginFromURL(request.frameUrl); + fctxt.setType(request.cpt); + this.evaluateCellZY( + fctxt.getTabHostname(), + fctxt.getHostname(), + fctxt.type + ); + } + const t1 = self.performance.now(); + const dur = t1 - t0; + log.print(`Evaluated ${requests.length} requests in ${dur.toFixed(0)} ms`); + log.print(`\tAverage: ${(dur / requests.length).toFixed(3)} ms per request`); + } }; Matrix.prototype.intToActionMap = new Map([ @@ -418,168 +537,14 @@ Matrix.prototype.intToActionMap = new Map([ [ 3, 'noop' ] ]); -/******************************************************************************/ - -Matrix.prototype.srcHostnameFromRule = function(rule) { - return rule.slice(0, rule.indexOf(' ')); -}; +Matrix.prototype.magicId = 1; /******************************************************************************/ -Matrix.prototype.desHostnameFromRule = function(rule) { - return rule.slice(rule.indexOf(' ') + 1); -}; +µBlock.Firewall = Matrix; -/******************************************************************************/ - -Matrix.prototype.toArray = function() { - var out = [], - toUnicode = punycode.toUnicode; - for ( var key of this.rules.keys() ) { - var srcHostname = this.srcHostnameFromRule(key); - var desHostname = this.desHostnameFromRule(key); - for ( var type in typeBitOffsets ) { - if ( typeBitOffsets.hasOwnProperty(type) === false ) { continue; } - var val = this.evaluateCell(srcHostname, desHostname, type); - if ( val === 0 ) { continue; } - if ( srcHostname.indexOf('xn--') !== -1 ) { - srcHostname = toUnicode(srcHostname); - } - if ( desHostname.indexOf('xn--') !== -1 ) { - desHostname = toUnicode(desHostname); - } - out.push( - srcHostname + ' ' + - desHostname + ' ' + - type + ' ' + - actionToNameMap[val] - ); - } - } - return out; -}; - -Matrix.prototype.toString = function() { - return this.toArray().join('\n'); -}; - -/******************************************************************************/ - -Matrix.prototype.fromString = function(text, append) { - var lineIter = new µBlock.LineIterator(text); - if ( append !== true ) { this.reset(); } - while ( lineIter.eot() === false ) { - this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); - } -}; - -/******************************************************************************/ - -Matrix.prototype.validateRuleParts = function(parts) { - if ( parts.length < 4 ) { return; } - - // Ignore hostname-based switch rules - if ( parts[0].endsWith(':') ) { return; } - - // Ignore URL-based rules - if ( parts[1].indexOf('/') !== -1 ) { return; } - - if ( typeBitOffsets.hasOwnProperty(parts[2]) === false ) { return; } - - if ( nameToActionMap.hasOwnProperty(parts[3]) === false ) { return; } - - // https://github.com/chrisaljoudi/uBlock/issues/840 - // Discard invalid rules - if ( parts[1] !== '*' && parts[2] !== '*' ) { return; } - - // Performance: avoid punycoding if hostnames are made only of ASCII chars. - if ( reNotASCII.test(parts[0]) ) { parts[0] = punycode.toASCII(parts[0]); } - if ( reNotASCII.test(parts[1]) ) { parts[1] = punycode.toASCII(parts[1]); } - - // https://github.com/chrisaljoudi/uBlock/issues/1082 - // Discard rules with invalid hostnames - if ( - (parts[0] !== '*' && reBadHostname.test(parts[0])) || - (parts[1] !== '*' && reBadHostname.test(parts[1])) - ) { - return; - } - - return parts; -}; - -/******************************************************************************/ - -Matrix.prototype.addFromRuleParts = function(parts) { - if ( this.validateRuleParts(parts) !== undefined ) { - this.setCell(parts[0], parts[1], parts[2], nameToActionMap[parts[3]]); - return true; - } - return false; -}; - -Matrix.prototype.removeFromRuleParts = function(parts) { - if ( this.validateRuleParts(parts) !== undefined ) { - this.setCell(parts[0], parts[1], parts[2], 0); - return true; - } - return false; -}; - -/******************************************************************************/ - -const magicId = 1; - -Matrix.prototype.toSelfie = function() { - return { - magicId: magicId, - rules: Array.from(this.rules) - }; -}; - -Matrix.prototype.fromSelfie = function(selfie) { - if ( selfie.magicId !== magicId ) { return false; } - this.rules = new Map(selfie.rules); - this.changed = true; - return true; -}; - -/******************************************************************************/ - -Matrix.prototype.benchmark = async function() { - const requests = await µBlock.loadBenchmarkDataset(); - if ( Array.isArray(requests) === false || requests.length === 0 ) { - log.print('No requests found to benchmark'); - return; - } - log.print(`Benchmarking sessionFirewall.evaluateCellZY()...`); - const fctxt = µBlock.filteringContext.duplicate(); - const t0 = self.performance.now(); - for ( const request of requests ) { - fctxt.setURL(request.url); - fctxt.setTabOriginFromURL(request.frameUrl); - fctxt.setType(request.cpt); - this.evaluateCellZY( - fctxt.getTabHostname(), - fctxt.getHostname(), - fctxt.type - ); - } - const t1 = self.performance.now(); - const dur = t1 - t0; - log.print(`Evaluated ${requests.length} requests in ${dur.toFixed(0)} ms`); - log.print(`\tAverage: ${(dur / requests.length).toFixed(3)} ms per request`); -}; - -/******************************************************************************/ - -return Matrix; - -/******************************************************************************/ - -// http://youtu.be/5-K8R1hDG9E?t=31m1s - -})(); +// <<<<< end of local scope +} /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 1bba6889d..74c80d25a 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -216,78 +216,78 @@ vAPI.messaging.setup(onMessage); const µb = µBlock; -const getHostnameDict = function(hostnameToCountMap, out) { +const createCounts = ( ) => { + return { + blocked: { any: 0, frame: 0, script: 0 }, + allowed: { any: 0, frame: 0, script: 0 }, + }; +}; + +const getHostnameDict = function(hostnameDetailsMap, out) { const hnDict = Object.create(null); const cnMap = []; - for ( const [ hostname, hnCounts ] of hostnameToCountMap ) { - if ( hnDict[hostname] !== undefined ) { continue; } - const domain = vAPI.domainFromHostname(hostname) || hostname; - const dnCounts = hostnameToCountMap.get(domain) || 0; - let blockCount = dnCounts & 0xFFFF; - let allowCount = dnCounts >>> 16 & 0xFFFF; - if ( hnDict[domain] === undefined ) { - hnDict[domain] = { - domain, - blockCount, - allowCount, - totalBlockCount: blockCount, - totalAllowCount: allowCount, - }; - const cname = vAPI.net.canonicalNameFromHostname(domain); - if ( cname !== undefined ) { - cnMap.push([ cname, domain ]); - } - } - const domainEntry = hnDict[domain]; - blockCount = hnCounts & 0xFFFF; - allowCount = hnCounts >>> 16 & 0xFFFF; - domainEntry.totalBlockCount += blockCount; - domainEntry.totalAllowCount += allowCount; - if ( hostname === domain ) { continue; } - hnDict[hostname] = { - domain, - blockCount, - allowCount, - totalBlockCount: 0, - totalAllowCount: 0, - }; + + const createDictEntry = (domain, hostname, details) => { const cname = vAPI.net.canonicalNameFromHostname(hostname); if ( cname !== undefined ) { cnMap.push([ cname, hostname ]); } + hnDict[hostname] = { domain, counts: details.counts }; + }; + + for ( const hnDetails of hostnameDetailsMap.values() ) { + const hostname = hnDetails.hostname; + if ( hnDict[hostname] !== undefined ) { continue; } + const domain = vAPI.domainFromHostname(hostname) || hostname; + const dnDetails = + hostnameDetailsMap.get(domain) || { counts: createCounts() }; + if ( hnDict[domain] === undefined ) { + createDictEntry(domain, domain, dnDetails); + } + if ( hostname === domain ) { continue; } + createDictEntry(domain, hostname, hnDetails); } + out.hostnameDict = hnDict; out.cnameMap = cnMap; }; -const getFirewallRules = function(srcHostname, desHostnames) { - const out = {}; +const firewallRuleTypes = [ + '*', + 'image', + '3p', + 'inline-script', + '1p-script', + '3p-script', + '3p-frame', +]; + +const getFirewallRules = function(src, out) { + const { hostnameDict } = out; + const ruleset = {}; const df = µb.sessionFirewall; - out['/ * *'] = df.lookupRuleData('*', '*', '*'); - out['/ * image'] = df.lookupRuleData('*', '*', 'image'); - out['/ * 3p'] = df.lookupRuleData('*', '*', '3p'); - out['/ * inline-script'] = df.lookupRuleData('*', '*', 'inline-script'); - out['/ * 1p-script'] = df.lookupRuleData('*', '*', '1p-script'); - out['/ * 3p-script'] = df.lookupRuleData('*', '*', '3p-script'); - out['/ * 3p-frame'] = df.lookupRuleData('*', '*', '3p-frame'); - if ( typeof srcHostname !== 'string' ) { return out; } - out['. * *'] = df.lookupRuleData(srcHostname, '*', '*'); - out['. * image'] = df.lookupRuleData(srcHostname, '*', 'image'); - out['. * 3p'] = df.lookupRuleData(srcHostname, '*', '3p'); - out['. * inline-script'] = df.lookupRuleData(srcHostname, - '*', - 'inline-script' - ); - out['. * 1p-script'] = df.lookupRuleData(srcHostname, '*', '1p-script'); - out['. * 3p-script'] = df.lookupRuleData(srcHostname, '*', '3p-script'); - out['. * 3p-frame'] = df.lookupRuleData(srcHostname, '*', '3p-frame'); - - for ( const desHostname in desHostnames ) { - out[`/ ${desHostname} *`] = df.lookupRuleData('*', desHostname, '*'); - out[`. ${desHostname} *`] = df.lookupRuleData(srcHostname, desHostname, '*'); + for ( const type of firewallRuleTypes ) { + let r = df.lookupRuleData('*', '*', type); + if ( r === undefined ) { continue; } + ruleset[`/ * ${type}`] = r; } - return out; + if ( typeof src !== 'string' ) { return out; } + + for ( const type of firewallRuleTypes ) { + let r = df.lookupRuleData(src, '*', type); + if ( r === undefined ) { continue; } + ruleset[`. * ${type}`] = r; + } + + for ( const des in hostnameDict ) { + let r = df.lookupRuleData('*', des, '*'); + if ( r !== undefined ) { ruleset[`/ ${des} *`] = r; } + r = df.lookupRuleData(src, des, '*'); + if ( r !== undefined ) { ruleset[`. ${des} *`] = r; } + } + + out.firewallRules = ruleset; }; const popupDataFromTabId = function(tabId, tabTitle) { @@ -311,8 +311,6 @@ const popupDataFromTabId = function(tabId, tabTitle) { pageURL: tabContext.normalURL, pageHostname: rootHostname, pageDomain: tabContext.rootDomain, - pageAllowedRequestCount: 0, - pageBlockedRequestCount: 0, popupBlockedCount: 0, popupPanelSections: µbus.popupPanelSections, popupPanelDisabledSections: µbhs.popupPanelDisabledSections, @@ -329,23 +327,11 @@ const popupDataFromTabId = function(tabId, tabTitle) { const pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore ) { - // https://github.com/gorhill/uBlock/issues/2105 - // Be sure to always include the current page's hostname -- it - // might not be present when the page itself is pulled from the - // browser's short-term memory cache. This needs to be done - // before calling getHostnameDict(). - if ( - pageStore.hostnameToCountMap.has(rootHostname) === false && - µb.URI.isNetworkURI(tabContext.rawURL) - ) { - pageStore.hostnameToCountMap.set(rootHostname, 0); - } - r.pageBlockedRequestCount = pageStore.perLoadBlockedRequestCount; - r.pageAllowedRequestCount = pageStore.perLoadAllowedRequestCount; + r.pageCounts = pageStore.counts; r.netFilteringSwitch = pageStore.getNetFilteringSwitch(); - getHostnameDict(pageStore.hostnameToCountMap, r); + getHostnameDict(pageStore.getAllHostnameDetails(), r); r.contentLastModified = pageStore.contentLastModified; - r.firewallRules = getFirewallRules(rootHostname, r.hostnameDict); + getFirewallRules(rootHostname, r); r.canElementPicker = µb.URI.isNetworkURI(r.rawURL); r.noPopups = µb.sessionSwitches.evaluateZ( 'no-popups', diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 904a099f7..0bd8a952b 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -176,10 +176,6 @@ NetFilteringResultCache.prototype.extensionOriginURL = vAPI.getURL('/'); // Frame stores are used solely to associate a URL with a frame id. -// To mitigate memory churning -const frameStoreJunkyard = []; -const frameStoreJunkyardMax = 50; - const FrameStore = class { constructor(frameURL, parentId) { this.init(frameURL, parentId); @@ -201,14 +197,14 @@ const FrameStore = class { dispose() { this.rawURL = this.hostname = this.domain = ''; - if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { - frameStoreJunkyard.push(this); + if ( FrameStore.junkyard.length < FrameStore.junkyardMax ) { + FrameStore.junkyard.push(this); } return null; } static factory(frameURL, parentId = -1) { - const entry = frameStoreJunkyard.pop(); + const entry = FrameStore.junkyard.pop(); if ( entry === undefined ) { return new FrameStore(frameURL, parentId); } @@ -216,11 +212,62 @@ const FrameStore = class { } }; +// To mitigate memory churning +FrameStore.junkyard = []; +FrameStore.junkyardMax = 50; + /******************************************************************************/ -// To mitigate memory churning -const pageStoreJunkyard = []; -const pageStoreJunkyardMax = 10; +const CountDetails = class { + constructor() { + this.allowed = { any: 0, frame: 0, script: 0 }; + this.blocked = { any: 0, frame: 0, script: 0 }; + } + reset() { + const { allowed, blocked } = this; + blocked.any = blocked.frame = blocked.script = + allowed.any = allowed.frame = allowed.script = 0; + } + inc(blocked, type = undefined) { + const stat = blocked ? this.blocked : this.allowed; + if ( type !== undefined ) { stat[type] += 1; } + stat.any += 1; + } +}; + +const HostnameDetails = class { + constructor(hostname) { + this.counts = new CountDetails(); + this.init(hostname); + } + init(hostname) { + this.hostname = hostname; + this.counts.reset(); + } + dispose() { + this.hostname = ''; + if ( HostnameDetails.junkyard.length < HostnameDetails.junkyardMax ) { + HostnameDetails.junkyard.push(this); + } + } +}; + +HostnameDetails.junkyard = []; +HostnameDetails.junkyardMax = 100; + +const HostnameDetailsMap = class extends Map { + reset() { + this.clear(); + } + dispose() { + for ( const item of this.values() ) { + item.dispose(); + } + this.reset(); + } +}; + +/******************************************************************************/ const PageStore = class { constructor(tabId, context) { @@ -230,11 +277,13 @@ const PageStore = class { this.journalLastCommitted = this.journalLastUncommitted = -1; this.journalLastUncommittedOrigin = undefined; this.netFilteringCache = NetFilteringResultCache.factory(); + this.hostnameDetailsMap = new HostnameDetailsMap(); + this.counts = new CountDetails(); this.init(tabId, context); } static factory(tabId, context) { - let entry = pageStoreJunkyard.pop(); + let entry = PageStore.junkyard.pop(); if ( entry === undefined ) { entry = new PageStore(tabId, context); } else { @@ -263,11 +312,10 @@ const PageStore = class { this.tabHostname = tabContext.rootHostname; this.title = tabContext.rawURL; this.rawURL = tabContext.rawURL; - this.hostnameToCountMap = new Map(); + this.hostnameDetailsMap.reset(); this.contentLastModified = 0; this.logData = undefined; - this.perLoadBlockedRequestCount = 0; - this.perLoadAllowedRequestCount = 0; + this.counts.reset(); this.remoteFontCount = 0; this.popupBlockedCount = 0; this.largeMediaCount = 0; @@ -342,7 +390,7 @@ const PageStore = class { this.tabHostname = ''; this.title = ''; this.rawURL = ''; - this.hostnameToCountMap = null; + this.hostnameDetailsMap.dispose(); this.netFilteringCache.empty(); this.allowLargeMediaElementsUntil = Date.now(); this.allowLargeMediaElementsRegex = undefined; @@ -358,8 +406,8 @@ const PageStore = class { this.journal = []; this.journalLastUncommittedOrigin = undefined; this.journalLastCommitted = this.journalLastUncommitted = -1; - if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { - pageStoreJunkyard.push(this); + if ( PageStore.junkyard.length < PageStore.junkyardMax ) { + PageStore.junkyard.push(this); } return null; } @@ -454,6 +502,23 @@ const PageStore = class { this.netFilteringCache.empty(); } + // https://github.com/gorhill/uBlock/issues/2105 + // Be sure to always include the current page's hostname -- it might not + // be present when the page itself is pulled from the browser's + // short-term memory cache. + getAllHostnameDetails() { + if ( + this.hostnameDetailsMap.has(this.tabHostname) === false && + µb.URI.isNetworkURI(this.rawURL) + ) { + this.hostnameDetailsMap.set( + this.tabHostname, + new HostnameDetails(this.tabHostname) + ); + } + return this.hostnameDetailsMap; + } + injectLargeMediaElementScriptlet() { vAPI.tabs.executeScript(this.tabId, { file: '/js/scriptlets/load-large-media-interactive.js', @@ -478,18 +543,15 @@ const PageStore = class { // https://github.com/gorhill/uBlock/issues/2053 // There is no way around using journaling to ensure we deal properly with // potentially out of order navigation events vs. network request events. - journalAddRequest(hostname, result) { + journalAddRequest(fctxt, result) { + const hostname = fctxt.getHostname(); if ( hostname === '' ) { return; } - this.journal.push( - hostname, - result === 1 ? 0x00000001 : 0x00010000 + this.journal.push(hostname, result, fctxt.itype); + if ( this.journalTimer !== undefined ) { return; } + this.journalTimer = vAPI.setTimeout( + ( ) => { this.journalProcess(true); }, + µb.hiddenSettings.requestJournalProcessPeriod ); - if ( this.journalTimer === undefined ) { - this.journalTimer = vAPI.setTimeout( - ( ) => { this.journalProcess(true); }, - µb.hiddenSettings.requestJournalProcessPeriod - ); - } } journalAddRootFrame(type, url) { @@ -528,40 +590,57 @@ const PageStore = class { const journal = this.journal; const pivot = Math.max(0, this.journalLastCommitted); const now = Date.now(); - let aggregateCounts = 0; + const { SCRIPT, SUB_FRAME } = µb.FilteringContext; + let aggregateAllowed = 0; + let aggregateBlocked = 0; // Everything after pivot originates from current page. - for ( let i = pivot; i < journal.length; i += 2 ) { - const hostname = journal[i]; - let hostnameCounts = this.hostnameToCountMap.get(hostname); - if ( hostnameCounts === undefined ) { - hostnameCounts = 0; + for ( let i = pivot; i < journal.length; i += 3 ) { + const hostname = journal[i+0]; + let hnDetails = this.hostnameDetailsMap.get(hostname); + if ( hnDetails === undefined ) { + hnDetails = new HostnameDetails(hostname); + this.hostnameDetailsMap.set(hostname, hnDetails); this.contentLastModified = now; } - let count = journal[i+1]; - this.hostnameToCountMap.set(hostname, hostnameCounts + count); - aggregateCounts += count; + const blocked = journal[i+1] === 1; + const itype = journal[i+2]; + if ( itype === SCRIPT ) { + hnDetails.counts.inc(blocked, 'script'); + this.counts.inc(blocked, 'script'); + } else if ( itype === SUB_FRAME ) { + hnDetails.counts.inc(blocked, 'frame'); + this.counts.inc(blocked, 'frame'); + } else { + hnDetails.counts.inc(blocked); + this.counts.inc(blocked); + } + if ( blocked ) { + aggregateBlocked += 1; + } else { + aggregateAllowed += 1; + } } - this.perLoadBlockedRequestCount += aggregateCounts & 0xFFFF; - this.perLoadAllowedRequestCount += aggregateCounts >>> 16 & 0xFFFF; this.journalLastUncommitted = this.journalLastCommitted = -1; // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649 // No point updating the badge if it's not being displayed. - if ( (aggregateCounts & 0xFFFF) && µb.userSettings.showIconBadge ) { + if ( aggregateBlocked !== 0 && µb.userSettings.showIconBadge ) { µb.updateToolbarIcon(this.tabId, 0x02); } // Everything before pivot does not originate from current page -- we // still need to bump global blocked/allowed counts. - for ( let i = 0; i < pivot; i += 2 ) { - aggregateCounts += journal[i+1]; + for ( let i = 0; i < pivot; i += 3 ) { + if ( journal[i+1] === 1 ) { + aggregateBlocked += 1; + } else { + aggregateAllowed += 1; + } } - if ( aggregateCounts !== 0 ) { - µb.localSettings.blockedRequestCount += - aggregateCounts & 0xFFFF; - µb.localSettings.allowedRequestCount += - aggregateCounts >>> 16 & 0xFFFF; + if ( aggregateAllowed !== 0 || aggregateBlocked !== 0 ) { + µb.localSettings.blockedRequestCount += aggregateBlocked; + µb.localSettings.allowedRequestCount += aggregateAllowed; µb.localSettingsLastModified = now; } journal.length = 0; @@ -948,6 +1027,10 @@ PageStore.prototype.collapsibleResources = new Set([ µb.FilteringContext.SUB_FRAME, ]); +// To mitigate memory churning +PageStore.junkyard = []; +PageStore.junkyardMax = 10; + µb.PageStore = PageStore; /******************************************************************************/ diff --git a/src/js/popup-fenix.js b/src/js/popup-fenix.js index 37e028465..aac1d074e 100644 --- a/src/js/popup-fenix.js +++ b/src/js/popup-fenix.js @@ -49,7 +49,6 @@ vAPI.localStorage.getItemAsync('popupPanelSections').then(bits => { /******************************************************************************/ const messaging = vAPI.messaging; -const reIP = /^\d+(?:\.\d+){1,3}$/; const scopeToSrcHostnameMap = { '/': '*', '.': '' @@ -61,10 +60,7 @@ const domainsHitStr = vAPI.i18n('popupHitDomainCount'); let popupData = {}; let dfPaneBuilt = false; let dfHotspots = null; -let allDomains = {}; -let allDomainCount = 0; let allHostnameRows = []; -let touchedDomainCount = 0; let cachedPopupHash = ''; // https://github.com/gorhill/uBlock/issues/2550 @@ -125,13 +121,8 @@ const hashFromPopupData = function(reset) { const rules = popupData.firewallRules; for ( const key in rules ) { const rule = rules[key]; - if ( rule === null ) { continue; } - hasher.push( - rule.src + ' ' + - rule.des + ' ' + - rule.type + ' ' + - rule.action - ); + if ( rule === undefined ) { continue; } + hasher.push(rule); } hasher.sort(); hasher.push(uDom('body').hasClass('off')); @@ -149,6 +140,12 @@ const hashFromPopupData = function(reset) { /******************************************************************************/ +// greater-than-zero test + +const gtz = n => typeof n === 'number' && n > 0; + +/******************************************************************************/ + const formatNumber = function(count) { if ( typeof count !== 'number' ) { return ''; } if ( count < 1e6 ) { return count.toLocaleString(); } @@ -202,111 +199,153 @@ const safePunycodeToUnicode = function(hn) { /******************************************************************************/ -const rulekeyCompare = function(a, b) { - let ha = a.slice(2, a.indexOf(' ', 2)); - if ( !reIP.test(ha) ) { - ha = hostnameToSortableTokenMap.get(ha) || ' '; - } - let hb = b.slice(2, b.indexOf(' ', 2)); - if ( !reIP.test(hb) ) { - hb = hostnameToSortableTokenMap.get(hb) || ' '; - } - const ca = ha.charCodeAt(0); - const cb = hb.charCodeAt(0); - if ( ca !== cb ) { - return ca - cb; - } - return ha.localeCompare(hb); -}; - -/******************************************************************************/ - -const updateFirewallCell = function(scope, des, type, rule) { - const row = document.querySelector( - `#firewall div[data-des="${des}"][data-type="${type}"]` - ); - if ( row === null ) { return; } - - const cells = row.querySelectorAll(`:scope > span[data-src="${scope}"]`); - if ( cells.length === 0 ) { return; } - - if ( rule !== null ) { - cells.forEach(el => { el.setAttribute('class', rule.action + 'Rule'); }); - } else { - cells.forEach(el => { el.removeAttribute('class'); }); - } - - // Use dark shade visual cue if the rule is specific to the cell. - if ( - (rule !== null) && - (rule.des !== '*' || rule.type === type) && - (rule.des === des) && - (rule.src === scopeToSrcHostnameMap[scope]) - - ) { - cells.forEach(el => { el.classList.add('ownRule'); }); - } - - if ( scope !== '.' || des === '*' ) { return; } - - // Remember this may be a cell from a reused row, we need to clear text - // content if we can't compute request counts. - if ( popupData.hostnameDict.hasOwnProperty(des) === false ) { - cells.forEach(el => { - el.removeAttribute('data-acount'); - el.removeAttribute('data-bcount'); - }); - return; - } - - const hnDetails = popupData.hostnameDict[des]; - let cell = cells[0]; - if ( hnDetails.allowCount !== 0 ) { - cell.setAttribute('data-acount', Math.min(Math.ceil(Math.log(hnDetails.allowCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-acount', '0'); - } - if ( hnDetails.blockCount !== 0 ) { - cell.setAttribute('data-bcount', Math.min(Math.ceil(Math.log(hnDetails.blockCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-bcount', '0'); - } - - if ( hnDetails.domain !== des ) { - return; - } - - cell = cells[1]; - if ( hnDetails.totalAllowCount !== 0 ) { - cell.setAttribute('data-acount', Math.min(Math.ceil(Math.log(hnDetails.totalAllowCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-acount', '0'); - } - if ( hnDetails.totalBlockCount !== 0 ) { - cell.setAttribute('data-bcount', Math.min(Math.ceil(Math.log(hnDetails.totalBlockCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-bcount', '0'); +const updateFirewallCellCount = function(cells, allowed, blocked) { + for ( const cell of cells ) { + if ( gtz(allowed) ) { + cell.setAttribute( + 'data-acount', + Math.min(Math.ceil(Math.log(allowed + 1) / Math.LN10), 3) + ); + } else { + cell.setAttribute('data-acount', '0'); + } + if ( gtz(blocked) ) { + cell.setAttribute( + 'data-bcount', + Math.min(Math.ceil(Math.log(blocked + 1) / Math.LN10), 3) + ); + } else { + cell.setAttribute('data-bcount', '0'); + } } }; /******************************************************************************/ -const updateAllFirewallCells = function() { - const rules = popupData.firewallRules; - for ( const key in rules ) { - if ( rules.hasOwnProperty(key) === false ) { continue; } - updateFirewallCell( - key.charAt(0), - key.slice(2, key.indexOf(' ', 2)), - key.slice(key.lastIndexOf(' ') + 1), - rules[key] - ); +const updateFirewallCellRule = function(cells, scope, des, type, rule) { + const ruleParts = rule !== undefined ? rule.split(' ') : undefined; + + for ( const cell of cells ) { + if ( ruleParts === undefined ) { + cell.removeAttribute('class'); + continue; + } + + const action = updateFirewallCellRule.actionNames[ruleParts[3]]; + cell.setAttribute('class', `${action}Rule`); + + // Use dark shade visual cue if the rule is specific to the cell. + if ( + (ruleParts[1] !== '*' || ruleParts[2] === type) && + (ruleParts[1] === des) && + (ruleParts[0] === scopeToSrcHostnameMap[scope]) + + ) { + cell.classList.add('ownRule'); + } } +}; + +updateFirewallCellRule.actionNames = { '1': 'block', '2': 'allow', '3': 'noop' }; + +/******************************************************************************/ + +const updateAllFirewallCells = function(doRules = true, doCounts = true) { + const { pageDomain } = popupData; + const rowContainer = document.getElementById('firewall'); + const rows = rowContainer.querySelectorAll('#firewall > [data-des][data-type]'); + + let a1pScript = 0, b1pScript = 0; + let a3pScript = 0, b3pScript = 0; + let a3pFrame = 0, b3pFrame = 0; + + for ( const row of rows ) { + const des = row.getAttribute('data-des'); + const type = row.getAttribute('data-type'); + if ( doRules ) { + updateFirewallCellRule( + row.querySelectorAll(`:scope > span[data-src="/"]`), + '/', + des, + type, + popupData.firewallRules[`/ ${des} ${type}`] + ); + } + const cells = row.querySelectorAll(`:scope > span[data-src="."]`); + if ( doRules ) { + updateFirewallCellRule( + cells, + '.', + des, + type, + popupData.firewallRules[`. ${des} ${type}`] + ); + } + if ( des === '*' || type !== '*' ) { continue; } + if ( doCounts === false ) { continue; } + const hnDetails = popupData.hostnameDict[des]; + if ( hnDetails === undefined ) { + updateFirewallCellCount(cells); + continue; + } + const { allowed, blocked } = hnDetails.counts; + updateFirewallCellCount([ cells[0] ], allowed.any, blocked.any); + const { totals } = hnDetails; + if ( totals !== undefined ) { + updateFirewallCellCount([ cells[1] ], totals.allowed.any, totals.blocked.any); + } + if ( hnDetails.domain === pageDomain ) { + a1pScript += allowed.script; b1pScript += blocked.script; + } else { + a3pScript += allowed.script; b3pScript += blocked.script; + a3pFrame += allowed.frame; b3pFrame += blocked.frame; + } + } + + if ( doCounts ) { + const fromType = type => + document.querySelectorAll( + `#firewall > [data-des="*"][data-type="${type}"] > [data-src="."]` + ); + updateFirewallCellCount(fromType('1p-script'), a1pScript, b1pScript); + updateFirewallCellCount(fromType('3p-script'), a3pScript, b3pScript); + rowContainer.classList.toggle('has3pScript', a3pScript !== 0 || b3pScript !== 0); + updateFirewallCellCount(fromType('3p-frame'), a3pFrame, b3pFrame); + rowContainer.classList.toggle('has3pFrame', a3pFrame !== 0 || b3pFrame !== 0); + } + document.body.classList.toggle('needSave', popupData.matrixIsDirty === true); }; /******************************************************************************/ +// Compute statistics useful only to firewall entries -- we need to call +// this only when overview pane needs to be rendered. + +const expandHostnameStats = ( ) => { + let dnDetails; + for ( const des of allHostnameRows ) { + const hnDetails = popupData.hostnameDict[des]; + const { domain, counts } = hnDetails; + const isDomain = des === domain; + const { allowed: hnAllowed, blocked: hnBlocked } = counts; + if ( isDomain ) { + dnDetails = hnDetails; + dnDetails.totals = JSON.parse(JSON.stringify(dnDetails.counts)); + } else { + const { allowed: dnAllowed, blocked: dnBlocked } = dnDetails.totals; + dnAllowed.any += hnAllowed.any; + dnBlocked.any += hnBlocked.any; + } + hnDetails.hasScript = hnAllowed.script !== 0 || hnBlocked.script !== 0; + dnDetails.hasScript = dnDetails.hasScript || hnDetails.hasScript; + hnDetails.hasFrame = hnAllowed.frame !== 0 || hnBlocked.frame !== 0; + dnDetails.hasFrame = dnDetails.hasFrame || hnDetails.hasFrame; + } +}; + +/******************************************************************************/ + const buildAllFirewallRows = function() { // Do this before removing the rows if ( dfHotspots === null ) { @@ -315,33 +354,47 @@ const buildAllFirewallRows = function() { } dfHotspots.remove(); + // This must be called before we create the rows. + expandHostnameStats(); + // Update incrementally: reuse existing rows if possible. const rowContainer = document.getElementById('firewall'); const toAppend = document.createDocumentFragment(); - const rowTemplate = document.querySelector('#templates > div:nth-of-type(1)'); - let row = rowContainer.querySelector('div:nth-of-type(7) + div'); + const rowTemplate = document.querySelector( + '#templates > div[data-des=""][data-type="*"]' + ); + const { cnameMap, hostnameDict, pageDomain, pageHostname } = popupData; + + let row = rowContainer.querySelector( + 'div[data-des="*"][data-type="3p-frame"] + div' + ); for ( const des of allHostnameRows ) { if ( row === null ) { row = rowTemplate.cloneNode(true); toAppend.appendChild(row); } - row.setAttribute('data-des', des); - const hnDetails = popupData.hostnameDict[des] || {}; + const hnDetails = hostnameDict[des] || {}; const isDomain = des === hnDetails.domain; const prettyDomainName = punycode.toUnicode(des); const isPunycoded = prettyDomainName !== des; + if ( isDomain && row.childElementCount < 4 ) { + row.append(row.children[2].cloneNode(true)); + } else if ( isDomain === false && row.childElementCount === 4 ) { + row.children[3].remove(); + } + const span = row.querySelector('span:first-of-type'); span.querySelector('span').textContent = prettyDomainName; const classList = row.classList; let desExtra = ''; - if ( classList.toggle('isCname', popupData.cnameMap.has(des)) ) { - desExtra = punycode.toUnicode(popupData.cnameMap.get(des)); + if ( classList.toggle('isCname', cnameMap.has(des)) ) { + desExtra = punycode.toUnicode(cnameMap.get(des)); } else if ( isDomain && isPunycoded && reCyrillicAmbiguous.test(prettyDomainName) && @@ -351,13 +404,18 @@ const buildAllFirewallRows = function() { } span.querySelector('sub').textContent = desExtra; - classList.toggle('isRootContext', des === popupData.pageHostname); + classList.toggle('isRootContext', des === pageHostname); + classList.toggle('is3p', hnDetails.domain !== pageDomain); classList.toggle('isDomain', isDomain); classList.toggle('isSubDomain', !isDomain); - classList.toggle('allowed', hnDetails.allowCount !== 0); - classList.toggle('blocked', hnDetails.blockCount !== 0); - classList.toggle('totalAllowed', hnDetails.totalAllowCount !== 0); - classList.toggle('totalBlocked', hnDetails.totalBlockCount !== 0); + const { counts } = hnDetails; + classList.toggle('allowed', gtz(counts.allowed.any)); + classList.toggle('blocked', gtz(counts.blocked.any)); + const { totals } = hnDetails; + classList.toggle('totalAllowed', gtz(totals && totals.allowed.any)); + classList.toggle('totalBlocked', gtz(totals && totals.blocked.any)); + classList.toggle('hasScript', hnDetails.hasScript === true); + classList.toggle('hasFrame', hnDetails.hasFrame === true); classList.toggle('expandException', expandExceptions.has(hnDetails.domain)); row = row.nextElementSibling; @@ -366,14 +424,14 @@ const buildAllFirewallRows = function() { // Remove unused trailing rows if ( row !== null ) { while ( row.nextElementSibling !== null ) { - rowContainer.removeChild(row.nextElementSibling); + row.nextElementSibling.remove(); } - rowContainer.removeChild(row); + row.remove(); } // Add new rows all at once if ( toAppend.childElementCount !== 0 ) { - rowContainer.appendChild(toAppend); + rowContainer.append(toAppend); } if ( dfPaneBuilt !== true && popupData.advancedUserEnabled ) { @@ -389,28 +447,48 @@ const buildAllFirewallRows = function() { /******************************************************************************/ +const hostnameCompare = function(a, b) { + let ha = a; + if ( !reIP.test(ha) ) { + ha = hostnameToSortableTokenMap.get(ha) || ' '; + } + let hb = b; + if ( !reIP.test(hb) ) { + hb = hostnameToSortableTokenMap.get(hb) || ' '; + } + const ca = ha.charCodeAt(0); + const cb = hb.charCodeAt(0); + return ca !== cb ? ca - cb : ha.localeCompare(hb); +}; + +const reIP = /(\d|\])$/; + +/******************************************************************************/ + const renderPrivacyExposure = function() { - allDomains = {}; - allDomainCount = touchedDomainCount = 0; + const allDomains = {}; + let allDomainCount = 0; + let touchedDomainCount = 0; + allHostnameRows = []; // Sort hostnames. First-party hostnames must always appear at the top // of the list. const desHostnameDone = {}; - const keys = Object.keys(popupData.firewallRules) - .sort(rulekeyCompare); - for ( const key of keys ) { - const des = key.slice(2, key.indexOf(' ', 2)); + const keys = Object.keys(popupData.hostnameDict) + .sort(hostnameCompare); + for ( const des of keys ) { // Specific-type rules -- these are built-in if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; } - const hnDetails = popupData.hostnameDict[des] || {}; - if ( allDomains.hasOwnProperty(hnDetails.domain) === false ) { - allDomains[hnDetails.domain] = false; + const hnDetails = popupData.hostnameDict[des]; + const { domain, counts } = hnDetails; + if ( allDomains.hasOwnProperty(domain) === false ) { + allDomains[domain] = false; allDomainCount += 1; } - if ( hnDetails.allowCount !== 0 ) { - if ( allDomains[hnDetails.domain] === false ) { - allDomains[hnDetails.domain] = true; + if ( gtz(counts.allowed.any) ) { + if ( allDomains[domain] === false ) { + allDomains[domain] = true; touchedDomainCount += 1; } } @@ -419,9 +497,11 @@ const renderPrivacyExposure = function() { } const summary = domainsHitStr - .replace('{{count}}', touchedDomainCount.toLocaleString()) - .replace('{{total}}', allDomainCount.toLocaleString()); - uDom.nodeFromSelector('[data-i18n^="popupDomainsConnected"] + span').textContent = summary; + .replace('{{count}}', touchedDomainCount.toLocaleString()) + .replace('{{total}}', allDomainCount.toLocaleString()); + uDom.nodeFromSelector( + '[data-i18n^="popupDomainsConnected"] + span' + ).textContent = summary; }; /******************************************************************************/ @@ -481,8 +561,15 @@ const renderPopup = function() { uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker); - let blocked = popupData.pageBlockedRequestCount; - let total = popupData.pageAllowedRequestCount + blocked; + let blocked, total; + if ( popupData.pageCounts !== undefined ) { + const counts = popupData.pageCounts; + blocked = counts.blocked.any; + total = blocked + counts.allowed.any; + } else { + blocked = 0; + total = 0; + } let text; if ( total === 0 ) { text = formatNumber(0); @@ -885,7 +972,7 @@ const setFirewallRule = async function(src, des, type, action, persist) { } cachePopupData(response); - updateAllFirewallCells(); + updateAllFirewallCells(true, false); hashFromPopupData(); }; @@ -1075,8 +1162,7 @@ const revertFirewallRules = async function() { tabId: popupData.tabId, }); cachePopupData(response); - updateAllFirewallCells(); - updateHnSwitches(); + updateAllFirewallCells(true, false); hashFromPopupData(); }; @@ -1106,8 +1192,9 @@ const toggleHostnameSwitch = async function(ev) { }); cachePopupData(response); - updateAllFirewallCells(); hashFromPopupData(); + + document.body.classList.toggle('needSave', popupData.matrixIsDirty === true); }; /******************************************************************************* @@ -1276,6 +1363,8 @@ const getPopupData = async function(tabId) { }); } +/******************************************************************************/ + uDom('#switch').on('click', toggleNetFilteringSwitch); uDom('#gotoZap').on('click', gotoZap); uDom('#gotoPick').on('click', gotoPick); @@ -1284,6 +1373,36 @@ uDom('#saveRules').on('click', saveFirewallRules); uDom('#revertRules').on('click', ( ) => { revertFirewallRules(); }); uDom('a[href]').on('click', gotoURL); +// Toggle emphasis of rows with[out] 3rd-party scripts/frames +{ + const nextStep = (target, steps) => { + const firewall = document.getElementById('firewall'); + const cl = firewall.classList; + if ( cl.contains(steps[0]) ) { + cl.remove(steps[0]); + if ( firewall.querySelector(target) !== null ) { + cl.add(steps[1]); + } + return; + } + if ( cl.contains(steps[1]) ) { + cl.remove(steps[1]); + return; + } + cl.add(steps[0]); + }; + document.querySelector('#firewall > [data-type="3p-script"] .filter') + .addEventListener('click', ( ) => { + nextStep('.is3p.hasScript', [ 'show3pScript', 'hide3pScript' ]); + }); + + // Toggle visibility of rows with[out] 3rd-party frames + document.querySelector('#firewall > [data-type="3p-frame"] .filter') + .addEventListener('click', ( ) => { + nextStep('.is3p.hasFrame', [ 'show3pFrame', 'hide3pFrame' ]); + }); +} + /******************************************************************************/ // <<<<< end of local scope diff --git a/src/js/popup.js b/src/js/popup.js index 5442dbb24..b2cdc91a5 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -75,10 +75,7 @@ const domainsHitStr = vAPI.i18n('popupHitDomainCount'); let popupData = {}; let dfPaneBuilt = false; let dfHotspots = null; -let allDomains = {}; -let allDomainCount = 0; let allHostnameRows = []; -let touchedDomainCount = 0; let cachedPopupHash = ''; // https://github.com/gorhill/uBlock/issues/2550 @@ -181,6 +178,10 @@ const hashFromPopupData = function(reset) { /******************************************************************************/ +const gtz = n => typeof n === 'number' && n > 0; + +/******************************************************************************/ + const formatNumber = function(count) { return typeof count === 'number' ? count.toLocaleString() : ''; }; @@ -188,11 +189,11 @@ const formatNumber = function(count) { /******************************************************************************/ const rulekeyCompare = function(a, b) { - let ha = a.slice(2, a.indexOf(' ', 2)); + let ha = a; if ( !reIP.test(ha) ) { ha = hostnameToSortableTokenMap.get(ha) || ' '; } - let hb = b.slice(2, b.indexOf(' ', 2)); + let hb = b; if ( !reIP.test(hb) ) { hb = hostnameToSortableTokenMap.get(hb) || ' '; } @@ -206,69 +207,20 @@ const rulekeyCompare = function(a, b) { /******************************************************************************/ -const updateFirewallCell = function(scope, des, type, rule) { - const row = document.querySelector( - `#firewallContainer div[data-des="${des}"][data-type="${type}"]` - ); - if ( row === null ) { return; } - - const cells = row.querySelectorAll(`:scope > span[data-src="${scope}"]`); - if ( cells.length === 0 ) { return; } - - if ( rule !== null ) { - cells.forEach(el => { el.setAttribute('class', rule.action + 'Rule'); }); - } else { - cells.forEach(el => { el.removeAttribute('class'); }); - } - - // Use dark shade visual cue if the rule is specific to the cell. - if ( - (rule !== null) && - (rule.des !== '*' || rule.type === type) && - (rule.des === des) && - (rule.src === scopeToSrcHostnameMap[scope]) - - ) { - cells.forEach(el => { el.classList.add('ownRule'); }); - } - - if ( scope !== '.' || des === '*' ) { return; } - - // Remember this may be a cell from a reused row, we need to clear text - // content if we can't compute request counts. - if ( popupData.hostnameDict.hasOwnProperty(des) === false ) { - cells.forEach(el => { - el.removeAttribute('data-acount'); - el.removeAttribute('data-bcount'); - }); - return; - } - - const hnDetails = popupData.hostnameDict[des]; - let cell = cells[0]; - if ( hnDetails.allowCount !== 0 ) { - cell.setAttribute('data-acount', Math.min(Math.ceil(Math.log(hnDetails.allowCount + 1) / Math.LN10), 3)); +const updateFirewallCellCount = function(cell, allowed, blocked) { + if ( gtz(allowed) ) { + cell.setAttribute( + 'data-acount', + Math.min(Math.ceil(Math.log(allowed + 1) / Math.LN10), 3) + ); } else { cell.setAttribute('data-acount', '0'); } - if ( hnDetails.blockCount !== 0 ) { - cell.setAttribute('data-bcount', Math.min(Math.ceil(Math.log(hnDetails.blockCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-bcount', '0'); - } - - if ( hnDetails.domain !== des ) { - return; - } - - cell = cells[1]; - if ( hnDetails.totalAllowCount !== 0 ) { - cell.setAttribute('data-acount', Math.min(Math.ceil(Math.log(hnDetails.totalAllowCount + 1) / Math.LN10), 3)); - } else { - cell.setAttribute('data-acount', '0'); - } - if ( hnDetails.totalBlockCount !== 0 ) { - cell.setAttribute('data-bcount', Math.min(Math.ceil(Math.log(hnDetails.totalBlockCount + 1) / Math.LN10), 3)); + if ( gtz(blocked) ) { + cell.setAttribute( + 'data-bcount', + Math.min(Math.ceil(Math.log(blocked + 1) / Math.LN10), 3) + ); } else { cell.setAttribute('data-bcount', '0'); } @@ -276,16 +228,89 @@ const updateFirewallCell = function(scope, des, type, rule) { /******************************************************************************/ -const updateAllFirewallCells = function() { - const rules = popupData.firewallRules; - for ( const key in rules ) { - if ( rules.hasOwnProperty(key) === false ) { continue; } - updateFirewallCell( - key.charAt(0), - key.slice(2, key.indexOf(' ', 2)), - key.slice(key.lastIndexOf(' ') + 1), - rules[key] +const updateFirewallCellRule = function(cell, scope, des, type, ruleParts) { + if ( cell instanceof HTMLElement === false ) { return; } + + if ( ruleParts === undefined ) { + cell.removeAttribute('class'); + return; + } + + const action = updateFirewallCellRule.actionNames[ruleParts[3]]; + cell.setAttribute('class', `${action}Rule`); + + // Use dark shade visual cue if the rule is specific to the cell. + if ( + (ruleParts[1] !== '*' || ruleParts[2] === type) && + (ruleParts[1] === des) && + (ruleParts[0] === scopeToSrcHostnameMap[scope]) + + ) { + cell.classList.add('ownRule'); + } +}; + +updateFirewallCellRule.actionNames = { '1': 'block', '2': 'allow', '3': 'noop' }; + +/******************************************************************************/ + +const updateFirewallCell = function(row, scope, des, type, doCounts) { + if ( row instanceof HTMLElement === false ) { return; } + + const cells = row.querySelectorAll(`:scope > span[data-src="${scope}"]`); + if ( cells.length === 0 ) { return; } + + const rule = popupData.firewallRules[`${scope} ${des} ${type}`]; + const ruleParts = rule !== undefined ? rule.split(' ') : undefined; + for ( const cell of cells ) { + updateFirewallCellRule(cell, scope, des, type, ruleParts); + } + + if ( scope !== '.' || des === '*' ) { return; } + if ( doCounts !== true ) { return; } + + // Remember this may be a cell from a reused row, we need to clear text + // content if we can't compute request counts. + const hnDetails = popupData.hostnameDict[des]; + if ( hnDetails === undefined ) { + cells.forEach(el => { + updateFirewallCellCount(el); + }); + return; + } + + updateFirewallCellCount( + cells[0], + hnDetails.counts.allowed.any, + hnDetails.counts.blocked.any + ); + + if ( hnDetails.domain !== des || hnDetails.totals === undefined ) { + updateFirewallCellCount( + cells[1], + hnDetails.counts.allowed.any, + hnDetails.counts.blocked.any ); + return; + } + + updateFirewallCellCount( + cells[1], + hnDetails.totals.allowed.any, + hnDetails.totals.blocked.any + ); +}; + +/******************************************************************************/ + +const updateAllFirewallCells = function(doCounts = true) { + const rows = document.querySelectorAll('#firewallContainer > [data-des][data-type]'); + + for ( const row of rows ) { + const des = row.getAttribute('data-des'); + const type = row.getAttribute('data-type'); + updateFirewallCell(row, '/', des, type, doCounts); + updateFirewallCell(row, '.', des, type, doCounts); } const dirty = popupData.matrixIsDirty === true; @@ -297,6 +322,33 @@ const updateAllFirewallCells = function() { /******************************************************************************/ +// Compute statistics useful only to firewall entries -- we need to call +// this only when overview pane needs to be rendered. + +const expandHostnameStats = ( ) => { + let dnDetails; + for ( const des of allHostnameRows ) { + const hnDetails = popupData.hostnameDict[des]; + const { domain, counts } = hnDetails; + const isDomain = des === domain; + const { allowed: hnAllowed, blocked: hnBlocked } = counts; + if ( isDomain ) { + dnDetails = hnDetails; + dnDetails.totals = JSON.parse(JSON.stringify(dnDetails.counts)); + } else { + const { allowed: dnAllowed, blocked: dnBlocked } = dnDetails.totals; + dnAllowed.any += hnAllowed.any; + dnBlocked.any += hnBlocked.any; + } + hnDetails.hasScript = hnAllowed.script !== 0 || hnBlocked.script !== 0; + dnDetails.hasScript = dnDetails.hasScript || hnDetails.hasScript; + hnDetails.hasFrame = hnAllowed.frame !== 0 || hnBlocked.frame !== 0; + dnDetails.hasFrame = dnDetails.hasFrame || hnDetails.hasFrame; + } +}; + +/******************************************************************************/ + const buildAllFirewallRows = function() { // Do this before removing the rows if ( dfHotspots === null ) { @@ -305,6 +357,9 @@ const buildAllFirewallRows = function() { } dfHotspots.remove(); + // This must be called before we create the rows. + expandHostnameStats(); + // Update incrementally: reuse existing rows if possible. const rowContainer = document.getElementById('firewallContainer'); const toAppend = document.createDocumentFragment(); @@ -324,6 +379,8 @@ const buildAllFirewallRows = function() { const prettyDomainName = punycode.toUnicode(des); const isPunycoded = prettyDomainName !== des; + const { allowed: hnAllowed, blocked: hnBlocked } = hnDetails.counts; + const span = row.querySelector('span:first-of-type'); span.querySelector('span').textContent = prettyDomainName; @@ -344,12 +401,14 @@ const buildAllFirewallRows = function() { classList.toggle('isRootContext', des === popupData.pageHostname); classList.toggle('isDomain', isDomain); classList.toggle('isSubDomain', !isDomain); - classList.toggle('allowed', hnDetails.allowCount !== 0); - classList.toggle('blocked', hnDetails.blockCount !== 0); - classList.toggle('totalAllowed', hnDetails.totalAllowCount !== 0); - classList.toggle('totalBlocked', hnDetails.totalBlockCount !== 0); + classList.toggle('allowed', gtz(hnAllowed.any)); + classList.toggle('blocked', gtz(hnBlocked.any)); classList.toggle('expandException', expandExceptions.has(hnDetails.domain)); + const { totals } = hnDetails; + classList.toggle('totalAllowed', gtz(totals && totals.allowed.any)); + classList.toggle('totalBlocked', gtz(totals && totals.blocked.any)); + row = row.nextElementSibling; } @@ -380,27 +439,29 @@ const buildAllFirewallRows = function() { /******************************************************************************/ const renderPrivacyExposure = function() { - allDomains = {}; - allDomainCount = touchedDomainCount = 0; + const allDomains = {}; + let allDomainCount = 0; + let touchedDomainCount = 0; + allHostnameRows = []; // Sort hostnames. First-party hostnames must always appear at the top // of the list. const desHostnameDone = {}; - const keys = Object.keys(popupData.firewallRules) - .sort(rulekeyCompare); - for ( const key of keys ) { - const des = key.slice(2, key.indexOf(' ', 2)); + const keys = Object.keys(popupData.hostnameDict) + .sort(rulekeyCompare); + for ( const des of keys ) { // Specific-type rules -- these are built-in if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; } - const hnDetails = popupData.hostnameDict[des] || {}; - if ( allDomains.hasOwnProperty(hnDetails.domain) === false ) { - allDomains[hnDetails.domain] = false; + const hnDetails = popupData.hostnameDict[des]; + const { domain, counts } = hnDetails; + if ( allDomains.hasOwnProperty(domain) === false ) { + allDomains[domain] = false; allDomainCount += 1; } - if ( hnDetails.allowCount !== 0 ) { - if ( allDomains[hnDetails.domain] === false ) { - allDomains[hnDetails.domain] = true; + if ( gtz(counts.allowed.any) ) { + if ( allDomains[domain] === false ) { + allDomains[domain] = true; touchedDomainCount += 1; } } @@ -462,9 +523,16 @@ const renderPopup = function() { uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker); - let blocked = popupData.pageBlockedRequestCount, - total = popupData.pageAllowedRequestCount + blocked, - text; + let blocked, total; + if ( popupData.pageCounts !== undefined ) { + const counts = popupData.pageCounts; + blocked = counts.blocked.any; + total = blocked + counts.allowed.any; + } else { + blocked = 0; + total = 0; + } + let text; if ( total === 0 ) { text = formatNumber(0); } else { @@ -855,7 +923,7 @@ const setFirewallRule = async function(src, des, type, action, persist) { } cachePopupData(response); - updateAllFirewallCells(); + updateAllFirewallCells(false); hashFromPopupData(); }; @@ -1046,7 +1114,7 @@ const revertFirewallRules = async function() { tabId: popupData.tabId, }); cachePopupData(response); - updateAllFirewallCells(); + updateAllFirewallCells(false); updateHnSwitches(); hashFromPopupData(); }; @@ -1077,8 +1145,9 @@ const toggleHostnameSwitch = async function(ev) { }); cachePopupData(response); - updateAllFirewallCells(); hashFromPopupData(); + + document.body.classList.toggle('needSave', popupData.matrixIsDirty === true); }; /******************************************************************************/ diff --git a/src/js/tab.js b/src/js/tab.js index fe04dbe08..541b3d950 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -360,7 +360,7 @@ // filtering pane. const pageStore = µb.pageStoreFromTabId(openerTabId); if ( pageStore ) { - pageStore.journalAddRequest(fctxt.getHostname(), result); + pageStore.journalAddRequest(fctxt, result); pageStore.popupBlockedCount += 1; } @@ -1038,14 +1038,15 @@ vAPI.tabs = new vAPI.Tabs(); let badge = ''; let color = '#666'; - let pageStore = µb.pageStoreFromTabId(tabId); + const pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore !== null ) { state = pageStore.getNetFilteringSwitch() ? 1 : 0; if ( state === 1 ) { - if ( (parts & 0b0010) !== 0 && pageStore.perLoadBlockedRequestCount ) { - badge = µb.formatCount( - pageStore.perLoadBlockedRequestCount - ); + if ( (parts & 0b0010) !== 0 ) { + const blockCount = pageStore.counts.blocked.any; + if ( blockCount !== 0 ) { + badge = µb.formatCount(blockCount); + } } if ( (parts & 0b0100) !== 0 ) { color = computeBadgeColor( @@ -1071,7 +1072,7 @@ vAPI.tabs = new vAPI.Tabs(); return function(tabId, newParts = 0b0111) { if ( typeof tabId !== 'number' ) { return; } if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } - let currentParts = tabIdToDetails.get(tabId); + const currentParts = tabIdToDetails.get(tabId); if ( currentParts === newParts ) { return; } if ( currentParts === undefined ) { self.requestIdleCallback( diff --git a/src/js/traffic.js b/src/js/traffic.js index 003ec9a2f..7eacf89bb 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -88,7 +88,7 @@ const onBeforeRequest = function(details) { const result = pageStore.filterRequest(fctxt); - pageStore.journalAddRequest(fctxt.getHostname(), result); + pageStore.journalAddRequest(fctxt, result); if ( µb.logger.enabled ) { fctxt.setRealm('network').toLogger(); @@ -208,7 +208,7 @@ const onBeforeRootFrameRequest = function(fctxt) { const pageStore = µb.bindTabToPageStore(fctxt.tabId, 'beforeRequest'); if ( pageStore !== null ) { pageStore.journalAddRootFrame('uncommitted', requestURL); - pageStore.journalAddRequest(requestHostname, result); + pageStore.journalAddRequest(fctxt, result); } if ( loggerEnabled ) { @@ -400,7 +400,7 @@ const onBeforeBehindTheSceneRequest = function(fctxt) { gcTimer = vAPI.setTimeout(gc, 30011); } for ( const pageStore of pageStores ) { - pageStore.journalAddRequest(fctxt.getHostname(), result); + pageStore.journalAddRequest(fctxt, result); } }; } @@ -451,7 +451,7 @@ const onHeadersReceived = function(details) { fctxt.setRealm('network').toLogger(); } if ( result === 1 ) { - pageStore.journalAddRequest(fctxt.getHostname(), 1); + pageStore.journalAddRequest(fctxt, 1); return { cancel: true }; } } diff --git a/src/popup-fenix.html b/src/popup-fenix.html index 73c08e045..2ef5ef2d6 100644 --- a/src/popup-fenix.html +++ b/src/popup-fenix.html @@ -82,13 +82,13 @@
-
-
+
+