hosts-files.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. 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. let ca = reExternalHostFile.test(ta);
  143. let cb = reExternalHostFile.test(tb);
  144. if (ca === cb) {
  145. return ta.localeCompare(tb);
  146. }
  147. return (cb) ? -1 : 1;
  148. });
  149. let ulList = document.querySelector('#lists');
  150. for (let i=0; i<listKeys.length; ++i) {
  151. let liEntry = liFromListEntry(listKeys[i], ulList.children[i]);
  152. if (liEntry.parentElement === null) {
  153. ulList.appendChild(liEntry);
  154. }
  155. }
  156. uDom('#lists .listEntry.discard').remove();
  157. uDom('#listsOfBlockedHostsPrompt')
  158. .text(vAPI.i18n('hostsFilesStats')
  159. .replace('{{blockedHostnameCount}}',
  160. renderNumber(details.blockedHostnameCount)));
  161. uDom('#autoUpdate').prop('checked',
  162. listDetails.autoUpdate === true);
  163. if (!soft) {
  164. hostsFilesSettingsHash = hashFromCurrentFromSettings();
  165. }
  166. renderWidgets();
  167. };
  168. vAPI.messaging.send('hosts-files.js', {
  169. what: 'getLists'
  170. }, onListsReceived);
  171. };
  172. let renderWidgets = function () {
  173. let sel1 =
  174. 'body:not(.updating) #lists .listEntry.obsolete '
  175. + '> input[type="checkbox"]:checked';
  176. let sel2 = '#lists .listEntry.cached';
  177. uDom('#buttonUpdate')
  178. .toggleClass('disabled', document.querySelector(sel1) === null);
  179. uDom('#buttonPurgeAll')
  180. .toggleClass('disabled', document.querySelector(sel2) === null);
  181. uDom('#buttonApply')
  182. .toggleClass('disabled',
  183. hostsFilesSettingsHash ===
  184. hashFromCurrentFromSettings());
  185. };
  186. let updateAssetStatus = function (details) {
  187. let li = document
  188. .querySelector('#lists .listEntry[data-listkey="'+details.key+'"]');
  189. if (li === null) {
  190. return;
  191. }
  192. li.classList.toggle('failed', !!details.failed);
  193. li.classList.toggle('obsolete', !details.cached);
  194. li.classList.toggle('cached', !!details.cached);
  195. if (details.cached) {
  196. let str = vAPI.i18n.renderElapsedTimeToString(Date.now());
  197. li.querySelector('.status.cache')
  198. .setAttribute('title',
  199. lastUpdateTemplateString.replace('{{ago}}', str));
  200. }
  201. renderWidgets();
  202. };
  203. /**
  204. Compute a hash from all the settings affecting how filter lists are loaded
  205. in memory.
  206. **/
  207. let hashFromCurrentFromSettings = function () {
  208. let hash = [];
  209. let listHash = [];
  210. let sel = '#lists .listEntry[data-listkey]:not(.toRemove)';
  211. let ext = 'externalHostsFiles';
  212. let listEntries = document.querySelectorAll(sel);
  213. for (let i=listEntries.length-1; i>=0; --i) {
  214. let li = listEntries[i];
  215. if (li.querySelector('input[type="checkbox"]:checked') !== null) {
  216. listHash.push(li.getAttribute('data-listkey'));
  217. }
  218. }
  219. hash.push(listHash.sort().join(),
  220. reValidExternalList.test(document.getElementById(ext).value),
  221. document.querySelector('#lists .listEntry.toRemove') !== null);
  222. return hash.join();
  223. };
  224. let onHostsFilesSettingsChanged = function () {
  225. renderWidgets();
  226. };
  227. let onRemoveExternalHostsFile = function (ev) {
  228. let liEntry = uDom(this).ancestors('[data-listkey]');
  229. let listKey = liEntry.attr('data-listkey');
  230. if (listKey) {
  231. liEntry.toggleClass('toRemove');
  232. renderWidgets();
  233. }
  234. ev.preventDefault();
  235. };
  236. let onPurgeClicked = function () {
  237. let button = uDom(this);
  238. let liEntry = button.ancestors('[data-listkey]');
  239. let listKey = liEntry.attr('data-listkey');
  240. if (!listKey) {
  241. return;
  242. }
  243. vAPI.messaging.send('hosts-files.js', {
  244. what: 'purgeCache',
  245. assetKey: listKey
  246. });
  247. liEntry.addClass('obsolete');
  248. liEntry.removeClass('cached');
  249. if (liEntry.descendants('input').first().prop('checked')) {
  250. renderWidgets();
  251. }
  252. };
  253. let selectHostsFiles = function (callback) {
  254. // Hosts files to select
  255. let toSelect = [];
  256. let sel = '#lists .listEntry[data-listkey]:not(.toRemove)';
  257. let sel2 = '#lists .listEntry.toRemove[data-listkey]';
  258. let liEntries = document.querySelectorAll(sel);
  259. for (let i=liEntries.length-1; i>=0; --i) {
  260. let li = liEntries[i];
  261. if (li.querySelector('input[type="checkbox"]:checked') !== null) {
  262. toSelect.push(li.getAttribute('data-listkey'));
  263. }
  264. }
  265. // External hosts files to remove
  266. let toRemove = [];
  267. liEntries = document.querySelectorAll(sel2);
  268. for (let i=liEntries.length-1; i>=0; --i) {
  269. toRemove.push(liEntries[i].getAttribute('data-listkey'));
  270. }
  271. // External hosts files to import
  272. let externalListsElem = document.getElementById('externalHostsFiles');
  273. let toImport = externalListsElem.value.trim();
  274. externalListsElem.value = '';
  275. vAPI.messaging.send('hosts-files.js', {
  276. what: 'selectHostsFiles',
  277. toSelect: toSelect,
  278. toImport: toImport,
  279. toRemove: toRemove
  280. }, callback);
  281. hostsFilesSettingsHash = hashFromCurrentFromSettings();
  282. };
  283. let buttonApplyHandler = function () {
  284. uDom('#buttonApply').removeClass('enabled');
  285. selectHostsFiles(function () {
  286. vAPI.messaging.send('hosts-files.js', {
  287. what: 'reloadHostsFiles'
  288. });
  289. });
  290. renderWidgets();
  291. };
  292. let buttonUpdateHandler = function () {
  293. uDom('#buttonUpdate').removeClass('enabled');
  294. selectHostsFiles(function () {
  295. document.body.classList.add('updating');
  296. vAPI.messaging.send('hosts-files.js', {
  297. what: 'forceUpdateAssets'
  298. });
  299. renderWidgets();
  300. });
  301. renderWidgets();
  302. };
  303. let buttonPurgeAllHandler = function () {
  304. uDom('#buttonPurgeAll').removeClass('enabled');
  305. vAPI.messaging.send('hosts-files.js', {
  306. what: 'purgeAllCaches'
  307. }, function () {
  308. renderHostsFiles(true);
  309. });
  310. };
  311. let autoUpdateCheckboxChanged = function () {
  312. vAPI.messaging.send('hosts-files.js', {
  313. what: 'userSettings',
  314. name: 'autoUpdate',
  315. value: this.checked
  316. });
  317. };
  318. uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
  319. uDom('#buttonApply').on('click', buttonApplyHandler);
  320. uDom('#buttonUpdate').on('click', buttonUpdateHandler);
  321. uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
  322. uDom('#lists').on('change', '.listEntry > input',
  323. onHostsFilesSettingsChanged);
  324. uDom('#lists').on('click', '.listEntry > a.remove',
  325. onRemoveExternalHostsFile);
  326. uDom('#lists').on('click', 'span.cache', onPurgeClicked);
  327. uDom('#externalHostsFiles').on('input', onHostsFilesSettingsChanged);
  328. renderHostsFiles();
  329. })();