/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill Copyright (C) 2019-2020 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://libregit.spks.xyz/heckyel/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; (function () { let listDetails = {}; let lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate'); let hostsFilesSettingsHash; let reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/; vAPI.messaging.addListener(function (msg) { switch (msg.what) { case 'assetUpdated': updateAssetStatus(msg); break; case 'assetsUpdated': document.body.classList.remove('updating'); break; case 'loadHostsFilesCompleted': renderHostsFiles(); break; default: break; } }); let renderNumber = function (value) { return value.toLocaleString(); }; let renderHostsFiles = function (soft) { let listEntryTemplate = uDom('#templates .listEntry'); let listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats'); let renderETTS = vAPI.i18n.renderElapsedTimeToString; let reExternalHostFile = /^https?:/; // Assemble a pretty list name if possible let listNameFromListKey = function (listKey) { let list = listDetails.current[listKey] || listDetails.available[listKey]; let listTitle = list ? list.title : ''; if (listTitle === '') { return listKey; } return listTitle; }; let liFromListEntry = function (listKey, li) { let entry = listDetails.available[listKey]; let elem; if (!li) { li = listEntryTemplate.clone().nodeAt(0); } if (li.getAttribute('data-listkey') !== listKey) { li.setAttribute('data-listkey', listKey); elem = li.querySelector('input[type="checkbox"]'); elem.checked = (entry.off !== true); elem = li.querySelector('a:nth-of-type(1)'); elem.setAttribute('href', 'asset-viewer.html?url=' + encodeURI(listKey)); elem.setAttribute('type', 'text/html'); elem.textContent = listNameFromListKey(listKey); li.classList.remove('toRemove'); if (entry.supportName) { li.classList.add('support'); elem = li.querySelector('a.support'); elem.setAttribute('href', entry.supportURL); elem.setAttribute('title', entry.supportName); } else { li.classList.remove('support'); } if (entry.external) { li.classList.add('external'); } else { li.classList.remove('external'); } if (entry.instructionURL) { li.classList.add('mustread'); elem = li.querySelector('a.mustread'); elem.setAttribute('href', entry.instructionURL); } else { li.classList.remove('mustread'); } } // https://github.com/gorhill/uBlock/issues/1429 if (!soft) { elem = li.querySelector('input[type="checkbox"]'); elem.checked = entry.off !== true; } elem = li.querySelector('span.counts'); let text = ''; if (!isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount)) { text = listStatsTemplate .replace('{{used}}', renderNumber(entry.off ? 0 : entry.entryUsedCount)) .replace('{{total}}', renderNumber(entry.entryCount)); } elem.textContent = text; // https://github.com/chrisaljoudi/uBlock/issues/104 let asset = listDetails.cache[listKey] || {}; let remoteURL = asset.remoteURL; li.classList.toggle('unsecure', typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0); li.classList.toggle('failed', asset.error !== undefined); li.classList.toggle('obsolete', asset.obsolete === true); li.classList.toggle('cached', asset.cached === true && asset.writeTime > 0); if (asset.cached) { li.querySelector('.status.cache') .setAttribute('title', lastUpdateTemplateString .replace('{{ago}}', renderETTS(asset.writeTime))); } li.classList.remove('discard'); return li; }; let onListsReceived = function (details) { // Before all, set context vars listDetails = details; // Incremental rendering: this will allow us to easily discard unused // DOM list entries. uDom('#lists .listEntry').addClass('discard'); let availableLists = details.available; let listKeys = Object.keys(details.available); // Sort works this way: // - Send /^https?:/ items at the end (custom hosts file URL) listKeys.sort(function (a, b) { let ta = availableLists[a].title || a; let tb = availableLists[b].title || b; if (reExternalHostFile.test(ta) === reExternalHostFile.test(tb)) { return ta.localeCompare(tb); } return reExternalHostFile.test(tb) ? -1 : 1; }); let ulList = document.querySelector('#lists'); for (let i=0; i input[type="checkbox"]:checked'; let sel2 = '#lists .listEntry.cached'; uDom('#buttonUpdate') .toggleClass('disabled', document.querySelector(sel1) === null); uDom('#buttonPurgeAll') .toggleClass('disabled', document.querySelector(sel2) === null); uDom('#buttonApply') .toggleClass('disabled', hostsFilesSettingsHash === hashFromCurrentFromSettings()); }; let updateAssetStatus = function (details) { let li = document .querySelector('#lists .listEntry[data-listkey="'+details.key+'"]'); if (li === null) { return; } li.classList.toggle('failed', !!details.failed); li.classList.toggle('obsolete', !details.cached); li.classList.toggle('cached', !!details.cached); if (details.cached) { let str = vAPI.i18n.renderElapsedTimeToString(Date.now()); li.querySelector('.status.cache') .setAttribute('title', lastUpdateTemplateString.replace('{{ago}}', str)); } renderWidgets(); }; /** Compute a hash from all the settings affecting how filter lists are loaded in memory. **/ let hashFromCurrentFromSettings = function () { let hash = []; let listHash = []; let sel = '#lists .listEntry[data-listkey]:not(.toRemove)'; let ext = 'externalHostsFiles'; let listEntries = document.querySelectorAll(sel); for (let i=listEntries.length-1; i>=0; --i) { let li = listEntries[i]; if (li.querySelector('input[type="checkbox"]:checked') !== null) { listHash.push(li.getAttribute('data-listkey')); } } hash.push(listHash.sort().join(), reValidExternalList.test(document.getElementById(ext).value), document.querySelector('#lists .listEntry.toRemove') !== null); return hash.join(); }; let onHostsFilesSettingsChanged = function () { renderWidgets(); }; let onRemoveExternalHostsFile = function (ev) { let liEntry = uDom(this).ancestors('[data-listkey]'); let listKey = liEntry.attr('data-listkey'); if (listKey) { liEntry.toggleClass('toRemove'); renderWidgets(); } ev.preventDefault(); }; let onPurgeClicked = function () { let button = uDom(this); let liEntry = button.ancestors('[data-listkey]'); let listKey = liEntry.attr('data-listkey'); if (!listKey) { return; } vAPI.messaging.send('hosts-files.js', { what: 'purgeCache', assetKey: listKey }); liEntry.addClass('obsolete'); liEntry.removeClass('cached'); if (liEntry.descendants('input').first().prop('checked')) { renderWidgets(); } }; let selectHostsFiles = function (callback) { // Hosts files to select let toSelect = []; let sel = '#lists .listEntry[data-listkey]:not(.toRemove)'; let sel2 = '#lists .listEntry.toRemove[data-listkey]'; let liEntries = document.querySelectorAll(sel); for (let i=liEntries.length-1; i>=0; --i) { let li = liEntries[i]; if (li.querySelector('input[type="checkbox"]:checked') !== null) { toSelect.push(li.getAttribute('data-listkey')); } } // External hosts files to remove let toRemove = []; liEntries = document.querySelectorAll(sel2); for (let i=liEntries.length-1; i>=0; --i) { toRemove.push(liEntries[i].getAttribute('data-listkey')); } // External hosts files to import let externalListsElem = document.getElementById('externalHostsFiles'); let toImport = externalListsElem.value.trim(); externalListsElem.value = ''; vAPI.messaging.send('hosts-files.js', { what: 'selectHostsFiles', toSelect: toSelect, toImport: toImport, toRemove: toRemove }, callback); hostsFilesSettingsHash = hashFromCurrentFromSettings(); }; let buttonApplyHandler = function () { uDom('#buttonApply').removeClass('enabled'); selectHostsFiles(function () { vAPI.messaging.send('hosts-files.js', { what: 'reloadHostsFiles' }); }); renderWidgets(); }; let buttonUpdateHandler = function () { uDom('#buttonUpdate').removeClass('enabled'); selectHostsFiles(function () { document.body.classList.add('updating'); vAPI.messaging.send('hosts-files.js', { what: 'forceUpdateAssets' }); renderWidgets(); }); renderWidgets(); }; let buttonPurgeAllHandler = function () { uDom('#buttonPurgeAll').removeClass('enabled'); vAPI.messaging.send('hosts-files.js', { what: 'purgeAllCaches' }, function () { renderHostsFiles(true); }); }; let autoUpdateCheckboxChanged = function () { vAPI.messaging.send('hosts-files.js', { what: 'userSettings', name: 'autoUpdate', value: this.checked }); }; uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); uDom('#buttonApply').on('click', buttonApplyHandler); uDom('#buttonUpdate').on('click', buttonUpdateHandler); uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler); uDom('#lists').on('change', '.listEntry > input', onHostsFilesSettingsChanged); uDom('#lists').on('click', '.listEntry > a.remove', onRemoveExternalHostsFile); uDom('#lists').on('click', 'span.cache', onPurgeClicked); uDom('#externalHostsFiles').on('input', onHostsFilesSettingsChanged); renderHostsFiles(); })();