/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill Copyright (C) 2019-2020 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/}. Home: https://libregit.org/heckyel/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; (function () { Cu.import('chrome://ematrix/content/lib/Punycode.jsm'); // Stuff which is good to do very early so as to avoid visual glitches. (function () { let paneContentPaddingTop = vAPI.localStorage.getItem('paneContentPaddingTop'); let touchDevice = vAPI.localStorage.getItem('touchDevice'); if (typeof paneContentPaddingTop === 'string') { document .querySelector('.paneContent') .style .setProperty('padding-top', paneContentPaddingTop); } /* This is for CSS */ 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(); }); } })(); let popupWasResized = function () { document.body.setAttribute('data-resize-popup', ''); }; let resizePopup = (function () { let timer; let fix = function () { timer = undefined; let doc = document; // Manually adjust the position of the main matrix according to the // height of the toolbar/matrix header. let paddingTop = (doc.querySelector('.paneHead').clientHeight + 2) + 'px'; let 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 let Dark = 0x80; let Red = 1; let Green = 2; let DarkRed = Dark | Red; let DarkGreen = Dark | Green; let matrixSnapshot = {}; let groupsSnapshot = []; let allHostnamesSnapshot = 'do not leave this initial string empty'; let matrixCellHotspots = null; let matrixHeaderPrettyNames = { 'all': '', 'cookie': '', 'css': '', 'image': '', 'media': '', 'script': '', 'xhr': '', 'frame': '', 'other': '' }; let firstPartyLabel = ''; let blacklistedHostnamesLabel = ''; let expandosIdGenerator = 1; let nodeToExpandosMap = (function () { if (typeof window.Map === 'function') { return new window.Map(); } })(); let expandosFromNode = function (node) { if (node instanceof HTMLElement === false && typeof node.nodeAt === 'function') { node = node.nodeAt(0); } if (nodeToExpandosMap) { let expandosId = node.getAttribute('data-expandos'); if (!expandosId) { expandosId = '' + (expandosIdGenerator++); node.setAttribute('data-expandos', expandosId); } let expandos = nodeToExpandosMap.get(expandosId); if (expandos === undefined) { expandos = Object.create(null); nodeToExpandosMap.set(expandosId, expandos); } 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) { let 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. let 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 let pageDomain = matrixSnapshot.domain; let rows = matrixSnapshot.rows; let anyTypeOffset = matrixSnapshot.headerIndices.get('*'); let hostname, domain; let row, color, count; let 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 let groups = [ {}, {}, {}, {}, {} ]; for (hostname in rows) { if (rows.hasOwnProperty(hostname) === false) { continue; } if ( hostname === '*' ) { continue; } domain = rows[hostname].domain; let groupIndex = domainToGroupMap[domain]; let 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) { let 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) { let 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) { let matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed'); let collapsed = matrixSnapshot.collapseAllDomains = matHead.hasClass('collapsed'); uDom('#matList .matSection.collapsible') .toggleClass('collapsed', collapsed); setUserSetting('popupCollapseAllDomains', collapsed); let specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {}; let domains = Object.keys(specificCollapseStates); for (let i=domains.length-1; i>=0; --i) { let 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. let section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed'); let domain = expandosFromNode(section).domain; let collapsed = section.hasClass('collapsed'); let mainCollapseState = matrixSnapshot.collapseAllDomains === true; let 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() { let matCells = uDom('.matrix .matRow.rw > .matCell'); let matRow, matCell, count, counts; let headerIndices = matrixSnapshot.headerIndices; let rows = matrixSnapshot.rows; let expandos; for (let i=matCells.length-1; i>=0; --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() { let cells = uDom('.matrix .matRow.rw > .matCell').removeClass(); let cell, expandos; for (let i=cells.length-1; i>=0; --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'); let sections = matrixList.descendants('.matSection'); let section, subdomainRows, subdomainRow; for (let i=sections.length-1; i>=0; --i) { section = sections.at(i); subdomainRows = section.descendants('.l2:not(.g4)'); for (let j=subdomainRows.length-1; j>=0; --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) { let temporaryColor = getTemporaryColor(hostname, type); let 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 let saturation = temporaryColor & 0x80; if (saturation === Dark) { return 'graylistMatrixCell'; } return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell'; } function handleFilter(button, leaning) { // our parent cell knows who we are let cell = button.ancestors('div.matCell'); let expandos = expandosFromNode(cell); let type = expandos.reqType; let desHostname = expandos.hostname; // https://github.com/gorhill/uMatrix/issues/24 // No hostname can happen -- like with blacklist meta row if (desHostname === '') { return; } let 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'); } let matrixRowPool = []; let matrixSectionPool = []; let matrixGroupPool = []; let matrixRowTemplate = null; let matrixList = null; let startMatrixUpdate = function () { matrixList = matrixList || uDom('#matList'); matrixList.detach(); let rows = matrixList.descendants('.matRow'); rows.detach(); matrixRowPool = matrixRowPool.concat(rows.toArray()); let sections = matrixList.descendants('.matSection'); sections.detach(); matrixSectionPool = matrixSectionPool.concat(sections.toArray()); let groups = matrixList.descendants('.matGroup'); groups.detach(); matrixGroupPool = matrixGroupPool.concat(groups.toArray()); }; let 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'); }; let createMatrixGroup = function () { let group = matrixGroupPool.pop(); if (group) { return uDom(group).removeClass().addClass('matGroup'); } return uDom(document.createElement('div')).addClass('matGroup'); }; let createMatrixSection = function () { let section = matrixSectionPool.pop(); if (section) { return uDom(section).removeClass().addClass('matSection'); } return uDom(document.createElement('div')).addClass('matSection'); }; let createMatrixRow = function () { let 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() { let matHead = uDom('#matHead.collapsible'); matHead.toggleClass('collapsed', matrixSnapshot.collapseAllDomains === true); let cells = matHead.descendants('.matCell') let cell = cells.nodeAt(0); let 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) { let expandos = expandosFromNode(cell); expandos.hostname = domain; expandos.reqType = '*'; addCellClass(cell.nodeAt(0), domain, '*'); let contents = cell.contents(); contents.nodeAt(0).textContent = domain === '1st-party' ? firstPartyLabel : Punycode.toUnicode(domain); contents.nodeAt(1).textContent = ' '; } function renderMatrixCellSubdomain(cell, domain, subomain) { let expandos = expandosFromNode(cell); expandos.hostname = subomain; expandos.reqType = '*'; addCellClass(cell.nodeAt(0), subomain, '*'); let 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) { let expandos = expandosFromNode(cell); expandos.hostname = domain; expandos.reqType = '*'; addCellClass(cell.nodeAt(0), domain, '*'); let contents = cell.contents(); contents.nodeAt(0).textContent = '\u2217.' + Punycode.toUnicode(domain); contents.nodeAt(1).textContent = ' '; } function renderMatrixCellType(cell, hostname, type, count) { let node = cell.nodeAt(0); let 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) { let counts = matrixSnapshot.rows[hostname][countName]; let 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) { let matrixRow = createMatrixRow().addClass('rw'); let cells = matrixRow.descendants('.matCell'); renderMatrixCellDomain(cells.at(0), domain); renderMatrixCellTypes(cells, domain, 'counts'); return matrixRow; } function makeMatrixRowSubdomain(domain, subdomain) { let matrixRow = createMatrixRow().addClass('rw'); let cells = matrixRow.descendants('.matCell'); renderMatrixCellSubdomain(cells.at(0), domain, subdomain); renderMatrixCellTypes(cells, subdomain, 'counts'); return matrixRow; } function makeMatrixMetaRowDomain(domain) { let matrixRow = createMatrixRow().addClass('rw'); let 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 let node = cell.nodeAt(0); let expandos = expandosFromNode(node); expandos.hostname = ''; expandos.reqType = ''; expandos.count = count; cell.addClass('t1'); node.textContent = cellTextFromCount(count); } function makeMatrixMetaRow(totals) { let headerIndices = matrixSnapshot.headerIndices; let matrixRow = createMatrixRow().at(0).addClass('ro'); let cells = matrixRow.descendants('.matCell'); let contents = cells.at(0).addClass('t81').contents(); let 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) { let headerIndices = matrixSnapshot.headerIndices; let anyTypeIndex = headerIndices.get('*'); let totals = new Array(headerIndices.size); for (let i=headerIndices.size-1; i>=0; --i) { totals[i] = 0; } let rows = matrixSnapshot.rows; let row; for (let 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 (let i=1; i 1) { makeMatrixGroup1SectionMetaDomain(domain).appendTo(domainDiv); } makeMatrixGroup1SectionDomain(domain).appendTo(domainDiv); for (let i=1; i 1) { makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv); } makeMatrixGroup2SectionDomain(domain).appendTo(domainDiv); for (let i=1; i 1) { makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv); } makeMatrixGroup3SectionDomain(domain).appendTo(domainDiv); for (let i=1; i=0; --i) { let key = keys[i]; let cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]'); let 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) { let 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 ηMatrix. if (matrixSnapshot.url === '') { return; } let specificScope = uDom.nodeFromId('specificScope'); while (specificScope.firstChild !== null) { specificScope.removeChild(specificScope.firstChild); } // Fill in the scope menu entries let pos = matrixSnapshot.domain.indexOf('.'); let tld, labels; if (pos === -1) { tld = ''; labels = matrixSnapshot.hostname; } else { tld = matrixSnapshot.domain.slice(pos + 1); labels = matrixSnapshot.hostname.slice(0, -tld.length); } let beg = 0; let 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() { let specificScope = uDom.nodeFromId('specificScope'); let isGlobal = matrixSnapshot.scope === '*'; document.body.classList.toggle('globalScope', isGlobal); specificScope.classList.toggle('on', !isGlobal); uDom.nodeFromId('globalScope').classList.toggle('on', isGlobal); for (let node of specificScope.children) { node.classList.toggle('on', !isGlobal && matrixSnapshot .scope .endsWith(node.getAttribute('data-scope'))); } } function updateMatrixSwitches() { let count = 0; let enabled; let switches = matrixSnapshot.tSwitches; for (let 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; } let elem = ev.currentTarget; let pos = elem.id.indexOf('_'); if (pos === -1) { return; } let switchName = elem.id.slice(pos + 1); let request = { what: 'toggleMatrixSwitch', switchName: switchName, srcHostname: matrixSnapshot.scope }; vAPI.messaging.send('popup.js', request, updateMatrixSnapshot); } function updatePersistButton() { let diffCount = matrixSnapshot.diff.length; let 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 : ''); let disabled = diffCount === 0; button.toggleClass('disabled', disabled); uDom('#buttonRevertScope').toggleClass('disabled', disabled); } function persistMatrix() { let 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() { let 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() { let 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) { let 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) { let button = ev.target; let menuOverlay = document.getElementById(button.getAttribute('data-dropdown-menu')); let butnRect = button.getBoundingClientRect(); let viewRect = document.body.getBoundingClientRect(); let butnNormalLeft = butnRect.left / (viewRect.width - butnRect.width); menuOverlay.classList.add('show'); let menu = menuOverlay.querySelector('.dropdown-menu'); let menuRect = menu.getBoundingClientRect(); let 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'); } let 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: // ηMatrix: not sure what the purpose of highlighting is... // Maybe telling the user that the page needs refreshing? But // that's hardly useful (and by now people have gotten used to // the lack of such a feature.) // Not really going to do it, but let's leave the comment. }; let matrixSnapshotPoller = (function () { let timer = null; let preprocessMatrixSnapshot = function (snapshot) { if (Array.isArray(snapshot.headerIndices)) { snapshot.headerIndices = new Map(snapshot.headerIndices); } return snapshot; }; let 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(); } }; let onPolled = function (response) { processPollResult(response); pollAsync(); }; let 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); }; let poll = function () { timer = null; pollNow(); }; let pollAsync = function () { if (timer !== null) { return; } if (document.defaultView === null) { return; } timer = vAPI.setTimeout(poll, 1414); }; let unpollAsync = function () { if (timer !== null) { clearTimeout(timer); timer = null; } }; (function () { let tabId = matrixSnapshot.tabId; // If no tab id yet, see if there is one specified in our URL if (tabId === undefined) { let 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(); } } let 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); }); })();