/*******************************************************************************
µMatrix - a Chromium browser extension to black/white list requests.
Copyright (C) 2014 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uMatrix
*/
/* global punycode, vAPI, uDom */
/* jshint esnext: true, bitwise: false */
/******************************************************************************/
/******************************************************************************/
(function() {
'use strict';
/******************************************************************************/
/******************************************************************************/
// Must be consistent with definitions in matrix.js
const Pale = 0x00;
const Dark = 0x80;
const Transparent = 0;
const Red = 1;
const Green = 2;
const Gray = 3;
const DarkRed = Dark | Red;
const PaleRed = Pale | Red;
const DarkGreen = Dark | Green;
const PaleGreen = Pale | Green;
const DarkGray = Dark | Gray;
const PaleGray = Pale | Gray;
var matrixSnapshot = {};
var groupsSnapshot = [];
var allHostnamesSnapshot = 'do not leave this initial string empty';
var targetTabId;
var matrixCellHotspots = null;
var matrixHeaderPrettyNames = {
'all': '',
'cookie': '',
'css': '',
'image': '',
'plugin': '',
'script': '',
'xhr': '',
'frame': '',
'other': ''
};
var firstPartyLabel = '';
var blacklistedHostnamesLabel = '';
/******************************************************************************/
/******************************************************************************/
// https://github.com/gorhill/httpswitchboard/issues/345
var onMessage = function(msg) {
if ( msg.what !== 'urlStatsChanged' ) {
return;
}
if ( matrixSnapshot.url !== msg.pageURL ) {
return;
}
queryMatrixSnapshot(makeMenu);
};
var messager = vAPI.messaging.channel('popup.js', onMessage);
/******************************************************************************/
/******************************************************************************/
function getUserSetting(setting) {
return matrixSnapshot.userSettings[setting];
}
function setUserSetting(setting, value) {
matrixSnapshot.userSettings[setting] = value;
messager.send({
what: 'userSettings',
name: setting,
value: value
});
}
/******************************************************************************/
function updateMatrixSnapshot() {
var snapshotReady = function() {
updateMatrixColors();
updateMatrixBehavior();
updateMatrixButtons();
};
queryMatrixSnapshot(snapshotReady);
}
/******************************************************************************/
// For display purpose, create four distinct groups of rows:
// 0th: literal "1st-party" row
// 1st: page domain's related
// 2nd: whitelisted
// 3rd: graylisted
// 4th: blacklisted
function getGroupStats() {
// Try to not reshuffle groups around while popup is opened if
// no new hostname added.
var latestDomainListSnapshot = Object.keys(matrixSnapshot.rows).sort().join();
if ( latestDomainListSnapshot === allHostnamesSnapshot ) {
return groupsSnapshot;
}
allHostnamesSnapshot = latestDomainListSnapshot;
// First, group according to whether at least one node in the domain
// hierarchy is white or blacklisted
var pageDomain = matrixSnapshot.domain;
var rows = matrixSnapshot.rows;
var columnOffsets = matrixSnapshot.headers;
var anyTypeOffset = columnOffsets['*'];
var hostname, domain;
var row, color, count, groupIndex;
var domainToGroupMap = {};
// These have hard-coded position which cannot be overriden
domainToGroupMap['1st-party'] = 0;
domainToGroupMap[pageDomain] = 1;
// 1st pass: domain wins if it has an explicit rule or a count
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
if ( hostname === '*' || hostname === '1st-party' ) {
continue;
}
domain = rows[hostname].domain;
if ( domain === pageDomain || hostname !== domain ) {
continue;
}
row = rows[domain];
color = row.temporary[anyTypeOffset];
if ( color === DarkGreen ) {
domainToGroupMap[domain] = 2;
continue;
}
if ( color === DarkRed ) {
domainToGroupMap[domain] = 4;
continue;
}
count = row.counts[anyTypeOffset];
if ( count !== 0 ) {
domainToGroupMap[domain] = 3;
continue;
}
}
// 2nd pass: green wins
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
row = rows[hostname];
domain = row.domain;
if ( domainToGroupMap.hasOwnProperty(domain) ) {
continue;
}
color = row.temporary[anyTypeOffset];
if ( color === DarkGreen ) {
domainToGroupMap[domain] = 2;
}
}
// 3rd pass: gray with count wins
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
row = rows[hostname];
domain = row.domain;
if ( domainToGroupMap.hasOwnProperty(domain) ) {
continue;
}
color = row.temporary[anyTypeOffset];
count = row.counts[anyTypeOffset];
if ( color !== DarkRed && count !== 0 ) {
domainToGroupMap[domain] = 3;
}
}
// 4th pass: red wins whatever is left
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
row = rows[hostname];
domain = row.domain;
if ( domainToGroupMap.hasOwnProperty(domain) ) {
continue;
}
color = row.temporary[anyTypeOffset];
if ( color === DarkRed ) {
domainToGroupMap[domain] = 4;
}
}
// 5th pass: gray wins whatever is left
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
domain = rows[hostname].domain;
if ( domainToGroupMap.hasOwnProperty(domain) ) {
continue;
}
domainToGroupMap[domain] = 3;
}
// Last pass: put each domain in a group
var groups = [ {}, {}, {}, {}, {} ];
var group;
for ( hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
if ( hostname === '*' ) {
continue;
}
domain = rows[hostname].domain;
groupIndex = domainToGroupMap[domain];
group = groups[groupIndex];
if ( group.hasOwnProperty(domain) === false ) {
group[domain] = {};
}
group[domain][hostname] = true;
}
groupsSnapshot = groups;
return groups;
}
/******************************************************************************/
// helpers
function getTemporaryColor(hostname, type) {
return matrixSnapshot.rows[hostname].temporary[matrixSnapshot.headers[type]];
}
function getPermanentColor(hostname, type) {
return matrixSnapshot.rows[hostname].permanent[matrixSnapshot.headers[type]];
}
function getCellClass(hostname, type) {
return 't' + getTemporaryColor(hostname, type).toString(16) +
' p' + getPermanentColor(hostname, type).toString(16);
}
/******************************************************************************/
// This is required for when we update the matrix while it is open:
// the user might have collapsed/expanded one or more domains, and we don't
// want to lose all his hardwork.
function getCollapseState(domain) {
var states = getUserSetting('popupCollapseSpecificDomains');
if ( states !== undefined && states[domain] !== undefined ) {
return states[domain];
}
return getUserSetting('popupCollapseDomains');
}
function toggleCollapseState(elem) {
if ( elem.ancestors('#matHead.collapsible').length > 0 ) {
toggleMainCollapseState(elem);
} else {
toggleSpecificCollapseState(elem);
}
}
function toggleMainCollapseState(uelem) {
var matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
var collapsed = matHead.hasClass('collapsed');
uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed);
setUserSetting('popupCollapseDomains', collapsed);
var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
var domains = Object.keys(specificCollapseStates);
var i = domains.length;
var domain;
while ( i-- ) {
domain = domains[i];
if ( specificCollapseStates[domain] === collapsed ) {
delete specificCollapseStates[domain];
}
}
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
}
function toggleSpecificCollapseState(uelem) {
// Remember collapse state forever, but only if it is different
// from main collapse switch.
var section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed');
var domain = section.prop('domain');
var collapsed = section.hasClass('collapsed');
var mainCollapseState = getUserSetting('popupCollapseDomains');
var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
if ( collapsed !== mainCollapseState ) {
specificCollapseStates[domain] = collapsed;
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
} else if ( specificCollapseStates[domain] !== undefined ) {
delete specificCollapseStates[domain];
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
}
}
/******************************************************************************/
// Update color of matrix cells(s)
// Color changes when rules change
function updateMatrixColors() {
var cells = uDom('.matrix .matRow.rw > .matCell').removeClass();
var i = cells.length;
var cell;
while ( i-- ) {
cell = cells.nodeAt(i);
cell.className = 'matCell ' + getCellClass(cell.hostname, cell.reqType);
}
}
/******************************************************************************/
// Update behavior of matrix:
// - Whether a section is collapsible or not. It is collapsible if:
// - It has at least one subdomain AND
// - There is no explicit rule anywhere in the subdomain cells AND
// - It is not part of group 3 (blacklisted hostnames)
function updateMatrixBehavior() {
matrixList = matrixList || uDom('#matList');
var sections = matrixList.descendants('.matSection');
var i = sections.length;
var section, subdomainRows, j, subdomainRow;
while ( i-- ) {
section = sections.at(i);
subdomainRows = section.descendants('.l2:not(.g4)');
j = subdomainRows.length;
while ( j-- ) {
subdomainRow = subdomainRows.at(j);
subdomainRow.toggleClass('collapsible', subdomainRow.descendants('.t81,.t82').length === 0);
}
section.toggleClass('collapsible', subdomainRows.filter('.collapsible').length > 0);
}
}
/******************************************************************************/
// handle user interaction with filters
function getCellAction(hostname, type, leaning) {
var temporaryColor = getTemporaryColor(hostname, type);
var hue = temporaryColor & 0x03;
// Special case: root toggle only between two states
if ( type === '*' && hostname === '*' ) {
return hue === Green ? 'blacklistMatrixCell' : 'whitelistMatrixCell';
}
// When explicitly blocked/allowed, can only graylist
var saturation = temporaryColor & 0x80;
if ( saturation === Dark ) {
return 'graylistMatrixCell';
}
return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell';
}
function handleFilter(button, leaning) {
// our parent cell knows who we are
var cell = button.ancestors('div.matCell');
var type = cell.prop('reqType');
var desHostname = cell.prop('hostname');
// https://github.com/gorhill/uMatrix/issues/24
// No hostname can happen -- like with blacklist meta row
if ( desHostname === '' ) {
return;
}
var request = {
what: getCellAction(desHostname, type, leaning),
srcHostname: matrixSnapshot.scope,
desHostname: desHostname,
type: type
};
messager.send(request, updateMatrixSnapshot);
}
function handleWhitelistFilter(button) {
handleFilter(button, 'whitelisting');
}
function handleBlacklistFilter(button) {
handleFilter(button, 'blacklisting');
}
/******************************************************************************/
var matrixRowPool = [];
var matrixSectionPool = [];
var matrixGroupPool = [];
var matrixRowTemplate = null;
var matrixList = null;
var startMatrixUpdate = function() {
matrixList = matrixList || uDom('#matList');
matrixList.detach();
var rows = matrixList.descendants('.matRow');
rows.detach();
matrixRowPool = matrixRowPool.concat(rows.toArray());
var sections = matrixList.descendants('.matSection');
sections.detach();
matrixSectionPool = matrixSectionPool.concat(sections.toArray());
var groups = matrixList.descendants('.matGroup');
groups.detach();
matrixGroupPool = matrixGroupPool.concat(groups.toArray());
};
var endMatrixUpdate = function() {
// https://github.com/gorhill/httpswitchboard/issues/246
// If the matrix has no rows, we need to insert a dummy one, invisible,
// to ensure the extension pop-up is properly sized. This is needed because
// the header pane's `position` property is `fixed`, which means it doesn't
// affect layout size, hence the matrix header row will be truncated.
if ( matrixSnapshot.rowCount <= 1 ) {
matrixList.append(createMatrixRow().css('visibility', 'hidden'));
}
updateMatrixBehavior();
matrixList.css('display', '');
matrixList.appendTo('.paneContent');
};
var createMatrixGroup = function() {
var group = matrixGroupPool.pop();
if ( group ) {
return uDom(group).removeClass().addClass('matGroup');
}
return uDom('
').addClass('matGroup');
};
var createMatrixSection = function() {
var section = matrixSectionPool.pop();
if ( section ) {
return uDom(section).removeClass().addClass('matSection');
}
return uDom('
').addClass('matSection');
};
var createMatrixRow = function() {
var row = matrixRowPool.pop();
if ( row ) {
row.style.visibility = '';
row = uDom(row);
row.descendants('.matCell').removeClass().addClass('matCell');
row.removeClass().addClass('matRow');
return row;
}
if ( matrixRowTemplate === null ) {
matrixRowTemplate = uDom('#templates .matRow');
}
return matrixRowTemplate.clone();
};
/******************************************************************************/
function renderMatrixHeaderRow() {
var matHead = uDom('#matHead.collapsible');
matHead.toggleClass('collapsed', getUserSetting('popupCollapseDomains'));
var cells = matHead.descendants('.matCell');
cells.at(0)
.prop('reqType', '*')
.prop('hostname', '*')
.addClass(getCellClass('*', '*'));
cells.at(1)
.prop('reqType', 'cookie')
.prop('hostname', '*')
.addClass(getCellClass('*', 'cookie'));
cells.at(2)
.prop('reqType', 'css')
.prop('hostname', '*')
.addClass(getCellClass('*', 'css'));
cells.at(3)
.prop('reqType', 'image')
.prop('hostname', '*')
.addClass(getCellClass('*', 'image'));
cells.at(4)
.prop('reqType', 'plugin')
.prop('hostname', '*')
.addClass(getCellClass('*', 'plugin'));
cells.at(5)
.prop('reqType', 'script')
.prop('hostname', '*')
.addClass(getCellClass('*', 'script'));
cells.at(6)
.prop('reqType', 'xhr')
.prop('hostname', '*')
.addClass(getCellClass('*', 'xhr'));
cells.at(7)
.prop('reqType', 'frame')
.prop('hostname', '*')
.addClass(getCellClass('*', 'frame'));
cells.at(8)
.prop('reqType', 'other')
.prop('hostname', '*')
.addClass(getCellClass('*', 'other'));
uDom('#matHead .matRow').css('display', '');
}
/******************************************************************************/
function renderMatrixCellDomain(cell, domain) {
var contents = cell.prop('reqType', '*')
.prop('hostname', domain)
.addClass(getCellClass(domain, '*'))
.contents();
contents.nodeAt(0).textContent = domain === '1st-party' ?
firstPartyLabel :
punycode.toUnicode(domain);
contents.nodeAt(1).textContent = ' ';
}
function renderMatrixCellSubdomain(cell, domain, subomain) {
var contents = cell.prop('reqType', '*')
.prop('hostname', subomain)
.addClass(getCellClass(subomain, '*'))
.contents();
contents.nodeAt(0).textContent = punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.';
contents.nodeAt(1).textContent = punycode.toUnicode(domain);
}
function renderMatrixMetaCellDomain(cell, domain) {
var contents = cell.prop('reqType', '*')
.prop('hostname', domain)
.addClass(getCellClass(domain, '*'))
.contents();
contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain);
contents.nodeAt(1).textContent = ' ';
}
function renderMatrixCellType(cell, hostname, type, count) {
cell.prop('reqType', type)
.prop('hostname', hostname)
.prop('count', count)
.addClass(getCellClass(hostname, type));
if ( count ) {
cell.text(count);
} else {
cell.text('\u00A0');
}
}
function renderMatrixCellTypes(cells, hostname, countName) {
var counts = matrixSnapshot.rows[hostname][countName];
var countIndices = matrixSnapshot.headers;
renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[countIndices.cookie]);
renderMatrixCellType(cells.at(2), hostname, 'css', counts[countIndices.css]);
renderMatrixCellType(cells.at(3), hostname, 'image', counts[countIndices.image]);
renderMatrixCellType(cells.at(4), hostname, 'plugin', counts[countIndices.plugin]);
renderMatrixCellType(cells.at(5), hostname, 'script', counts[countIndices.script]);
renderMatrixCellType(cells.at(6), hostname, 'xhr', counts[countIndices.xhr]);
renderMatrixCellType(cells.at(7), hostname, 'frame', counts[countIndices.frame]);
renderMatrixCellType(cells.at(8), hostname, 'other', counts[countIndices.other]);
}
/******************************************************************************/
function makeMatrixRowDomain(domain) {
var matrixRow = createMatrixRow().addClass('rw');
var cells = matrixRow.descendants('.matCell');
renderMatrixCellDomain(cells.at(0), domain);
renderMatrixCellTypes(cells, domain, 'counts');
return matrixRow;
}
function makeMatrixRowSubdomain(domain, subdomain) {
var matrixRow = createMatrixRow().addClass('rw');
var cells = matrixRow.descendants('.matCell');
renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
renderMatrixCellTypes(cells, subdomain, 'counts');
return matrixRow;
}
function makeMatrixMetaRowDomain(domain) {
var matrixRow = createMatrixRow().addClass('rw');
var cells = matrixRow.descendants('.matCell');
renderMatrixMetaCellDomain(cells.at(0), domain);
renderMatrixCellTypes(cells, domain, 'totals');
return matrixRow;
}
/******************************************************************************/
function renderMatrixMetaCellType(cell, count) {
// https://github.com/gorhill/uMatrix/issues/24
// Don't forget to reset cell properties
cell.addClass('t1')
.prop('reqType', '')
.prop('hostname', '')
.prop('count', count);
if ( count ) {
cell.text(count);
} else {
cell.text('\u00A0');
}
}
function makeMatrixMetaRow(totals) {
var typeOffsets = matrixSnapshot.headers;
var matrixRow = createMatrixRow().at(0).addClass('ro');
var cells = matrixRow.descendants('.matCell');
var contents = cells.at(0).addClass('t81').contents();
cells.at(0).prop('reqType', '*').prop('hostname', '');
contents.nodeAt(0).textContent = ' ';
contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace('{{count}}', totals[typeOffsets['*']]);
renderMatrixMetaCellType(cells.at(1), totals[typeOffsets.cookie]);
renderMatrixMetaCellType(cells.at(2), totals[typeOffsets.css]);
renderMatrixMetaCellType(cells.at(3), totals[typeOffsets.image]);
renderMatrixMetaCellType(cells.at(4), totals[typeOffsets.plugin]);
renderMatrixMetaCellType(cells.at(5), totals[typeOffsets.script]);
renderMatrixMetaCellType(cells.at(6), totals[typeOffsets.xhr]);
renderMatrixMetaCellType(cells.at(7), totals[typeOffsets.frame]);
renderMatrixMetaCellType(cells.at(8), totals[typeOffsets.other]);
return matrixRow;
}
/******************************************************************************/
function computeMatrixGroupMetaStats(group) {
var headers = matrixSnapshot.headers;
var n = Object.keys(headers).length;
var totals = new Array(n);
var i = n;
while ( i-- ) {
totals[i] = 0;
}
var rows = matrixSnapshot.rows, row;
for ( var hostname in rows ) {
if ( rows.hasOwnProperty(hostname) === false ) {
continue;
}
row = rows[hostname];
if ( group.hasOwnProperty(row.domain) === false ) {
continue;
}
if ( row.counts[headers['*']] === 0 ) {
continue;
}
totals[0] += 1;
for ( i = 1; i < n; i++ ) {
totals[i] += row.counts[i];
}
}
return totals;
}
/******************************************************************************/
// Compare hostname helper, to order hostname in a logical manner:
// top-most < bottom-most, take into account whether IP address or
// named hostname
function hostnameCompare(a,b) {
// Normalize: most significant parts first
if ( !a.match(/^\d+(\.\d+){1,3}$/) ) {
var aa = a.split('.');
a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
}
if ( !b.match(/^\d+(\.\d+){1,3}$/) ) {
var bb = b.split('.');
b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
}
return a.localeCompare(b);
}
/******************************************************************************/
function makeMatrixGroup0SectionDomain() {
return makeMatrixRowDomain('1st-party').addClass('g0 l1');
}
function makeMatrixGroup0Section() {
var domainDiv = createMatrixSection().prop('domain', '1st-party');
makeMatrixGroup0SectionDomain().appendTo(domainDiv);
return domainDiv;
}
function makeMatrixGroup0() {
// Show literal "1st-party" row only if there is
// at least one 1st-party hostname
if ( Object.keys(groupsSnapshot[1]).length === 0 ) {
return;
}
var groupDiv = createMatrixGroup().addClass('g0');
makeMatrixGroup0Section().appendTo(groupDiv);
groupDiv.appendTo(matrixList);
}
/******************************************************************************/
function makeMatrixGroup1SectionDomain(domain) {
return makeMatrixRowDomain(domain)
.addClass('g1 l1');
}
function makeMatrixGroup1SectionSubomain(domain, subdomain) {
return makeMatrixRowSubdomain(domain, subdomain)
.addClass('g1 l2');
}
function makeMatrixGroup1SectionMetaDomain(domain) {
return makeMatrixMetaRowDomain(domain).addClass('g1 l1 meta');
}
function makeMatrixGroup1Section(hostnames) {
var domain = hostnames[0];
var domainDiv = createMatrixSection()
.toggleClass('collapsed', getCollapseState(domain))
.prop('domain', domain);
if ( hostnames.length > 1 ) {
makeMatrixGroup1SectionMetaDomain(domain)
.appendTo(domainDiv);
}
makeMatrixGroup1SectionDomain(domain)
.appendTo(domainDiv);
for ( var i = 1; i < hostnames.length; i++ ) {
makeMatrixGroup1SectionSubomain(domain, hostnames[i])
.appendTo(domainDiv);
}
return domainDiv;
}
function makeMatrixGroup1(group) {
var domains = Object.keys(group).sort(hostnameCompare);
if ( domains.length ) {
var groupDiv = createMatrixGroup().addClass('g1');
makeMatrixGroup1Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
.appendTo(groupDiv);
for ( var i = 1; i < domains.length; i++ ) {
makeMatrixGroup1Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
.appendTo(groupDiv);
}
groupDiv.appendTo(matrixList);
}
}
/******************************************************************************/
function makeMatrixGroup2SectionDomain(domain) {
return makeMatrixRowDomain(domain)
.addClass('g2 l1');
}
function makeMatrixGroup2SectionSubomain(domain, subdomain) {
return makeMatrixRowSubdomain(domain, subdomain)
.addClass('g2 l2');
}
function makeMatrixGroup2SectionMetaDomain(domain) {
return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
}
function makeMatrixGroup2Section(hostnames) {
var domain = hostnames[0];
var domainDiv = createMatrixSection()
.toggleClass('collapsed', getCollapseState(domain))
.prop('domain', domain);
if ( hostnames.length > 1 ) {
makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
}
makeMatrixGroup2SectionDomain(domain)
.appendTo(domainDiv);
for ( var i = 1; i < hostnames.length; i++ ) {
makeMatrixGroup2SectionSubomain(domain, hostnames[i])
.appendTo(domainDiv);
}
return domainDiv;
}
function makeMatrixGroup2(group) {
var domains = Object.keys(group).sort(hostnameCompare);
if ( domains.length) {
var groupDiv = createMatrixGroup()
.addClass('g2');
makeMatrixGroup2Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
.appendTo(groupDiv);
for ( var i = 1; i < domains.length; i++ ) {
makeMatrixGroup2Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
.appendTo(groupDiv);
}
groupDiv.appendTo(matrixList);
}
}
/******************************************************************************/
function makeMatrixGroup3SectionDomain(domain) {
return makeMatrixRowDomain(domain)
.addClass('g3 l1');
}
function makeMatrixGroup3SectionSubomain(domain, subdomain) {
return makeMatrixRowSubdomain(domain, subdomain)
.addClass('g3 l2');
}
function makeMatrixGroup3SectionMetaDomain(domain) {
return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
}
function makeMatrixGroup3Section(hostnames) {
var domain = hostnames[0];
var domainDiv = createMatrixSection()
.toggleClass('collapsed', getCollapseState(domain))
.prop('domain', domain);
if ( hostnames.length > 1 ) {
makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
}
makeMatrixGroup3SectionDomain(domain)
.appendTo(domainDiv);
for ( var i = 1; i < hostnames.length; i++ ) {
makeMatrixGroup3SectionSubomain(domain, hostnames[i])
.appendTo(domainDiv);
}
return domainDiv;
}
function makeMatrixGroup3(group) {
var domains = Object.keys(group).sort(hostnameCompare);
if ( domains.length) {
var groupDiv = createMatrixGroup()
.addClass('g3');
makeMatrixGroup3Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
.appendTo(groupDiv);
for ( var i = 1; i < domains.length; i++ ) {
makeMatrixGroup3Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
.appendTo(groupDiv);
}
groupDiv.appendTo(matrixList);
}
}
/******************************************************************************/
function makeMatrixGroup4SectionDomain(domain) {
return makeMatrixRowDomain(domain)
.addClass('g4 l1');
}
function makeMatrixGroup4SectionSubomain(domain, subdomain) {
return makeMatrixRowSubdomain(domain, subdomain)
.addClass('g4 l2');
}
function makeMatrixGroup4Section(hostnames) {
var domain = hostnames[0];
var domainDiv = createMatrixSection()
.prop('domain', domain);
makeMatrixGroup4SectionDomain(domain)
.appendTo(domainDiv);
for ( var i = 1; i < hostnames.length; i++ ) {
makeMatrixGroup4SectionSubomain(domain, hostnames[i])
.appendTo(domainDiv);
}
return domainDiv;
}
function makeMatrixGroup4(group) {
var domains = Object.keys(group).sort(hostnameCompare);
if ( domains.length === 0 ) {
return;
}
var groupDiv = createMatrixGroup().addClass('g4');
createMatrixSection()
.addClass('g4Meta')
.toggleClass('g4Collapsed', !!getUserSetting('popupHideBlacklisted'))
.appendTo(groupDiv);
makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
.appendTo(groupDiv);
makeMatrixGroup4Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
.appendTo(groupDiv);
for ( var i = 1; i < domains.length; i++ ) {
makeMatrixGroup4Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
.appendTo(groupDiv);
}
groupDiv.appendTo(matrixList);
}
/******************************************************************************/
var makeMenu = function() {
var groupStats = getGroupStats();
if ( Object.keys(groupStats).length === 0 ) {
return;
}
// https://github.com/gorhill/httpswitchboard/issues/31
if ( matrixCellHotspots ) {
matrixCellHotspots.detach();
}
renderMatrixHeaderRow();
startMatrixUpdate();
makeMatrixGroup0(groupStats[0]);
makeMatrixGroup1(groupStats[1]);
makeMatrixGroup2(groupStats[2]);
makeMatrixGroup3(groupStats[3]);
makeMatrixGroup4(groupStats[4]);
endMatrixUpdate();
initScopeCell();
updateMatrixButtons();
};
/******************************************************************************/
// Do all the stuff that needs to be done before building menu et al.
function initMenuEnvironment() {
uDom('body').css('font-size', getUserSetting('displayTextSize'));
uDom('body').toggleClass('colorblind', getUserSetting('colorBlindFriendly') === true);
var prettyNames = matrixHeaderPrettyNames;
var keys = Object.keys(prettyNames);
var i = keys.length;
var cell, key, text;
while ( i-- ) {
key = keys[i];
cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
text = vAPI.i18n(key + 'PrettyName');
cell.text(text);
prettyNames[key] = text;
}
firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
blacklistedHostnamesLabel = uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
}
/******************************************************************************/
// Create page scopes for the web page
function selectGlobalScope() {
setUserSetting('popupScopeLevel', '*');
updateMatrixSnapshot();
dropDownMenuHide();
}
function selectDomainScope() {
setUserSetting('popupScopeLevel', 'domain');
updateMatrixSnapshot();
dropDownMenuHide();
}
function selectSiteScope() {
setUserSetting('popupScopeLevel', 'site');
updateMatrixSnapshot();
dropDownMenuHide();
}
function getClassFromScope() {
if ( matrixSnapshot.scope === '*' ) {
return 'tScopeGlobal';
}
if ( matrixSnapshot.scope === matrixSnapshot.domain ) {
return 'tScopeDomain';
}
return 'tScopeSite';
}
function initScopeCell() {
// It's possible there is no page URL at this point: some pages cannot
// be filtered by µMatrix.
if ( matrixSnapshot.url === '' ) {
return;
}
// Fill in the scope menu entries
if ( matrixSnapshot.hostname === matrixSnapshot.domain ) {
uDom('#scopeKeySite').css('display', 'none');
} else {
uDom('#scopeKeySite').text(matrixSnapshot.hostname);
}
uDom('#scopeKeyDomain').text(matrixSnapshot.domain);
updateScopeCell();
}
function updateScopeCell() {
uDom('body')
.removeClass('tScopeGlobal tScopeDomain tScopeSite')
.addClass(getClassFromScope());
uDom('#scopeCell').text(matrixSnapshot.scope.replace('*', '\u2217'));
}
/******************************************************************************/
function updateMatrixSwitches() {
var switches = matrixSnapshot.tSwitches;
for ( var switchName in switches ) {
if ( switches.hasOwnProperty(switchName) === false ) {
continue;
}
uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', switches[switchName]);
}
var count = matrixSnapshot.blockedCount;
var button = uDom('#mtxSwitch_matrix-off');
button.descendants('span.badge').text(count.toLocaleString());
button.attr('data-tip', button.attr('data-tip').replace('{{count}}', count));
uDom('body').toggleClass('powerOff', switches['matrix-off']);
}
function toggleMatrixSwitch() {
var pos = this.id.indexOf('_');
if ( pos === -1 ) {
return;
}
var switchName = this.id.slice(pos + 1);
var request = {
what: 'toggleMatrixSwitch',
switchName: switchName,
srcHostname: matrixSnapshot.scope
};
messager.send(request, updateMatrixSnapshot);
}
/******************************************************************************/
function updatePersistButton() {
var diffCount = matrixSnapshot.diff.length;
var button = uDom('#buttonPersist');
button.contents()
.filter(function(){return this.nodeType===3;})
.first()
.text(diffCount > 0 ? '\uf13e' : '\uf023');
button.descendants('span.badge').text(diffCount > 0 ? diffCount : '');
var disabled = diffCount === 0;
button.toggleClass('disabled', disabled);
uDom('#buttonRevertScope').toggleClass('disabled', disabled);
}
/******************************************************************************/
function persistMatrix() {
var request = {
what: 'applyDiffToPermanentMatrix',
diff: matrixSnapshot.diff
};
messager.send(request, updateMatrixSnapshot);
}
/******************************************************************************/
// rhill 2014-03-12: revert completely ALL changes related to the
// current page, including scopes.
function revertMatrix() {
var request = {
what: 'applyDiffToTemporaryMatrix',
diff: matrixSnapshot.diff
};
messager.send(request, updateMatrixSnapshot);
}
/******************************************************************************/
// Buttons which are affected by any changes in the matrix
function updateMatrixButtons() {
updateScopeCell();
updateMatrixSwitches();
updatePersistButton();
}
/******************************************************************************/
function revertAll() {
var request = {
what: 'revertTemporaryMatrix'
};
messager.send(request, updateMatrixSnapshot);
}
/******************************************************************************/
function buttonReloadHandler() {
messager.send({
what: 'forceReloadTab',
tabId: targetTabId
});
}
/******************************************************************************/
function mouseenterMatrixCellHandler() {
matrixCellHotspots.appendTo(this);
}
function mouseleaveMatrixCellHandler() {
matrixCellHotspots.detach();
}
/******************************************************************************/
function gotoExtensionURL() {
var url = uDom(this).attr('data-extension-url');
if ( url ) {
messager.send({ what: 'gotoExtensionURL', url: url });
}
}
/******************************************************************************/
function gotoExternalURL() {
var url = this.getAttribute('data-external-url');
if ( url ) {
messager.send({ what: 'gotoURL', url: url });
}
}
/******************************************************************************/
function dropDownMenuShow() {
uDom(this).next('.dropdown-menu').addClass('show');
}
function dropDownMenuHide() {
uDom('.dropdown-menu').removeClass('show');
}
/******************************************************************************/
var onMatrixSnapshotReady = function(response) {
// Now that tabId and pageURL are set, we can build our menu
initMenuEnvironment();
makeMenu();
// After popup menu is built, check whether there is a non-empty matrix
if ( matrixSnapshot.url === '' ) {
uDom('#matHead').remove();
uDom('#toolbarLeft').remove();
// https://github.com/gorhill/httpswitchboard/issues/191
uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
uDom('#noNetTrafficPrompt').css('display', '');
}
};
/******************************************************************************/
var queryMatrixSnapshot = function(callback) {
var request = {
what: 'matrixSnapshot',
tabId: targetTabId,
tabURL: matrixSnapshot.url
};
var snapshotReceived = function(response) {
matrixSnapshot = response;
callback();
};
messager.send(request, snapshotReceived);
};
/******************************************************************************/
// Make menu only when popup html is fully loaded
uDom.onLoad(function() {
queryMatrixSnapshot(onMatrixSnapshotReady);
// Below is UI stuff which is not key to make the menu, so this can
// be done without having to wait for a tab to be bound to the menu.
// We reuse for all cells the one and only cell hotspots.
uDom('#whitelist').on('click', function() {
handleWhitelistFilter(uDom(this));
return false;
});
uDom('#blacklist').on('click', function() {
handleBlacklistFilter(uDom(this));
return false;
});
uDom('#domainOnly').on('click', function() {
toggleCollapseState(uDom(this));
return false;
});
matrixCellHotspots = uDom('#cellHotspots').detach();
uDom('body')
.on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
.on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
uDom('#scopeKeyGlobal').on('click', selectGlobalScope);
uDom('#scopeKeyDomain').on('click', selectDomainScope);
uDom('#scopeKeySite').on('click', selectSiteScope);
uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
uDom('#buttonPersist').on('click', persistMatrix);
uDom('#buttonRevertScope').on('click', revertMatrix);
uDom('#buttonRevertAll').on('click', revertAll);
uDom('#buttonReload').on('click', buttonReloadHandler);
uDom('.extensionURL').on('click', gotoExtensionURL);
uDom('.externalURL').on('click', gotoExternalURL);
uDom('body').on('click', '.dropdown-menu-button', dropDownMenuShow);
uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);
uDom('#matList').on('click', '.g4Meta', function() {
var collapsed = uDom(this)
.toggleClass('g4Collapsed')
.hasClass('g4Collapsed');
setUserSetting('popupHideBlacklisted', collapsed);
});
});
/******************************************************************************/
})();