diff options
Diffstat (limited to 'js/popup.js')
-rw-r--r-- | js/popup.js | 1538 |
1 files changed, 1538 insertions, 0 deletions
diff --git a/js/popup.js b/js/popup.js new file mode 100644 index 0000000..98781b4 --- /dev/null +++ b/js/popup.js @@ -0,0 +1,1538 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 Raymond Hill + Copyright (C) 2019 Alessio Vanni + + 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/}. + + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +/* global punycode, uDom */ +/* jshint esnext: true, bitwise: false */ + +'use strict'; + +/******************************************************************************/ +/******************************************************************************/ + +(function() { + +/******************************************************************************/ +/******************************************************************************/ + +// Stuff which is good to do very early so as to avoid visual glitches. + +(function() { + var paneContentPaddingTop = vAPI.localStorage.getItem('paneContentPaddingTop'), + touchDevice = vAPI.localStorage.getItem('touchDevice'); + + if ( typeof paneContentPaddingTop === 'string' ) { + document.querySelector('.paneContent').style.setProperty( + 'padding-top', + paneContentPaddingTop + ); + } + if ( touchDevice === 'true' ) { + document.body.setAttribute('data-touch', 'true'); + } else { + document.addEventListener('touchstart', function onTouched(ev) { + document.removeEventListener(ev.type, onTouched); + document.body.setAttribute('data-touch', 'true'); + vAPI.localStorage.setItem('touchDevice', 'true'); + resizePopup(); + }); + } +})(); + +var popupWasResized = function() { + document.body.setAttribute('data-resize-popup', ''); +}; + +var resizePopup = (function() { + var timer; + var fix = function() { + timer = undefined; + var doc = document; + // Manually adjust the position of the main matrix according to the + // height of the toolbar/matrix header. + var paddingTop = (doc.querySelector('.paneHead').clientHeight + 2) + 'px', + paneContent = doc.querySelector('.paneContent'); + if ( paddingTop !== paneContent.style.paddingTop ) { + paneContent.style.setProperty('padding-top', paddingTop); + vAPI.localStorage.setItem('paneContentPaddingTop', paddingTop); + } + document.body.classList.toggle( + 'hConstrained', + window.innerWidth < document.body.clientWidth + ); + popupWasResized(); + }; + return function() { + if ( timer !== undefined ) { + clearTimeout(timer); + } + timer = vAPI.setTimeout(fix, 97); + }; +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// Must be consistent with definitions in matrix.js +var Dark = 0x80; +var Red = 1; +var Green = 2; +var DarkRed = Dark | Red; +var DarkGreen = Dark | Green; + +var matrixSnapshot = {}; +var groupsSnapshot = []; +var allHostnamesSnapshot = 'do not leave this initial string empty'; + +var matrixCellHotspots = null; + +var matrixHeaderPrettyNames = { + 'all': '', + 'cookie': '', + 'css': '', + 'image': '', + 'media': '', + 'script': '', + 'xhr': '', + 'frame': '', + 'other': '' +}; + +var firstPartyLabel = ''; +var blacklistedHostnamesLabel = ''; + +var expandosIdGenerator = 1; +var nodeToExpandosMap = (function() { + if ( typeof window.Map === 'function' ) { + return new window.Map(); + } +})(); + +var expandosFromNode = function(node) { + if ( + node instanceof HTMLElement === false && + typeof node.nodeAt === 'function' + ) { + node = node.nodeAt(0); + } + if ( nodeToExpandosMap ) { + var expandosId = node.getAttribute('data-expandos'); + if ( !expandosId ) { + expandosId = '' + (expandosIdGenerator++); + node.setAttribute('data-expandos', expandosId); + } + var expandos = nodeToExpandosMap.get(expandosId); + if ( expandos === undefined ) { + nodeToExpandosMap.set(expandosId, (expandos = Object.create(null))); + } + return expandos; + } + return node; +}; + +/******************************************************************************/ +/******************************************************************************/ + +function getUserSetting(setting) { + return matrixSnapshot.userSettings[setting]; + } + +function setUserSetting(setting, value) { + matrixSnapshot.userSettings[setting] = value; + vAPI.messaging.send('popup.js', { + what: 'userSettings', + name: setting, + value: value + }); +} + +/******************************************************************************/ + +function getUISetting(setting) { + var r = vAPI.localStorage.getItem(setting); + if ( typeof r !== 'string' ) { + return undefined; + } + return JSON.parse(r); +} + +function setUISetting(setting, value) { + vAPI.localStorage.setItem( + setting, + JSON.stringify(value) + ); +} + +/******************************************************************************/ + +function updateMatrixSnapshot() { + matrixSnapshotPoller.pollNow(); +} + +/******************************************************************************/ + +// 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 anyTypeOffset = matrixSnapshot.headerIndices.get('*'); + 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.headerIndices.get(type)]; +} + +function getPermanentColor(hostname, type) { + return matrixSnapshot.rows[hostname].permanent[matrixSnapshot.headerIndices.get(type)]; +} + +function addCellClass(cell, hostname, type) { + var cl = cell.classList; + cl.add('matCell'); + cl.add('t' + getTemporaryColor(hostname, type).toString(16)); + cl.add('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 = getUISetting('popupCollapseSpecificDomains'); + if ( typeof states === 'object' && states[domain] !== undefined ) { + return states[domain]; + } + return matrixSnapshot.collapseAllDomains === true; +} + +function toggleCollapseState(elem) { + if ( elem.ancestors('#matHead.collapsible').length > 0 ) { + toggleMainCollapseState(elem); + } else { + toggleSpecificCollapseState(elem); + } + popupWasResized(); +} + +function toggleMainCollapseState(uelem) { + var matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed'); + var collapsed = matrixSnapshot.collapseAllDomains = matHead.hasClass('collapsed'); + uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed); + setUserSetting('popupCollapseAllDomains', collapsed); + + var specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {}; + var domains = Object.keys(specificCollapseStates); + var i = domains.length; + var domain; + while ( i-- ) { + domain = domains[i]; + if ( specificCollapseStates[domain] === collapsed ) { + delete specificCollapseStates[domain]; + } + } + setUISetting('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'), + domain = expandosFromNode(section).domain, + collapsed = section.hasClass('collapsed'), + mainCollapseState = matrixSnapshot.collapseAllDomains === true, + specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {}; + if ( collapsed !== mainCollapseState ) { + specificCollapseStates[domain] = collapsed; + setUISetting('popupCollapseSpecificDomains', specificCollapseStates); + } else if ( specificCollapseStates[domain] !== undefined ) { + delete specificCollapseStates[domain]; + setUISetting('popupCollapseSpecificDomains', specificCollapseStates); + } +} + +/******************************************************************************/ + +// Update count value of matrix cells(s) + +function updateMatrixCounts() { + var matCells = uDom('.matrix .matRow.rw > .matCell'), + i = matCells.length, + matRow, matCell, count, counts, + headerIndices = matrixSnapshot.headerIndices, + rows = matrixSnapshot.rows, + expandos; + while ( i-- ) { + matCell = matCells.nodeAt(i); + expandos = expandosFromNode(matCell); + if ( expandos.hostname === '*' || expandos.reqType === '*' ) { + continue; + } + matRow = matCell.parentNode; + counts = matRow.classList.contains('meta') ? 'totals' : 'counts'; + count = rows[expandos.hostname][counts][headerIndices.get(expandos.reqType)]; + if ( count === expandos.count ) { continue; } + expandos.count = count; + matCell.textContent = cellTextFromCount(count); + } +} + +function cellTextFromCount(count) { + if ( count === 0 ) { return '\u00A0'; } + if ( count < 100 ) { return count; } + return '99+'; +} + +/******************************************************************************/ + +// Update color of matrix cells(s) +// Color changes when rules change + +function updateMatrixColors() { + var cells = uDom('.matrix .matRow.rw > .matCell').removeClass(), + i = cells.length, + cell, expandos; + while ( i-- ) { + cell = cells.nodeAt(i); + expandos = expandosFromNode(cell); + addCellClass(cell, expandos.hostname, expandos.reqType); + } + popupWasResized(); +} + +/******************************************************************************/ + +// 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'), + expandos = expandosFromNode(cell), + type = expandos.reqType, + desHostname = expandos.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 + }; + vAPI.messaging.send('popup.js', 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(document.createElement('div')).addClass('matGroup'); +}; + +var createMatrixSection = function() { + var section = matrixSectionPool.pop(); + if ( section ) { + return uDom(section).removeClass().addClass('matSection'); + } + return uDom(document.createElement('div')).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', matrixSnapshot.collapseAllDomains === true); + var cells = matHead.descendants('.matCell'), cell, expandos; + cell = cells.nodeAt(0); + expandos = expandosFromNode(cell); + expandos.reqType = '*'; + expandos.hostname = '*'; + addCellClass(cell, '*', '*'); + cell = cells.nodeAt(1); + expandos = expandosFromNode(cell); + expandos.reqType = 'cookie'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'cookie'); + cell = cells.nodeAt(2); + expandos = expandosFromNode(cell); + expandos.reqType = 'css'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'css'); + cell = cells.nodeAt(3); + expandos = expandosFromNode(cell); + expandos.reqType = 'image'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'image'); + cell = cells.nodeAt(4); + expandos = expandosFromNode(cell); + expandos.reqType = 'media'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'media'); + cell = cells.nodeAt(5); + expandos = expandosFromNode(cell); + expandos.reqType = 'script'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'script'); + cell = cells.nodeAt(6); + expandos = expandosFromNode(cell); + expandos.reqType = 'xhr'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'xhr'); + cell = cells.nodeAt(7); + expandos = expandosFromNode(cell); + expandos.reqType = 'frame'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'frame'); + cell = cells.nodeAt(8); + expandos = expandosFromNode(cell); + expandos.reqType = 'other'; + expandos.hostname = '*'; + addCellClass(cell, '*', 'other'); + uDom('#matHead .matRow').css('display', ''); +} + +/******************************************************************************/ + +function renderMatrixCellDomain(cell, domain) { + var expandos = expandosFromNode(cell); + expandos.hostname = domain; + expandos.reqType = '*'; + addCellClass(cell.nodeAt(0), domain, '*'); + var contents = cell.contents(); + contents.nodeAt(0).textContent = domain === '1st-party' ? + firstPartyLabel : + punycode.toUnicode(domain); + contents.nodeAt(1).textContent = ' '; +} + +function renderMatrixCellSubdomain(cell, domain, subomain) { + var expandos = expandosFromNode(cell); + expandos.hostname = subomain; + expandos.reqType = '*'; + addCellClass(cell.nodeAt(0), subomain, '*'); + var contents = cell.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 expandos = expandosFromNode(cell); + expandos.hostname = domain; + expandos.reqType = '*'; + addCellClass(cell.nodeAt(0), domain, '*'); + var contents = cell.contents(); + contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain); + contents.nodeAt(1).textContent = ' '; +} + +function renderMatrixCellType(cell, hostname, type, count) { + var node = cell.nodeAt(0), + expandos = expandosFromNode(node); + expandos.hostname = hostname; + expandos.reqType = type; + expandos.count = count; + addCellClass(node, hostname, type); + node.textContent = cellTextFromCount(count); +} + +function renderMatrixCellTypes(cells, hostname, countName) { + var counts = matrixSnapshot.rows[hostname][countName]; + var headerIndices = matrixSnapshot.headerIndices; + renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[headerIndices.get('cookie')]); + renderMatrixCellType(cells.at(2), hostname, 'css', counts[headerIndices.get('css')]); + renderMatrixCellType(cells.at(3), hostname, 'image', counts[headerIndices.get('image')]); + renderMatrixCellType(cells.at(4), hostname, 'media', counts[headerIndices.get('media')]); + renderMatrixCellType(cells.at(5), hostname, 'script', counts[headerIndices.get('script')]); + renderMatrixCellType(cells.at(6), hostname, 'xhr', counts[headerIndices.get('xhr')]); + renderMatrixCellType(cells.at(7), hostname, 'frame', counts[headerIndices.get('frame')]); + renderMatrixCellType(cells.at(8), hostname, 'other', counts[headerIndices.get('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 + var node = cell.nodeAt(0), + expandos = expandosFromNode(node); + expandos.hostname = ''; + expandos.reqType = ''; + expandos.count = count; + cell.addClass('t1'); + node.textContent = cellTextFromCount(count); +} + +function makeMatrixMetaRow(totals) { + var headerIndices = matrixSnapshot.headerIndices, + matrixRow = createMatrixRow().at(0).addClass('ro'), + cells = matrixRow.descendants('.matCell'), + contents = cells.at(0).addClass('t81').contents(), + expandos = expandosFromNode(cells.nodeAt(0)); + expandos.hostname = ''; + expandos.reqType = '*'; + contents.nodeAt(0).textContent = ' '; + contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace( + '{{count}}', + totals[headerIndices.get('*')].toLocaleString() + ); + renderMatrixMetaCellType(cells.at(1), totals[headerIndices.get('cookie')]); + renderMatrixMetaCellType(cells.at(2), totals[headerIndices.get('css')]); + renderMatrixMetaCellType(cells.at(3), totals[headerIndices.get('image')]); + renderMatrixMetaCellType(cells.at(4), totals[headerIndices.get('media')]); + renderMatrixMetaCellType(cells.at(5), totals[headerIndices.get('script')]); + renderMatrixMetaCellType(cells.at(6), totals[headerIndices.get('xhr')]); + renderMatrixMetaCellType(cells.at(7), totals[headerIndices.get('frame')]); + renderMatrixMetaCellType(cells.at(8), totals[headerIndices.get('other')]); + return matrixRow; +} + +/******************************************************************************/ + +function computeMatrixGroupMetaStats(group) { + var headerIndices = matrixSnapshot.headerIndices, + anyTypeIndex = headerIndices.get('*'), + n = headerIndices.size, + totals = new Array(n), + 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[anyTypeIndex] === 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(); + expandosFromNode(domainDiv).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)); + expandosFromNode(domainDiv).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)); + expandosFromNode(domainDiv).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)); + expandosFromNode(domainDiv).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(); + expandosFromNode(domainDiv).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', !!matrixSnapshot.collapseBlacklistedDomains) + .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(); + resizePopup(); +}; + +/******************************************************************************/ + +// Do all the stuff that needs to be done before building menu et al. + +function initMenuEnvironment() { + document.body.style.setProperty( + 'font-size', + getUserSetting('displayTextSize') + ); + document.body.classList.toggle( + 'colorblind', + getUserSetting('colorBlindFriendly') + ); + uDom.nodeFromId('version').textContent = matrixSnapshot.appVersion || ''; + + 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() { + if ( matrixSnapshot.scope === '*' ) { return; } + matrixSnapshot.scope = '*'; + document.body.classList.add('globalScope'); + matrixSnapshot.tMatrixModifiedTime = undefined; + updateMatrixSnapshot(); + dropDownMenuHide(); +} + +function selectSpecificScope(ev) { + var newScope = ev.target.getAttribute('data-scope'); + if ( !newScope || matrixSnapshot.scope === newScope ) { return; } + document.body.classList.remove('globalScope'); + matrixSnapshot.scope = newScope; + matrixSnapshot.tMatrixModifiedTime = undefined; + updateMatrixSnapshot(); + dropDownMenuHide(); +} + +function initScopeCell() { + // It's possible there is no page URL at this point: some pages cannot + // be filtered by uMatrix. + if ( matrixSnapshot.url === '' ) { return; } + var specificScope = uDom.nodeFromId('specificScope'); + + while ( specificScope.firstChild !== null ) { + specificScope.removeChild(specificScope.firstChild); + } + + // Fill in the scope menu entries + var pos = matrixSnapshot.domain.indexOf('.'); + var tld, labels; + if ( pos === -1 ) { + tld = ''; + labels = matrixSnapshot.hostname; + } else { + tld = matrixSnapshot.domain.slice(pos + 1); + labels = matrixSnapshot.hostname.slice(0, -tld.length); + } + var beg = 0, span, label; + while ( beg < labels.length ) { + pos = labels.indexOf('.', beg); + if ( pos === -1 ) { + pos = labels.length; + } else { + pos += 1; + } + label = document.createElement('span'); + label.appendChild( + document.createTextNode(punycode.toUnicode(labels.slice(beg, pos))) + ); + span = document.createElement('span'); + span.setAttribute('data-scope', labels.slice(beg) + tld); + span.appendChild(label); + specificScope.appendChild(span); + beg = pos; + } + if ( tld !== '' ) { + label = document.createElement('span'); + label.appendChild(document.createTextNode(punycode.toUnicode(tld))); + span = document.createElement('span'); + span.setAttribute('data-scope', tld); + span.appendChild(label); + specificScope.appendChild(span); + } + updateScopeCell(); +} + +function updateScopeCell() { + var specificScope = uDom.nodeFromId('specificScope'), + isGlobal = matrixSnapshot.scope === '*'; + document.body.classList.toggle('globalScope', isGlobal); + specificScope.classList.toggle('on', !isGlobal); + uDom.nodeFromId('globalScope').classList.toggle('on', isGlobal); + for ( var node of specificScope.children ) { + node.classList.toggle( + 'on', + !isGlobal && + matrixSnapshot.scope.endsWith(node.getAttribute('data-scope')) + ); + } +} + +/******************************************************************************/ + +function updateMatrixSwitches() { + var count = 0, + enabled, + switches = matrixSnapshot.tSwitches; + for ( var switchName in switches ) { + if ( switches.hasOwnProperty(switchName) === false ) { continue; } + enabled = switches[switchName]; + if ( enabled && switchName !== 'matrix-off' ) { + count += 1; + } + uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', enabled); + } + uDom.nodeFromId('mtxSwitch_https-strict').classList.toggle( + 'relevant', + matrixSnapshot.hasMixedContent + ); + uDom.nodeFromId('mtxSwitch_no-workers').classList.toggle( + 'relevant', + matrixSnapshot.hasWebWorkers + ); + uDom.nodeFromId('mtxSwitch_referrer-spoof').classList.toggle( + 'relevant', + matrixSnapshot.has3pReferrer + ); + uDom.nodeFromId('mtxSwitch_noscript-spoof').classList.toggle( + 'relevant', + matrixSnapshot.hasNoscriptTags + ); + uDom.nodeFromSelector('#buttonMtxSwitches span.badge').textContent = + count.toLocaleString(); + uDom.nodeFromSelector('#mtxSwitch_matrix-off span.badge').textContent = + matrixSnapshot.blockedCount.toLocaleString(); + document.body.classList.toggle('powerOff', switches['matrix-off']); +} + +function toggleMatrixSwitch(ev) { + if ( ev.target.localName === 'a' ) { return; } + var elem = ev.currentTarget; + var pos = elem.id.indexOf('_'); + if ( pos === -1 ) { return; } + var switchName = elem.id.slice(pos + 1); + var request = { + what: 'toggleMatrixSwitch', + switchName: switchName, + srcHostname: matrixSnapshot.scope + }; + vAPI.messaging.send('popup.js', 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 + }; + vAPI.messaging.send('popup.js', 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 + }; + vAPI.messaging.send('popup.js', request, updateMatrixSnapshot); +} + +/******************************************************************************/ + +// Buttons which are affected by any changes in the matrix + +function updateMatrixButtons() { + updateScopeCell(); + updateMatrixSwitches(); + updatePersistButton(); +} + +/******************************************************************************/ + +function revertAll() { + var request = { + what: 'revertTemporaryMatrix' + }; + vAPI.messaging.send('popup.js', request, updateMatrixSnapshot); + dropDownMenuHide(); +} + +/******************************************************************************/ + +function buttonReloadHandler(ev) { + vAPI.messaging.send('popup.js', { + what: 'forceReloadTab', + tabId: matrixSnapshot.tabId, + bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey + }); +} + +/******************************************************************************/ + +function mouseenterMatrixCellHandler(ev) { + matrixCellHotspots.appendTo(ev.target); +} + +function mouseleaveMatrixCellHandler() { + matrixCellHotspots.detach(); +} + +/******************************************************************************/ + +function gotoExtensionURL(ev) { + var url = uDom(ev.currentTarget).attr('data-extension-url'); + if ( url ) { + vAPI.messaging.send('popup.js', { + what: 'gotoExtensionURL', + url: url, + shiftKey: ev.shiftKey + }); + } + dropDownMenuHide(); + vAPI.closePopup(); +} + +/******************************************************************************/ + +function dropDownMenuShow(ev) { + var button = ev.target; + var menuOverlay = document.getElementById(button.getAttribute('data-dropdown-menu')); + var butnRect = button.getBoundingClientRect(); + var viewRect = document.body.getBoundingClientRect(); + var butnNormalLeft = butnRect.left / (viewRect.width - butnRect.width); + menuOverlay.classList.add('show'); + var menu = menuOverlay.querySelector('.dropdown-menu'); + var menuRect = menu.getBoundingClientRect(); + var menuLeft = butnNormalLeft * (viewRect.width - menuRect.width); + menu.style.left = menuLeft.toFixed(0) + 'px'; + menu.style.top = butnRect.bottom + 'px'; +} + +function dropDownMenuHide() { + uDom('.dropdown-menu-capture').removeClass('show'); +} + +/******************************************************************************/ + +var onMatrixSnapshotReady = function(response) { + if ( response === 'ENOTFOUND' ) { + uDom.nodeFromId('noTabFound').textContent = + vAPI.i18n('matrixNoTabFound'); + document.body.classList.add('noTabFound'); + return; + } + + // 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('#toolbarContainer').remove(); + + // https://github.com/gorhill/httpswitchboard/issues/191 + uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt')); + uDom('#noNetTrafficPrompt').css('display', ''); + } + + // Create a hash to find out whether the reload button needs to be + // highlighted. + // TODO: +}; + +/******************************************************************************/ + +var matrixSnapshotPoller = (function() { + var timer = null; + + var preprocessMatrixSnapshot = function(snapshot) { + if ( Array.isArray(snapshot.headerIndices) ) { + snapshot.headerIndices = new Map(snapshot.headerIndices); + } + return snapshot; + }; + + var processPollResult = function(response) { + if ( typeof response !== 'object' ) { + return; + } + if ( + response.mtxContentModified === false && + response.mtxCountModified === false && + response.pMatrixModified === false && + response.tMatrixModified === false + ) { + return; + } + matrixSnapshot = preprocessMatrixSnapshot(response); + + if ( response.mtxContentModified ) { + makeMenu(); + return; + } + if ( response.mtxCountModified ) { + updateMatrixCounts(); + } + if ( + response.pMatrixModified || + response.tMatrixModified || + response.scopeModified + ) { + updateMatrixColors(); + updateMatrixBehavior(); + updateMatrixButtons(); + } + }; + + var onPolled = function(response) { + processPollResult(response); + pollAsync(); + }; + + var pollNow = function() { + unpollAsync(); + vAPI.messaging.send('popup.js', { + what: 'matrixSnapshot', + tabId: matrixSnapshot.tabId, + scope: matrixSnapshot.scope, + mtxContentModifiedTime: matrixSnapshot.mtxContentModifiedTime, + mtxCountModifiedTime: matrixSnapshot.mtxCountModifiedTime, + mtxDiffCount: matrixSnapshot.diff.length, + pMatrixModifiedTime: matrixSnapshot.pMatrixModifiedTime, + tMatrixModifiedTime: matrixSnapshot.tMatrixModifiedTime, + }, onPolled); + }; + + var poll = function() { + timer = null; + pollNow(); + }; + + var pollAsync = function() { + if ( timer !== null ) { + return; + } + if ( document.defaultView === null ) { + return; + } + timer = vAPI.setTimeout(poll, 1414); + }; + + var unpollAsync = function() { + if ( timer !== null ) { + clearTimeout(timer); + timer = null; + } + }; + + (function() { + var tabId = matrixSnapshot.tabId; + + // If no tab id yet, see if there is one specified in our URL + if ( tabId === undefined ) { + var matches = window.location.search.match(/(?:\?|&)tabId=([^&]+)/); + if ( matches !== null ) { + tabId = matches[1]; + // No need for logger button when embedded in logger + uDom('[data-extension-url="logger-ui.html"]').remove(); + } + } + + var snapshotFetched = function(response) { + if ( typeof response === 'object' ) { + matrixSnapshot = preprocessMatrixSnapshot(response); + } + onMatrixSnapshotReady(response); + pollAsync(); + }; + + vAPI.messaging.send('popup.js', { + what: 'matrixSnapshot', + tabId: tabId + }, snapshotFetched); + })(); + + return { + pollNow: pollNow + }; +})(); + +/******************************************************************************/ + +// 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('#specificScope').on('click', selectSpecificScope); +uDom('#globalScope').on('click', selectGlobalScope); +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('body').on('click', '[data-dropdown-menu]', dropDownMenuShow); +uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide); + +uDom('#matList').on('click', '.g4Meta', function(ev) { + matrixSnapshot.collapseBlacklistedDomains = + ev.target.classList.toggle('g4Collapsed'); + setUserSetting( + 'popupCollapseBlacklistedDomains', + matrixSnapshot.collapseBlacklistedDomains + ); +}); + +/******************************************************************************/ + +})(); |