hosts-files.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 Raymond Hill
  4. Copyright (C) 2019-2020 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://libregit.spks.xyz/heckyel/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. (function () {
  20. let listDetails = {};
  21. let lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate');
  22. let hostsFilesSettingsHash;
  23. let reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/;
  24. vAPI.messaging.addListener(function (msg) {
  25. switch (msg.what) {
  26. case 'assetUpdated':
  27. updateAssetStatus(msg);
  28. break;
  29. case 'assetsUpdated':
  30. document.body.classList.remove('updating');
  31. break;
  32. case 'loadHostsFilesCompleted':
  33. renderHostsFiles();
  34. break;
  35. default:
  36. break;
  37. }
  38. });
  39. let renderNumber = function (value) {
  40. return value.toLocaleString();
  41. };
  42. let renderHostsFiles = function (soft) {
  43. let listEntryTemplate = uDom('#templates .listEntry');
  44. let listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats');
  45. let renderETTS = vAPI.i18n.renderElapsedTimeToString;
  46. let reExternalHostFile = /^https?:/;
  47. // Assemble a pretty list name if possible
  48. let listNameFromListKey = function (listKey) {
  49. let list =
  50. listDetails.current[listKey] || listDetails.available[listKey];
  51. let listTitle = list ? list.title : '';
  52. if (listTitle === '') {
  53. return listKey;
  54. }
  55. return listTitle;
  56. };
  57. let liFromListEntry = function (listKey, li) {
  58. let entry = listDetails.available[listKey];
  59. let elem;
  60. if (!li) {
  61. li = listEntryTemplate.clone().nodeAt(0);
  62. }
  63. if (li.getAttribute('data-listkey') !== listKey) {
  64. li.setAttribute('data-listkey', listKey);
  65. elem = li.querySelector('input[type="checkbox"]');
  66. elem.checked = (entry.off !== true);
  67. elem = li.querySelector('a:nth-of-type(1)');
  68. elem.setAttribute('href',
  69. 'asset-viewer.html?url=' + encodeURI(listKey));
  70. elem.setAttribute('type', 'text/html');
  71. elem.textContent = listNameFromListKey(listKey);
  72. li.classList.remove('toRemove');
  73. if (entry.supportName) {
  74. li.classList.add('support');
  75. elem = li.querySelector('a.support');
  76. elem.setAttribute('href', entry.supportURL);
  77. elem.setAttribute('title', entry.supportName);
  78. } else {
  79. li.classList.remove('support');
  80. }
  81. if (entry.external) {
  82. li.classList.add('external');
  83. } else {
  84. li.classList.remove('external');
  85. }
  86. if (entry.instructionURL) {
  87. li.classList.add('mustread');
  88. elem = li.querySelector('a.mustread');
  89. elem.setAttribute('href', entry.instructionURL);
  90. } else {
  91. li.classList.remove('mustread');
  92. }
  93. }
  94. // https://github.com/gorhill/uBlock/issues/1429
  95. if (!soft) {
  96. elem = li.querySelector('input[type="checkbox"]');
  97. elem.checked = entry.off !== true;
  98. }
  99. elem = li.querySelector('span.counts');
  100. let text = '';
  101. if (!isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount)) {
  102. text = listStatsTemplate
  103. .replace('{{used}}',
  104. renderNumber(entry.off ? 0 : entry.entryUsedCount))
  105. .replace('{{total}}',
  106. renderNumber(entry.entryCount));
  107. }
  108. elem.textContent = text;
  109. // https://github.com/chrisaljoudi/uBlock/issues/104
  110. let asset = listDetails.cache[listKey] || {};
  111. let remoteURL = asset.remoteURL;
  112. li.classList.toggle('unsecure',
  113. typeof remoteURL === 'string'
  114. && remoteURL.lastIndexOf('http:', 0) === 0);
  115. li.classList.toggle('failed', asset.error !== undefined);
  116. li.classList.toggle('obsolete', asset.obsolete === true);
  117. li.classList.toggle('cached',
  118. asset.cached === true && asset.writeTime > 0);
  119. if (asset.cached) {
  120. li.querySelector('.status.cache')
  121. .setAttribute('title',
  122. lastUpdateTemplateString
  123. .replace('{{ago}}',
  124. renderETTS(asset.writeTime)));
  125. }
  126. li.classList.remove('discard');
  127. return li;
  128. };
  129. let onListsReceived = function (details) {
  130. // Before all, set context vars
  131. listDetails = details;
  132. // Incremental rendering: this will allow us to easily discard unused
  133. // DOM list entries.
  134. uDom('#lists .listEntry').addClass('discard');
  135. let availableLists = details.available;
  136. let listKeys = Object.keys(details.available);
  137. // Sort works this way:
  138. // - Send /^https?:/ items at the end (custom hosts file URL)
  139. listKeys.sort(function (a, b) {
  140. let ta = availableLists[a].title || a;
  141. let tb = availableLists[b].title || b;
  142. if (reExternalHostFile.test(ta) === reExternalHostFile.test(tb)) {
  143. return ta.localeCompare(tb);
  144. }
  145. return reExternalHostFile.test(tb) ? -1 : 1;
  146. });
  147. let ulList = document.querySelector('#lists');
  148. for (let i=0; i<listKeys.length; ++i) {
  149. let liEntry = liFromListEntry(listKeys[i], ulList.children[i]);
  150. if (liEntry.parentElement === null) {
  151. ulList.appendChild(liEntry);
  152. }
  153. }
  154. uDom('#lists .listEntry.discard').remove();
  155. uDom('#listsOfBlockedHostsPrompt')
  156. .text(vAPI.i18n('hostsFilesStats')
  157. .replace('{{blockedHostnameCount}}',
  158. renderNumber(details.blockedHostnameCount)));
  159. uDom('#autoUpdate').prop('checked',
  160. listDetails.autoUpdate === true);
  161. if (!soft) {
  162. hostsFilesSettingsHash = hashFromCurrentFromSettings();
  163. }
  164. renderWidgets();
  165. };
  166. vAPI.messaging.send('hosts-files.js', {
  167. what: 'getLists'
  168. }, onListsReceived);
  169. };
  170. let renderWidgets = function () {
  171. let sel1 =
  172. 'body:not(.updating) #lists .listEntry.obsolete '
  173. + '> input[type="checkbox"]:checked';
  174. let sel2 = '#lists .listEntry.cached';
  175. uDom('#buttonUpdate')
  176. .toggleClass('disabled', document.querySelector(sel1) === null);
  177. uDom('#buttonPurgeAll')
  178. .toggleClass('disabled', document.querySelector(sel2) === null);
  179. uDom('#buttonApply')
  180. .toggleClass('disabled',
  181. hostsFilesSettingsHash ===
  182. hashFromCurrentFromSettings());
  183. };
  184. let updateAssetStatus = function (details) {
  185. let li = document
  186. .querySelector('#lists .listEntry[data-listkey="'+details.key+'"]');
  187. if (li === null) {
  188. return;
  189. }
  190. li.classList.toggle('failed', !!details.failed);
  191. li.classList.toggle('obsolete', !details.cached);
  192. li.classList.toggle('cached', !!details.cached);
  193. if (details.cached) {
  194. let str = vAPI.i18n.renderElapsedTimeToString(Date.now());
  195. li.querySelector('.status.cache')
  196. .setAttribute('title',
  197. lastUpdateTemplateString.replace('{{ago}}', str));
  198. }
  199. renderWidgets();
  200. };
  201. /**
  202. Compute a hash from all the settings affecting how filter lists are loaded
  203. in memory.
  204. **/
  205. let hashFromCurrentFromSettings = function () {
  206. let hash = [];
  207. let listHash = [];
  208. let sel = '#lists .listEntry[data-listkey]:not(.toRemove)';
  209. let ext = 'externalHostsFiles';
  210. let listEntries = document.querySelectorAll(sel);
  211. for (let i=listEntries.length-1; i>=0; --i) {
  212. let li = listEntries[i];
  213. if (li.querySelector('input[type="checkbox"]:checked') !== null) {
  214. listHash.push(li.getAttribute('data-listkey'));
  215. }
  216. }
  217. hash.push(listHash.sort().join(),
  218. reValidExternalList.test(document.getElementById(ext).value),
  219. document.querySelector('#lists .listEntry.toRemove') !== null);
  220. return hash.join();
  221. };
  222. let onHostsFilesSettingsChanged = function () {
  223. renderWidgets();
  224. };
  225. let onRemoveExternalHostsFile = function (ev) {
  226. let liEntry = uDom(this).ancestors('[data-listkey]');
  227. let listKey = liEntry.attr('data-listkey');
  228. if (listKey) {
  229. liEntry.toggleClass('toRemove');
  230. renderWidgets();
  231. }
  232. ev.preventDefault();
  233. };
  234. let onPurgeClicked = function () {
  235. let button = uDom(this);
  236. let liEntry = button.ancestors('[data-listkey]');
  237. let listKey = liEntry.attr('data-listkey');
  238. if (!listKey) {
  239. return;
  240. }
  241. vAPI.messaging.send('hosts-files.js', {
  242. what: 'purgeCache',
  243. assetKey: listKey
  244. });
  245. liEntry.addClass('obsolete');
  246. liEntry.removeClass('cached');
  247. if (liEntry.descendants('input').first().prop('checked')) {
  248. renderWidgets();
  249. }
  250. };
  251. let selectHostsFiles = function (callback) {
  252. // Hosts files to select
  253. let toSelect = [];
  254. let sel = '#lists .listEntry[data-listkey]:not(.toRemove)';
  255. let sel2 = '#lists .listEntry.toRemove[data-listkey]';
  256. let liEntries = document.querySelectorAll(sel);
  257. for (let i=liEntries.length-1; i>=0; --i) {
  258. let li = liEntries[i];
  259. if (li.querySelector('input[type="checkbox"]:checked') !== null) {
  260. toSelect.push(li.getAttribute('data-listkey'));
  261. }
  262. }
  263. // External hosts files to remove
  264. let toRemove = [];
  265. liEntries = document.querySelectorAll(sel2);
  266. for (let i=liEntries.length-1; i>=0; --i) {
  267. toRemove.push(liEntries[i].getAttribute('data-listkey'));
  268. }
  269. // External hosts files to import
  270. let externalListsElem = document.getElementById('externalHostsFiles');
  271. let toImport = externalListsElem.value.trim();
  272. externalListsElem.value = '';
  273. vAPI.messaging.send('hosts-files.js', {
  274. what: 'selectHostsFiles',
  275. toSelect: toSelect,
  276. toImport: toImport,
  277. toRemove: toRemove
  278. }, callback);
  279. hostsFilesSettingsHash = hashFromCurrentFromSettings();
  280. };
  281. let buttonApplyHandler = function () {
  282. uDom('#buttonApply').removeClass('enabled');
  283. selectHostsFiles(function () {
  284. vAPI.messaging.send('hosts-files.js', {
  285. what: 'reloadHostsFiles'
  286. });
  287. });
  288. renderWidgets();
  289. };
  290. let buttonUpdateHandler = function () {
  291. uDom('#buttonUpdate').removeClass('enabled');
  292. selectHostsFiles(function () {
  293. document.body.classList.add('updating');
  294. vAPI.messaging.send('hosts-files.js', {
  295. what: 'forceUpdateAssets'
  296. });
  297. renderWidgets();
  298. });
  299. renderWidgets();
  300. };
  301. let buttonPurgeAllHandler = function () {
  302. uDom('#buttonPurgeAll').removeClass('enabled');
  303. vAPI.messaging.send('hosts-files.js', {
  304. what: 'purgeAllCaches'
  305. }, function () {
  306. renderHostsFiles(true);
  307. });
  308. };
  309. let autoUpdateCheckboxChanged = function () {
  310. vAPI.messaging.send('hosts-files.js', {
  311. what: 'userSettings',
  312. name: 'autoUpdate',
  313. value: this.checked
  314. });
  315. };
  316. uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
  317. uDom('#buttonApply').on('click', buttonApplyHandler);
  318. uDom('#buttonUpdate').on('click', buttonUpdateHandler);
  319. uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
  320. uDom('#lists').on('change', '.listEntry > input',
  321. onHostsFilesSettingsChanged);
  322. uDom('#lists').on('click', '.listEntry > a.remove',
  323. onRemoveExternalHostsFile);
  324. uDom('#lists').on('click', 'span.cache', onPurgeClicked);
  325. uDom('#externalHostsFiles').on('input', onHostsFilesSettingsChanged);
  326. renderHostsFiles();
  327. })();