popup.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. (function () {
  20. Cu.import('chrome://ematrix/content/lib/Punycode.jsm');
  21. // Stuff which is good to do very early so as to avoid visual glitches.
  22. (function () {
  23. let paneContentPaddingTop =
  24. vAPI.localStorage.getItem('paneContentPaddingTop');
  25. let touchDevice = vAPI.localStorage.getItem('touchDevice');
  26. if (typeof paneContentPaddingTop === 'string') {
  27. document
  28. .querySelector('.paneContent')
  29. .style
  30. .setProperty('padding-top',
  31. paneContentPaddingTop);
  32. }
  33. /* This is for CSS */
  34. if (touchDevice === 'true') {
  35. document.body.setAttribute('data-touch', 'true');
  36. } else {
  37. document.addEventListener('touchstart', function onTouched(ev) {
  38. document.removeEventListener(ev.type, onTouched);
  39. document.body.setAttribute('data-touch', 'true');
  40. vAPI.localStorage.setItem('touchDevice', 'true');
  41. resizePopup();
  42. });
  43. }
  44. })();
  45. let popupWasResized = function () {
  46. document.body.setAttribute('data-resize-popup', '');
  47. };
  48. let resizePopup = (function () {
  49. let timer;
  50. let fix = function () {
  51. timer = undefined;
  52. let doc = document;
  53. // Manually adjust the position of the main matrix according to the
  54. // height of the toolbar/matrix header.
  55. let paddingTop =
  56. (doc.querySelector('.paneHead').clientHeight + 2) + 'px';
  57. let paneContent = doc.querySelector('.paneContent');
  58. if (paddingTop !== paneContent.style.paddingTop) {
  59. paneContent.style.setProperty('padding-top', paddingTop);
  60. vAPI.localStorage.setItem('paneContentPaddingTop', paddingTop);
  61. }
  62. document
  63. .body
  64. .classList
  65. .toggle('hConstrained',
  66. window.innerWidth < document.body.clientWidth);
  67. popupWasResized();
  68. };
  69. return function () {
  70. if (timer !== undefined) {
  71. clearTimeout(timer);
  72. }
  73. timer = vAPI.setTimeout(fix, 97);
  74. };
  75. })();
  76. // Must be consistent with definitions in matrix.js
  77. let Dark = 0x80;
  78. let Red = 1;
  79. let Green = 2;
  80. let DarkRed = Dark | Red;
  81. let DarkGreen = Dark | Green;
  82. let matrixSnapshot = {};
  83. let groupsSnapshot = [];
  84. let allHostnamesSnapshot = 'do not leave this initial string empty';
  85. let matrixCellHotspots = null;
  86. let matrixHeaderPrettyNames = {
  87. 'all': '',
  88. 'cookie': '',
  89. 'css': '',
  90. 'image': '',
  91. 'media': '',
  92. 'script': '',
  93. 'xhr': '',
  94. 'frame': '',
  95. 'other': ''
  96. };
  97. let firstPartyLabel = '';
  98. let blacklistedHostnamesLabel = '';
  99. let expandosIdGenerator = 1;
  100. let nodeToExpandosMap = (function () {
  101. if (typeof window.Map === 'function') {
  102. return new window.Map();
  103. }
  104. })();
  105. let expandosFromNode = function (node) {
  106. if (node instanceof HTMLElement === false
  107. && typeof node.nodeAt === 'function') {
  108. node = node.nodeAt(0);
  109. }
  110. if (nodeToExpandosMap) {
  111. let expandosId = node.getAttribute('data-expandos');
  112. if (!expandosId) {
  113. expandosId = '' + (expandosIdGenerator++);
  114. node.setAttribute('data-expandos', expandosId);
  115. }
  116. let expandos = nodeToExpandosMap.get(expandosId);
  117. if (expandos === undefined) {
  118. expandos = Object.create(null);
  119. nodeToExpandosMap.set(expandosId, expandos);
  120. }
  121. return expandos;
  122. }
  123. return node;
  124. };
  125. function getUserSetting(setting) {
  126. return matrixSnapshot.userSettings[setting];
  127. }
  128. function setUserSetting(setting, value) {
  129. matrixSnapshot.userSettings[setting] = value;
  130. vAPI.messaging.send('popup.js', {
  131. what: 'userSettings',
  132. name: setting,
  133. value: value
  134. });
  135. }
  136. function getUISetting(setting) {
  137. let r = vAPI.localStorage.getItem(setting);
  138. if (typeof r !== 'string') {
  139. return undefined;
  140. }
  141. return JSON.parse(r);
  142. }
  143. function setUISetting(setting, value) {
  144. vAPI.localStorage.setItem(setting, JSON.stringify(value));
  145. }
  146. function updateMatrixSnapshot() {
  147. matrixSnapshotPoller.pollNow();
  148. }
  149. // For display purpose, create four distinct groups of rows:
  150. // 0th: literal "1st-party" row
  151. // 1st: page domain's related
  152. // 2nd: whitelisted
  153. // 3rd: graylisted
  154. // 4th: blacklisted
  155. function getGroupStats() {
  156. // Try to not reshuffle groups around while popup is opened if
  157. // no new hostname added.
  158. let latestDomainListSnapshot =
  159. Object.keys(matrixSnapshot.rows).sort().join();
  160. if (latestDomainListSnapshot === allHostnamesSnapshot) {
  161. return groupsSnapshot;
  162. }
  163. allHostnamesSnapshot = latestDomainListSnapshot;
  164. // First, group according to whether at least one node in the domain
  165. // hierarchy is white or blacklisted
  166. let pageDomain = matrixSnapshot.domain;
  167. let rows = matrixSnapshot.rows;
  168. let anyTypeOffset = matrixSnapshot.headerIndices.get('*');
  169. let hostname, domain;
  170. let row, color, count;
  171. let domainToGroupMap = {};
  172. // These have hard-coded position which cannot be overriden
  173. domainToGroupMap['1st-party'] = 0;
  174. domainToGroupMap[pageDomain] = 1;
  175. // 1st pass: domain wins if it has an explicit rule or a count
  176. for (hostname in rows) {
  177. if (rows.hasOwnProperty(hostname) === false) {
  178. continue;
  179. }
  180. if (hostname === '*' || hostname === '1st-party') {
  181. continue;
  182. }
  183. domain = rows[hostname].domain;
  184. if (domain === pageDomain || hostname !== domain) {
  185. continue;
  186. }
  187. row = rows[domain];
  188. color = row.temporary[anyTypeOffset];
  189. if (color === DarkGreen) {
  190. domainToGroupMap[domain] = 2;
  191. continue;
  192. }
  193. if (color === DarkRed) {
  194. domainToGroupMap[domain] = 4;
  195. continue;
  196. }
  197. count = row.counts[anyTypeOffset];
  198. if (count !== 0) {
  199. domainToGroupMap[domain] = 3;
  200. continue;
  201. }
  202. }
  203. // 2nd pass: green wins
  204. for (hostname in rows) {
  205. if (rows.hasOwnProperty(hostname) === false) {
  206. continue;
  207. }
  208. row = rows[hostname];
  209. domain = row.domain;
  210. if (domainToGroupMap.hasOwnProperty(domain)) {
  211. continue;
  212. }
  213. color = row.temporary[anyTypeOffset];
  214. if (color === DarkGreen) {
  215. domainToGroupMap[domain] = 2;
  216. }
  217. }
  218. // 3rd pass: gray with count wins
  219. for (hostname in rows) {
  220. if (rows.hasOwnProperty(hostname) === false) {
  221. continue;
  222. }
  223. row = rows[hostname];
  224. domain = row.domain;
  225. if (domainToGroupMap.hasOwnProperty(domain)) {
  226. continue;
  227. }
  228. color = row.temporary[anyTypeOffset];
  229. count = row.counts[anyTypeOffset];
  230. if ( color !== DarkRed && count !== 0 ) {
  231. domainToGroupMap[domain] = 3;
  232. }
  233. }
  234. // 4th pass: red wins whatever is left
  235. for (hostname in rows) {
  236. if (rows.hasOwnProperty(hostname) === false) {
  237. continue;
  238. }
  239. row = rows[hostname];
  240. domain = row.domain;
  241. if (domainToGroupMap.hasOwnProperty(domain)) {
  242. continue;
  243. }
  244. color = row.temporary[anyTypeOffset];
  245. if (color === DarkRed) {
  246. domainToGroupMap[domain] = 4;
  247. }
  248. }
  249. // 5th pass: gray wins whatever is left
  250. for (hostname in rows) {
  251. if (rows.hasOwnProperty(hostname) === false) {
  252. continue;
  253. }
  254. domain = rows[hostname].domain;
  255. if (domainToGroupMap.hasOwnProperty(domain)) {
  256. continue;
  257. }
  258. domainToGroupMap[domain] = 3;
  259. }
  260. // Last pass: put each domain in a group
  261. let groups = [
  262. {}, {}, {}, {}, {}
  263. ];
  264. for (hostname in rows) {
  265. if (rows.hasOwnProperty(hostname) === false) {
  266. continue;
  267. }
  268. if ( hostname === '*' ) {
  269. continue;
  270. }
  271. domain = rows[hostname].domain;
  272. let groupIndex = domainToGroupMap[domain];
  273. let group = groups[groupIndex];
  274. if (group.hasOwnProperty(domain) === false) {
  275. group[domain] = {};
  276. }
  277. group[domain][hostname] = true;
  278. }
  279. groupsSnapshot = groups;
  280. return groups;
  281. }
  282. // helpers
  283. function getTemporaryColor(hostname, type) {
  284. return matrixSnapshot
  285. .rows[hostname]
  286. .temporary[matrixSnapshot.headerIndices.get(type)];
  287. }
  288. function getPermanentColor(hostname, type) {
  289. return matrixSnapshot
  290. .rows[hostname]
  291. .permanent[matrixSnapshot.headerIndices.get(type)];
  292. }
  293. function addCellClass(cell, hostname, type) {
  294. let cl = cell.classList;
  295. cl.add('matCell');
  296. cl.add('t' + getTemporaryColor(hostname, type).toString(16));
  297. cl.add('p' + getPermanentColor(hostname, type).toString(16));
  298. }
  299. // This is required for when we update the matrix while it is open:
  300. // the user might have collapsed/expanded one or more domains, and we don't
  301. // want to lose all his hardwork.
  302. function getCollapseState(domain) {
  303. let states = getUISetting('popupCollapseSpecificDomains');
  304. if (typeof states === 'object' && states[domain] !== undefined) {
  305. return states[domain];
  306. }
  307. return matrixSnapshot.collapseAllDomains === true;
  308. }
  309. function toggleCollapseState(elem) {
  310. if (elem.ancestors('#matHead.collapsible').length > 0) {
  311. toggleMainCollapseState(elem);
  312. } else {
  313. toggleSpecificCollapseState(elem);
  314. }
  315. popupWasResized();
  316. }
  317. function toggleMainCollapseState(uelem) {
  318. let matHead =
  319. uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
  320. let collapsed =
  321. matrixSnapshot.collapseAllDomains = matHead.hasClass('collapsed');
  322. uDom('#matList .matSection.collapsible')
  323. .toggleClass('collapsed', collapsed);
  324. setUserSetting('popupCollapseAllDomains', collapsed);
  325. let specificCollapseStates =
  326. getUISetting('popupCollapseSpecificDomains') || {};
  327. let domains = Object.keys(specificCollapseStates);
  328. for (let i=domains.length-1; i>=0; --i) {
  329. let domain = domains[i];
  330. if (specificCollapseStates[domain] === collapsed) {
  331. delete specificCollapseStates[domain];
  332. }
  333. }
  334. setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
  335. }
  336. function toggleSpecificCollapseState(uelem) {
  337. // Remember collapse state forever, but only if it is different
  338. // from main collapse switch.
  339. let section =
  340. uelem.ancestors('.matSection.collapsible').toggleClass('collapsed');
  341. let domain = expandosFromNode(section).domain;
  342. let collapsed = section.hasClass('collapsed');
  343. let mainCollapseState = matrixSnapshot.collapseAllDomains === true;
  344. let specificCollapseStates =
  345. getUISetting('popupCollapseSpecificDomains') || {};
  346. if (collapsed !== mainCollapseState) {
  347. specificCollapseStates[domain] = collapsed;
  348. setUISetting('popupCollapseSpecificDomains',
  349. specificCollapseStates);
  350. } else if (specificCollapseStates[domain] !== undefined) {
  351. delete specificCollapseStates[domain];
  352. setUISetting('popupCollapseSpecificDomains',
  353. specificCollapseStates);
  354. }
  355. }
  356. // Update count value of matrix cells(s)
  357. function updateMatrixCounts() {
  358. let matCells = uDom('.matrix .matRow.rw > .matCell');
  359. let matRow, matCell, count, counts;
  360. let headerIndices = matrixSnapshot.headerIndices;
  361. let rows = matrixSnapshot.rows;
  362. let expandos;
  363. for (let i=matCells.length-1; i>=0; --i) {
  364. matCell = matCells.nodeAt(i);
  365. expandos = expandosFromNode(matCell);
  366. if (expandos.hostname === '*' || expandos.reqType === '*') {
  367. continue;
  368. }
  369. matRow = matCell.parentNode;
  370. counts = matRow.classList.contains('meta') ? 'totals' : 'counts';
  371. count = rows[expandos.hostname][counts][headerIndices
  372. .get(expandos.reqType)];
  373. if (count === expandos.count) {
  374. continue;
  375. }
  376. expandos.count = count;
  377. matCell.textContent = cellTextFromCount(count);
  378. }
  379. }
  380. function cellTextFromCount(count) {
  381. if (count === 0) {
  382. return '\u00A0';
  383. }
  384. if (count < 100) {
  385. return count;
  386. }
  387. return '99+';
  388. }
  389. // Update color of matrix cells(s)
  390. // Color changes when rules change
  391. function updateMatrixColors() {
  392. let cells = uDom('.matrix .matRow.rw > .matCell').removeClass();
  393. let cell, expandos;
  394. for (let i=cells.length-1; i>=0; --i) {
  395. cell = cells.nodeAt(i);
  396. expandos = expandosFromNode(cell);
  397. addCellClass(cell, expandos.hostname, expandos.reqType);
  398. }
  399. popupWasResized();
  400. }
  401. // Update behavior of matrix:
  402. // - Whether a section is collapsible or not. It is collapsible if:
  403. // - It has at least one subdomain AND
  404. // - There is no explicit rule anywhere in the subdomain cells AND
  405. // - It is not part of group 3 (blacklisted hostnames)
  406. function updateMatrixBehavior() {
  407. matrixList = matrixList || uDom('#matList');
  408. let sections = matrixList.descendants('.matSection');
  409. let section, subdomainRows, subdomainRow;
  410. for (let i=sections.length-1; i>=0; --i) {
  411. section = sections.at(i);
  412. subdomainRows = section.descendants('.l2:not(.g4)');
  413. for (let j=subdomainRows.length-1; j>=0; --j) {
  414. subdomainRow = subdomainRows.at(j);
  415. subdomainRow.toggleClass('collapsible',
  416. subdomainRow
  417. .descendants('.t81,.t82')
  418. .length === 0);
  419. }
  420. section.toggleClass('collapsible',
  421. subdomainRows.filter('.collapsible').length > 0);
  422. }
  423. }
  424. // handle user interaction with filters
  425. function getCellAction(hostname, type, leaning) {
  426. let temporaryColor = getTemporaryColor(hostname, type);
  427. let hue = temporaryColor & 0x03;
  428. // Special case: root toggle only between two states
  429. if (type === '*' && hostname === '*') {
  430. return hue === Green ?
  431. 'blacklistMatrixCell' :
  432. 'whitelistMatrixCell';
  433. }
  434. // When explicitly blocked/allowed, can only graylist
  435. let saturation = temporaryColor & 0x80;
  436. if (saturation === Dark) {
  437. return 'graylistMatrixCell';
  438. }
  439. return leaning === 'whitelisting' ?
  440. 'whitelistMatrixCell' :
  441. 'blacklistMatrixCell';
  442. }
  443. function handleFilter(button, leaning) {
  444. // our parent cell knows who we are
  445. let cell = button.ancestors('div.matCell');
  446. let expandos = expandosFromNode(cell);
  447. let type = expandos.reqType;
  448. let desHostname = expandos.hostname;
  449. // https://github.com/gorhill/uMatrix/issues/24
  450. // No hostname can happen -- like with blacklist meta row
  451. if (desHostname === '') {
  452. return;
  453. }
  454. let request = {
  455. what: getCellAction(desHostname, type, leaning),
  456. srcHostname: matrixSnapshot.scope,
  457. desHostname: desHostname,
  458. type: type
  459. };
  460. vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
  461. }
  462. function handleWhitelistFilter(button) {
  463. handleFilter(button, 'whitelisting');
  464. }
  465. function handleBlacklistFilter(button) {
  466. handleFilter(button, 'blacklisting');
  467. }
  468. let matrixRowPool = [];
  469. let matrixSectionPool = [];
  470. let matrixGroupPool = [];
  471. let matrixRowTemplate = null;
  472. let matrixList = null;
  473. let startMatrixUpdate = function () {
  474. matrixList = matrixList || uDom('#matList');
  475. matrixList.detach();
  476. let rows = matrixList.descendants('.matRow');
  477. rows.detach();
  478. matrixRowPool = matrixRowPool.concat(rows.toArray());
  479. let sections = matrixList.descendants('.matSection');
  480. sections.detach();
  481. matrixSectionPool = matrixSectionPool.concat(sections.toArray());
  482. let groups = matrixList.descendants('.matGroup');
  483. groups.detach();
  484. matrixGroupPool = matrixGroupPool.concat(groups.toArray());
  485. };
  486. let endMatrixUpdate = function () {
  487. // https://github.com/gorhill/httpswitchboard/issues/246 If
  488. // the matrix has no rows, we need to insert a dummy one,
  489. // invisible, to ensure the extension pop-up is properly
  490. // sized. This is needed because the header pane's `position`
  491. // property is `fixed`, which means it doesn't affect layout
  492. // size, hence the matrix header row will be truncated.
  493. if (matrixSnapshot.rowCount <= 1) {
  494. matrixList.append(createMatrixRow().css('visibility', 'hidden'));
  495. }
  496. updateMatrixBehavior();
  497. matrixList.css('display', '');
  498. matrixList.appendTo('.paneContent');
  499. };
  500. let createMatrixGroup = function () {
  501. let group = matrixGroupPool.pop();
  502. if (group) {
  503. return uDom(group).removeClass().addClass('matGroup');
  504. }
  505. return uDom(document.createElement('div')).addClass('matGroup');
  506. };
  507. let createMatrixSection = function () {
  508. let section = matrixSectionPool.pop();
  509. if (section) {
  510. return uDom(section).removeClass().addClass('matSection');
  511. }
  512. return uDom(document.createElement('div')).addClass('matSection');
  513. };
  514. let createMatrixRow = function () {
  515. let row = matrixRowPool.pop();
  516. if (row) {
  517. row.style.visibility = '';
  518. row = uDom(row);
  519. row.descendants('.matCell').removeClass().addClass('matCell');
  520. row.removeClass().addClass('matRow');
  521. return row;
  522. }
  523. if (matrixRowTemplate === null) {
  524. matrixRowTemplate = uDom('#templates .matRow');
  525. }
  526. return matrixRowTemplate.clone();
  527. };
  528. function renderMatrixHeaderRow() {
  529. let matHead = uDom('#matHead.collapsible');
  530. matHead.toggleClass('collapsed',
  531. matrixSnapshot.collapseAllDomains === true);
  532. let cells = matHead.descendants('.matCell')
  533. let cell = cells.nodeAt(0);
  534. let expandos = expandosFromNode(cell);
  535. expandos.reqType = '*';
  536. expandos.hostname = '*';
  537. addCellClass(cell, '*', '*');
  538. cell = cells.nodeAt(1);
  539. expandos = expandosFromNode(cell);
  540. expandos.reqType = 'cookie';
  541. expandos.hostname = '*';
  542. addCellClass(cell, '*', 'cookie');
  543. cell = cells.nodeAt(2);
  544. expandos = expandosFromNode(cell);
  545. expandos.reqType = 'css';
  546. expandos.hostname = '*';
  547. addCellClass(cell, '*', 'css');
  548. cell = cells.nodeAt(3);
  549. expandos = expandosFromNode(cell);
  550. expandos.reqType = 'image';
  551. expandos.hostname = '*';
  552. addCellClass(cell, '*', 'image');
  553. cell = cells.nodeAt(4);
  554. expandos = expandosFromNode(cell);
  555. expandos.reqType = 'media';
  556. expandos.hostname = '*';
  557. addCellClass(cell, '*', 'media');
  558. cell = cells.nodeAt(5);
  559. expandos = expandosFromNode(cell);
  560. expandos.reqType = 'script';
  561. expandos.hostname = '*';
  562. addCellClass(cell, '*', 'script');
  563. cell = cells.nodeAt(6);
  564. expandos = expandosFromNode(cell);
  565. expandos.reqType = 'xhr';
  566. expandos.hostname = '*';
  567. addCellClass(cell, '*', 'xhr');
  568. cell = cells.nodeAt(7);
  569. expandos = expandosFromNode(cell);
  570. expandos.reqType = 'frame';
  571. expandos.hostname = '*';
  572. addCellClass(cell, '*', 'frame');
  573. cell = cells.nodeAt(8);
  574. expandos = expandosFromNode(cell);
  575. expandos.reqType = 'other';
  576. expandos.hostname = '*';
  577. addCellClass(cell, '*', 'other');
  578. uDom('#matHead .matRow').css('display', '');
  579. }
  580. function renderMatrixCellDomain(cell, domain) {
  581. let expandos = expandosFromNode(cell);
  582. expandos.hostname = domain;
  583. expandos.reqType = '*';
  584. addCellClass(cell.nodeAt(0), domain, '*');
  585. let contents = cell.contents();
  586. contents.nodeAt(0).textContent = domain === '1st-party' ?
  587. firstPartyLabel :
  588. Punycode.toUnicode(domain);
  589. contents.nodeAt(1).textContent = ' ';
  590. }
  591. function renderMatrixCellSubdomain(cell, domain, subomain) {
  592. let expandos = expandosFromNode(cell);
  593. expandos.hostname = subomain;
  594. expandos.reqType = '*';
  595. addCellClass(cell.nodeAt(0), subomain, '*');
  596. let contents = cell.contents();
  597. contents.nodeAt(0).textContent =
  598. Punycode.toUnicode(subomain.slice(0,
  599. subomain.lastIndexOf(domain)-1))
  600. + '.';
  601. contents.nodeAt(1).textContent = Punycode.toUnicode(domain);
  602. }
  603. function renderMatrixMetaCellDomain(cell, domain) {
  604. let expandos = expandosFromNode(cell);
  605. expandos.hostname = domain;
  606. expandos.reqType = '*';
  607. addCellClass(cell.nodeAt(0), domain, '*');
  608. let contents = cell.contents();
  609. contents.nodeAt(0).textContent = '\u2217.' + Punycode.toUnicode(domain);
  610. contents.nodeAt(1).textContent = ' ';
  611. }
  612. function renderMatrixCellType(cell, hostname, type, count) {
  613. let node = cell.nodeAt(0);
  614. let expandos = expandosFromNode(node);
  615. expandos.hostname = hostname;
  616. expandos.reqType = type;
  617. expandos.count = count;
  618. addCellClass(node, hostname, type);
  619. node.textContent = cellTextFromCount(count);
  620. }
  621. function renderMatrixCellTypes(cells, hostname, countName) {
  622. let counts = matrixSnapshot.rows[hostname][countName];
  623. let headerIndices = matrixSnapshot.headerIndices;
  624. renderMatrixCellType(cells.at(1), hostname, 'cookie',
  625. counts[headerIndices.get('cookie')]);
  626. renderMatrixCellType(cells.at(2), hostname, 'css',
  627. counts[headerIndices.get('css')]);
  628. renderMatrixCellType(cells.at(3), hostname, 'image',
  629. counts[headerIndices.get('image')]);
  630. renderMatrixCellType(cells.at(4), hostname, 'media',
  631. counts[headerIndices.get('media')]);
  632. renderMatrixCellType(cells.at(5), hostname, 'script',
  633. counts[headerIndices.get('script')]);
  634. renderMatrixCellType(cells.at(6), hostname, 'xhr',
  635. counts[headerIndices.get('xhr')]);
  636. renderMatrixCellType(cells.at(7), hostname, 'frame',
  637. counts[headerIndices.get('frame')]);
  638. renderMatrixCellType(cells.at(8), hostname, 'other',
  639. counts[headerIndices.get('other')]);
  640. }
  641. function makeMatrixRowDomain(domain) {
  642. let matrixRow = createMatrixRow().addClass('rw');
  643. let cells = matrixRow.descendants('.matCell');
  644. renderMatrixCellDomain(cells.at(0), domain);
  645. renderMatrixCellTypes(cells, domain, 'counts');
  646. return matrixRow;
  647. }
  648. function makeMatrixRowSubdomain(domain, subdomain) {
  649. let matrixRow = createMatrixRow().addClass('rw');
  650. let cells = matrixRow.descendants('.matCell');
  651. renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
  652. renderMatrixCellTypes(cells, subdomain, 'counts');
  653. return matrixRow;
  654. }
  655. function makeMatrixMetaRowDomain(domain) {
  656. let matrixRow = createMatrixRow().addClass('rw');
  657. let cells = matrixRow.descendants('.matCell');
  658. renderMatrixMetaCellDomain(cells.at(0), domain);
  659. renderMatrixCellTypes(cells, domain, 'totals');
  660. return matrixRow;
  661. }
  662. function renderMatrixMetaCellType(cell, count) {
  663. // https://github.com/gorhill/uMatrix/issues/24
  664. // Don't forget to reset cell properties
  665. let node = cell.nodeAt(0);
  666. let expandos = expandosFromNode(node);
  667. expandos.hostname = '';
  668. expandos.reqType = '';
  669. expandos.count = count;
  670. cell.addClass('t1');
  671. node.textContent = cellTextFromCount(count);
  672. }
  673. function makeMatrixMetaRow(totals) {
  674. let headerIndices = matrixSnapshot.headerIndices;
  675. let matrixRow = createMatrixRow().at(0).addClass('ro');
  676. let cells = matrixRow.descendants('.matCell');
  677. let contents = cells.at(0).addClass('t81').contents();
  678. let expandos = expandosFromNode(cells.nodeAt(0));
  679. expandos.hostname = '';
  680. expandos.reqType = '*';
  681. contents.nodeAt(0).textContent = ' ';
  682. contents.nodeAt(1).textContent =
  683. blacklistedHostnamesLabel
  684. .replace('{{count}}',
  685. totals[headerIndices.get('*')].toLocaleString());
  686. renderMatrixMetaCellType(cells.at(1),
  687. totals[headerIndices.get('cookie')]);
  688. renderMatrixMetaCellType(cells.at(2),
  689. totals[headerIndices.get('css')]);
  690. renderMatrixMetaCellType(cells.at(3),
  691. totals[headerIndices.get('image')]);
  692. renderMatrixMetaCellType(cells.at(4),
  693. totals[headerIndices.get('media')]);
  694. renderMatrixMetaCellType(cells.at(5),
  695. totals[headerIndices.get('script')]);
  696. renderMatrixMetaCellType(cells.at(6),
  697. totals[headerIndices.get('xhr')]);
  698. renderMatrixMetaCellType(cells.at(7),
  699. totals[headerIndices.get('frame')]);
  700. renderMatrixMetaCellType(cells.at(8),
  701. totals[headerIndices.get('other')]);
  702. return matrixRow;
  703. }
  704. function computeMatrixGroupMetaStats(group) {
  705. let headerIndices = matrixSnapshot.headerIndices;
  706. let anyTypeIndex = headerIndices.get('*');
  707. let totals = new Array(headerIndices.size);
  708. for (let i=headerIndices.size-1; i>=0; --i) {
  709. totals[i] = 0;
  710. }
  711. let rows = matrixSnapshot.rows;
  712. let row;
  713. for (let hostname in rows) {
  714. if (rows.hasOwnProperty(hostname) === false) {
  715. continue;
  716. }
  717. row = rows[hostname];
  718. if (group.hasOwnProperty(row.domain) === false) {
  719. continue;
  720. }
  721. if (row.counts[anyTypeIndex] === 0) {
  722. continue;
  723. }
  724. totals[0] += 1;
  725. for (let i=1; i<headerIndices.size; ++i) {
  726. totals[i] += row.counts[i];
  727. }
  728. }
  729. return totals;
  730. }
  731. // Compare hostname helper, to order hostname in a logical manner:
  732. // top-most < bottom-most, take into account whether IP address or
  733. // named hostname
  734. function hostnameCompare(a, b) {
  735. // Normalize: most significant parts first
  736. if (!a.match(/^\d+(\.\d+){1,3}$/)) {
  737. let aa = a.split('.');
  738. a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
  739. }
  740. if (!b.match(/^\d+(\.\d+){1,3}$/)) {
  741. let bb = b.split('.');
  742. b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
  743. }
  744. return a.localeCompare(b);
  745. }
  746. function makeMatrixGroup0SectionDomain() {
  747. return makeMatrixRowDomain('1st-party').addClass('g0 l1');
  748. }
  749. function makeMatrixGroup0Section() {
  750. let domainDiv = createMatrixSection();
  751. expandosFromNode(domainDiv).domain = '1st-party';
  752. makeMatrixGroup0SectionDomain().appendTo(domainDiv);
  753. return domainDiv;
  754. }
  755. function makeMatrixGroup0() {
  756. // Show literal "1st-party" row only if there is
  757. // at least one 1st-party hostname
  758. if (Object.keys(groupsSnapshot[1]).length === 0) {
  759. return;
  760. }
  761. let groupDiv = createMatrixGroup().addClass('g0');
  762. makeMatrixGroup0Section().appendTo(groupDiv);
  763. groupDiv.appendTo(matrixList);
  764. }
  765. function makeMatrixGroup1SectionDomain(domain) {
  766. return makeMatrixRowDomain(domain).addClass('g1 l1');
  767. }
  768. function makeMatrixGroup1SectionSubomain(domain, subdomain) {
  769. return makeMatrixRowSubdomain(domain, subdomain).addClass('g1 l2');
  770. }
  771. function makeMatrixGroup1SectionMetaDomain(domain) {
  772. return makeMatrixMetaRowDomain(domain).addClass('g1 l1 meta');
  773. }
  774. function makeMatrixGroup1Section(hostnames) {
  775. let domain = hostnames[0];
  776. let domainDiv =
  777. createMatrixSection().toggleClass('collapsed',
  778. getCollapseState(domain));
  779. expandosFromNode(domainDiv).domain = domain;
  780. if (hostnames.length > 1) {
  781. makeMatrixGroup1SectionMetaDomain(domain).appendTo(domainDiv);
  782. }
  783. makeMatrixGroup1SectionDomain(domain).appendTo(domainDiv);
  784. for (let i=1; i<hostnames.length; ++i) {
  785. makeMatrixGroup1SectionSubomain(domain, hostnames[i])
  786. .appendTo(domainDiv);
  787. }
  788. return domainDiv;
  789. }
  790. function makeMatrixGroup1(group) {
  791. let domains = Object.keys(group).sort(hostnameCompare);
  792. if (domains.length) {
  793. let groupDiv = createMatrixGroup().addClass('g1');
  794. makeMatrixGroup1Section(Object.keys(group[domains[0]])
  795. .sort(hostnameCompare))
  796. .appendTo(groupDiv);
  797. for (let i=1; i<domains.length; ++i) {
  798. makeMatrixGroup1Section(Object.keys(group[domains[i]])
  799. .sort(hostnameCompare))
  800. .appendTo(groupDiv);
  801. }
  802. groupDiv.appendTo(matrixList);
  803. }
  804. }
  805. function makeMatrixGroup2SectionDomain(domain) {
  806. return makeMatrixRowDomain(domain).addClass('g2 l1');
  807. }
  808. function makeMatrixGroup2SectionSubomain(domain, subdomain) {
  809. return makeMatrixRowSubdomain(domain, subdomain).addClass('g2 l2');
  810. }
  811. function makeMatrixGroup2SectionMetaDomain(domain) {
  812. return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
  813. }
  814. function makeMatrixGroup2Section(hostnames) {
  815. let domain = hostnames[0];
  816. let domainDiv =
  817. createMatrixSection().toggleClass('collapsed',
  818. getCollapseState(domain));
  819. expandosFromNode(domainDiv).domain = domain;
  820. if (hostnames.length > 1) {
  821. makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
  822. }
  823. makeMatrixGroup2SectionDomain(domain).appendTo(domainDiv);
  824. for (let i=1; i<hostnames.length; ++i) {
  825. makeMatrixGroup2SectionSubomain(domain, hostnames[i])
  826. .appendTo(domainDiv);
  827. }
  828. return domainDiv;
  829. }
  830. function makeMatrixGroup2(group) {
  831. let domains = Object.keys(group).sort(hostnameCompare);
  832. if (domains.length) {
  833. let groupDiv = createMatrixGroup().addClass('g2');
  834. makeMatrixGroup2Section(Object.keys(group[domains[0]])
  835. .sort(hostnameCompare))
  836. .appendTo(groupDiv);
  837. for (let i=1; i<domains.length; ++i) {
  838. makeMatrixGroup2Section(Object.keys(group[domains[i]])
  839. .sort(hostnameCompare))
  840. .appendTo(groupDiv);
  841. }
  842. groupDiv.appendTo(matrixList);
  843. }
  844. }
  845. function makeMatrixGroup3SectionDomain(domain) {
  846. return makeMatrixRowDomain(domain).addClass('g3 l1');
  847. }
  848. function makeMatrixGroup3SectionSubomain(domain, subdomain) {
  849. return makeMatrixRowSubdomain(domain, subdomain).addClass('g3 l2');
  850. }
  851. function makeMatrixGroup3SectionMetaDomain(domain) {
  852. return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
  853. }
  854. function makeMatrixGroup3Section(hostnames) {
  855. let domain = hostnames[0];
  856. let domainDiv = createMatrixSection().toggleClass('collapsed',
  857. getCollapseState(domain));
  858. expandosFromNode(domainDiv).domain = domain;
  859. if (hostnames.length > 1) {
  860. makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
  861. }
  862. makeMatrixGroup3SectionDomain(domain).appendTo(domainDiv);
  863. for (let i=1; i<hostnames.length; ++i) {
  864. makeMatrixGroup3SectionSubomain(domain, hostnames[i])
  865. .appendTo(domainDiv);
  866. }
  867. return domainDiv;
  868. }
  869. function makeMatrixGroup3(group) {
  870. let domains = Object.keys(group).sort(hostnameCompare);
  871. if (domains.length) {
  872. let groupDiv = createMatrixGroup().addClass('g3');
  873. makeMatrixGroup3Section(Object.keys(group[domains[0]])
  874. .sort(hostnameCompare))
  875. .appendTo(groupDiv);
  876. for (let i=1; i<domains.length; ++i) {
  877. makeMatrixGroup3Section(Object.keys(group[domains[i]])
  878. .sort(hostnameCompare))
  879. .appendTo(groupDiv);
  880. }
  881. groupDiv.appendTo(matrixList);
  882. }
  883. }
  884. function makeMatrixGroup4SectionDomain(domain) {
  885. return makeMatrixRowDomain(domain).addClass('g4 l1');
  886. }
  887. function makeMatrixGroup4SectionSubomain(domain, subdomain) {
  888. return makeMatrixRowSubdomain(domain, subdomain).addClass('g4 l2');
  889. }
  890. function makeMatrixGroup4Section(hostnames) {
  891. let domain = hostnames[0];
  892. let domainDiv = createMatrixSection();
  893. expandosFromNode(domainDiv).domain = domain;
  894. makeMatrixGroup4SectionDomain(domain).appendTo(domainDiv);
  895. for (let i=1; i<hostnames.length; ++i) {
  896. makeMatrixGroup4SectionSubomain(domain, hostnames[i])
  897. .appendTo(domainDiv);
  898. }
  899. return domainDiv;
  900. }
  901. function makeMatrixGroup4(group) {
  902. let domains = Object.keys(group).sort(hostnameCompare);
  903. if (domains.length === 0) {
  904. return;
  905. }
  906. let groupDiv = createMatrixGroup().addClass('g4');
  907. createMatrixSection()
  908. .addClass('g4Meta')
  909. .toggleClass('g4Collapsed',
  910. !!matrixSnapshot.collapseBlacklistedDomains)
  911. .appendTo(groupDiv);
  912. makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
  913. .appendTo(groupDiv);
  914. makeMatrixGroup4Section(Object.keys(group[domains[0]])
  915. .sort(hostnameCompare))
  916. .appendTo(groupDiv);
  917. for (let i=1; i<domains.length; ++i) {
  918. makeMatrixGroup4Section(Object.keys(group[domains[i]])
  919. .sort(hostnameCompare))
  920. .appendTo(groupDiv);
  921. }
  922. groupDiv.appendTo(matrixList);
  923. }
  924. let makeMenu = function () {
  925. let groupStats = getGroupStats();
  926. if (Object.keys(groupStats).length === 0) {
  927. return;
  928. }
  929. // https://github.com/gorhill/httpswitchboard/issues/31
  930. if (matrixCellHotspots) {
  931. matrixCellHotspots.detach();
  932. }
  933. renderMatrixHeaderRow();
  934. startMatrixUpdate();
  935. makeMatrixGroup0(groupStats[0]);
  936. makeMatrixGroup1(groupStats[1]);
  937. makeMatrixGroup2(groupStats[2]);
  938. makeMatrixGroup3(groupStats[3]);
  939. makeMatrixGroup4(groupStats[4]);
  940. endMatrixUpdate();
  941. initScopeCell();
  942. updateMatrixButtons();
  943. resizePopup();
  944. };
  945. // Do all the stuff that needs to be done before building menu et al.
  946. function initMenuEnvironment() {
  947. document.body.style.setProperty('font-size',
  948. getUserSetting('displayTextSize'));
  949. document.body.classList.toggle('colorblind',
  950. getUserSetting('colorBlindFriendly'));
  951. uDom.nodeFromId('version').textContent =
  952. matrixSnapshot.appVersion || '';
  953. let prettyNames = matrixHeaderPrettyNames;
  954. let keys = Object.keys(prettyNames);
  955. for (let i=keys.length-1; i>=0; --i) {
  956. let key = keys[i];
  957. let cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
  958. let text = vAPI.i18n(key + 'PrettyName');
  959. cell.text(text);
  960. prettyNames[key] = text;
  961. }
  962. firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
  963. blacklistedHostnamesLabel =
  964. uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
  965. }
  966. // Create page scopes for the web page
  967. function selectGlobalScope() {
  968. if (matrixSnapshot.scope === '*') {
  969. return;
  970. }
  971. matrixSnapshot.scope = '*';
  972. document.body.classList.add('globalScope');
  973. matrixSnapshot.tMatrixModifiedTime = undefined;
  974. updateMatrixSnapshot();
  975. dropDownMenuHide();
  976. }
  977. function selectSpecificScope(ev) {
  978. let newScope = ev.target.getAttribute('data-scope');
  979. if (!newScope || matrixSnapshot.scope === newScope) {
  980. return;
  981. }
  982. document.body.classList.remove('globalScope');
  983. matrixSnapshot.scope = newScope;
  984. matrixSnapshot.tMatrixModifiedTime = undefined;
  985. updateMatrixSnapshot();
  986. dropDownMenuHide();
  987. }
  988. function initScopeCell() {
  989. // It's possible there is no page URL at this point: some pages cannot
  990. // be filtered by ηMatrix.
  991. if (matrixSnapshot.url === '') {
  992. return;
  993. }
  994. let specificScope = uDom.nodeFromId('specificScope');
  995. while (specificScope.firstChild !== null) {
  996. specificScope.removeChild(specificScope.firstChild);
  997. }
  998. // Fill in the scope menu entries
  999. let pos = matrixSnapshot.domain.indexOf('.');
  1000. let tld, labels;
  1001. if (pos === -1) {
  1002. tld = '';
  1003. labels = matrixSnapshot.hostname;
  1004. } else {
  1005. tld = matrixSnapshot.domain.slice(pos + 1);
  1006. labels = matrixSnapshot.hostname.slice(0, -tld.length);
  1007. }
  1008. let beg = 0;
  1009. let span, label;
  1010. while (beg < labels.length) {
  1011. pos = labels.indexOf('.', beg);
  1012. if (pos === -1) {
  1013. pos = labels.length;
  1014. } else {
  1015. pos += 1;
  1016. }
  1017. label = document.createElement('span');
  1018. label.appendChild(document
  1019. .createTextNode(Punycode
  1020. .toUnicode(labels.slice(beg,
  1021. pos))));
  1022. span = document.createElement('span');
  1023. span.setAttribute('data-scope', labels.slice(beg) + tld);
  1024. span.appendChild(label);
  1025. specificScope.appendChild(span);
  1026. beg = pos;
  1027. }
  1028. if (tld !== '') {
  1029. label = document.createElement('span');
  1030. label.appendChild(document.createTextNode(Punycode.toUnicode(tld)));
  1031. span = document.createElement('span');
  1032. span.setAttribute('data-scope', tld);
  1033. span.appendChild(label);
  1034. specificScope.appendChild(span);
  1035. }
  1036. updateScopeCell();
  1037. }
  1038. function updateScopeCell() {
  1039. let specificScope = uDom.nodeFromId('specificScope');
  1040. let isGlobal = matrixSnapshot.scope === '*';
  1041. document.body.classList.toggle('globalScope', isGlobal);
  1042. specificScope.classList.toggle('on', !isGlobal);
  1043. uDom.nodeFromId('globalScope').classList.toggle('on', isGlobal);
  1044. for (let node of specificScope.children) {
  1045. node.classList.toggle('on',
  1046. !isGlobal
  1047. && matrixSnapshot
  1048. .scope
  1049. .endsWith(node.getAttribute('data-scope')));
  1050. }
  1051. }
  1052. function updateMatrixSwitches() {
  1053. let count = 0;
  1054. let enabled;
  1055. let switches = matrixSnapshot.tSwitches;
  1056. for (let switchName in switches) {
  1057. if (switches.hasOwnProperty(switchName) === false) {
  1058. continue;
  1059. }
  1060. enabled = switches[switchName];
  1061. if (enabled && switchName !== 'matrix-off') {
  1062. count += 1;
  1063. }
  1064. uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', enabled);
  1065. }
  1066. uDom.nodeFromId('mtxSwitch_https-strict')
  1067. .classList
  1068. .toggle('relevant', matrixSnapshot.hasMixedContent);
  1069. uDom.nodeFromId('mtxSwitch_no-workers')
  1070. .classList
  1071. .toggle('relevant', matrixSnapshot.hasWebWorkers);
  1072. uDom.nodeFromId('mtxSwitch_referrer-spoof')
  1073. .classList.toggle('relevant', matrixSnapshot.has3pReferrer);
  1074. uDom.nodeFromId('mtxSwitch_noscript-spoof')
  1075. .classList
  1076. .toggle('relevant', matrixSnapshot.hasNoscriptTags);
  1077. uDom.nodeFromSelector('#buttonMtxSwitches span.badge').textContent =
  1078. count.toLocaleString();
  1079. uDom.nodeFromSelector('#mtxSwitch_matrix-off span.badge').textContent =
  1080. matrixSnapshot.blockedCount.toLocaleString();
  1081. document.body.classList.toggle('powerOff', switches['matrix-off']);
  1082. }
  1083. function toggleMatrixSwitch(ev) {
  1084. if (ev.target.localName === 'a') {
  1085. return;
  1086. }
  1087. let elem = ev.currentTarget;
  1088. let pos = elem.id.indexOf('_');
  1089. if (pos === -1) {
  1090. return;
  1091. }
  1092. let switchName = elem.id.slice(pos + 1);
  1093. let request = {
  1094. what: 'toggleMatrixSwitch',
  1095. switchName: switchName,
  1096. srcHostname: matrixSnapshot.scope
  1097. };
  1098. vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
  1099. }
  1100. function updatePersistButton() {
  1101. let diffCount = matrixSnapshot.diff.length;
  1102. let button = uDom('#buttonPersist');
  1103. button.contents()
  1104. .filter(function () {
  1105. return this.nodeType===3;
  1106. })
  1107. .first()
  1108. .text(diffCount > 0 ? '\uf13e' : '\uf023');
  1109. button.descendants('span.badge').text(diffCount > 0 ? diffCount : '');
  1110. let disabled = diffCount === 0;
  1111. button.toggleClass('disabled', disabled);
  1112. uDom('#buttonRevertScope').toggleClass('disabled', disabled);
  1113. }
  1114. function persistMatrix() {
  1115. let request = {
  1116. what: 'applyDiffToPermanentMatrix',
  1117. diff: matrixSnapshot.diff
  1118. };
  1119. vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
  1120. }
  1121. // rhill 2014-03-12: revert completely ALL changes related to the
  1122. // current page, including scopes.
  1123. function revertMatrix() {
  1124. let request = {
  1125. what: 'applyDiffToTemporaryMatrix',
  1126. diff: matrixSnapshot.diff
  1127. };
  1128. vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
  1129. }
  1130. // Buttons which are affected by any changes in the matrix
  1131. function updateMatrixButtons() {
  1132. updateScopeCell();
  1133. updateMatrixSwitches();
  1134. updatePersistButton();
  1135. }
  1136. function revertAll() {
  1137. let request = {
  1138. what: 'revertTemporaryMatrix'
  1139. };
  1140. vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
  1141. dropDownMenuHide();
  1142. }
  1143. function buttonReloadHandler(ev) {
  1144. vAPI.messaging.send('popup.js', {
  1145. what: 'forceReloadTab',
  1146. tabId: matrixSnapshot.tabId,
  1147. bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey
  1148. });
  1149. }
  1150. function mouseenterMatrixCellHandler(ev) {
  1151. matrixCellHotspots.appendTo(ev.target);
  1152. }
  1153. function mouseleaveMatrixCellHandler() {
  1154. matrixCellHotspots.detach();
  1155. }
  1156. function gotoExtensionURL(ev) {
  1157. let url = uDom(ev.currentTarget).attr('data-extension-url');
  1158. if (url) {
  1159. vAPI.messaging.send('popup.js', {
  1160. what: 'gotoExtensionURL',
  1161. url: url,
  1162. shiftKey: ev.shiftKey
  1163. });
  1164. }
  1165. dropDownMenuHide();
  1166. vAPI.closePopup();
  1167. }
  1168. function dropDownMenuShow(ev) {
  1169. let button = ev.target;
  1170. let menuOverlay = document.getElementById(button.getAttribute('data-dropdown-menu'));
  1171. let butnRect = button.getBoundingClientRect();
  1172. let viewRect = document.body.getBoundingClientRect();
  1173. let butnNormalLeft = butnRect.left / (viewRect.width - butnRect.width);
  1174. menuOverlay.classList.add('show');
  1175. let menu = menuOverlay.querySelector('.dropdown-menu');
  1176. let menuRect = menu.getBoundingClientRect();
  1177. let menuLeft = butnNormalLeft * (viewRect.width - menuRect.width);
  1178. menu.style.left = menuLeft.toFixed(0) + 'px';
  1179. menu.style.top = butnRect.bottom + 'px';
  1180. }
  1181. function dropDownMenuHide() {
  1182. uDom('.dropdown-menu-capture').removeClass('show');
  1183. }
  1184. let onMatrixSnapshotReady = function (response) {
  1185. if (response === 'ENOTFOUND') {
  1186. uDom.nodeFromId('noTabFound').textContent =
  1187. vAPI.i18n('matrixNoTabFound');
  1188. document.body.classList.add('noTabFound');
  1189. return;
  1190. }
  1191. // Now that tabId and pageURL are set, we can build our menu
  1192. initMenuEnvironment();
  1193. makeMenu();
  1194. // After popup menu is built, check whether there is a non-empty matrix
  1195. if (matrixSnapshot.url === '') {
  1196. uDom('#matHead').remove();
  1197. uDom('#toolbarContainer').remove();
  1198. // https://github.com/gorhill/httpswitchboard/issues/191
  1199. uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
  1200. uDom('#noNetTrafficPrompt').css('display', '');
  1201. }
  1202. // Create a hash to find out whether the reload button needs to be
  1203. // highlighted.
  1204. // TODO:
  1205. // ηMatrix: not sure what the purpose of highlighting is...
  1206. // Maybe telling the user that the page needs refreshing? But
  1207. // that's hardly useful (and by now people have gotten used to
  1208. // the lack of such a feature.)
  1209. // Not really going to do it, but let's leave the comment.
  1210. };
  1211. let matrixSnapshotPoller = (function () {
  1212. let timer = null;
  1213. let preprocessMatrixSnapshot = function (snapshot) {
  1214. if (Array.isArray(snapshot.headerIndices)) {
  1215. snapshot.headerIndices = new Map(snapshot.headerIndices);
  1216. }
  1217. return snapshot;
  1218. };
  1219. let processPollResult = function (response) {
  1220. if (typeof response !== 'object') {
  1221. return;
  1222. }
  1223. if (response.mtxContentModified === false
  1224. && response.mtxCountModified === false
  1225. && response.pMatrixModified === false
  1226. && response.tMatrixModified === false) {
  1227. return;
  1228. }
  1229. matrixSnapshot = preprocessMatrixSnapshot(response);
  1230. if (response.mtxContentModified) {
  1231. makeMenu();
  1232. return;
  1233. }
  1234. if (response.mtxCountModified) {
  1235. updateMatrixCounts();
  1236. }
  1237. if (response.pMatrixModified
  1238. || response.tMatrixModified
  1239. || response.scopeModified) {
  1240. updateMatrixColors();
  1241. updateMatrixBehavior();
  1242. updateMatrixButtons();
  1243. }
  1244. };
  1245. let onPolled = function (response) {
  1246. processPollResult(response);
  1247. pollAsync();
  1248. };
  1249. let pollNow = function () {
  1250. unpollAsync();
  1251. vAPI.messaging.send('popup.js', {
  1252. what: 'matrixSnapshot',
  1253. tabId: matrixSnapshot.tabId,
  1254. scope: matrixSnapshot.scope,
  1255. mtxContentModifiedTime: matrixSnapshot.mtxContentModifiedTime,
  1256. mtxCountModifiedTime: matrixSnapshot.mtxCountModifiedTime,
  1257. mtxDiffCount: matrixSnapshot.diff.length,
  1258. pMatrixModifiedTime: matrixSnapshot.pMatrixModifiedTime,
  1259. tMatrixModifiedTime: matrixSnapshot.tMatrixModifiedTime,
  1260. }, onPolled);
  1261. };
  1262. let poll = function () {
  1263. timer = null;
  1264. pollNow();
  1265. };
  1266. let pollAsync = function () {
  1267. if (timer !== null) {
  1268. return;
  1269. }
  1270. if (document.defaultView === null) {
  1271. return;
  1272. }
  1273. timer = vAPI.setTimeout(poll, 1414);
  1274. };
  1275. let unpollAsync = function () {
  1276. if (timer !== null) {
  1277. clearTimeout(timer);
  1278. timer = null;
  1279. }
  1280. };
  1281. (function () {
  1282. let tabId = matrixSnapshot.tabId;
  1283. // If no tab id yet, see if there is one specified in our URL
  1284. if (tabId === undefined) {
  1285. let matches = window
  1286. .location
  1287. .search
  1288. .match(/(?:\?|&)tabId=([^&]+)/);
  1289. if (matches !== null) {
  1290. tabId = matches[1];
  1291. // No need for logger button when embedded in logger
  1292. uDom('[data-extension-url="logger-ui.html"]').remove();
  1293. }
  1294. }
  1295. let snapshotFetched = function (response) {
  1296. if (typeof response === 'object') {
  1297. matrixSnapshot = preprocessMatrixSnapshot(response);
  1298. }
  1299. onMatrixSnapshotReady(response);
  1300. pollAsync();
  1301. };
  1302. vAPI.messaging.send('popup.js', {
  1303. what: 'matrixSnapshot',
  1304. tabId: tabId
  1305. }, snapshotFetched);
  1306. })();
  1307. return {
  1308. pollNow: pollNow
  1309. };
  1310. })();
  1311. // Below is UI stuff which is not key to make the menu, so this can
  1312. // be done without having to wait for a tab to be bound to the menu.
  1313. // We reuse for all cells the one and only cell hotspots.
  1314. uDom('#whitelist').on('click', function () {
  1315. handleWhitelistFilter(uDom(this));
  1316. return false;
  1317. });
  1318. uDom('#blacklist').on('click', function () {
  1319. handleBlacklistFilter(uDom(this));
  1320. return false;
  1321. });
  1322. uDom('#domainOnly').on('click', function () {
  1323. toggleCollapseState(uDom(this));
  1324. return false;
  1325. });
  1326. matrixCellHotspots = uDom('#cellHotspots').detach();
  1327. uDom('body')
  1328. .on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
  1329. .on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
  1330. uDom('#specificScope').on('click', selectSpecificScope);
  1331. uDom('#globalScope').on('click', selectGlobalScope);
  1332. uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
  1333. uDom('#buttonPersist').on('click', persistMatrix);
  1334. uDom('#buttonRevertScope').on('click', revertMatrix);
  1335. uDom('#buttonRevertAll').on('click', revertAll);
  1336. uDom('#buttonReload').on('click', buttonReloadHandler);
  1337. uDom('.extensionURL').on('click', gotoExtensionURL);
  1338. uDom('body').on('click', '[data-dropdown-menu]', dropDownMenuShow);
  1339. uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);
  1340. uDom('#matList').on('click', '.g4Meta', function (ev) {
  1341. matrixSnapshot.collapseBlacklistedDomains =
  1342. ev.target.classList.toggle('g4Collapsed');
  1343. setUserSetting('popupCollapseBlacklistedDomains',
  1344. matrixSnapshot.collapseBlacklistedDomains);
  1345. });
  1346. })();