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.
This commit is contained in:
Raymond Hill 2021-02-15 06:52:31 -05:00
parent e2b988aed9
commit 435c91636f
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
9 changed files with 1095 additions and 839 deletions

View File

@ -262,7 +262,6 @@ body[data-more=""] #lessButton {
min-width: var(--popup-firewall-min-width); min-width: var(--popup-firewall-min-width);
padding: 0; padding: 0;
overflow-y: auto; overflow-y: auto;
text-align: right;
} }
:root.desktop body.vMin #firewall { :root.desktop body.vMin #firewall {
max-height: 100vh; max-height: 100vh;
@ -271,7 +270,6 @@ body[data-more=""] #lessButton {
border: 0; border: 0;
direction: ltr; direction: ltr;
display: flex; display: flex;
justify-content: flex-end;
margin: 0; margin: 0;
margin-top: 1px; margin-top: 1px;
padding: 0; padding: 0;
@ -305,10 +303,45 @@ body[data-more=""] #lessButton {
flex-grow: 1; flex-grow: 1;
justify-content: flex-end; justify-content: flex-end;
padding-right: 2px; padding-right: 2px;
text-align: right;
white-space: normal; white-space: normal;
width: calc(100% - var(--popup-rule-cell-width)); width: calc(100% - var(--popup-rule-cell-width));
word-break: break-word; 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 { #firewall > div.isCname > span:first-of-type {
color: var(--fg-popup-cell-cname); color: var(--fg-popup-cell-cname);
} }
@ -431,33 +464,33 @@ body.advancedUser #firewall > div > span:first-of-type ~ span {
width: 7px; width: 7px;
} }
#firewall > div.isRootContext > span:first-of-type::before { #firewall > div.isRootContext > span:first-of-type::before {
background-color: var(--fg-0-50); background: var(--fg-0-50);
width: 14px !important; width: 14px !important;
} }
#firewall > div.allowed > span:first-of-type::before, #firewall > div.allowed > span:first-of-type::before,
#firewall > div.isDomain.totalAllowed > 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.blocked > span:first-of-type::before,
#firewall > div.isDomain.totalBlocked > 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.allowed.blocked > span:first-of-type::before,
#firewall > div.isDomain.totalAllowed.totalBlocked > 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 */ /* Rule cells */
body.advancedUser #firewall > div > span.allowRule, body.advancedUser #firewall > div > span.allowRule,
#actionSelector > #dynaAllow { #actionSelector > #dynaAllow {
background-color: var(--bg-popup-cell-allow); background: var(--bg-popup-cell-allow);
} }
body.advancedUser #firewall > div > span.blockRule, body.advancedUser #firewall > div > span.blockRule,
#actionSelector > #dynaBlock { #actionSelector > #dynaBlock {
background-color: var(--bg-popup-cell-block); background: var(--bg-popup-cell-block);
} }
body.advancedUser #firewall > div > span.noopRule, body.advancedUser #firewall > div > span.noopRule,
#actionSelector > #dynaNoop { #actionSelector > #dynaNoop {
background-color: var(--bg-popup-cell-noop); background: var(--bg-popup-cell-noop);
} }
body.advancedUser #firewall > div > span.ownRule, body.advancedUser #firewall > div > span.ownRule,
#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, body.advancedUser #firewall > div > span.allowRule.ownRule,
:root:not(.mobile) #actionSelector > #dynaAllow:hover { :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, body.advancedUser #firewall > div > span.blockRule.ownRule,
:root:not(.mobile) #actionSelector > #dynaBlock:hover { :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, body.advancedUser #firewall > div > span.noopRule.ownRule,
:root:not(.mobile) #actionSelector > #dynaNoop:hover { :root:not(.mobile) #actionSelector > #dynaNoop:hover {
background-color: var(--bg-popup-cell-noop-own); background: var(--bg-popup-cell-noop-own);
} }
#actionSelector { #actionSelector {

View File

