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.

450 lines
14 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
  1. /*******************************************************************************
  2. µMatrix - a Chromium browser extension to black/white list requests.
  3. Copyright (C) 2014-2015 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 vAPI, uDom */
  17. /******************************************************************************/
  18. (function() {
  19. 'use strict';
  20. /******************************************************************************/
  21. var listDetails = {};
  22. var externalHostsFiles = '';
  23. var cacheWasPurged = false;
  24. var needUpdate = false;
  25. var hasCachedContent = false;
  26. /******************************************************************************/
  27. var onMessage = function(msg) {
  28. switch ( msg.what ) {
  29. case 'loadHostsFilesCompleted':
  30. renderHostsFiles();
  31. break;
  32. case 'forceUpdateAssetsProgress':
  33. renderBusyOverlay(true, msg.progress);
  34. if ( msg.done ) {
  35. messager.send({ what: 'reloadHostsFiles' });
  36. }
  37. break;
  38. default:
  39. break;
  40. }
  41. };
  42. var messager = vAPI.messaging.channel('hosts-files.js', onMessage);
  43. /******************************************************************************/
  44. var renderNumber = function(value) {
  45. return value.toLocaleString();
  46. };
  47. /******************************************************************************/
  48. // TODO: get rid of background page dependencies
  49. var renderHostsFiles = function() {
  50. var listEntryTemplate = uDom('#templates .listEntry');
  51. var listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats');
  52. var lastUpdateString = vAPI.i18n('hostsFilesLastUpdate');
  53. var renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString;
  54. var reExternalHostFile = /^https?:/;
  55. // Assemble a pretty blacklist name if possible
  56. var listNameFromListKey = function(listKey) {
  57. var list = listDetails.current[listKey] || listDetails.available[listKey];
  58. var listTitle = list ? list.title : '';
  59. if ( listTitle === '' ) {
  60. return listKey;
  61. }
  62. return listTitle;
  63. };
  64. var liFromListEntry = function(listKey) {
  65. var elem, text;
  66. var entry = listDetails.available[listKey];
  67. var li = listEntryTemplate.clone();
  68. if ( entry.off !== true ) {
  69. li.descendants('input').attr('checked', '');
  70. }
  71. elem = li.descendants('a:nth-of-type(1)');
  72. elem.attr('href', encodeURI(listKey));
  73. elem.text(listNameFromListKey(listKey) + '\u200E');
  74. elem = li.descendants('a:nth-of-type(2)');
  75. if ( entry.homeDomain ) {
  76. elem.attr('href', 'http://' + encodeURI(entry.homeHostname));
  77. elem.text('(' + entry.homeDomain + ')');
  78. elem.css('display', '');
  79. }
  80. elem = li.descendants('span:nth-of-type(1)');
  81. text = listStatsTemplate
  82. .replace('{{used}}', renderNumber(!entry.off && !isNaN(+entry.entryUsedCount) ? entry.entryUsedCount : 0))
  83. .replace('{{total}}', !isNaN(+entry.entryCount) ? renderNumber(entry.entryCount) : '?');
  84. elem.text(text);
  85. // https://github.com/gorhill/uBlock/issues/78
  86. // Badge for non-secure connection
  87. var remoteURL = listKey;
  88. if ( remoteURL.lastIndexOf('http:', 0) !== 0 ) {
  89. remoteURL = entry.homeURL || '';
  90. }
  91. if ( remoteURL.lastIndexOf('http:', 0) === 0 ) {
  92. li.descendants('span.status.unsecure').css('display', '');
  93. }
  94. // https://github.com/chrisaljoudi/uBlock/issues/104
  95. var asset = listDetails.cache[listKey] || {};
  96. // Badge for update status
  97. if ( entry.off !== true ) {
  98. if ( asset.repoObsolete ) {
  99. li.descendants('span.status.new').css('display', '');
  100. needUpdate = true;
  101. } else if ( asset.cacheObsolete ) {
  102. li.descendants('span.status.obsolete').css('display', '');
  103. needUpdate = true;
  104. } else if ( entry.external && !asset.cached ) {
  105. li.descendants('span.status.obsolete').css('display', '');
  106. needUpdate = true;
  107. }
  108. }
  109. // In cache
  110. if ( asset.cached ) {
  111. elem = li.descendants('span.status.purge');
  112. elem.css('display', '');
  113. elem.attr('title', lastUpdateString.replace('{{ago}}', renderElapsedTimeToString(asset.lastModified)));
  114. hasCachedContent = true;
  115. }
  116. return li;
  117. };
  118. var onListsReceived = function(details) {
  119. // Before all, set context vars
  120. listDetails = details;
  121. needUpdate = false;
  122. hasCachedContent = false;
  123. var availableLists = details.available;
  124. var listKeys = Object.keys(details.available);
  125. // Sort works this way:
  126. // - Send /^https?:/ items at the end (custom hosts file URL)
  127. listKeys.sort(function(a, b) {
  128. var ta = availableLists[a].title || a;
  129. var tb = availableLists[b].title || b;
  130. if ( reExternalHostFile.test(ta) === reExternalHostFile.test(tb) ) {
  131. return ta.localeCompare(tb);
  132. }
  133. if ( reExternalHostFile.test(tb) ) {
  134. return -1;
  135. }
  136. return 1;
  137. });
  138. var ulList = uDom('#lists').empty();
  139. for ( var i = 0; i < listKeys.length; i++ ) {
  140. ulList.append(liFromListEntry(listKeys[i]));
  141. }
  142. uDom('#listsOfBlockedHostsPrompt').text(
  143. vAPI.i18n('hostsFilesStats').replace(
  144. '{{blockedHostnameCount}}',
  145. renderNumber(details.blockedHostnameCount)
  146. )
  147. );
  148. uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true);
  149. renderWidgets();
  150. renderBusyOverlay(details.manualUpdate, details.manualUpdateProgress);
  151. };
  152. messager.send({ what: 'getLists' }, onListsReceived);
  153. };
  154. /******************************************************************************/
  155. // Progress must be normalized to [0, 1], or can be undefined.
  156. var renderBusyOverlay = function(state, progress) {
  157. progress = progress || {};
  158. var showProgress = typeof progress.value === 'number';
  159. if ( showProgress ) {
  160. uDom('#busyOverlay > div:nth-of-type(2) > div:first-child').css(
  161. 'width',
  162. (progress.value * 100).toFixed(1) + '%'
  163. );
  164. var text = progress.text || '';
  165. if ( text !== '' ) {
  166. uDom('#busyOverlay > div:nth-of-type(2) > div:last-child').text(text);
  167. }
  168. }
  169. uDom('#busyOverlay > div:nth-of-type(2)').css('display', showProgress ? '' : 'none');
  170. uDom('body').toggleClass('busy', !!state);
  171. };
  172. /******************************************************************************/
  173. // This is to give a visual hint that the selection of blacklists has changed.
  174. var renderWidgets = function() {
  175. uDom('#buttonApply').toggleClass('disabled', !listsSelectionChanged());
  176. uDom('#buttonUpdate').toggleClass('disabled', !listsContentChanged());
  177. uDom('#buttonPurgeAll').toggleClass('disabled', !hasCachedContent);
  178. };
  179. /******************************************************************************/
  180. // Return whether selection of lists changed.
  181. var listsSelectionChanged = function() {
  182. if ( cacheWasPurged ) {
  183. return true;
  184. }
  185. var availableLists = listDetails.available;
  186. var currentLists = listDetails.current;
  187. var location, availableOff, currentOff;
  188. // This check existing entries
  189. for ( location in availableLists ) {
  190. if ( availableLists.hasOwnProperty(location) === false ) {
  191. continue;
  192. }
  193. availableOff = availableLists[location].off === true;
  194. currentOff = currentLists[location] === undefined || currentLists[location].off === true;
  195. if ( availableOff !== currentOff ) {
  196. return true;
  197. }
  198. }
  199. // This check removed entries
  200. for ( location in currentLists ) {
  201. if ( currentLists.hasOwnProperty(location) === false ) {
  202. continue;
  203. }
  204. currentOff = currentLists[location].off === true;
  205. availableOff = availableLists[location] === undefined || availableLists[location].off === true;
  206. if ( availableOff !== currentOff ) {
  207. return true;
  208. }
  209. }
  210. return false;
  211. };
  212. /******************************************************************************/
  213. // Return whether content need update.
  214. var listsContentChanged = function() {
  215. return needUpdate;
  216. };
  217. /******************************************************************************/
  218. // This is to give a visual hint that the selection of blacklists has changed.
  219. var updateWidgets = function() {
  220. uDom('#buttonApply').toggleClass('disabled', !listsSelectionChanged());
  221. uDom('#buttonUpdate').toggleClass('disabled', !listsContentChanged());
  222. uDom('#buttonPurgeAll').toggleClass('disabled', !hasCachedContent);
  223. uDom('body').toggleClass('busy', false);
  224. };
  225. /******************************************************************************/
  226. var onListCheckboxChanged = function() {
  227. var href = uDom(this).parent().descendants('a').first().attr('href');
  228. if ( typeof href !== 'string' ) {
  229. return;
  230. }
  231. if ( listDetails.available[href] === undefined ) {
  232. return;
  233. }
  234. listDetails.available[href].off = !this.checked;
  235. updateWidgets();
  236. };
  237. /******************************************************************************/
  238. var onListLinkClicked = function(ev) {
  239. messager.send({
  240. what: 'gotoExtensionURL',
  241. url: 'asset-viewer.html?url=' + uDom(this).attr('href')
  242. });
  243. ev.preventDefault();
  244. };
  245. /******************************************************************************/
  246. var onPurgeClicked = function() {
  247. var button = uDom(this);
  248. var li = button.parent();
  249. var href = li.descendants('a').first().attr('href');
  250. if ( !href ) {
  251. return;
  252. }
  253. messager.send({ what: 'purgeCache', path: href });
  254. button.remove();
  255. if ( li.descendants('input').first().prop('checked') ) {
  256. cacheWasPurged = true;
  257. updateWidgets();
  258. }
  259. };
  260. /******************************************************************************/
  261. var selectHostsFiles = function(callback) {
  262. var switches = [];
  263. var lis = uDom('#lists .listEntry'), li;
  264. var i = lis.length;
  265. while ( i-- ) {
  266. li = lis.at(i);
  267. switches.push({
  268. location: li.descendants('a').attr('href'),
  269. off: li.descendants('input').prop('checked') === false
  270. });
  271. }
  272. messager.send({
  273. what: 'selectHostsFiles',
  274. switches: switches
  275. }, callback);
  276. };
  277. /******************************************************************************/
  278. var buttonApplyHandler = function() {
  279. uDom('#buttonApply').removeClass('enabled');
  280. renderBusyOverlay(true);
  281. var onSelectionDone = function() {
  282. messager.send({ what: 'reloadHostsFiles' });
  283. };
  284. selectHostsFiles(onSelectionDone);
  285. cacheWasPurged = false;
  286. };
  287. /******************************************************************************/
  288. var buttonUpdateHandler = function() {
  289. uDom('#buttonUpdate').removeClass('enabled');
  290. if ( needUpdate ) {
  291. renderBusyOverlay(true);
  292. var onSelectionDone = function() {
  293. messager.send({ what: 'forceUpdateAssets' });
  294. };
  295. selectHostsFiles(onSelectionDone);
  296. cacheWasPurged = false;
  297. }
  298. };
  299. /******************************************************************************/
  300. var buttonPurgeAllHandler = function() {
  301. uDom('#buttonPurgeAll').removeClass('enabled');
  302. renderBusyOverlay(true);
  303. var onCompleted = function() {
  304. cacheWasPurged = true;
  305. renderHostsFiles();
  306. };
  307. messager.send({ what: 'purgeAllCaches' }, onCompleted);
  308. };
  309. /******************************************************************************/
  310. var autoUpdateCheckboxChanged = function() {
  311. messager.send({
  312. what: 'userSettings',
  313. name: 'autoUpdate',
  314. value: this.checked
  315. });
  316. };
  317. /******************************************************************************/
  318. var renderExternalLists = function() {
  319. var onReceived = function(details) {
  320. uDom('#externalHostsFiles').val(details);
  321. externalHostsFiles = details;
  322. };
  323. messager.send({ what: 'userSettings', name: 'externalHostsFiles' }, onReceived);
  324. };
  325. /******************************************************************************/
  326. var externalListsChangeHandler = function() {
  327. uDom('#externalListsParse').prop(
  328. 'disabled',
  329. this.value.trim() === externalHostsFiles
  330. );
  331. };
  332. /******************************************************************************/
  333. var externalListsApplyHandler = function() {
  334. externalHostsFiles = uDom('#externalHostsFiles').val();
  335. messager.send({
  336. what: 'userSettings',
  337. name: 'externalHostsFiles',
  338. value: externalHostsFiles
  339. });
  340. renderHostsFiles();
  341. uDom('#externalListsParse').prop('disabled', true);
  342. };
  343. /******************************************************************************/
  344. uDom.onLoad(function() {
  345. uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
  346. uDom('#buttonApply').on('click', buttonApplyHandler);
  347. uDom('#buttonUpdate').on('click', buttonUpdateHandler);
  348. uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
  349. uDom('#lists').on('change', '.listEntry > input', onListCheckboxChanged);
  350. uDom('#lists').on('click', '.listEntry > a:nth-of-type(1)', onListLinkClicked);
  351. uDom('#lists').on('click', 'span.purge', onPurgeClicked);
  352. uDom('#externalHostsFiles').on('input', externalListsChangeHandler);
  353. uDom('#externalListsParse').on('click', externalListsApplyHandler);
  354. renderHostsFiles();
  355. renderExternalLists();
  356. });
  357. /******************************************************************************/
  358. })();