You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1239 lines
40 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /*******************************************************************************
  2. µMatrix - a Chromium browser extension to black/white list requests.
  3. Copyright (C) 2014 Raymond Hill
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see {http://www.gnu.org/licenses/}.
  14. Home: https://github.com/gorhill/uMatrix
  15. */
  16. /* global punycode, vAPI, uDom */
  17. /* jshint esnext: true, bitwise: false */
  18. /******************************************************************************/
  19. /******************************************************************************/
  20. (function() {
  21. 'use strict';
  22. /******************************************************************************/
  23. /******************************************************************************/
  24. // Must be consistent with definitions in matrix.js
  25. const Pale = 0x00;
  26. const Dark = 0x80;
  27. const Transparent = 0;
  28. const Red = 1;
  29. const Green = 2;
  30. const Gray = 3;
  31. const DarkRed = Dark | Red;
  32. const PaleRed = Pale | Red;
  33. const DarkGreen = Dark | Green;
  34. const PaleGreen = Pale | Green;
  35. const DarkGray = Dark | Gray;
  36. const PaleGray = Pale | Gray;
  37. var matrixSnapshot = {};
  38. var groupsSnapshot = [];
  39. var allHostnamesSnapshot = 'do not leave this initial string empty';
  40. var targetTabId;
  41. var matrixCellHotspots = null;
  42. var matrixHeaderPrettyNames = {
  43. 'all': '',
  44. 'cookie': '',
  45. 'css': '',
  46. 'image': '',
  47. 'plugin': '',
  48. 'script': '',
  49. 'xhr': '',
  50. 'frame': '',
  51. 'other': ''
  52. };
  53. var firstPartyLabel = '';
  54. var blacklistedHostnamesLabel = '';
  55. /******************************************************************************/
  56. /******************************************************************************/
  57. // https://github.com/gorhill/httpswitchboard/issues/345
  58. var onMessage = function(msg) {
  59. if ( msg.what !== 'urlStatsChanged' ) {
  60. return;
  61. }
  62. if ( matrixSnapshot.url !== msg.pageURL ) {
  63. return;
  64. }
  65. queryMatrixSnapshot(makeMenu);
  66. };
  67. var messager = vAPI.messaging.channel('popup.js', onMessage);
  68. /******************************************************************************/
  69. /******************************************************************************/
  70. function getUserSetting(setting) {
  71. return matrixSnapshot.userSettings[setting];
  72. }
  73. function setUserSetting(setting, value) {
  74. matrixSnapshot.userSettings[setting] = value;
  75. messager.send({
  76. what: 'userSettings',
  77. name: setting,
  78. value: value
  79. });
  80. }
  81. /******************************************************************************/
  82. function updateMatrixSnapshot() {
  83. var snapshotReady = function() {
  84. updateMatrixColors();
  85. updateMatrixBehavior();
  86. updateMatrixButtons();
  87. };
  88. queryMatrixSnapshot(snapshotReady);
  89. }
  90. /******************************************************************************/
  91. // For display purpose, create four distinct groups of rows:
  92. // 0th: literal "1st-party" row
  93. // 1st: page domain's related
  94. // 2nd: whitelisted
  95. // 3rd: graylisted
  96. // 4th: blacklisted
  97. function getGroupStats() {
  98. // Try to not reshuffle groups around while popup is opened if
  99. // no new hostname added.
  100. var latestDomainListSnapshot = Object.keys(matrixSnapshot.rows).sort().join();
  101. if ( latestDomainListSnapshot === allHostnamesSnapshot ) {
  102. return groupsSnapshot;
  103. }
  104. allHostnamesSnapshot = latestDomainListSnapshot;
  105. // First, group according to whether at least one node in the domain
  106. // hierarchy is white or blacklisted
  107. var pageDomain = matrixSnapshot.domain;
  108. var rows = matrixSnapshot.rows;
  109. var columnOffsets = matrixSnapshot.headers;
  110. var anyTypeOffset = columnOffsets['*'];
  111. var hostname, domain;
  112. var row, color, count, groupIndex;
  113. var domainToGroupMap = {};
  114. // These have hard-coded position which cannot be overriden
  115. domainToGroupMap['1st-party'] = 0;
  116. domainToGroupMap[pageDomain] = 1;
  117. // 1st pass: domain wins if it has an explicit rule or a count
  118. for ( hostname in rows ) {
  119. if ( rows.hasOwnProperty(hostname) === false ) {
  120. continue;
  121. }
  122. if ( hostname === '*' || hostname === '1st-party' ) {
  123. continue;
  124. }
  125. domain = rows[hostname].domain;
  126. if ( domain === pageDomain || hostname !== domain ) {
  127. continue;
  128. }
  129. row = rows[domain];
  130. color = row.temporary[anyTypeOffset];
  131. if ( color === DarkGreen ) {
  132. domainToGroupMap[domain] = 2;
  133. continue;
  134. }
  135. if ( color === DarkRed ) {
  136. domainToGroupMap[domain] = 4;
  137. continue;
  138. }
  139. count = row.counts[anyTypeOffset];
  140. if ( count !== 0 ) {
  141. domainToGroupMap[domain] = 3;
  142. continue;
  143. }
  144. }
  145. // 2nd pass: green wins
  146. for ( hostname in rows ) {
  147. if ( rows.hasOwnProperty(hostname) === false ) {
  148. continue;
  149. }
  150. row = rows[hostname];
  151. domain = row.domain;
  152. if ( domainToGroupMap.hasOwnProperty(domain) ) {
  153. continue;
  154. }
  155. color = row.temporary[anyTypeOffset];
  156. if ( color === DarkGreen ) {
  157. domainToGroupMap[domain] = 2;
  158. }
  159. }
  160. // 3rd pass: gray with count wins
  161. for ( hostname in rows ) {
  162. if ( rows.hasOwnProperty(hostname) === false ) {
  163. continue;
  164. }
  165. row = rows[hostname];
  166. domain = row.domain;
  167. if ( domainToGroupMap.hasOwnProperty(domain) ) {
  168. continue;
  169. }
  170. color = row.temporary[anyTypeOffset];
  171. count = row.counts[anyTypeOffset];
  172. if ( color !== DarkRed && count !== 0 ) {
  173. domainToGroupMap[domain] = 3;
  174. }
  175. }
  176. // 4th pass: red wins whatever is left
  177. for ( hostname in rows ) {
  178. if ( rows.hasOwnProperty(hostname) === false ) {
  179. continue;
  180. }
  181. row = rows[hostname];
  182. domain = row.domain;
  183. if ( domainToGroupMap.hasOwnProperty(domain) ) {
  184. continue;
  185. }
  186. color = row.temporary[anyTypeOffset];
  187. if ( color === DarkRed ) {
  188. domainToGroupMap[domain] = 4;
  189. }
  190. }
  191. // 5th pass: gray wins whatever is left
  192. for ( hostname in rows ) {
  193. if ( rows.hasOwnProperty(hostname) === false ) {
  194. continue;
  195. }
  196. domain = rows[hostname].domain;
  197. if ( domainToGroupMap.hasOwnProperty(domain) ) {
  198. continue;
  199. }
  200. domainToGroupMap[domain] = 3;
  201. }
  202. // Last pass: put each domain in a group
  203. var groups = [ {}, {}, {}, {}, {} ];
  204. var group;
  205. for ( hostname in rows ) {
  206. if ( rows.hasOwnProperty(hostname) === false ) {
  207. continue;
  208. }
  209. if ( hostname === '*' ) {
  210. continue;
  211. }
  212. domain = rows[hostname].domain;
  213. groupIndex = domainToGroupMap[domain];
  214. group = groups[groupIndex];
  215. if ( group.hasOwnProperty(domain) === false ) {
  216. group[domain] = {};
  217. }
  218. group[domain][hostname] = true;
  219. }
  220. groupsSnapshot = groups;
  221. return groups;
  222. }
  223. /******************************************************************************/
  224. // helpers
  225. function getTemporaryColor(hostname, type) {
  226. return matrixSnapshot.rows[hostname].temporary[matrixSnapshot.headers[type]];
  227. }
  228. function getPermanentColor(hostname, type) {
  229. return matrixSnapshot.rows[hostname].permanent[matrixSnapshot.headers[type]];
  230. }
  231. function getCellClass(hostname, type) {
  232. return 't' + getTemporaryColor(hostname, type).toString(16) +
  233. ' p' + getPermanentColor(hostname, type).toString(16);
  234. }
  235. /******************************************************************************/
  236. // This is required for when we update the matrix while it is open:
  237. // the user might have collapsed/expanded one or more domains, and we don't
  238. // want to lose all his hardwork.
  239. function getCollapseState(domain) {
  240. var states = getUserSetting('popupCollapseSpecificDomains');
  241. if ( states !== undefined && states[domain] !== undefined ) {
  242. return states[domain];
  243. }
  244. return getUserSetting('popupCollapseDomains');
  245. }
  246. function toggleCollapseState(elem) {
  247. if ( elem.ancestors('#matHead.collapsible').length > 0 ) {
  248. toggleMainCollapseState(elem);
  249. } else {
  250. toggleSpecificCollapseState(elem);
  251. }
  252. }
  253. function toggleMainCollapseState(uelem) {
  254. var matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
  255. var collapsed = matHead.hasClass('collapsed');
  256. uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed);
  257. setUserSetting('popupCollapseDomains', collapsed);
  258. var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
  259. var domains = Object.keys(specificCollapseStates);
  260. var i = domains.length;
  261. var domain;
  262. while ( i-- ) {
  263. domain = domains[i];
  264. if ( specificCollapseStates[domain] === collapsed ) {
  265. delete specificCollapseStates[domain];
  266. }
  267. }
  268. setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
  269. }
  270. function toggleSpecificCollapseState(uelem) {
  271. // Remember collapse state forever, but only if it is different
  272. // from main collapse switch.
  273. var section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed');
  274. var domain = section.prop('domain');
  275. var collapsed = section.hasClass('collapsed');
  276. var mainCollapseState = getUserSetting('popupCollapseDomains');
  277. var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
  278. if ( collapsed !== mainCollapseState ) {
  279. specificCollapseStates[domain] = collapsed;
  280. setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
  281. } else if ( specificCollapseStates[domain] !== undefined ) {
  282. delete specificCollapseStates[domain];
  283. setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
  284. }
  285. }
  286. /******************************************************************************/
  287. // Update color of matrix cells(s)
  288. // Color changes when rules change
  289. function updateMatrixColors() {
  290. var cells = uDom('.matrix .matRow.rw > .matCell').removeClass();
  291. var i = cells.length;
  292. var cell;
  293. while ( i-- ) {
  294. cell = cells.nodeAt(i);
  295. cell.className = 'matCell ' + getCellClass(cell.hostname, cell.reqType);
  296. }
  297. }
  298. /******************************************************************************/
  299. // Update behavior of matrix:
  300. // - Whether a section is collapsible or not. It is collapsible if:
  301. // - It has at least one subdomain AND
  302. // - There is no explicit rule anywhere in the subdomain cells AND
  303. // - It is not part of group 3 (blacklisted hostnames)
  304. function updateMatrixBehavior() {
  305. matrixList = matrixList || uDom('#matList');
  306. var sections = matrixList.descendants('.matSection');
  307. var i = sections.length;
  308. var section, subdomainRows, j, subdomainRow;
  309. while ( i-- ) {
  310. section = sections.at(i);
  311. subdomainRows = section.descendants('.l2:not(.g4)');
  312. j = subdomainRows.length;
  313. while ( j-- ) {
  314. subdomainRow = subdomainRows.at(j);
  315. subdomainRow.toggleClass('collapsible', subdomainRow.descendants('.t81,.t82').length === 0);
  316. }
  317. section.toggleClass('collapsible', subdomainRows.filter('.collapsible').length > 0);
  318. }
  319. }
  320. /******************************************************************************/
  321. // handle user interaction with filters
  322. function getCellAction(hostname, type, leaning) {
  323. var temporaryColor = getTemporaryColor(hostname, type);
  324. var hue = temporaryColor & 0x03;
  325. // Special case: root toggle only between two states
  326. if ( type === '*' && hostname === '*' ) {
  327. return hue === Green ? 'blacklistMatrixCell' : 'whitelistMatrixCell';
  328. }
  329. // When explicitly blocked/allowed, can only graylist
  330. var saturation = temporaryColor & 0x80;
  331. if ( saturation === Dark ) {
  332. return 'graylistMatrixCell';
  333. }
  334. return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell';
  335. }
  336. function handleFilter(button, leaning) {
  337. // our parent cell knows who we are
  338. var cell = button.ancestors('div.matCell');
  339. var type = cell.prop('reqType');
  340. var desHostname = cell.prop('hostname');
  341. // https://github.com/gorhill/uMatrix/issues/24
  342. // No hostname can happen -- like with blacklist meta row
  343. if ( desHostname === '' ) {
  344. return;
  345. }
  346. var request = {
  347. what: getCellAction(desHostname, type, leaning),
  348. srcHostname: matrixSnapshot.scope,
  349. desHostname: desHostname,
  350. type: type
  351. };
  352. messager.send(request, updateMatrixSnapshot);
  353. }
  354. function handleWhitelistFilter(button) {
  355. handleFilter(button, 'whitelisting');
  356. }
  357. function handleBlacklistFilter(button) {
  358. handleFilter(button, 'blacklisting');
  359. }
  360. /******************************************************************************/
  361. var matrixRowPool = [];
  362. var matrixSectionPool = [];
  363. var matrixGroupPool = [];
  364. var matrixRowTemplate = null;
  365. var matrixList = null;
  366. var startMatrixUpdate = function() {
  367. matrixList = matrixList || uDom('#matList');
  368. matrixList.detach();
  369. var rows = matrixList.descendants('.matRow');
  370. rows.detach();
  371. matrixRowPool = matrixRowPool.concat(rows.toArray());
  372. var sections = matrixList.descendants('.matSection');
  373. sections.detach();
  374. matrixSectionPool = matrixSectionPool.concat(sections.toArray());
  375. var groups = matrixList.descendants('.matGroup');
  376. groups.detach();
  377. matrixGroupPool = matrixGroupPool.concat(groups.toArray());
  378. };
  379. var endMatrixUpdate = function() {
  380. // https://github.com/gorhill/httpswitchboard/issues/246
  381. // If the matrix has no rows, we need to insert a dummy one, invisible,
  382. // to ensure the extension pop-up is properly sized. This is needed because
  383. // the header pane's `position` property is `fixed`, which means it doesn't
  384. // affect layout size, hence the matrix header row will be truncated.
  385. if ( matrixSnapshot.rowCount <= 1 ) {
  386. matrixList.append(createMatrixRow().css('visibility', 'hidden'));
  387. }
  388. updateMatrixBehavior();
  389. matrixList.css('display', '');
  390. matrixList.appendTo('.paneContent');
  391. };
  392. var createMatrixGroup = function() {
  393. var group = matrixGroupPool.pop();
  394. if ( group ) {
  395. return uDom(group).removeClass().addClass('matGroup');
  396. }
  397. return uDom('<div>').addClass('matGroup');
  398. };
  399. var createMatrixSection = function() {
  400. var section = matrixSectionPool.pop();
  401. if ( section ) {
  402. return uDom(section).removeClass().addClass('matSection');
  403. }
  404. return uDom('<div>').addClass('matSection');
  405. };
  406. var createMatrixRow = function() {
  407. var row = matrixRowPool.pop();
  408. if ( row ) {
  409. row.style.visibility = '';
  410. row = uDom(row);
  411. row.descendants('.matCell').removeClass().addClass('matCell');
  412. row.removeClass().addClass('matRow');
  413. return row;
  414. }
  415. if ( matrixRowTemplate === null ) {
  416. matrixRowTemplate = uDom('#templates .matRow');
  417. }
  418. return matrixRowTemplate.clone();
  419. };
  420. /******************************************************************************/
  421. function renderMatrixHeaderRow() {
  422. var matHead = uDom('#matHead.collapsible');
  423. matHead.toggleClass('collapsed', getUserSetting('popupCollapseDomains'));
  424. var cells = matHead.descendants('.matCell');
  425. cells.at(0)
  426. .prop('reqType', '*')
  427. .prop('hostname', '*')
  428. .addClass(getCellClass('*', '*'));
  429. cells.at(1)
  430. .prop('reqType', 'cookie')
  431. .prop('hostname', '*')
  432. .addClass(getCellClass('*', 'cookie'));
  433. cells.at(2)
  434. .prop('reqType', 'css')
  435. .prop('hostname', '*')
  436. .addClass(getCellClass('*', 'css'));
  437. cells.at(3)
  438. .prop('reqType', 'image')
  439. .prop('hostname', '*')
  440. .addClass(getCellClass('*', 'image'));
  441. cells.at(4)
  442. .prop('reqType', 'plugin')
  443. .prop('hostname', '*')
  444. .addClass(getCellClass('*', 'plugin'));
  445. cells.at(5)
  446. .prop('reqType', 'script')
  447. .prop('hostname', '*')
  448. .addClass(getCellClass('*', 'script'));
  449. cells.at(6)
  450. .prop('reqType', 'xhr')
  451. .prop('hostname', '*')
  452. .addClass(getCellClass('*', 'xhr'));
  453. cells.at(7)
  454. .prop('reqType', 'frame')
  455. .prop('hostname', '*')
  456. .addClass(getCellClass('*', 'frame'));
  457. cells.at(8)
  458. .prop('reqType', 'other')
  459. .prop('hostname', '*')
  460. .addClass(getCellClass('*', 'other'));
  461. uDom('#matHead .matRow').css('display', '');
  462. }
  463. /******************************************************************************/
  464. function renderMatrixCellDomain(cell, domain) {
  465. var contents = cell.prop('reqType', '*')
  466. .prop('hostname', domain)
  467. .addClass(getCellClass(domain, '*'))
  468. .contents();
  469. contents.nodeAt(0).textContent = domain === '1st-party' ?
  470. firstPartyLabel :
  471. punycode.toUnicode(domain);
  472. contents.nodeAt(1).textContent = ' ';
  473. }
  474. function renderMatrixCellSubdomain(cell, domain, subomain) {
  475. var contents = cell.prop('reqType', '*')
  476. .prop('hostname', subomain)
  477. .addClass(getCellClass(subomain, '*'))
  478. .contents();
  479. contents.nodeAt(0).textContent = punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.';
  480. contents.nodeAt(1).textContent = punycode.toUnicode(domain);
  481. }
  482. function renderMatrixMetaCellDomain(cell, domain) {
  483. var contents = cell.prop('reqType', '*')
  484. .prop('hostname', domain)
  485. .addClass(getCellClass(domain, '*'))
  486. .contents();
  487. contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain);
  488. contents.nodeAt(1).textContent = ' ';
  489. }
  490. function renderMatrixCellType(cell, hostname, type, count) {
  491. cell.prop('reqType', type)
  492. .prop('hostname', hostname)
  493. .prop('count', count)
  494. .addClass(getCellClass(hostname, type));
  495. if ( count ) {
  496. cell.text(count);
  497. } else {
  498. cell.text('\u00A0');
  499. }
  500. }
  501. function renderMatrixCellTypes(cells, hostname, countName) {
  502. var counts = matrixSnapshot.rows[hostname][countName];
  503. var countIndices = matrixSnapshot.headers;
  504. renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[countIndices.cookie]);
  505. renderMatrixCellType(cells.at(2), hostname, 'css', counts[countIndices.css]);
  506. renderMatrixCellType(cells.at(3), hostname, 'image', counts[countIndices.image]);
  507. renderMatrixCellType(cells.at(4), hostname, 'plugin', counts[countIndices.plugin]);
  508. renderMatrixCellType(cells.at(5), hostname, 'script', counts[countIndices.script]);
  509. renderMatrixCellType(cells.at(6), hostname, 'xhr', counts[countIndices.xhr]);
  510. renderMatrixCellType(cells.at(7), hostname, 'frame', counts[countIndices.frame]);
  511. renderMatrixCellType(cells.at(8), hostname, 'other', counts[countIndices.other]);
  512. }
  513. /******************************************************************************/
  514. function makeMatrixRowDomain(domain) {
  515. var matrixRow = createMatrixRow().addClass('rw');
  516. var cells = matrixRow.descendants('.matCell');
  517. renderMatrixCellDomain(cells.at(0), domain);
  518. renderMatrixCellTypes(cells, domain, 'counts');
  519. return matrixRow;
  520. }
  521. function makeMatrixRowSubdomain(domain, subdomain) {
  522. var matrixRow = createMatrixRow().addClass('rw');
  523. var cells = matrixRow.descendants('.matCell');
  524. renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
  525. renderMatrixCellTypes(cells, subdomain, 'counts');
  526. return matrixRow;
  527. }
  528. function makeMatrixMetaRowDomain(domain) {
  529. var matrixRow = createMatrixRow().addClass('rw');
  530. var cells = matrixRow.descendants('.matCell');
  531. renderMatrixMetaCellDomain(cells.at(0), domain);
  532. renderMatrixCellTypes(cells, domain, 'totals');
  533. return matrixRow;
  534. }
  535. /******************************************************************************/
  536. function renderMatrixMetaCellType(cell, count) {
  537. // https://github.com/gorhill/uMatrix/issues/24
  538. // Don't forget to reset cell properties
  539. cell.addClass('t1')
  540. .prop('reqType', '')
  541. .prop('hostname', '')
  542. .prop('count', count);
  543. if ( count ) {
  544. cell.text(count);
  545. } else {
  546. cell.text('\u00A0');
  547. }
  548. }
  549. function makeMatrixMetaRow(totals) {
  550. var typeOffsets = matrixSnapshot.headers;
  551. var matrixRow = createMatrixRow().at(0).addClass('ro');
  552. var cells = matrixRow.descendants('.matCell');
  553. var contents = cells.at(0).addClass('t81').contents();
  554. cells.at(0).prop('reqType', '*').prop('hostname', '');
  555. contents.nodeAt(0).textContent = ' ';
  556. contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace('{{count}}', totals[typeOffsets['*']]);
  557. renderMatrixMetaCellType(cells.at(1), totals[typeOffsets.cookie]);
  558. renderMatrixMetaCellType(cells.at(2), totals[typeOffsets.css]);
  559. renderMatrixMetaCellType(cells.at(3), totals[typeOffsets.image]);
  560. renderMatrixMetaCellType(cells.at(4), totals[typeOffsets.plugin]);
  561. renderMatrixMetaCellType(cells.at(5), totals[typeOffsets.script]);
  562. renderMatrixMetaCellType(cells.at(6), totals[typeOffsets.xhr]);
  563. renderMatrixMetaCellType(cells.at(7), totals[typeOffsets.frame]);
  564. renderMatrixMetaCellType(cells.at(8), totals[typeOffsets.other]);
  565. return matrixRow;
  566. }
  567. /******************************************************************************/
  568. function computeMatrixGroupMetaStats(group) {
  569. var headers = matrixSnapshot.headers;
  570. var n = Object.keys(headers).length;
  571. var totals = new Array(n);
  572. var i = n;
  573. while ( i-- ) {
  574. totals[i] = 0;
  575. }
  576. var rows = matrixSnapshot.rows, row;
  577. for ( var hostname in rows ) {
  578. if ( rows.hasOwnProperty(hostname) === false ) {
  579. continue;
  580. }
  581. row = rows[hostname];
  582. if ( group.hasOwnProperty(row.domain) === false ) {
  583. continue;
  584. }
  585. if ( row.counts[headers['*']] === 0 ) {
  586. continue;
  587. }
  588. totals[0] += 1;
  589. for ( i = 1; i < n; i++ ) {
  590. totals[i] += row.counts[i];
  591. }
  592. }
  593. return totals;
  594. }
  595. /******************************************************************************/
  596. // Compare hostname helper, to order hostname in a logical manner:
  597. // top-most < bottom-most, take into account whether IP address or
  598. // named hostname
  599. function hostnameCompare(a,b) {
  600. // Normalize: most significant parts first
  601. if ( !a.match(/^\d+(\.\d+){1,3}$/) ) {
  602. var aa = a.split('.');
  603. a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
  604. }
  605. if ( !b.match(/^\d+(\.\d+){1,3}$/) ) {
  606. var bb = b.split('.');
  607. b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
  608. }
  609. return a.localeCompare(b);
  610. }
  611. /******************************************************************************/
  612. function makeMatrixGroup0SectionDomain() {
  613. return makeMatrixRowDomain('1st-party').addClass('g0 l1');
  614. }
  615. function makeMatrixGroup0Section() {
  616. var domainDiv = createMatrixSection().prop('domain', '1st-party');
  617. makeMatrixGroup0SectionDomain().appendTo(domainDiv);
  618. return domainDiv;
  619. }
  620. function makeMatrixGroup0() {
  621. // Show literal "1st-party" row only if there is
  622. // at least one 1st-party hostname
  623. if ( Object.keys(groupsSnapshot[1]).length === 0 ) {
  624. return;
  625. }
  626. var groupDiv = createMatrixGroup().addClass('g0');
  627. makeMatrixGroup0Section().appendTo(groupDiv);
  628. groupDiv.appendTo(matrixList);
  629. }
  630. /******************************************************************************/
  631. function makeMatrixGroup1SectionDomain(domain) {
  632. return makeMatrixRowDomain(domain)
  633. .addClass('g1 l1');
  634. }
  635. function makeMatrixGroup1SectionSubomain(domain, subdomain) {
  636. return makeMatrixRowSubdomain(domain, subdomain)
  637. .addClass('g1 l2');
  638. }
  639. function makeMatrixGroup1SectionMetaDomain(domain) {
  640. return makeMatrixMetaRowDomain(domain).addClass('g1 l1 meta');
  641. }
  642. function makeMatrixGroup1Section(hostnames) {
  643. var domain = hostnames[0];
  644. var domainDiv = createMatrixSection()
  645. .toggleClass('collapsed', getCollapseState(domain))
  646. .prop('domain', domain);
  647. if ( hostnames.length > 1 ) {
  648. makeMatrixGroup1SectionMetaDomain(domain)
  649. .appendTo(domainDiv);
  650. }
  651. makeMatrixGroup1SectionDomain(domain)
  652. .appendTo(domainDiv);
  653. for ( var i = 1; i < hostnames.length; i++ ) {
  654. makeMatrixGroup1SectionSubomain(domain, hostnames[i])
  655. .appendTo(domainDiv);
  656. }
  657. return domainDiv;
  658. }
  659. function makeMatrixGroup1(group) {
  660. var domains = Object.keys(group).sort(hostnameCompare);
  661. if ( domains.length ) {
  662. var groupDiv = createMatrixGroup().addClass('g1');
  663. makeMatrixGroup1Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
  664. .appendTo(groupDiv);
  665. for ( var i = 1; i < domains.length; i++ ) {
  666. makeMatrixGroup1Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
  667. .appendTo(groupDiv);
  668. }
  669. groupDiv.appendTo(matrixList);
  670. }
  671. }
  672. /******************************************************************************/
  673. function makeMatrixGroup2SectionDomain(domain) {
  674. return makeMatrixRowDomain(domain)
  675. .addClass('g2 l1');
  676. }
  677. function makeMatrixGroup2SectionSubomain(domain, subdomain) {
  678. return makeMatrixRowSubdomain(domain, subdomain)
  679. .addClass('g2 l2');
  680. }
  681. function makeMatrixGroup2SectionMetaDomain(domain) {
  682. return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
  683. }
  684. function makeMatrixGroup2Section(hostnames) {
  685. var domain = hostnames[0];
  686. var domainDiv = createMatrixSection()
  687. .toggleClass('collapsed', getCollapseState(domain))
  688. .prop('domain', domain);
  689. if ( hostnames.length > 1 ) {
  690. makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
  691. }
  692. makeMatrixGroup2SectionDomain(domain)
  693. .appendTo(domainDiv);
  694. for ( var i = 1; i < hostnames.length; i++ ) {
  695. makeMatrixGroup2SectionSubomain(domain, hostnames[i])
  696. .appendTo(domainDiv);
  697. }
  698. return domainDiv;
  699. }
  700. function makeMatrixGroup2(group) {
  701. var domains = Object.keys(group).sort(hostnameCompare);
  702. if ( domains.length) {
  703. var groupDiv = createMatrixGroup()
  704. .addClass('g2');
  705. makeMatrixGroup2Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
  706. .appendTo(groupDiv);
  707. for ( var i = 1; i < domains.length; i++ ) {
  708. makeMatrixGroup2Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
  709. .appendTo(groupDiv);
  710. }
  711. groupDiv.appendTo(matrixList);
  712. }
  713. }
  714. /******************************************************************************/
  715. function makeMatrixGroup3SectionDomain(domain) {
  716. return makeMatrixRowDomain(domain)
  717. .addClass('g3 l1');
  718. }
  719. function makeMatrixGroup3SectionSubomain(domain, subdomain) {
  720. return makeMatrixRowSubdomain(domain, subdomain)
  721. .addClass('g3 l2');
  722. }
  723. function makeMatrixGroup3SectionMetaDomain(domain) {
  724. return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
  725. }
  726. function makeMatrixGroup3Section(hostnames) {
  727. var domain = hostnames[0];
  728. var domainDiv = createMatrixSection()
  729. .toggleClass('collapsed', getCollapseState(domain))
  730. .prop('domain', domain);
  731. if ( hostnames.length > 1 ) {
  732. makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
  733. }
  734. makeMatrixGroup3SectionDomain(domain)
  735. .appendTo(domainDiv);
  736. for ( var i = 1; i < hostnames.length; i++ ) {
  737. makeMatrixGroup3SectionSubomain(domain, hostnames[i])
  738. .appendTo(domainDiv);
  739. }
  740. return domainDiv;
  741. }
  742. function makeMatrixGroup3(group) {
  743. var domains = Object.keys(group).sort(hostnameCompare);
  744. if ( domains.length) {
  745. var groupDiv = createMatrixGroup()
  746. .addClass('g3');
  747. makeMatrixGroup3Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
  748. .appendTo(groupDiv);
  749. for ( var i = 1; i < domains.length; i++ ) {
  750. makeMatrixGroup3Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
  751. .appendTo(groupDiv);
  752. }
  753. groupDiv.appendTo(matrixList);
  754. }
  755. }
  756. /******************************************************************************/
  757. function makeMatrixGroup4SectionDomain(domain) {
  758. return makeMatrixRowDomain(domain)
  759. .addClass('g4 l1');
  760. }
  761. function makeMatrixGroup4SectionSubomain(domain, subdomain) {
  762. return makeMatrixRowSubdomain(domain, subdomain)
  763. .addClass('g4 l2');
  764. }
  765. function makeMatrixGroup4Section(hostnames) {
  766. var domain = hostnames[0];
  767. var domainDiv = createMatrixSection()
  768. .prop('domain', domain);
  769. makeMatrixGroup4SectionDomain(domain)
  770. .appendTo(domainDiv);
  771. for ( var i = 1; i < hostnames.length; i++ ) {
  772. makeMatrixGroup4SectionSubomain(domain, hostnames[i])
  773. .appendTo(domainDiv);
  774. }
  775. return domainDiv;
  776. }
  777. function makeMatrixGroup4(group) {
  778. var domains = Object.keys(group).sort(hostnameCompare);
  779. if ( domains.length === 0 ) {
  780. return;
  781. }
  782. var groupDiv = createMatrixGroup().addClass('g4');
  783. createMatrixSection()
  784. .addClass('g4Meta')
  785. .toggleClass('g4Collapsed', !!getUserSetting('popupHideBlacklisted'))
  786. .appendTo(groupDiv);
  787. makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
  788. .appendTo(groupDiv);
  789. makeMatrixGroup4Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
  790. .appendTo(groupDiv);
  791. for ( var i = 1; i < domains.length; i++ ) {
  792. makeMatrixGroup4Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
  793. .appendTo(groupDiv);
  794. }
  795. groupDiv.appendTo(matrixList);
  796. }
  797. /******************************************************************************/
  798. var makeMenu = function() {
  799. var groupStats = getGroupStats();
  800. if ( Object.keys(groupStats).length === 0 ) {
  801. return;
  802. }
  803. // https://github.com/gorhill/httpswitchboard/issues/31
  804. if ( matrixCellHotspots ) {
  805. matrixCellHotspots.detach();
  806. }
  807. renderMatrixHeaderRow();
  808. startMatrixUpdate();
  809. makeMatrixGroup0(groupStats[0]);
  810. makeMatrixGroup1(groupStats[1]);
  811. makeMatrixGroup2(groupStats[2]);
  812. makeMatrixGroup3(groupStats[3]);
  813. makeMatrixGroup4(groupStats[4]);
  814. endMatrixUpdate();
  815. initScopeCell();
  816. updateMatrixButtons();
  817. };
  818. /******************************************************************************/
  819. // Do all the stuff that needs to be done before building menu et al.
  820. function initMenuEnvironment() {
  821. uDom('body').css('font-size', getUserSetting('displayTextSize'));
  822. uDom('body').toggleClass('colorblind', getUserSetting('colorBlindFriendly') === true);
  823. var prettyNames = matrixHeaderPrettyNames;
  824. var keys = Object.keys(prettyNames);
  825. var i = keys.length;
  826. var cell, key, text;
  827. while ( i-- ) {
  828. key = keys[i];
  829. cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
  830. text = vAPI.i18n(key + 'PrettyName');
  831. cell.text(text);
  832. prettyNames[key] = text;
  833. }
  834. firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
  835. blacklistedHostnamesLabel = uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
  836. }
  837. /******************************************************************************/
  838. // Create page scopes for the web page
  839. function selectGlobalScope() {
  840. setUserSetting('popupScopeLevel', '*');
  841. updateMatrixSnapshot();
  842. dropDownMenuHide();
  843. }
  844. function selectDomainScope() {
  845. setUserSetting('popupScopeLevel', 'domain');
  846. updateMatrixSnapshot();
  847. dropDownMenuHide();
  848. }
  849. function selectSiteScope() {
  850. setUserSetting('popupScopeLevel', 'site');
  851. updateMatrixSnapshot();
  852. dropDownMenuHide();
  853. }
  854. function getClassFromScope() {
  855. if ( matrixSnapshot.scope === '*' ) {
  856. return 'tScopeGlobal';
  857. }
  858. if ( matrixSnapshot.scope === matrixSnapshot.domain ) {
  859. return 'tScopeDomain';
  860. }
  861. return 'tScopeSite';
  862. }
  863. function initScopeCell() {
  864. // It's possible there is no page URL at this point: some pages cannot
  865. // be filtered by µMatrix.
  866. if ( matrixSnapshot.url === '' ) {
  867. return;
  868. }
  869. // Fill in the scope menu entries
  870. if ( matrixSnapshot.hostname === matrixSnapshot.domain ) {
  871. uDom('#scopeKeySite').css('display', 'none');
  872. } else {
  873. uDom('#scopeKeySite').text(matrixSnapshot.hostname);
  874. }
  875. uDom('#scopeKeyDomain').text(matrixSnapshot.domain);
  876. updateScopeCell();
  877. }
  878. function updateScopeCell() {
  879. uDom('body')
  880. .removeClass('tScopeGlobal tScopeDomain tScopeSite')
  881. .addClass(getClassFromScope());
  882. uDom('#scopeCell').text(matrixSnapshot.scope.replace('*', '\u2217'));
  883. }
  884. /******************************************************************************/
  885. function updateMatrixSwitches() {
  886. var switches = matrixSnapshot.tSwitches;
  887. for ( var switchName in switches ) {
  888. if ( switches.hasOwnProperty(switchName) === false ) {
  889. continue;
  890. }
  891. uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', switches[switchName]);
  892. }
  893. var count = matrixSnapshot.blockedCount;
  894. var button = uDom('#mtxSwitch_matrix-off');
  895. button.descendants('span.badge').text(count.toLocaleString());
  896. button.attr('data-tip', button.attr('data-tip').replace('{{count}}', count));
  897. uDom('body').toggleClass('powerOff', switches['matrix-off']);
  898. }
  899. function toggleMatrixSwitch() {
  900. var pos = this.id.indexOf('_');
  901. if ( pos === -1 ) {
  902. return;
  903. }
  904. var switchName = this.id.slice(pos + 1);
  905. var request = {
  906. what: 'toggleMatrixSwitch',
  907. switchName: switchName,
  908. srcHostname: matrixSnapshot.scope
  909. };
  910. messager.send(request, updateMatrixSnapshot);
  911. }
  912. /******************************************************************************/
  913. function updatePersistButton() {
  914. var diffCount = matrixSnapshot.diff.length;
  915. var button = uDom('#buttonPersist');
  916. button.contents()
  917. .filter(function(){return this.nodeType===3;})
  918. .first()
  919. .text(diffCount > 0 ? '\uf13e' : '\uf023');
  920. button.descendants('span.badge').text(diffCount > 0 ? diffCount : '');
  921. var disabled = diffCount === 0;
  922. button.toggleClass('disabled', disabled);
  923. uDom('#buttonRevertScope').toggleClass('disabled', disabled);
  924. }
  925. /******************************************************************************/
  926. function persistMatrix() {
  927. var request = {
  928. what: 'applyDiffToPermanentMatrix',
  929. diff: matrixSnapshot.diff
  930. };
  931. messager.send(request, updateMatrixSnapshot);
  932. }
  933. /******************************************************************************/
  934. // rhill 2014-03-12: revert completely ALL changes related to the
  935. // current page, including scopes.
  936. function revertMatrix() {
  937. var request = {
  938. what: 'applyDiffToTemporaryMatrix',
  939. diff: matrixSnapshot.diff
  940. };
  941. messager.send(request, updateMatrixSnapshot);
  942. }
  943. /******************************************************************************/
  944. // Buttons which are affected by any changes in the matrix
  945. function updateMatrixButtons() {
  946. updateScopeCell();
  947. updateMatrixSwitches();
  948. updatePersistButton();
  949. }
  950. /******************************************************************************/
  951. function revertAll() {
  952. var request = {
  953. what: 'revertTemporaryMatrix'
  954. };
  955. messager.send(request, updateMatrixSnapshot);
  956. }
  957. /******************************************************************************/
  958. function buttonReloadHandler() {
  959. messager.send({
  960. what: 'forceReloadTab',
  961. tabId: targetTabId
  962. });
  963. }
  964. /******************************************************************************/
  965. function mouseenterMatrixCellHandler() {
  966. matrixCellHotspots.appendTo(this);
  967. }
  968. function mouseleaveMatrixCellHandler() {
  969. matrixCellHotspots.detach();
  970. }
  971. /******************************************************************************/
  972. function gotoExtensionURL() {
  973. var url = uDom(this).attr('data-extension-url');
  974. if ( url ) {
  975. messager.send({ what: 'gotoExtensionURL', url: url });
  976. }
  977. }
  978. /******************************************************************************/
  979. function gotoExternalURL() {
  980. var url = this.getAttribute('data-external-url');
  981. if ( url ) {
  982. messager.send({ what: 'gotoURL', url: url });
  983. }
  984. }
  985. /******************************************************************************/
  986. function dropDownMenuShow() {
  987. uDom(this).next('.dropdown-menu').addClass('show');
  988. }
  989. function dropDownMenuHide() {
  990. uDom('.dropdown-menu').removeClass('show');
  991. }
  992. /******************************************************************************/
  993. var onMatrixSnapshotReady = function(response) {
  994. // Now that tabId and pageURL are set, we can build our menu
  995. initMenuEnvironment();
  996. makeMenu();
  997. // After popup menu is built, check whether there is a non-empty matrix
  998. if ( matrixSnapshot.url === '' ) {
  999. uDom('#matHead').remove();
  1000. uDom('#toolbarLeft').remove();
  1001. // https://github.com/gorhill/httpswitchboard/issues/191
  1002. uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
  1003. uDom('#noNetTrafficPrompt').css('display', '');
  1004. }
  1005. };
  1006. /******************************************************************************/
  1007. var queryMatrixSnapshot = function(callback) {
  1008. var request = {
  1009. what: 'matrixSnapshot',
  1010. tabId: targetTabId,
  1011. tabURL: matrixSnapshot.url
  1012. };
  1013. var snapshotReceived = function(response) {
  1014. matrixSnapshot = response;
  1015. callback();
  1016. };
  1017. messager.send(request, snapshotReceived);
  1018. };
  1019. /******************************************************************************/
  1020. // Make menu only when popup html is fully loaded
  1021. uDom.onLoad(function() {
  1022. queryMatrixSnapshot(onMatrixSnapshotReady);
  1023. // Below is UI stuff which is not key to make the menu, so this can
  1024. // be done without having to wait for a tab to be bound to the menu.
  1025. // We reuse for all cells the one and only cell hotspots.
  1026. uDom('#whitelist').on('click', function() {
  1027. handleWhitelistFilter(uDom(this));
  1028. return false;
  1029. });
  1030. uDom('#blacklist').on('click', function() {
  1031. handleBlacklistFilter(uDom(this));
  1032. return false;
  1033. });
  1034. uDom('#domainOnly').on('click', function() {
  1035. toggleCollapseState(uDom(this));
  1036. return false;
  1037. });
  1038. matrixCellHotspots = uDom('#cellHotspots').detach();
  1039. uDom('body')
  1040. .on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
  1041. .on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
  1042. uDom('#scopeKeyGlobal').on('click', selectGlobalScope);
  1043. uDom('#scopeKeyDomain').on('click', selectDomainScope);
  1044. uDom('#scopeKeySite').on('click', selectSiteScope);
  1045. uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
  1046. uDom('#buttonPersist').on('click', persistMatrix);
  1047. uDom('#buttonRevertScope').on('click', revertMatrix);
  1048. uDom('#buttonRevertAll').on('click', revertAll);
  1049. uDom('#buttonReload').on('click', buttonReloadHandler);
  1050. uDom('.extensionURL').on('click', gotoExtensionURL);
  1051. uDom('.externalURL').on('click', gotoExternalURL);
  1052. uDom('body').on('click', '.dropdown-menu-button', dropDownMenuShow);
  1053. uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);
  1054. uDom('#matList').on('click', '.g4Meta', function() {
  1055. var collapsed = uDom(this)
  1056. .toggleClass('g4Collapsed')
  1057. .hasClass('g4Collapsed');
  1058. setUserSetting('popupHideBlacklisted', collapsed);
  1059. });
  1060. });
  1061. /******************************************************************************/
  1062. })();