@ -26,17 +26,12 @@
/******************************************************************************/ /******************************************************************************/
µBlock.Firewall = (function() { {
// >>>>> start of local scope
/******************************************************************************/ /******************************************************************************/
var Matrix = function() { const supportedDynamicTypes = {
this.reset();
};
/******************************************************************************/
var supportedDynamicTypes = {
'3p': true, '3p': true,
'image': true, 'image': true,
'inline-script': true, 'inline-script': true,
@ -45,7 +40,7 @@ var supportedDynamicTypes = {
'3p-frame': true '3p-frame': true
}; };
var typeBitOffsets = { const typeBitOffsets = {
'*': 0, '*': 0,
'inline-script': 2, 'inline-script': 2,
'1p-script': 4, '1p-script': 4,
@ -55,13 +50,13 @@ var typeBitOffsets = {
'3p': 12 '3p': 12
}; };
var actionToNameMap = { const actionToNameMap = {
'1': 'block', '1': 'block',
'2': 'allow', '2': 'allow',
'3': 'noop' '3': 'noop'
}; };
var nameToActionMap = { const nameToActionMap = {
'block': 1, 'block': 1,
'allow': 2, 'allow': 2,
'noop': 3 'noop': 3
@ -70,187 +65,10 @@ var nameToActionMap = {
/******************************************************************************/ /******************************************************************************/
// For performance purpose, as simple tests as possible // For performance purpose, as simple tests as possible
var reBadHostname = /[^0-9a-z_.\[\]:%-]/; const reBadHostname = /[^0-9a-z_.\[\]:%-]/;
var reNotASCII = /[^\x20-\x7F]/; const reNotASCII = /[^\x20-\x7F]/;
/******************************************************************************/ const is3rdParty = function(srcHostname, desHostname) {
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) {
// If at least one is party-less, the relation can't be labelled // If at least one is party-less, the relation can't be labelled
// "3rd-party" // "3rd-party"
if ( desHostname === '*' || srcHostname === '*' || srcHostname === '' ) { if ( desHostname === '*' || srcHostname === '*' || srcHostname === '' ) {
@ -261,7 +79,7 @@ var is3rdParty = function(srcHostname, desHostname) {
// - localhost // - localhost
// - file-scheme // - file-scheme
// etc. // etc.
var srcDomain = domainFromHostname(srcHostname) || srcHostname; const srcDomain = domainFromHostname(srcHostname) || srcHostname;
if ( desHostname.endsWith(srcDomain) === false ) { if ( desHostname.endsWith(srcDomain) === false ) {
return true; return true;
@ -271,145 +89,446 @@ var is3rdParty = function(srcHostname, desHostname) {
desHostname.charAt(desHostname.length - srcDomain.length - 1) !== '.'; desHostname.charAt(desHostname.length - srcDomain.length - 1) !== '.';
}; };
var domainFromHostname = µBlock.URI.domainFromHostname; const domainFromHostname = µBlock.URI.domainFromHostname;
/******************************************************************************/ /******************************************************************************/
Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) { const Matrix = class {
µBlock.decomposeHostname(srcHostname, this.decomposedSource);
this.type = type; constructor() {
let bitOffset = typeBitOffsets[type]; this.reset();
for ( let shn of this.decomposedSource ) { }
this.z = shn;
let v = this.rules.get(shn + ' ' + desHostname);
if ( v !== undefined ) { reset() {
v = v >>> bitOffset & 3; this.r = 0;
if ( v !== 0 ) { this.type = '';
this.r = v; this.y = '';
return v; 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) { copyRules(from, srcHostname, desHostnames) {
// Pathological cases. // Specific types
if ( desHostname === '' ) { 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; this.r = 0;
return 0; return 0;
} }
// Precedence: from most specific to least specific
// Specific-destination, any party, any type evaluateCellZY(srcHostname, desHostname, type) {
µBlock.decomposeHostname(desHostname, this.decomposedDestination); // Pathological cases.
for ( let dhn of this.decomposedDestination ) { if ( desHostname === '' ) {
if ( dhn === '*' ) { break; } this.r = 0;
this.y = dhn; return 0;
if ( this.evaluateCellZ(srcHostname, dhn, '*') !== 0 ) {
return this.r;
} }
}
let thirdParty = is3rdParty(srcHostname, desHostname); // Precedence: from most specific to least specific
// Any destination // Specific-destination, any party, any type
this.y = '*'; µBlock.decomposeHostname(desHostname, this.decomposedDestination);
for ( const dhn of this.decomposedDestination ) {
// Specific party if ( dhn === '*' ) { break; }
if ( thirdParty ) { this.y = dhn;
// 3rd-party, specific type if ( this.evaluateCellZ(srcHostname, dhn, '*') !== 0 ) {
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; 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; return this.r;
} }
} else if ( type === 'script' ) {
// 1st party, specific type this.type = '';
if ( this.evaluateCellZ(srcHostname, '*', '1p-script') !== 0 ) { return 0;
return this.r; }
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) ) { validateRuleParts(parts) {
if ( this.evaluateCellZ(srcHostname, '*', type) !== 0 ) { if ( parts.length < 4 ) { return; }
return this.r;
// 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 ) { addFromRuleParts(parts) {
return this.r; 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 removeFromRuleParts(parts) {
if ( this.validateRuleParts(parts) !== undefined ) {
/******************************************************************************/ this.setCell(parts[0], parts[1], parts[2], 0);
return true;
Matrix.prototype.mustAllowCellZY = function(srcHostname, desHostname, type) { }
return this.evaluateCellZY(srcHostname, desHostname, type) === 2; return false;
};
/******************************************************************************/
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;
} }
return {
src: this.z,
des: this.y,
type: this.type,
action: r === 1 ? 'block' : (r === 2 ? 'allow' : 'noop')
};
};
/******************************************************************************/
Matrix.prototype.toLogData = function() { toSelfie() {
if ( this.r === 0 || this.type === '' ) { return; } return {
return { magicId: this.magicId,
source: 'dynamicHost', rules: Array.from(this.rules)
result: this.r, };
raw: `${this.z} ${this.y} ${this.type} ${this.intToActionMap.get(this.r)}` }
};
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([ Matrix.prototype.intToActionMap = new Map([
@ -418,168 +537,14 @@ Matrix.prototype.intToActionMap = new Map([
[ 3, 'noop' ] [ 3, 'noop' ]
]); ]);
/******************************************************************************/ Matrix.prototype.magicId = 1;
Matrix.prototype.srcHostnameFromRule = function(rule) {
return rule.slice(0, rule.indexOf(' '));
};
/******************************************************************************/ /******************************************************************************/
Matrix.prototype.desHostnameFromRule = function(rule) { µBlock.Firewall = Matrix;
return rule.slice(rule.indexOf(' ') + 1);
};
/******************************************************************************/ // <<<<< end of local scope
}
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
})();
/******************************************************************************/ /******************************************************************************/

View File

@ -216,78 +216,78 @@ vAPI.messaging.setup(onMessage);
const µb = µBlock; 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 hnDict = Object.create(null);
const cnMap = []; const cnMap = [];
for ( const [ hostname, hnCounts ] of hostnameToCountMap ) {
if ( hnDict[hostname] !== undefined ) { continue; } const createDictEntry = (domain, hostname, details) => {
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 cname = vAPI.net.canonicalNameFromHostname(hostname); const cname = vAPI.net.canonicalNameFromHostname(hostname);
if ( cname !== undefined ) { if ( cname !== undefined ) {
cnMap.push([ cname, hostname ]); 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.hostnameDict = hnDict;
out.cnameMap = cnMap; out.cnameMap = cnMap;
}; };
const getFirewallRules = function(srcHostname, desHostnames) { const firewallRuleTypes = [
const out = {}; '*',
'image',
'3p',
'inline-script',
'1p-script',
'3p-script',
'3p-frame',
];
const getFirewallRules = function(src, out) {
const { hostnameDict } = out;
const ruleset = {};
const df = µb.sessionFirewall; 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, '*', '*'); for ( const type of firewallRuleTypes ) {
out['. * image'] = df.lookupRuleData(srcHostname, '*', 'image'); let r = df.lookupRuleData('*', '*', type);
out['. * 3p'] = df.lookupRuleData(srcHostname, '*', '3p'); if ( r === undefined ) { continue; }
out['. * inline-script'] = df.lookupRuleData(srcHostname, ruleset[`/ * ${type}`] = r;
'*',
'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, '*');
} }
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) { const popupDataFromTabId = function(tabId, tabTitle) {
@ -311,8 +311,6 @@ const popupDataFromTabId = function(tabId, tabTitle) {
pageURL: tabContext.normalURL, pageURL: tabContext.normalURL,
pageHostname: rootHostname, pageHostname: rootHostname,
pageDomain: tabContext.rootDomain, pageDomain: tabContext.rootDomain,
pageAllowedRequestCount: 0,
pageBlockedRequestCount: 0,
popupBlockedCount: 0, popupBlockedCount: 0,
popupPanelSections: µbus.popupPanelSections, popupPanelSections: µbus.popupPanelSections,
popupPanelDisabledSections: µbhs.popupPanelDisabledSections, popupPanelDisabledSections: µbhs.popupPanelDisabledSections,
@ -329,23 +327,11 @@ const popupDataFromTabId = function(tabId, tabTitle) {
const pageStore = µb.pageStoreFromTabId(tabId); const pageStore = µb.pageStoreFromTabId(tabId);
if ( pageStore ) { if ( pageStore ) {
// https://github.com/gorhill/uBlock/issues/2105 r.pageCounts = pageStore.counts;
// 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.netFilteringSwitch = pageStore.getNetFilteringSwitch(); r.netFilteringSwitch = pageStore.getNetFilteringSwitch();
getHostnameDict(pageStore.hostnameToCountMap, r); getHostnameDict(pageStore.getAllHostnameDetails(), r);
r.contentLastModified = pageStore.contentLastModified; r.contentLastModified = pageStore.contentLastModified;
r.firewallRules = getFirewallRules(rootHostname, r.hostnameDict); getFirewallRules(rootHostname, r);
r.canElementPicker = µb.URI.isNetworkURI(r.rawURL); r.canElementPicker = µb.URI.isNetworkURI(r.rawURL);
r.noPopups = µb.sessionSwitches.evaluateZ( r.noPopups = µb.sessionSwitches.evaluateZ(
'no-popups', 'no-popups',

View File

@ -176,10 +176,6 @@ NetFilteringResultCache.prototype.extensionOriginURL = vAPI.getURL('/');
// Frame stores are used solely to associate a URL with a frame id. // 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 { const FrameStore = class {
constructor(frameURL, parentId) { constructor(frameURL, parentId) {
this.init(frameURL, parentId); this.init(frameURL, parentId);
@ -201,14 +197,14 @@ const FrameStore = class {
dispose() { dispose() {
this.rawURL = this.hostname = this.domain = ''; this.rawURL = this.hostname = this.domain = '';
if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { if ( FrameStore.junkyard.length < FrameStore.junkyardMax ) {
frameStoreJunkyard.push(this); FrameStore.junkyard.push(this);
} }
return null; return null;
} }
static factory(frameURL, parentId = -1) { static factory(frameURL, parentId = -1) {
const entry = frameStoreJunkyard.pop(); const entry = FrameStore.junkyard.pop();
if ( entry === undefined ) { if ( entry === undefined ) {
return new FrameStore(frameURL, parentId); 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 CountDetails = class {
const pageStoreJunkyard = []; constructor() {
const pageStoreJunkyardMax = 10; 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 { const PageStore = class {
constructor(tabId, context) { constructor(tabId, context) {
@ -230,11 +277,13 @@ const PageStore = class {
this.journalLastCommitted = this.journalLastUncommitted = -1; this.journalLastCommitted = this.journalLastUncommitted = -1;
this.journalLastUncommittedOrigin = undefined; this.journalLastUncommittedOrigin = undefined;
this.netFilteringCache = NetFilteringResultCache.factory(); this.netFilteringCache = NetFilteringResultCache.factory();
this.hostnameDetailsMap = new HostnameDetailsMap();
this.counts = new CountDetails();
this.init(tabId, context); this.init(tabId, context);
} }
static factory(tabId, context) { static factory(tabId, context) {
let entry = pageStoreJunkyard.pop(); let entry = PageStore.junkyard.pop();
if ( entry === undefined ) { if ( entry === undefined ) {
entry = new PageStore(tabId, context); entry = new PageStore(tabId, context);
} else { } else {
@ -263,11 +312,10 @@ const PageStore = class {
this.tabHostname = tabContext.rootHostname; this.tabHostname = tabContext.rootHostname;
this.title = tabContext.rawURL; this.title = tabContext.rawURL;
this.rawURL = tabContext.rawURL; this.rawURL = tabContext.rawURL;
this.hostnameToCountMap = new Map(); this.hostnameDetailsMap.reset();
this.contentLastModified = 0; this.contentLastModified = 0;
this.logData = undefined; this.logData = undefined;
this.perLoadBlockedRequestCount = 0; this.counts.reset();
this.perLoadAllowedRequestCount = 0;
this.remoteFontCount = 0; this.remoteFontCount = 0;
this.popupBlockedCount = 0; this.popupBlockedCount = 0;
this.largeMediaCount = 0; this.largeMediaCount = 0;
@ -342,7 +390,7 @@ const PageStore = class {
this.tabHostname = ''; this.tabHostname = '';
this.title = ''; this.title = '';
this.rawURL = ''; this.rawURL = '';
this.hostnameToCountMap = null; this.hostnameDetailsMap.dispose();
this.netFilteringCache.empty(); this.netFilteringCache.empty();
this.allowLargeMediaElementsUntil = Date.now(); this.allowLargeMediaElementsUntil = Date.now();
this.allowLargeMediaElementsRegex = undefined; this.allowLargeMediaElementsRegex = undefined;
@ -358,8 +406,8 @@ const PageStore = class {
this.journal = []; this.journal = [];
this.journalLastUncommittedOrigin = undefined; this.journalLastUncommittedOrigin = undefined;
this.journalLastCommitted = this.journalLastUncommitted = -1; this.journalLastCommitted = this.journalLastUncommitted = -1;
if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { if ( PageStore.junkyard.length < PageStore.junkyardMax ) {
pageStoreJunkyard.push(this); PageStore.junkyard.push(this);
} }
return null; return null;
} }
@ -454,6 +502,23 @@ const PageStore = class {
this.netFilteringCache.empty(); 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() { injectLargeMediaElementScriptlet() {
vAPI.tabs.executeScript(this.tabId, { vAPI.tabs.executeScript(this.tabId, {
file: '/js/scriptlets/load-large-media-interactive.js', file: '/js/scriptlets/load-large-media-interactive.js',
@ -478,18 +543,15 @@ const PageStore = class {
// https://github.com/gorhill/uBlock/issues/2053 // https://github.com/gorhill/uBlock/issues/2053
// There is no way around using journaling to ensure we deal properly with // There is no way around using journaling to ensure we deal properly with
// potentially out of order navigation events vs. network request events. // potentially out of order navigation events vs. network request events.
journalAddRequest(hostname, result) { journalAddRequest(fctxt, result) {
const hostname = fctxt.getHostname();
if ( hostname === '' ) { return; } if ( hostname === '' ) { return; }
this.journal.push( this.journal.push(hostname, result, fctxt.itype);
hostname, if ( this.journalTimer !== undefined ) { return; }
result === 1 ? 0x00000001 : 0x00010000 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) { journalAddRootFrame(type, url) {
@ -528,40 +590,57 @@ const PageStore = class {
const journal = this.journal; const journal = this.journal;
const pivot = Math.max(0, this.journalLastCommitted); const pivot = Math.max(0, this.journalLastCommitted);
const now = Date.now(); 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. // Everything after pivot originates from current page.
for ( let i = pivot; i < journal.length; i += 2 ) { for ( let i = pivot; i < journal.length; i += 3 ) {
const hostname = journal[i]; const hostname = journal[i+0];
let hostnameCounts = this.hostnameToCountMap.get(hostname); let hnDetails = this.hostnameDetailsMap.get(hostname);
if ( hostnameCounts === undefined ) { if ( hnDetails === undefined ) {
hostnameCounts = 0; hnDetails = new HostnameDetails(hostname);
this.hostnameDetailsMap.set(hostname, hnDetails);
this.contentLastModified = now; this.contentLastModified = now;
} }
let count = journal[i+1]; const blocked = journal[i+1] === 1;
this.hostnameToCountMap.set(hostname, hostnameCounts + count); const itype = journal[i+2];
aggregateCounts += count; 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; this.journalLastUncommitted = this.journalLastCommitted = -1;
// https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649 // https://github.com/chrisaljoudi/uBlock/issues/905#issuecomment-76543649
// No point updating the badge if it's not being displayed. // 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); µb.updateToolbarIcon(this.tabId, 0x02);
} }
// Everything before pivot does not originate from current page -- we // Everything before pivot does not originate from current page -- we
// still need to bump global blocked/allowed counts. // still need to bump global blocked/allowed counts.
for ( let i = 0; i < pivot; i += 2 ) { for ( let i = 0; i < pivot; i += 3 ) {
aggregateCounts += journal[i+1]; if ( journal[i+1] === 1 ) {
aggregateBlocked += 1;
} else {
aggregateAllowed += 1;
}
} }
if ( aggregateCounts !== 0 ) { if ( aggregateAllowed !== 0 || aggregateBlocked !== 0 ) {
µb.localSettings.blockedRequestCount += µb.localSettings.blockedRequestCount += aggregateBlocked;
aggregateCounts & 0xFFFF; µb.localSettings.allowedRequestCount += aggregateAllowed;
µb.localSettings.allowedRequestCount +=
aggregateCounts >>> 16 & 0xFFFF;
µb.localSettingsLastModified = now; µb.localSettingsLastModified = now;
} }
journal.length = 0; journal.length = 0;
@ -948,6 +1027,10 @@ PageStore.prototype.collapsibleResources = new Set([
µb.FilteringContext.SUB_FRAME, µb.FilteringContext.SUB_FRAME,
]); ]);
// To mitigate memory churning
PageStore.junkyard = [];
PageStore.junkyardMax = 10;
µb.PageStore = PageStore; µb.PageStore = PageStore;
/******************************************************************************/ /******************************************************************************/

View File

@ -49,7 +49,6 @@ vAPI.localStorage.getItemAsync('popupPanelSections').then(bits => {
/******************************************************************************/ /******************************************************************************/
const messaging = vAPI.messaging; const messaging = vAPI.messaging;
const reIP = /^\d+(?:\.\d+){1,3}$/;
const scopeToSrcHostnameMap = { const scopeToSrcHostnameMap = {
'/': '*', '/': '*',
'.': '' '.': ''
@ -61,10 +60,7 @@ const domainsHitStr = vAPI.i18n('popupHitDomainCount');
let popupData = {}; let popupData = {};
let dfPaneBuilt = false; let dfPaneBuilt = false;
let dfHotspots = null; let dfHotspots = null;
let allDomains = {};
let allDomainCount = 0;
let allHostnameRows = []; let allHostnameRows = [];
let touchedDomainCount = 0;
let cachedPopupHash = ''; let cachedPopupHash = '';
// https://github.com/gorhill/uBlock/issues/2550 // https://github.com/gorhill/uBlock/issues/2550
@ -125,13 +121,8 @@ const hashFromPopupData = function(reset) {
const rules = popupData.firewallRules; const rules = popupData.firewallRules;
for ( const key in rules ) { for ( const key in rules ) {
const rule = rules[key]; const rule = rules[key];
if ( rule === null ) { continue; } if ( rule === undefined ) { continue; }
hasher.push( hasher.push(rule);
rule.src + ' ' +
rule.des + ' ' +
rule.type + ' ' +
rule.action
);
} }
hasher.sort(); hasher.sort();
hasher.push(uDom('body').hasClass('off')); 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) { const formatNumber = function(count) {
if ( typeof count !== 'number' ) { return ''; } if ( typeof count !== 'number' ) { return ''; }
if ( count < 1e6 ) { return count.toLocaleString(); } if ( count < 1e6 ) { return count.toLocaleString(); }
@ -202,111 +199,153 @@ const safePunycodeToUnicode = function(hn) {
/******************************************************************************/ /******************************************************************************/
const rulekeyCompare = function(a, b) { const updateFirewallCellCount = function(cells, allowed, blocked) {
let ha = a.slice(2, a.indexOf(' ', 2)); for ( const cell of cells ) {
if ( !reIP.test(ha) ) { if ( gtz(allowed) ) {
ha = hostnameToSortableTokenMap.get(ha) || ' '; cell.setAttribute(
} 'data-acount',
let hb = b.slice(2, b.indexOf(' ', 2)); Math.min(Math.ceil(Math.log(allowed + 1) / Math.LN10), 3)
if ( !reIP.test(hb) ) { );
hb = hostnameToSortableTokenMap.get(hb) || ' '; } else {
} cell.setAttribute('data-acount', '0');
const ca = ha.charCodeAt(0); }
const cb = hb.charCodeAt(0); if ( gtz(blocked) ) {
if ( ca !== cb ) { cell.setAttribute(
return ca - cb; 'data-bcount',
} Math.min(Math.ceil(Math.log(blocked + 1) / Math.LN10), 3)
return ha.localeCompare(hb); );
}; } else {
cell.setAttribute('data-bcount', '0');
/******************************************************************************/ }
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 updateAllFirewallCells = function() { const updateFirewallCellRule = function(cells, scope, des, type, rule) {
const rules = popupData.firewallRules; const ruleParts = rule !== undefined ? rule.split(' ') : undefined;
for ( const key in rules ) {
if ( rules.hasOwnProperty(key) === false ) { continue; } for ( const cell of cells ) {
updateFirewallCell( if ( ruleParts === undefined ) {
key.charAt(0), cell.removeAttribute('class');
key.slice(2, key.indexOf(' ', 2)), continue;
key.slice(key.lastIndexOf(' ') + 1), }
rules[key]
); 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); 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() { const buildAllFirewallRows = function() {
// Do this before removing the rows // Do this before removing the rows
if ( dfHotspots === null ) { if ( dfHotspots === null ) {
@ -315,33 +354,47 @@ const buildAllFirewallRows = function() {
} }
dfHotspots.remove(); dfHotspots.remove();
// This must be called before we create the rows.
expandHostnameStats();
// Update incrementally: reuse existing rows if possible. // Update incrementally: reuse existing rows if possible.
const rowContainer = document.getElementById('firewall'); const rowContainer = document.getElementById('firewall');
const toAppend = document.createDocumentFragment(); const toAppend = document.createDocumentFragment();
const rowTemplate = document.querySelector('#templates > div:nth-of-type(1)'); const rowTemplate = document.querySelector(
let row = rowContainer.querySelector('div:nth-of-type(7) + div'); '#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 ) { for ( const des of allHostnameRows ) {
if ( row === null ) { if ( row === null ) {
row = rowTemplate.cloneNode(true); row = rowTemplate.cloneNode(true);
toAppend.appendChild(row); toAppend.appendChild(row);
} }
row.setAttribute('data-des', des); row.setAttribute('data-des', des);
const hnDetails = popupData.hostnameDict[des] || {}; const hnDetails = hostnameDict[des] || {};
const isDomain = des === hnDetails.domain; const isDomain = des === hnDetails.domain;
const prettyDomainName = punycode.toUnicode(des); const prettyDomainName = punycode.toUnicode(des);
const isPunycoded = prettyDomainName !== 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'); const span = row.querySelector('span:first-of-type');
span.querySelector('span').textContent = prettyDomainName; span.querySelector('span').textContent = prettyDomainName;
const classList = row.classList; const classList = row.classList;
let desExtra = ''; let desExtra = '';
if ( classList.toggle('isCname', popupData.cnameMap.has(des)) ) { if ( classList.toggle('isCname', cnameMap.has(des)) ) {
desExtra = punycode.toUnicode(popupData.cnameMap.get(des)); desExtra = punycode.toUnicode(cnameMap.get(des));
} else if ( } else if (
isDomain && isPunycoded && isDomain && isPunycoded &&
reCyrillicAmbiguous.test(prettyDomainName) && reCyrillicAmbiguous.test(prettyDomainName) &&
@ -351,13 +404,18 @@ const buildAllFirewallRows = function() {
} }
span.querySelector('sub').textContent = desExtra; 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('isDomain', isDomain);
classList.toggle('isSubDomain', !isDomain); classList.toggle('isSubDomain', !isDomain);
classList.toggle('allowed', hnDetails.allowCount !== 0); const { counts } = hnDetails;
classList.toggle('blocked', hnDetails.blockCount !== 0); classList.toggle('allowed', gtz(counts.allowed.any));
classList.toggle('totalAllowed', hnDetails.totalAllowCount !== 0); classList.toggle('blocked', gtz(counts.blocked.any));
classList.toggle('totalBlocked', hnDetails.totalBlockCount !== 0); 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)); classList.toggle('expandException', expandExceptions.has(hnDetails.domain));
row = row.nextElementSibling; row = row.nextElementSibling;
@ -366,14 +424,14 @@ const buildAllFirewallRows = function() {
// Remove unused trailing rows // Remove unused trailing rows
if ( row !== null ) { if ( row !== null ) {
while ( row.nextElementSibling !== null ) { while ( row.nextElementSibling !== null ) {
rowContainer.removeChild(row.nextElementSibling); row.nextElementSibling.remove();
} }
rowContainer.removeChild(row); row.remove();
} }
// Add new rows all at once // Add new rows all at once
if ( toAppend.childElementCount !== 0 ) { if ( toAppend.childElementCount !== 0 ) {
rowContainer.appendChild(toAppend); rowContainer.append(toAppend);
} }
if ( dfPaneBuilt !== true && popupData.advancedUserEnabled ) { 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() { const renderPrivacyExposure = function() {
allDomains = {}; const allDomains = {};
allDomainCount = touchedDomainCount = 0; let allDomainCount = 0;
let touchedDomainCount = 0;
allHostnameRows = []; allHostnameRows = [];
// Sort hostnames. First-party hostnames must always appear at the top // Sort hostnames. First-party hostnames must always appear at the top
// of the list. // of the list.
const desHostnameDone = {}; const desHostnameDone = {};
const keys = Object.keys(popupData.firewallRules) const keys = Object.keys(popupData.hostnameDict)
.sort(rulekeyCompare); .sort(hostnameCompare);
for ( const key of keys ) { for ( const des of keys ) {
const des = key.slice(2, key.indexOf(' ', 2));
// Specific-type rules -- these are built-in // Specific-type rules -- these are built-in
if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; } if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; }
const hnDetails = popupData.hostnameDict[des] || {}; const hnDetails = popupData.hostnameDict[des];
if ( allDomains.hasOwnProperty(hnDetails.domain) === false ) { const { domain, counts } = hnDetails;
allDomains[hnDetails.domain] = false; if ( allDomains.hasOwnProperty(domain) === false ) {
allDomains[domain] = false;
allDomainCount += 1; allDomainCount += 1;
} }
if ( hnDetails.allowCount !== 0 ) { if ( gtz(counts.allowed.any) ) {
if ( allDomains[hnDetails.domain] === false ) { if ( allDomains[domain] === false ) {
allDomains[hnDetails.domain] = true; allDomains[domain] = true;
touchedDomainCount += 1; touchedDomainCount += 1;
} }
} }
@ -419,9 +497,11 @@ const renderPrivacyExposure = function() {
} }
const summary = domainsHitStr const summary = domainsHitStr
.replace('{{count}}', touchedDomainCount.toLocaleString()) .replace('{{count}}', touchedDomainCount.toLocaleString())
.replace('{{total}}', allDomainCount.toLocaleString()); .replace('{{total}}', allDomainCount.toLocaleString());
uDom.nodeFromSelector('[data-i18n^="popupDomainsConnected"] + span').textContent = summary; uDom.nodeFromSelector(
'[data-i18n^="popupDomainsConnected"] + span'
).textContent = summary;
}; };
/******************************************************************************/ /******************************************************************************/
@ -481,8 +561,15 @@ const renderPopup = function() {
uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker);
uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker);
let blocked = popupData.pageBlockedRequestCount; let blocked, total;
let total = popupData.pageAllowedRequestCount + blocked; if ( popupData.pageCounts !== undefined ) {
const counts = popupData.pageCounts;
blocked = counts.blocked.any;
total = blocked + counts.allowed.any;
} else {
blocked = 0;
total = 0;
}
let text; let text;
if ( total === 0 ) { if ( total === 0 ) {
text = formatNumber(0); text = formatNumber(0);
@ -885,7 +972,7 @@ const setFirewallRule = async function(src, des, type, action, persist) {
} }
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells(); updateAllFirewallCells(true, false);
hashFromPopupData(); hashFromPopupData();
}; };
@ -1075,8 +1162,7 @@ const revertFirewallRules = async function() {
tabId: popupData.tabId, tabId: popupData.tabId,
}); });
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells(); updateAllFirewallCells(true, false);
updateHnSwitches();
hashFromPopupData(); hashFromPopupData();
}; };
@ -1106,8 +1192,9 @@ const toggleHostnameSwitch = async function(ev) {
}); });
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells();
hashFromPopupData(); hashFromPopupData();
document.body.classList.toggle('needSave', popupData.matrixIsDirty === true);
}; };
/******************************************************************************* /*******************************************************************************
@ -1276,6 +1363,8 @@ const getPopupData = async function(tabId) {
}); });
} }
/******************************************************************************/
uDom('#switch').on('click', toggleNetFilteringSwitch); uDom('#switch').on('click', toggleNetFilteringSwitch);
uDom('#gotoZap').on('click', gotoZap); uDom('#gotoZap').on('click', gotoZap);
uDom('#gotoPick').on('click', gotoPick); uDom('#gotoPick').on('click', gotoPick);
@ -1284,6 +1373,36 @@ uDom('#saveRules').on('click', saveFirewallRules);
uDom('#revertRules').on('click', ( ) => { revertFirewallRules(); }); uDom('#revertRules').on('click', ( ) => { revertFirewallRules(); });
uDom('a[href]').on('click', gotoURL); 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 // <<<<< end of local scope

View File

@ -75,10 +75,7 @@ const domainsHitStr = vAPI.i18n('popupHitDomainCount');
let popupData = {}; let popupData = {};
let dfPaneBuilt = false; let dfPaneBuilt = false;
let dfHotspots = null; let dfHotspots = null;
let allDomains = {};
let allDomainCount = 0;
let allHostnameRows = []; let allHostnameRows = [];
let touchedDomainCount = 0;
let cachedPopupHash = ''; let cachedPopupHash = '';
// https://github.com/gorhill/uBlock/issues/2550 // 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) { const formatNumber = function(count) {
return typeof count === 'number' ? count.toLocaleString() : ''; return typeof count === 'number' ? count.toLocaleString() : '';
}; };
@ -188,11 +189,11 @@ const formatNumber = function(count) {
/******************************************************************************/ /******************************************************************************/
const rulekeyCompare = function(a, b) { const rulekeyCompare = function(a, b) {
let ha = a.slice(2, a.indexOf(' ', 2)); let ha = a;
if ( !reIP.test(ha) ) { if ( !reIP.test(ha) ) {
ha = hostnameToSortableTokenMap.get(ha) || ' '; ha = hostnameToSortableTokenMap.get(ha) || ' ';
} }
let hb = b.slice(2, b.indexOf(' ', 2)); let hb = b;
if ( !reIP.test(hb) ) { if ( !reIP.test(hb) ) {
hb = hostnameToSortableTokenMap.get(hb) || ' '; hb = hostnameToSortableTokenMap.get(hb) || ' ';
} }
@ -206,69 +207,20 @@ const rulekeyCompare = function(a, b) {
/******************************************************************************/ /******************************************************************************/
const updateFirewallCell = function(scope, des, type, rule) { const updateFirewallCellCount = function(cell, allowed, blocked) {
const row = document.querySelector( if ( gtz(allowed) ) {
`#firewallContainer div[data-des="${des}"][data-type="${type}"]` cell.setAttribute(
); 'data-acount',
if ( row === null ) { return; } Math.min(Math.ceil(Math.log(allowed + 1) / Math.LN10), 3)
);
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 { } else {
cell.setAttribute('data-acount', '0'); cell.setAttribute('data-acount', '0');
} }
if ( hnDetails.blockCount !== 0 ) { if ( gtz(blocked) ) {
cell.setAttribute('data-bcount', Math.min(Math.ceil(Math.log(hnDetails.blockCount + 1) / Math.LN10), 3)); cell.setAttribute(
} else { 'data-bcount',
cell.setAttribute('data-bcount', '0'); Math.min(Math.ceil(Math.log(blocked + 1) / Math.LN10), 3)
} );
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 { } else {
cell.setAttribute('data-bcount', '0'); cell.setAttribute('data-bcount', '0');
} }
@ -276,16 +228,89 @@ const updateFirewallCell = function(scope, des, type, rule) {
/******************************************************************************/ /******************************************************************************/
const updateAllFirewallCells = function() { const updateFirewallCellRule = function(cell, scope, des, type, ruleParts) {
const rules = popupData.firewallRules; if ( cell instanceof HTMLElement === false ) { return; }
for ( const key in rules ) {
if ( rules.hasOwnProperty(key) === false ) { continue; } if ( ruleParts === undefined ) {
updateFirewallCell( cell.removeAttribute('class');
key.charAt(0), return;
key.slice(2, key.indexOf(' ', 2)), }
key.slice(key.lastIndexOf(' ') + 1),
rules[key] 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; 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() { const buildAllFirewallRows = function() {
// Do this before removing the rows // Do this before removing the rows
if ( dfHotspots === null ) { if ( dfHotspots === null ) {
@ -305,6 +357,9 @@ const buildAllFirewallRows = function() {
} }
dfHotspots.remove(); dfHotspots.remove();
// This must be called before we create the rows.
expandHostnameStats();
// Update incrementally: reuse existing rows if possible. // Update incrementally: reuse existing rows if possible.
const rowContainer = document.getElementById('firewallContainer'); const rowContainer = document.getElementById('firewallContainer');
const toAppend = document.createDocumentFragment(); const toAppend = document.createDocumentFragment();
@ -324,6 +379,8 @@ const buildAllFirewallRows = function() {
const prettyDomainName = punycode.toUnicode(des); const prettyDomainName = punycode.toUnicode(des);
const isPunycoded = prettyDomainName !== des; const isPunycoded = prettyDomainName !== des;
const { allowed: hnAllowed, blocked: hnBlocked } = hnDetails.counts;
const span = row.querySelector('span:first-of-type'); const span = row.querySelector('span:first-of-type');
span.querySelector('span').textContent = prettyDomainName; span.querySelector('span').textContent = prettyDomainName;
@ -344,12 +401,14 @@ const buildAllFirewallRows = function() {
classList.toggle('isRootContext', des === popupData.pageHostname); classList.toggle('isRootContext', des === popupData.pageHostname);
classList.toggle('isDomain', isDomain); classList.toggle('isDomain', isDomain);
classList.toggle('isSubDomain', !isDomain); classList.toggle('isSubDomain', !isDomain);
classList.toggle('allowed', hnDetails.allowCount !== 0); classList.toggle('allowed', gtz(hnAllowed.any));
classList.toggle('blocked', hnDetails.blockCount !== 0); classList.toggle('blocked', gtz(hnBlocked.any));
classList.toggle('totalAllowed', hnDetails.totalAllowCount !== 0);
classList.toggle('totalBlocked', hnDetails.totalBlockCount !== 0);
classList.toggle('expandException', expandExceptions.has(hnDetails.domain)); 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; row = row.nextElementSibling;
} }
@ -380,27 +439,29 @@ const buildAllFirewallRows = function() {
/******************************************************************************/ /******************************************************************************/
const renderPrivacyExposure = function() { const renderPrivacyExposure = function() {
allDomains = {}; const allDomains = {};
allDomainCount = touchedDomainCount = 0; let allDomainCount = 0;
let touchedDomainCount = 0;
allHostnameRows = []; allHostnameRows = [];
// Sort hostnames. First-party hostnames must always appear at the top // Sort hostnames. First-party hostnames must always appear at the top
// of the list. // of the list.
const desHostnameDone = {}; const desHostnameDone = {};
const keys = Object.keys(popupData.firewallRules) const keys = Object.keys(popupData.hostnameDict)
.sort(rulekeyCompare); .sort(rulekeyCompare);
for ( const key of keys ) { for ( const des of keys ) {
const des = key.slice(2, key.indexOf(' ', 2));
// Specific-type rules -- these are built-in // Specific-type rules -- these are built-in
if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; } if ( des === '*' || desHostnameDone.hasOwnProperty(des) ) { continue; }
const hnDetails = popupData.hostnameDict[des] || {}; const hnDetails = popupData.hostnameDict[des];
if ( allDomains.hasOwnProperty(hnDetails.domain) === false ) { const { domain, counts } = hnDetails;
allDomains[hnDetails.domain] = false; if ( allDomains.hasOwnProperty(domain) === false ) {
allDomains[domain] = false;
allDomainCount += 1; allDomainCount += 1;
} }
if ( hnDetails.allowCount !== 0 ) { if ( gtz(counts.allowed.any) ) {
if ( allDomains[hnDetails.domain] === false ) { if ( allDomains[domain] === false ) {
allDomains[hnDetails.domain] = true; allDomains[domain] = true;
touchedDomainCount += 1; touchedDomainCount += 1;
} }
} }
@ -462,9 +523,16 @@ const renderPopup = function() {
uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoPick').classList.toggle('enabled', canElementPicker);
uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker); uDom.nodeFromId('gotoZap').classList.toggle('enabled', canElementPicker);
let blocked = popupData.pageBlockedRequestCount, let blocked, total;
total = popupData.pageAllowedRequestCount + blocked, if ( popupData.pageCounts !== undefined ) {
text; const counts = popupData.pageCounts;
blocked = counts.blocked.any;
total = blocked + counts.allowed.any;
} else {
blocked = 0;
total = 0;
}
let text;
if ( total === 0 ) { if ( total === 0 ) {
text = formatNumber(0); text = formatNumber(0);
} else { } else {
@ -855,7 +923,7 @@ const setFirewallRule = async function(src, des, type, action, persist) {
} }
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells(); updateAllFirewallCells(false);
hashFromPopupData(); hashFromPopupData();
}; };
@ -1046,7 +1114,7 @@ const revertFirewallRules = async function() {
tabId: popupData.tabId, tabId: popupData.tabId,
}); });
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells(); updateAllFirewallCells(false);
updateHnSwitches(); updateHnSwitches();
hashFromPopupData(); hashFromPopupData();
}; };
@ -1077,8 +1145,9 @@ const toggleHostnameSwitch = async function(ev) {
}); });
cachePopupData(response); cachePopupData(response);
updateAllFirewallCells();
hashFromPopupData(); hashFromPopupData();
document.body.classList.toggle('needSave', popupData.matrixIsDirty === true);
}; };
/******************************************************************************/ /******************************************************************************/

View File

@ -360,7 +360,7 @@
// filtering pane. // filtering pane.
const pageStore = µb.pageStoreFromTabId(openerTabId); const pageStore = µb.pageStoreFromTabId(openerTabId);
if ( pageStore ) { if ( pageStore ) {
pageStore.journalAddRequest(fctxt.getHostname(), result); pageStore.journalAddRequest(fctxt, result);
pageStore.popupBlockedCount += 1; pageStore.popupBlockedCount += 1;
} }
@ -1038,14 +1038,15 @@ vAPI.tabs = new vAPI.Tabs();
let badge = ''; let badge = '';
let color = '#666'; let color = '#666';
let pageStore = µb.pageStoreFromTabId(tabId); const pageStore = µb.pageStoreFromTabId(tabId);
if ( pageStore !== null ) { if ( pageStore !== null ) {
state = pageStore.getNetFilteringSwitch() ? 1 : 0; state = pageStore.getNetFilteringSwitch() ? 1 : 0;
if ( state === 1 ) { if ( state === 1 ) {
if ( (parts & 0b0010) !== 0 && pageStore.perLoadBlockedRequestCount ) { if ( (parts & 0b0010) !== 0 ) {
badge = µb.formatCount( const blockCount = pageStore.counts.blocked.any;
pageStore.perLoadBlockedRequestCount if ( blockCount !== 0 ) {
); badge = µb.formatCount(blockCount);
}
} }
if ( (parts & 0b0100) !== 0 ) { if ( (parts & 0b0100) !== 0 ) {
color = computeBadgeColor( color = computeBadgeColor(
@ -1071,7 +1072,7 @@ vAPI.tabs = new vAPI.Tabs();
return function(tabId, newParts = 0b0111) { return function(tabId, newParts = 0b0111) {
if ( typeof tabId !== 'number' ) { return; } if ( typeof tabId !== 'number' ) { return; }
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
let currentParts = tabIdToDetails.get(tabId); const currentParts = tabIdToDetails.get(tabId);
if ( currentParts === newParts ) { return; } if ( currentParts === newParts ) { return; }
if ( currentParts === undefined ) { if ( currentParts === undefined ) {
self.requestIdleCallback( self.requestIdleCallback(

View File

@ -88,7 +88,7 @@ const onBeforeRequest = function(details) {
const result = pageStore.filterRequest(fctxt); const result = pageStore.filterRequest(fctxt);
pageStore.journalAddRequest(fctxt.getHostname(), result); pageStore.journalAddRequest(fctxt, result);
if ( µb.logger.enabled ) { if ( µb.logger.enabled ) {
fctxt.setRealm('network').toLogger(); fctxt.setRealm('network').toLogger();
@ -208,7 +208,7 @@ const onBeforeRootFrameRequest = function(fctxt) {
const pageStore = µb.bindTabToPageStore(fctxt.tabId, 'beforeRequest'); const pageStore = µb.bindTabToPageStore(fctxt.tabId, 'beforeRequest');
if ( pageStore !== null ) { if ( pageStore !== null ) {
pageStore.journalAddRootFrame('uncommitted', requestURL); pageStore.journalAddRootFrame('uncommitted', requestURL);
pageStore.journalAddRequest(requestHostname, result); pageStore.journalAddRequest(fctxt, result);
} }
if ( loggerEnabled ) { if ( loggerEnabled ) {
@ -400,7 +400,7 @@ const onBeforeBehindTheSceneRequest = function(fctxt) {
gcTimer = vAPI.setTimeout(gc, 30011); gcTimer = vAPI.setTimeout(gc, 30011);
} }
for ( const pageStore of pageStores ) { 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(); fctxt.setRealm('network').toLogger();
} }
if ( result === 1 ) { if ( result === 1 ) {
pageStore.journalAddRequest(fctxt.getHostname(), 1); pageStore.journalAddRequest(fctxt, 1);
return { cancel: true }; return { cancel: true };
} }
} }

View File

@ -82,13 +82,13 @@
<div data-des="*" data-type="3p"><span data-i18n="popup3pAnyRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div> <div data-des="*" data-type="3p"><span data-i18n="popup3pAnyRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div>
<div data-des="*" data-type="inline-script"><span data-i18n="popupInlineScriptRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div> <div data-des="*" data-type="inline-script"><span data-i18n="popupInlineScriptRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div>
<div data-des="*" data-type="1p-script"><span data-i18n="popup1pScriptRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div> <div data-des="*" data-type="1p-script"><span data-i18n="popup1pScriptRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div>
<div data-des="*" data-type="3p-script"><span data-i18n="popup3pScriptRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div> <div data-des="*" data-type="3p-script"><span><span class="filter" title="&#x2191;: Emphasize rows which have 3rd-party scripts&#x0A;&#x2193;: De-emphasize rows which have 3rd-party scripts"></span><span data-i18n="popup3pScriptRulePrompt"></span></span><span data-src="/"> </span><span data-src="."> </span></div>
<div data-des="*" data-type="3p-frame"><span data-i18n="popup3pFrameRulePrompt"></span><span data-src="/"> </span><span data-src="."> </span></div> <div data-des="*" data-type="3p-frame"><span><span class="filter" title="&#x2191;: Emphasize rows which have 3rd-party frames&#x0A;&#x2193;: De-emphasize rows which have 3rd-party frames"></span><span data-i18n="popup3pFrameRulePrompt"></span></span><span data-src="/"> </span><span data-src="."> </span></div>
</div> </div>
</div> </div>
<div id="templates" style="display: none"> <div id="templates" style="display: none">
<div data-des="" data-type="*"><span><span></span><sub></sub></span><span data-src="/"></span><span data-src="."></span><span data-src="."></span></div> <div data-des="" data-type="*"><span><span></span><sub></sub></span><span data-src="/"></span><span data-src="."></span></div>
<div id="actionSelector"><span id="dynaAllow"></span><span id="dynaNoop"></span><span id="dynaBlock"></span><span id="dynaCounts"></span></div> <div id="actionSelector"><span id="dynaAllow"></span><span id="dynaNoop"></span><span id="dynaBlock"></span><span id="dynaCounts"></span></div>
<div id="hotspotTip"></div> <div id="hotspotTip"></div>
<div id="tooltip"></div> <div id="tooltip"></div>