diff --git a/src/about.html b/src/about.html index 6ae10e3..efa0210 100644 --- a/src/about.html +++ b/src/about.html @@ -66,34 +66,12 @@ table td:first-child {

+

-

-
-

-
- - -
-
-

-

-

-
-
- - + diff --git a/src/asset-viewer.html b/src/asset-viewer.html new file mode 100644 index 0000000..c4a0f4c --- /dev/null +++ b/src/asset-viewer.html @@ -0,0 +1,19 @@ + + + + +µMatrix — Asset + + + +
+ + + + + diff --git a/src/background.html b/src/background.html index 1b47aa3..f0ea1d8 100644 --- a/src/background.html +++ b/src/background.html @@ -17,7 +17,6 @@ - diff --git a/src/css/dashboard-common.css b/src/css/dashboard-common.css index f13d0c3..172ef9c 100644 --- a/src/css/dashboard-common.css +++ b/src/css/dashboard-common.css @@ -1,7 +1,10 @@ body { margin: 0; padding: 0 0.5em 5em 0.5em; - font: 15px httpsb,sans-serif; + font: 15px sans-serif; + } +body > *:first-child { + margin-top: 0; } h2, h3 { margin: 1em 0; @@ -24,7 +27,7 @@ a { text-decoration: none; } button { - padding: 0.4em; + padding: 0.3em 0.5em; } .para { @@ -49,6 +52,7 @@ button { .whatisthis-expandable { margin: 0.5em 0 1em 1.25em; padding: 0.5em; + font-size: smaller; display: none; border: 1px dotted black; background-color: #F8F8F8; diff --git a/src/css/hosts-files.css b/src/css/hosts-files.css new file mode 100644 index 0000000..05671ac --- /dev/null +++ b/src/css/hosts-files.css @@ -0,0 +1,115 @@ +ul { + padding: 0; + list-style-type: none; + } +ul#options { + margin-top: 0; + } +ul#options li { + margin-bottom: 0.5em; + } +ul#lists { + margin: 0.5em 0 0 0; + padding-__MSG_@@bidi_end_edge__: 0em; + padding-__MSG_@@bidi_start_edge__: 1em; + } +li.listDetails { + font-size: 14px; + margin: 0 auto 0 auto; + margin-__MSG_@@bidi_start_edge__: 1em; + margin-__MSG_@@bidi_end_edge__: 0em; + } +li.listDetails > * { + unicode-bidi: embed; + } +li.listDetails > a:nth-of-type(2) { + font-size: 13px; + opacity: 0.5; + } +.dim { + opacity: 0.5; + } +/* I designed the button with: http://charliepark.org/bootstrap_buttons/ */ +button.custom { + padding: 5px; + border: 1px solid transparent; + border-color: #80b3ff #80b3ff hsl(216, 100%, 75%); + border-radius: 3px; + background-color: hsl(216, 100%, 75%); + background-image: linear-gradient(#a8cbff, #80b3ff); + background-repeat: repeat-x; + color: #222; + cursor: pointer; + opacity: 0.8; + } +button.custom.disabled { + border-color: #dddddd #dddddd hsl(36, 0%, 85%); + background-color: hsl(36, 0%, 72%); + background-image: linear-gradient(#f2f2f2, #dddddd); + color: #aaa; + pointer-events: none; + } +button.custom:hover { + opacity: 1.0; + } +button.custom.reloadAll:not(.disabled) { + border-color: #ffcc7f #ffcc7f hsl(36, 100%, 73%); + background-color: hsl(36, 100%, 75%); + background-image: linear-gradient(#ffdca8, #ffcc7f); + } +#buttonApply { + position: fixed; + display: initial; + top: 1em; + __MSG_@@bidi_end_edge__: 1em; + } +#buttonApply.disabled { + display: none; + } +span.status { + margin: 0; + border: 1px solid transparent; + padding: 1px 2px; + display: inline-block; + font-size: 11px; + opacity: 0.7; +} +span.purge { + border-color: #ddd; + color: #444; + background-color: #eee; + cursor: pointer; + } +span.purge:hover { + opacity: 1; + } +span.obsolete { + border-color: hsl(36, 100%, 73%); + color: #222; + background-color: hsl(36, 100%, 75%); + } +#externalListsDiv { + margin: 2em auto 0 auto; + margin-__MSG_@@bidi_start_edge__: 2em; + } +#externalHostsFiles { + font-size: smaller; + width: 48em; + height: 12em; + white-space: nowrap; + } +body #busyOverlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + opacity: 0.5; + cursor: wait; + display: none; + z-index: 1000; + } +body.busy #busyOverlay { + display: block; + } diff --git a/src/css/popup.css b/src/css/popup.css index 5a9c8bd..3034799 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -438,12 +438,12 @@ body.colorblind .t82 { body.colorblind .t1 { border-color: #333; color: white; - background-color: #555; + background-color: #666; } body.colorblind .t2 { border-color: #aaa; color: black; - background-color: #ddd; + background-color: #ccc; } body.colorblind .matCell.p81 { background-image: url('/img/permanent-black-small-cb.png'); diff --git a/src/dashboard.html b/src/dashboard.html index e989e4a..d58b927 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -8,7 +8,7 @@ body { margin: 0; border: 0; padding: 0; - font: 15px httpsb,sans-serif; + font: 15px sans-serif; position: relative; width: 100vw; height: 100vh; @@ -18,7 +18,6 @@ body { margin: 0; border: 0; padding: 0; - position: absolute; top: 0; width: 100vw; height: 50px; @@ -27,7 +26,8 @@ body { #dashboard-nav-widgets { margin: 0; border-bottom: 1px solid #ccc; - padding: 4px 0 3px 0; + padding: 4px 0; + box-sizing: border-box; white-space: nowrap; background-color: white; } @@ -41,6 +41,7 @@ body { border-top-left-radius: 3px; border-top-right-radius: 3px; padding: 4px; + box-sizing: border-box; color: black; background-color: #eee; font: inherit; @@ -79,7 +80,7 @@ iframe { - + diff --git a/src/hosts-files.html b/src/hosts-files.html index e8791da..593b103 100644 --- a/src/hosts-files.html +++ b/src/hosts-files.html @@ -2,55 +2,39 @@ -µMatrix — Ubiquitous rules +HTTP Switchboard — Ubiquitous rules - + -

-
-

+

+ + + + +
+

+ +

-

-
-

-

- - -
+
- - + + diff --git a/src/js/about.js b/src/js/about.js index aef7342..22bf593 100644 --- a/src/js/about.js +++ b/src/js/about.js @@ -19,17 +19,11 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, $ */ +/* global chrome, uDom */ /******************************************************************************/ -$(function() { - -/******************************************************************************/ - -var updateList = {}; -var assetListSwitches = ['o', 'o', 'o']; -var commitHistoryURLPrefix = 'https://github.com/gorhill/httpswitchboard/commits/master/'; +uDom.onLoad(function() { /******************************************************************************/ @@ -75,12 +69,7 @@ var backupUserDataToFile = function() { /******************************************************************************/ -var restoreUserDataFromFile = function() { - var input = $('').attr({ - type: 'file', - accept: 'text/plain' - }); - +function restoreUserDataFromFile() { var restartCountdown = 4; var doCountdown = function() { restartCountdown -= 1; @@ -151,23 +140,27 @@ var restoreUserDataFromFile = function() { } }; - var filePickerOnChangeHandler = function() { - $(this).off('change', filePickerOnChangeHandler); - var file = this.files[0]; - if ( !file ) { - return; - } - if ( file.type.indexOf('text') !== 0 ) { - return; - } - var fr = new FileReader(); - fr.onload = fileReaderOnLoadHandler; - fr.readAsText(file); - input.off('change', filePickerOnChangeHandler); - }; + var file = this.files[0]; + if ( file === undefined || file.name === '' ) { + return; + } + if ( file.type.indexOf('text') !== 0 ) { + return; + } + var fr = new FileReader(); + fr.onload = fileReaderOnLoadHandler; + fr.readAsText(file); +} - input.on('change', filePickerOnChangeHandler); - input.trigger('click'); +/******************************************************************************/ + +var startRestoreFilePicker = function() { + var input = document.getElementById('restoreFilePicker'); + // Reset to empty string, this will ensure an change event is properly + // triggered if the user pick a file, even if it is the same as the last + // one picked. + input.value = ''; + input.click(); }; /******************************************************************************/ @@ -181,106 +174,29 @@ var resetUserData = function() { /******************************************************************************/ -var setAssetListClassBit = function(bit, state) { - assetListSwitches[assetListSwitches.length-1-bit] = !state ? 'o' : 'x'; - $('#assetList') - .removeClass() - .addClass(assetListSwitches.join('')); -}; - -/******************************************************************************/ - -var renderAssetList = function(details) { - var dirty = false; - var paths = Object.keys(details.list).sort(); - if ( paths.length > 0 ) { - $('#assetList .assetEntry').remove(); - var assetTable = $('#assetList table'); - var i = 0; - var path, status, html; - while ( path = paths[i++] ) { - status = details.list[path].status; - dirty = dirty || status !== 'Unchanged'; - html = []; - html.push(''); - html.push(''); - html.push(''); - html.push(path.replace(/^(assets\/[^/]+\/)(.+)$/, '$1$2')); - html.push(''); - html.push(''); - html.push(chrome.i18n.getMessage('aboutAssetsUpdateStatus' + status)); - assetTable.append(html.join('')); - } - $('#assetList a').attr('target', '_blank'); - updateList = details.list; - } - setAssetListClassBit(0, paths.length !== 0); - setAssetListClassBit(1, dirty); - setAssetListClassBit(2, false); -}; - -/******************************************************************************/ - -var updateAssets = function() { - setAssetListClassBit(2, true); - var onDone = function(details) { - if ( details.changedCount !== 0 ) { - messaging.tell({ what: 'loadUpdatableAssets' }); - } - }; - messaging.ask({ what: 'launchAssetUpdater', list: updateList }, onDone); -}; - -/******************************************************************************/ - -var updateAssetsList = function() { - messaging.ask({ what: 'getAssetUpdaterList' }, renderAssetList); -}; - -/******************************************************************************/ - -// Updating all assets could be done from elsewhere and if so the -// list here needs to be updated. - -var onAnnounce = function(msg) { - switch ( msg.what ) { - case 'allLocalAssetsUpdated': - updateAssetsList(); - break; - - default: - break; - } -}; - messaging.start('about.js'); -messaging.listen(onAnnounce); /******************************************************************************/ (function() { - $('#aboutVersion').html(chrome.runtime.getManifest().version); + uDom('#aboutVersion').html(chrome.runtime.getManifest().version); var renderStats = function(details) { var template = chrome.i18n.getMessage('aboutStorageUsed'); var percent = 0; if ( details.storageQuota ) { percent = (details.storageUsed / details.storageQuota * 100).toFixed(1); } - $('#aboutStorageUsed').html(template.replace('{{storageUsed}}', percent)); + uDom('#aboutStorageUsed').html(template.replace('{{storageUsed}}', percent)); }; messaging.ask({ what: 'getSomeStats' }, renderStats); })(); /******************************************************************************/ -$('#aboutAssetsUpdateButton').on('click', updateAssets); -$('#backupUserDataButton').on('click', backupUserDataToFile); -$('#restoreUserDataButton').on('click', restoreUserDataFromFile); -$('#resetUserDataButton').on('click', resetUserData); - -/******************************************************************************/ - -updateAssetsList(); +uDom('#backupUserDataButton').on('click', backupUserDataToFile); +uDom('#restoreUserDataButton').on('click', startRestoreFilePicker); +uDom('#restoreFilePicker').on('change', restoreUserDataFromFile); +uDom('#resetUserDataButton').on('click', resetUserData); /******************************************************************************/ diff --git a/src/js/assets.js b/src/js/assets.js index d574119..cc5eb75 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -29,14 +29,49 @@ /******************************************************************************/ -var remoteRoot = µMatrix.projectServerRoot; +var oneSecond = 1000; +var oneMinute = 60 * oneSecond; +var oneHour = 60 * oneMinute; +var oneDay = 24 * oneHour; + +/******************************************************************************/ + +var repositoryRoot = µMatrix.projectServerRoot; var nullFunc = function() {}; +var reIsExternalPath = /^https?:\/\/[a-z0-9]/; +var reIsUserPath = /^assets\/user\//; +var lastRepoMetaTimestamp = 0; +var refreshRepoMetaPeriod = 5 * oneHour; + +// TODO: move chrome.i18n.getMessage to vAPI +var errorCantConnectTo = chrome.i18n.getMessage('errorCantConnectTo'); + +var exports = { + autoUpdate: true, + autoUpdateDelay: 4 * oneDay +}; + +/******************************************************************************/ + +var AssetEntry = function() { + this.localChecksum = ''; + this.repoChecksum = ''; + this.expireTimestamp = 0; + this.homeURL = ''; +}; + +var RepoMetadata = function() { + this.entries = {}; + this.waiting = []; +}; + +var repoMetadata = null; /******************************************************************************/ var cachedAssetsManager = (function() { - var entries = null; var exports = {}; + var entries = null; var cachedAssetPathPrefix = 'cached_asset_content://'; var getEntries = function(callback) { @@ -44,22 +79,40 @@ var cachedAssetsManager = (function() { callback(entries); return; } - var onLoaded = function(bin) { - if ( chrome.runtime.lastError ) { - console.error( - 'assets.js > cachedAssetsManager> getEntries():', - chrome.runtime.lastError.message - ); + // Flush cached non-user assets if these are from a prior version. + // https://github.com/gorhill/httpswitchboard/issues/212 + var onLastVersionRead = function(store) { + var currentVersion = chrome.runtime.getManifest().version; + var lastVersion = store.extensionLastVersion || '0.0.0.0'; + if ( currentVersion !== lastVersion ) { + chrome.storage.local.set({ 'extensionLastVersion': currentVersion }); + exports.remove(/^assets\/(umatrix|thirdparties)\//); + exports.remove('assets/checksums.txt'); } - entries = bin.cached_asset_entries || {}; callback(entries); }; + var onLoaded = function(bin) { + // https://github.com/gorhill/httpswitchboard/issues/381 + // Maybe the index was requested multiple times and already + // fetched by one of the occurrences. + if ( entries === null ) { + if ( chrome.runtime.lastError ) { + console.error( + 'assets.js > cachedAssetsManager> getEntries():', + chrome.runtime.lastError.message + ); + } + entries = bin.cached_asset_entries || {}; + } + chrome.storage.local.get('extensionLastVersion', onLastVersionRead); + }; chrome.storage.local.get('cached_asset_entries', onLoaded); }; + exports.entries = getEntries; exports.load = function(path, cbSuccess, cbError) { cbSuccess = cbSuccess || nullFunc; - cbError = cbError || nullFunc; + cbError = cbError || cbSuccess; var details = { 'path': path, 'content': '' @@ -67,20 +120,17 @@ var cachedAssetsManager = (function() { var cachedContentPath = cachedAssetPathPrefix + path; var onLoaded = function(bin) { if ( chrome.runtime.lastError ) { - console.error( - 'assets.js > cachedAssetsManager.load():', - chrome.runtime.lastError.message - ); details.error = 'Error: ' + chrome.runtime.lastError.message; + console.error('assets.js > cachedAssetsManager.load():', details.error); cbError(details); - return; + } else { + details.content = bin[cachedContentPath]; + cbSuccess(details); } - details.content = bin[cachedContentPath]; - cbSuccess(details); }; var onEntries = function(entries) { if ( entries[path] === undefined ) { - details.error = 'Error: not found' + details.error = 'Error: not found'; cbError(details); return; } @@ -91,57 +141,71 @@ var cachedAssetsManager = (function() { exports.save = function(path, content, cbSuccess, cbError) { cbSuccess = cbSuccess || nullFunc; - cbError = cbError || nullFunc; + cbError = cbError || cbSuccess; + var details = { + path: path, + content: content + }; var cachedContentPath = cachedAssetPathPrefix + path; var bin = {}; bin[cachedContentPath] = content; var onSaved = function() { if ( chrome.runtime.lastError ) { - console.error( - 'assets.js > cachedAssetsManager.save():', - chrome.runtime.lastError.message - ); - cbError(chrome.runtime.lastError.message); + details.error = 'Error: ' + chrome.runtime.lastError.message; + console.error('assets.js > cachedAssetsManager.save():', details.error); + cbError(details); } else { - cbSuccess(); + cbSuccess(details); } }; var onEntries = function(entries) { - if ( entries[path] === undefined ) { - entries[path] = true; - bin.cached_asset_entries = entries; - } + entries[path] = Date.now(); + bin.cached_asset_entries = entries; chrome.storage.local.set(bin, onSaved); }; getEntries(onEntries); }; - exports.remove = function(pattern) { + exports.remove = function(pattern, before) { var onEntries = function(entries) { - var mustSave = false; - var pathstoRemove = []; + var keystoRemove = []; var paths = Object.keys(entries); var i = paths.length; var path; while ( i-- ) { + path = paths[i]; if ( typeof pattern === 'string' && path !== pattern ) { continue; } if ( pattern instanceof RegExp && !pattern.test(path) ) { continue; } - pathstoRemove.push(cachedAssetPathPrefix + path); + if ( typeof before === 'number' && entries[path] >= before ) { + continue; + } + keystoRemove.push(cachedAssetPathPrefix + path); delete entries[path]; - mustSave = true; } - if ( mustSave ) { - chrome.storage.local.remove(pathstoRemove); + if ( keystoRemove.length ) { + chrome.storage.local.remove(keystoRemove); chrome.storage.local.set({ 'cached_asset_entries': entries }); } }; getEntries(onEntries); }; + exports.removeAll = function(callback) { + var onEntries = function() { + exports.remove(/^https?:\/\/[a-z0-9]+/); + exports.remove(/^assets\/(umatrix|thirdparties)\//); + exports.remove('assets/checksums.txt'); + if ( typeof callback === 'function' ) { + callback(); + } + }; + getEntries(onEntries); + }; + return exports; })(); @@ -151,6 +215,7 @@ var getTextFileFromURL = function(url, onLoad, onError) { // console.log('assets.js > getTextFileFromURL("%s"):', url); var xhr = new XMLHttpRequest(); xhr.responseType = 'text'; + xhr.timeout = 15000; xhr.onload = onLoad; xhr.onerror = onError; xhr.ontimeout = onError; @@ -160,72 +225,200 @@ var getTextFileFromURL = function(url, onLoad, onError) { /******************************************************************************/ -// Flush cached non-user assets if these are from a prior version. -// https://github.com/gorhill/httpswitchboard/issues/212 - -var cacheSynchronized = false; - -var synchronizeCache = function() { - if ( cacheSynchronized ) { - return; - } - cacheSynchronized = true; - - var onLastVersionRead = function(store) { - var currentVersion = chrome.runtime.getManifest().version; - var lastVersion = store.extensionLastVersion || '0.0.0.0'; - if ( currentVersion === lastVersion ) { - return; +var updateLocalChecksums = function() { + var localChecksums = []; + var entries = repoMetadata.entries; + var entry; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; } - chrome.storage.local.set({ 'extensionLastVersion': currentVersion }); - cachedAssetsManager.remove(/assets\/(umatrix|thirdparties)\//); - }; - - chrome.storage.local.get('extensionLastVersion', onLastVersionRead); + entry = entries[path]; + if ( entry.localChecksum !== '' ) { + localChecksums.push(entry.localChecksum + ' ' + path); + } + } + cachedAssetsManager.save('assets/checksums.txt', localChecksums.join('\n')); }; /******************************************************************************/ +// Gather meta data of all assets. + +var getRepoMetadata = function(callback) { + callback = callback || nullFunc; + + if ( (Date.now() - lastRepoMetaTimestamp) >= refreshRepoMetaPeriod ) { + repoMetadata = null; + } + if ( repoMetadata !== null ) { + if ( repoMetadata.waiting.length !== 0 ) { + repoMetadata.waiting.push(callback); + } else { + callback(repoMetadata); + } + return; + } + + lastRepoMetaTimestamp = Date.now(); + + var localChecksums; + var repoChecksums; + + var checksumsReceived = function() { + if ( localChecksums === undefined || repoChecksums === undefined ) { + return; + } + // Remove from cache assets which no longer exist in the repo + var entries = repoMetadata.entries; + var checksumsChanged = false; + var entry; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entry = entries[path]; + // If repo checksums could not be fetched, assume no change + if ( repoChecksums === '' ) { + entry.repoChecksum = entry.localChecksum; + } + if ( entry.repoChecksum !== '' || entry.localChecksum === '' ) { + continue; + } + checksumsChanged = true; + cachedAssetsManager.remove(path); + entry.localChecksum = ''; + } + if ( checksumsChanged ) { + updateLocalChecksums(); + } + // Notify all waiting callers + while ( callback = repoMetadata.waiting.pop() ) { + callback(repoMetadata); + } + }; + + var validateChecksums = function(details) { + if ( details.error || details.content === '' ) { + return ''; + } + if ( /^(?:[0-9a-f]{32}\s+\S+(?:\s+|$))+/.test(details.content) === false ) { + return ''; + } + return details.content; + }; + + var parseChecksums = function(text, which) { + var entries = repoMetadata.entries; + var lines = text.split(/\n+/); + var i = lines.length; + var fields, assetPath; + while ( i-- ) { + fields = lines[i].trim().split(/\s+/); + if ( fields.length !== 2 ) { + continue; + } + assetPath = fields[1]; + if ( entries[assetPath] === undefined ) { + entries[assetPath] = new AssetEntry(); + } + entries[assetPath][which + 'Checksum'] = fields[0]; + } + }; + + var onLocalChecksumsLoaded = function(details) { + if ( localChecksums = validateChecksums(details) ) { + parseChecksums(localChecksums, 'local'); + } + checksumsReceived(); + }; + + var onRepoChecksumsLoaded = function(details) { + if ( repoChecksums = validateChecksums(details) ) { + parseChecksums(repoChecksums, 'repo'); + } + checksumsReceived(); + }; + + repoMetadata = new RepoMetadata(); + repoMetadata.waiting.push(callback); + readRepoFile('assets/checksums.txt', onRepoChecksumsLoaded); + readLocalFile('assets/checksums.txt', onLocalChecksumsLoaded); +}; + +// https://www.youtube.com/watch?v=-t3WYfgM4x8 + +/******************************************************************************/ + +exports.setHomeURL = function(path, homeURL) { + var onRepoMetadataReady = function(metadata) { + var entry = metadata.entries[path]; + if ( entry === undefined ) { + entry = metadata.entries[path] = new AssetEntry(); + } + entry.homeURL = homeURL; + }; + getRepoMetadata(onRepoMetadataReady); +}; + +/******************************************************************************/ + +// Get a local asset, do not look-up repo or remote location if local asset +// is not found. + var readLocalFile = function(path, callback) { var reportBack = function(content, err) { var details = { 'path': path, - 'content': content, - 'error': err + 'content': content }; + if ( err ) { + details.error = err; + } callback(details); }; - var onLocalFileLoaded = function() { - // console.log('assets.js > onLocalFileLoaded()'); + var onInstallFileLoaded = function() { + this.onload = this.onerror = null; + //console.log('assets.js > readLocalFile("%s") / onInstallFileLoaded()', path); reportBack(this.responseText); - this.onload = this.onerror = null; }; - var onLocalFileError = function(ev) { - console.error('assets.js > readLocalFile() / onLocalFileError("%s")', path); + var onInstallFileError = function() { + this.onload = this.onerror = null; + console.error('assets.js > readLocalFile("%s") / onInstallFileError()', path); reportBack('', 'Error'); - this.onload = this.onerror = null; }; - var onCacheFileLoaded = function(details) { - // console.log('assets.js > readLocalFile() / onCacheFileLoaded()'); + var onCachedContentLoaded = function(details) { + //console.log('assets.js > readLocalFile("%s") / onCachedContentLoaded()', path); reportBack(details.content); }; - var onCacheFileError = function(details) { - // This handler may be called under normal circumstances: it appears - // the entry may still be present even after the file was removed. - console.error('assets.js > readLocalFile() / onCacheFileError("%s")', details.path); - getTextFileFromURL(chrome.runtime.getURL(details.path), onLocalFileLoaded, onLocalFileError); + var onCachedContentError = function(details) { + //console.error('assets.js > readLocalFile("%s") / onCachedContentError()', path); + if ( reIsExternalPath.test(path) ) { + reportBack('', 'Error: asset not found'); + return; + } + // It's ok for user data to not be found + if ( reIsUserPath.test(path) ) { + reportBack(''); + return; + } + getTextFileFromURL(chrome.runtime.getURL(details.path), onInstallFileLoaded, onInstallFileError); }; - cachedAssetsManager.load(path, onCacheFileLoaded, onCacheFileError); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; +// https://www.youtube.com/watch?v=r9KVpuFPtHc + /******************************************************************************/ -var readRemoteFile = function(path, callback) { +// Get the repository copy of a built-in asset. + +var readRepoFile = function(path, callback) { var reportBack = function(content, err) { var details = { 'path': path, @@ -235,120 +428,547 @@ var readRemoteFile = function(path, callback) { callback(details); }; - var onRemoteFileLoaded = function() { - // console.log('assets.js > readRemoteFile() / onRemoteFileLoaded()'); + var repositoryURL = repositoryRoot + path; + + var onRepoFileLoaded = function() { + this.onload = this.onerror = null; + //console.log('assets.js > readRepoFile("%s") / onRepoFileLoaded()', path); // https://github.com/gorhill/httpswitchboard/issues/263 if ( this.status === 200 ) { reportBack(this.responseText); } else { - reportBack('', 'Error ' + this.statusText); + reportBack('', 'Error: ' + this.statusText); } - this.onload = this.onerror = null; }; - var onRemoteFileError = function(ev) { - console.error('assets.js > readRemoteFile() / onRemoteFileError("%s")', path); - reportBack('', 'Error'); + var onRepoFileError = function() { this.onload = this.onerror = null; + console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); + reportBack('', 'Error'); }; // 'umatrix=...' is to skip browser cache getTextFileFromURL( - remoteRoot + path + '?umatrix=' + Date.now(), - onRemoteFileLoaded, - onRemoteFileError + repositoryURL + '?umatrix=' + Date.now(), + onRepoFileLoaded, + onRepoFileError ); }; /******************************************************************************/ -var writeLocalFile = function(path, content, callback) { - var reportBack = function(err) { +// An asset from an external source with a copy shipped with the extension: +// Path --> starts with 'assets/(thirdparties|umatrix)/', with a home URL +// External --> +// Repository --> has checksum (to detect need for update only) +// Cache --> has expiration timestamp (in cache) +// Local --> install time version + +var readRepoCopyAsset = function(path, callback) { + var assetEntry; + + var reportBack = function(content, err) { var details = { 'path': path, - 'content': content, - 'error': err + 'content': content }; + if ( err ) { + details.error = err; + } callback(details); }; - var onFileWriteSuccess = function() { - console.log('assets.js > writeLocalFile() / onFileWriteSuccess("%s")', path); - reportBack(); + var updateChecksum = function() { + if ( assetEntry !== undefined && assetEntry.repoChecksum !== assetEntry.localChecksum ) { + assetEntry.localChecksum = assetEntry.repoChecksum; + updateLocalChecksums(); + } }; - var onFileWriteError = function(err) { - console.error('assets.js > writeLocalFile() / onFileWriteError("%s"):', path, err); - reportBack(err); + var onInstallFileLoaded = function() { + this.onload = this.onerror = null; + //console.log('assets.js > readRepoCopyAsset("%s") / onInstallFileLoaded()', path); + reportBack(this.responseText); }; - cachedAssetsManager.save(path, content, onFileWriteSuccess, onFileWriteError); + var onInstallFileError = function() { + this.onload = this.onerror = null; + console.error('assets.js > readRepoCopyAsset("%s") / onInstallFileError():', path, this.statusText); + reportBack('', 'Error'); + }; + + var onCachedContentLoaded = function(details) { + //console.log('assets.js > readRepoCopyAsset("%s") / onCacheFileLoaded()', path); + reportBack(details.content); + }; + + var onCachedContentError = function(details) { + //console.log('assets.js > readRepoCopyAsset("%s") / onCacheFileError()', path); + getTextFileFromURL(chrome.runtime.getURL(details.path), onInstallFileLoaded, onInstallFileError); + }; + + var repositoryURL = repositoryRoot + path; + var repositoryURLSkipCache = repositoryURL + '?umatrix=' + Date.now(); + + var onRepoFileLoaded = function() { + this.onload = this.onerror = null; + if ( typeof this.responseText !== 'string' || this.responseText === '' ) { + console.error('assets.js > readRepoCopyAsset("%s") / onRepoFileLoaded("%s"): error', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + return; + } + //console.log('assets.js > readRepoCopyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + updateChecksum(); + cachedAssetsManager.save(path, this.responseText, callback); + }; + + var onRepoFileError = function() { + this.onload = this.onerror = null; + console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + var onHomeFileLoaded = function() { + this.onload = this.onerror = null; + if ( typeof this.responseText !== 'string' || this.responseText === '' ) { + console.error('assets.js > readRepoCopyAsset("%s") / onHomeFileLoaded("%s"): no response', path, assetEntry.homeURL); + // Fetch from repo only if obsolescence was due to repo checksum + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { + getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); + } else { + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + } + return; + } + //console.log('assets.js > readRepoCopyAsset("%s") / onHomeFileLoaded("%s")', path, assetEntry.homeURL); + updateChecksum(); + cachedAssetsManager.save(path, this.responseText, callback); + }; + + var onHomeFileError = function() { + this.onload = this.onerror = null; + console.error(errorCantConnectTo.replace('{{url}}', assetEntry.homeURL)); + // Fetch from repo only if obsolescence was due to repo checksum + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { + getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); + } else { + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + } + }; + + var onCacheMetaReady = function(entries) { + // Fetch from remote if: + // - Auto-update enabled AND (not in cache OR in cache but obsolete) + var timestamp = entries[path]; + var homeURL = assetEntry.homeURL; + if ( exports.autoUpdate && typeof homeURL === 'string' && homeURL !== '' ) { + var obsolete = Date.now() - exports.autoUpdateDelay; + if ( typeof timestamp !== 'number' || timestamp <= obsolete ) { + //console.log('assets.js > readRepoCopyAsset("%s") / onCacheMetaReady(): not cached or obsolete', path); + getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); + return; + } + } + + // In cache + if ( typeof timestamp === 'number' ) { + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + return; + } + + // Not in cache + getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); + }; + + var onRepoMetaReady = function(meta) { + assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + reportBack('', 'Error: asset not found'); + return; + } + + // Repo copy changed: fetch from home URL + if ( exports.autoUpdate && assetEntry.localChecksum !== assetEntry.repoChecksum ) { + //console.log('assets.js > readRepoCopyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + var homeURL = assetEntry.homeURL; + if ( typeof homeURL === 'string' && homeURL !== '' ) { + getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); + } else { + getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); + } + return; + } + + // Load from cache + cachedAssetsManager.entries(onCacheMetaReady); + }; + + getRepoMetadata(onRepoMetaReady); }; +// https://www.youtube.com/watch?v=uvUW4ozs7pY + /******************************************************************************/ -var updateFromRemote = function(details, callback) { - // 'umatrix=...' is to skip browser cache - var remoteURL = remoteRoot + details.path + '?umatrix=' + Date.now(); - var targetPath = details.path; - var targetMd5 = details.md5 || ''; +// An important asset shipped with the extension -- typically small, or +// doesn't change often: +// Path --> starts with 'assets/(thirdparties|umatrix)/', without a home URL +// Repository --> has checksum (to detect need for update and corruption) +// Cache --> whatever from above +// Local --> install time version - var reportBackError = function() { - callback({ - 'path': targetPath, - 'error': 'Error' - }); +var readRepoOnlyAsset = function(path, callback) { + + var assetEntry; + + var reportBack = function(content, err) { + var details = { + 'path': path, + 'content': content + }; + if ( err ) { + details.error = err; + } + callback(details); }; - var onRemoteFileLoaded = function() { + var onInstallFileLoaded = function() { + this.onload = this.onerror = null; + //console.log('assets.js > readRepoOnlyAsset("%s") / onInstallFileLoaded()', path); + reportBack(this.responseText); + }; + + var onInstallFileError = function() { + this.onload = this.onerror = null; + console.error('assets.js > readRepoOnlyAsset("%s") / onInstallFileError()', path); + reportBack('', 'Error'); + }; + + var onCachedContentLoaded = function(details) { + //console.log('assets.js > readRepoOnlyAsset("%s") / onCachedContentLoaded()', path); + reportBack(details.content); + }; + + var onCachedContentError = function() { + //console.log('assets.js > readRepoOnlyAsset("%s") / onCachedContentError()', path); + getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); + }; + + var repositoryURL = repositoryRoot + path + '?umatrix=' + Date.now(); + + var onRepoFileLoaded = function() { this.onload = this.onerror = null; if ( typeof this.responseText !== 'string' ) { - console.error('assets.js > updateFromRemote("%s") / onRemoteFileLoaded(): no response', remoteURL); - reportBackError(); + console.error('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - if ( YaMD5.hashStr(this.responseText) !== targetMd5 ) { - console.error('assets.js > updateFromRemote("%s") / onRemoteFileLoaded(): bad md5 checksum', remoteURL); - reportBackError(); + if ( YaMD5.hashStr(this.responseText) !== assetEntry.repoChecksum ) { + console.error('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): bad md5 checksum', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - // console.debug('assets.js > updateFromRemote("%s") / onRemoteFileLoaded()', remoteURL); - writeLocalFile(targetPath, this.responseText, callback); + //console.log('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + assetEntry.localChecksum = assetEntry.repoChecksum; + updateLocalChecksums(); + cachedAssetsManager.save(path, this.responseText, callback); }; - var onRemoteFileError = function(ev) { + var onRepoFileError = function() { this.onload = this.onerror = null; - console.error('assets.js > updateFromRemote() / onRemoteFileError("%s"):', remoteURL, this.statusText); - reportBackError(); + console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; - getTextFileFromURL( - remoteURL, - onRemoteFileLoaded, - onRemoteFileError - ); + var onRepoMetaReady = function(meta) { + assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + reportBack('', 'Error: asset not found'); + return; + } + + // Asset added or changed: load from repo URL and then cache result + if ( exports.autoUpdate && assetEntry.localChecksum !== assetEntry.repoChecksum ) { + //console.log('assets.js > readRepoOnlyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + return; + } + + // Load from cache + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + getRepoMetadata(onRepoMetaReady); }; /******************************************************************************/ -// Flush cached assets if cache content is from an older version: the extension -// always ships with the most up-to-date assets. +// Asset doesn't exist. Just for symmetry purpose. -synchronizeCache(); +var readNilAsset = function(path, callback) { + callback({ + 'path': path, + 'content': '', + 'error': 'Error: asset not found' + }); +}; /******************************************************************************/ -// Export API +// An external asset: +// Path --> starts with 'http' +// External --> https://..., http://... +// Cache --> has expiration timestamp (in cache) -return { - 'get': readLocalFile, - 'getRemote': readRemoteFile, - 'put': writeLocalFile, - 'update': updateFromRemote +var readExternalAsset = function(path, callback) { + var reportBack = function(content, err) { + var details = { + 'path': path, + 'content': content + }; + if ( err ) { + details.error = err; + } + callback(details); + }; + + var onCachedContentLoaded = function(details) { + //console.log('assets.js > readExternalAsset("%s") / onCachedContentLoaded()', path); + reportBack(details.content); + }; + + var onCachedContentError = function() { + console.error('assets.js > readExternalAsset("%s") / onCachedContentError()', path); + reportBack('', 'Error'); + }; + + var onExternalFileLoaded = function() { + this.onload = this.onerror = null; + //console.log('assets.js > readExternalAsset("%s") / onExternalFileLoaded1()', path); + cachedAssetsManager.save(path, this.responseText); + reportBack(this.responseText); + }; + + var onExternalFileError = function() { + this.onload = this.onerror = null; + console.error(errorCantConnectTo.replace('{{url}}', path)); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + var onCacheMetaReady = function(entries) { + // Fetch from remote if: + // - Not in cache OR + // + // - Auto-update enabled AND in cache but obsolete + var timestamp = entries[path]; + var obsolete = Date.now() - exports.autoUpdateDelay; + if ( typeof timestamp !== 'number' || (exports.autoUpdate && timestamp <= obsolete) ) { + getTextFileFromURL(path, onExternalFileLoaded, onExternalFileError); + return; + } + + // In cache + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + cachedAssetsManager.entries(onCacheMetaReady); }; /******************************************************************************/ +// User data: +// Path --> starts with 'assets/user/' +// Cache --> whatever user saved + +var readUserAsset = function(path, callback) { + var onCachedContentLoaded = function(details) { + //console.log('assets.js > readUserAsset("%s") / onCachedContentLoaded()', path); + callback({ 'path': path, 'content': details.content }); + }; + + var onCachedContentError = function() { + //console.log('assets.js > readUserAsset("%s") / onCachedContentError()', path); + callback({ 'path': path, 'content': '' }); + }; + + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); +}; + +/******************************************************************************/ + +// Assets +// +// A copy of an asset from an external source shipped with the extension: +// Path --> starts with 'assets/(thirdparties|umatrix)/', with a home URL +// External --> +// Repository --> has checksum (to detect obsolescence) +// Cache --> has expiration timestamp (to detect obsolescence) +// Local --> install time version +// +// An important asset shipped with the extension (usually small, or doesn't +// change often): +// Path --> starts with 'assets/(thirdparties|umatrix)/', without a home URL +// Repository --> has checksum (to detect obsolescence or data corruption) +// Cache --> whatever from above +// Local --> install time version +// +// An external filter list: +// Path --> starts with 'http' +// External --> +// Cache --> has expiration timestamp (to detect obsolescence) +// +// User data: +// Path --> starts with 'assets/user/' +// Cache --> whatever user saved +// +// When a checksum is present, it is used to determine whether the asset +// needs to be updated. +// When an expiration timestamp is present, it is used to determine whether +// the asset needs to be updated. +// +// If no update required, an asset if first fetched from the cache. If the +// asset is not cached it is fetched from the closest location: local for +// an asset shipped with the extension, external for an asset not shipped +// with the extension. + +exports.get = function(path, callback) { + + if ( reIsUserPath.test(path) ) { + readUserAsset(path, callback); + return; + } + + if ( reIsExternalPath.test(path) ) { + readExternalAsset(path, callback); + return; + } + + var onRepoMetaReady = function(meta) { + var assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + readNilAsset(path, callback); + return; + } + + // Asset is repo copy of external content + if ( assetEntry.homeURL !== '' ) { + readRepoCopyAsset(path, callback); + return; + } + + // Asset is repo only + readRepoOnlyAsset(path, callback); + }; + + getRepoMetadata(onRepoMetaReady); +}; + +// https://www.youtube.com/watch?v=98y0Q7nLGWk + +/******************************************************************************/ + +exports.getLocal = readLocalFile; + +/******************************************************************************/ + +exports.put = function(path, content, callback) { + cachedAssetsManager.save(path, content, callback); +}; + +/******************************************************************************/ + +exports.metadata = function(callback) { + var out = {}; + + // https://github.com/gorhill/uBlock/issues/186 + // We need to check cache obsolescence when both cache and repo meta data + // has been gathered. + var checkCacheObsolescence = function() { + var obsolete = Date.now() - exports.autoUpdateDelay; + var entry; + for ( var path in out ) { + if ( out.hasOwnProperty(path) === false ) { + continue; + } + entry = out[path]; + entry.cacheObsolete = + typeof entry.homeURL === 'string' && + entry.homeURL !== '' && + (typeof entry.lastModified !== 'number' || entry.lastModified <= obsolete); + } + callback(out); + }; + + var onRepoMetaReady = function(meta) { + var entries = meta.entries; + var entryRepo, entryOut; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entryRepo = entries[path]; + entryOut = out[path]; + if ( entryOut === undefined ) { + entryOut = out[path] = {}; + } + entryOut.localChecksum = entryRepo.localChecksum; + entryOut.repoChecksum = entryRepo.repoChecksum; + entryOut.homeURL = entryRepo.homeURL; + entryOut.repoObsolete = entryOut.localChecksum !== entryOut.repoChecksum; + } + checkCacheObsolescence(); + }; + + var onCacheMetaReady = function(entries) { + var entryOut; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entryOut = out[path]; + if ( entryOut === undefined ) { + entryOut = out[path] = {}; + } + entryOut.lastModified = entries[path]; + // User data is not literally cache data + if ( reIsUserPath.test(path) ) { + continue; + } + entryOut.cached = true; + if ( reIsExternalPath.test(path) ) { + entryOut.homeURL = path; + } + } + getRepoMetadata(onRepoMetaReady); + }; + + cachedAssetsManager.entries(onCacheMetaReady); +}; + +/******************************************************************************/ + +exports.purge = function(pattern, before) { + cachedAssetsManager.remove(pattern, before); +}; + +/******************************************************************************/ + +exports.purgeAll = function(callback) { + cachedAssetsManager.removeAll(callback); +}; + +/******************************************************************************/ + +return exports; + +/******************************************************************************/ + })(); /******************************************************************************/ diff --git a/src/js/background.js b/src/js/background.js index e6b663a..6b81cf3 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -45,6 +45,7 @@ return { manifest: chrome.runtime.getManifest(), userSettings: { + autoUpdate: false, clearBrowserCache: true, clearBrowserCacheAfter: 60, colorBlindFriendly: false, @@ -53,11 +54,12 @@ return { deleteUnusedSessionCookiesAfter: 60, deleteLocalStorage: false, displayTextSize: '13px', + externalHostsFiles: '', maxLoggedRequests: 50, popupCollapseDomains: false, popupCollapseSpecificDomains: {}, popupHideBlacklisted: false, - popupScopeLevel: '*', + popupScopeLevel: 'domain', processBehindTheSceneRequests: false, processHyperlinkAuditing: true, processReferer: false, @@ -74,13 +76,17 @@ return { updateAssetsEvery: 5 * 24 * 60 * 60 * 1000, projectServerRoot: 'https://raw.githubusercontent.com/gorhill/umatrix/master/', - // list of remote blacklist locations - remoteBlacklists: { - // uMatrix - 'assets/umatrix/blacklist.txt': { title: 'uMatrix' }, - - // 3rd-party lists now fetched dynamically - }, + // permanent hosts files + permanentHostsFiles: { + // µMatrix + 'assets/umatrix/blacklist.txt': { + title: 'µMatrix hosts file' + } + }, + + // list of live hosts files + liveHostsFiles: { + }, // urls stats are kept on the back burner while waiting to be reactivated // in a tab or another. @@ -99,10 +105,7 @@ return { tMatrix: null, pMatrix: null, - // Current entries from ubiquitous lists -- - // just hostnames, '*/' is implied, this saves significantly on memory. ubiquitousBlacklist: null, - ubiquitousWhitelist: null, // various stats requestStats: new WebRequestStats(), @@ -128,6 +131,8 @@ return { noopCSSURL: chrome.runtime.getURL('css/noop.css'), fontCSSURL: chrome.runtime.getURL('css/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf'), + noopFunc: function(){}, + // so that I don't have to care for last comma dummy: 0 }; diff --git a/src/js/hosts-files.js b/src/js/hosts-files.js index 83d00e9..4a1065a 100644 --- a/src/js/hosts-files.js +++ b/src/js/hosts-files.js @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, $ */ +/* global chrome, messaging, uDom */ /******************************************************************************/ @@ -27,17 +27,23 @@ /******************************************************************************/ -var selectedBlacklistsHash = ''; +var listDetails = {}; +var externalHostsFiles = ''; +var cacheWasPurged = false; +var needUpdate = false; +var hasCachedContent = false; + +var re3rdPartyExternalAsset = /^https?:\/\/[a-z0-9]+/; +var re3rdPartyRepoAsset = /^assets\/thirdparties\/([^\/]+)/; /******************************************************************************/ -messaging.start('ubiquitous-rules.js'); +messaging.start('hosts-files.js'); var onMessage = function(msg) { switch ( msg.what ) { - case 'loadUbiquitousBlacklistCompleted': + case 'loadHostsFilesCompleted': renderBlacklists(); - selectedBlacklistsChanged(); break; default: @@ -49,152 +55,352 @@ messaging.listen(onMessage); /******************************************************************************/ -function getµm() { - return chrome.extension.getBackgroundPage().µMatrix; -} - -/******************************************************************************/ - -function changeUserSettings(name, value) { - messaging.tell({ - what: 'userSettings', - name: name, - value: value - }); -} - -/******************************************************************************/ - // TODO: get rid of background page dependencies -function renderBlacklists() { - // empty list first - $('#blacklists .blacklistDetails').remove(); - - var µm = getµm(); - - $('#ubiquitousListsOfBlockedHostsPrompt2').text( - chrome.i18n.getMessage('ubiquitousListsOfBlockedHostsPrompt2') - .replace('{{ubiquitousBlacklistCount}}', µm.ubiquitousBlacklist.count.toLocaleString()) - ); +var renderBlacklists = function() { + uDom('body').toggleClass('busy', true); // Assemble a pretty blacklist name if possible - var prettifyListName = function(blacklistTitle, blacklistHref) { - if ( !blacklistTitle ) { - return blacklistHref; + var listNameFromListKey = function(listKey) { + var list = listDetails.current[listKey] || listDetails.available[listKey]; + var listTitle = list ? list.title : ''; + if ( listTitle === '' ) { + return listKey; } + return listTitle; + }; + + // Assemble a pretty blacklist name if possible + var htmlFromHomeURL = function(blacklistHref) { if ( blacklistHref.indexOf('assets/thirdparties/') !== 0 ) { - return blacklistTitle; + return ''; } - var matches = blacklistHref.match(/^assets\/thirdparties\/([^\/]+)/); + var matches = re3rdPartyRepoAsset.exec(blacklistHref); if ( matches === null || matches.length !== 2 ) { - return blacklistTitle; + return ''; } var hostname = matches[1]; - var domain = µm.URI.domainFromHostname(hostname); + var domain = hostname; if ( domain === '' ) { - return blacklistTitle; + return ''; } var html = [ - blacklistTitle, - ' (', + '" target="_blank">(', domain, - ')' + ')' ]; return html.join(''); }; - var blacklists = µm.remoteBlacklists; - var ul = $('#blacklists'); - var keys = Object.keys(blacklists); - var i = keys.length; - var blacklist, blacklistHref; - var liTemplate = $('#blacklistTemplate .blacklistDetails').first(); - var li, child, text; - while ( i-- ) { - blacklistHref = keys[i]; - blacklist = blacklists[blacklistHref]; - li = liTemplate.clone(); - child = $('input', li); - child.prop('checked', !blacklist.off); - child = $('a', li); - child.attr('href', encodeURI(blacklistHref)); - child.html(prettifyListName(blacklist.title, blacklistHref)); - child = $('span', li); - text = child.text() - .replace('{{used}}', !blacklist.off && !isNaN(+blacklist.entryUsedCount) ? blacklist.entryUsedCount.toLocaleString() : '0') - .replace('{{total}}', !isNaN(+blacklist.entryCount) ? blacklist.entryCount.toLocaleString() : '?') - ; - child.text(text); - ul.prepend(li); - } - selectedBlacklistsHash = getSelectedBlacklistsHash(); -} + var purgeButtontext = chrome.i18n.getMessage('hostsFilesExternalListPurge'); + var updateButtontext = chrome.i18n.getMessage('hostsFilesExternalListNew'); + var obsoleteButtontext = chrome.i18n.getMessage('hostsFilesExternalListObsolete'); + var liTemplate = [ + '
  • ', + '', + ' ', + '', + '{{name}}', + '\u200E', + '{{homeURL}}', + ': ', + '', + chrome.i18n.getMessage('hostsFilesPerFileStats'), + '' + ].join(''); + + var htmlFromLeaf = function(listKey) { + var html = []; + var hostsEntry = listDetails.available[listKey]; + var li = liTemplate + .replace('{{checked}}', hostsEntry.off ? '' : 'checked') + .replace('{{URL}}', encodeURI(listKey)) + .replace('{{name}}', listNameFromListKey(listKey)) + .replace('{{homeURL}}', htmlFromHomeURL(listKey)) + .replace('{{used}}', !hostsEntry.off && !isNaN(+hostsEntry.entryUsedCount) ? hostsEntry.entryUsedCount.toLocaleString() : '0') + .replace('{{total}}', !isNaN(+hostsEntry.entryCount) ? hostsEntry.entryCount.toLocaleString() : '?'); + html.push(li); + // https://github.com/gorhill/uBlock/issues/104 + var asset = listDetails.cache[listKey]; + if ( asset === undefined ) { + return html.join('\n'); + } + // Update status + if ( hostsEntry.off !== true ) { + var obsolete = asset.repoObsolete || + asset.cacheObsolete || + asset.cached !== true && re3rdPartyExternalAsset.test(listKey); + if ( obsolete ) { + html.push( + ' ', + '', + asset.repoObsolete ? updateButtontext : obsoleteButtontext, + '' + ); + needUpdate = true; + } + } + // In cache + if ( asset.cached ) { + html.push( + ' ', + '', + purgeButtontext, + '' + ); + hasCachedContent = true; + } + return html.join('\n'); + }; + + var onListsReceived = function(details) { + // Before all, set context vars + listDetails = details; + needUpdate = false; + hasCachedContent = false; + + // Visually split the filter lists in two groups: built-in and external + var html = []; + var hostsPaths = Object.keys(details.available); + var hostsEntry; + for ( i = 0; i < hostsPaths.length; i++ ) { + hostsPath = hostsPaths[i]; + hostsEntry = details.available[hostsPath]; + if ( !hostsEntry.external ) { + html.push(htmlFromLeaf(hostsPath, hostsEntry)); + } + } + for ( i = 0; i < hostsPaths.length; i++ ) { + hostsPath = hostsPaths[i]; + hostsEntry = details.available[hostsPath]; + if ( hostsEntry.external ) { + html.push(htmlFromLeaf(hostsPath, hostsEntry)); + } + } + + uDom('#listsOfBlockedHostsPrompt').text( + chrome.i18n.getMessage('hostsFilesStats') + .replace('{{blockedHostnameCount}}', details.blockedHostnameCount.toLocaleString()) + ); + uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true); + uDom('#lists').html(html.join('')); + uDom('a').attr('target', '_blank'); + + updateWidgets(); + }; + + messaging.ask({ what: 'getLists' }, onListsReceived); +}; /******************************************************************************/ -// Create a hash so that we know whether the selection of preset blacklists -// has changed. +// Return whether selection of lists changed. -function getSelectedBlacklistsHash() { - var hash = ''; - var inputs = $('#blacklists .blacklistDetails > input'); - var i = inputs.length; - var input, entryHash; - while ( i-- ) { - input = $(inputs[i]); - entryHash = input.prop('checked').toString(); - hash += entryHash; +var listsSelectionChanged = function() { + if ( cacheWasPurged ) { + return true; } + var availableLists = listDetails.available; + var currentLists = listDetails.current; + var location, availableOff, currentOff; + // This check existing entries + for ( location in availableLists ) { + if ( availableLists.hasOwnProperty(location) === false ) { + continue; + } + availableOff = availableLists[location].off === true; + currentOff = currentLists[location] === undefined || currentLists[location].off === true; + if ( availableOff !== currentOff ) { + return true; + } + } + // This check removed entries + for ( location in currentLists ) { + if ( currentLists.hasOwnProperty(location) === false ) { + continue; + } + currentOff = currentLists[location].off === true; + availableOff = availableLists[location] === undefined || availableLists[location].off === true; + if ( availableOff !== currentOff ) { + return true; + } + } + return false; +}; - return hash; -} +/******************************************************************************/ + +// Return whether content need update. + +var listsContentChanged = function() { + return needUpdate; +}; /******************************************************************************/ // This is to give a visual hint that the selection of blacklists has changed. -function selectedBlacklistsChanged() { - $('#blacklistsApply').attr( - 'disabled', - getSelectedBlacklistsHash() === selectedBlacklistsHash - ); -} +var updateWidgets = function() { + uDom('#buttonApply').toggleClass('disabled', !listsSelectionChanged()); + uDom('#buttonUpdate').toggleClass('disabled', !listsContentChanged()); + uDom('#buttonPurgeAll').toggleClass('disabled', !hasCachedContent); + uDom('body').toggleClass('busy', false); +}; /******************************************************************************/ -function blacklistsApplyHandler() { - var newHash = getSelectedBlacklistsHash(); - if ( newHash === selectedBlacklistsHash ) { +var onListCheckboxChanged = function() { + var href = uDom(this).parent().descendants('a').first().attr('href'); + if ( typeof href !== 'string' ) { return; } + if ( listDetails.available[href] === undefined ) { + return; + } + listDetails.available[href].off = !this.checked; + updateWidgets(); +}; + +/******************************************************************************/ + +var onListLinkClicked = function(ev) { + messaging.tell({ + what: 'gotoExtensionURL', + url: 'asset-viewer.html?url=' + uDom(this).attr('href') + }); + ev.preventDefault(); +}; + +/******************************************************************************/ + +var onPurgeClicked = function() { + var button = uDom(this); + var li = button.parent(); + var href = li.descendants('a').first().attr('href'); + if ( !href ) { + return; + } + messaging.tell({ what: 'purgeCache', path: href }); + button.remove(); + if ( li.descendants('input').first().prop('checked') ) { + cacheWasPurged = true; + updateWidgets(); + } +}; + +/******************************************************************************/ + +var reloadAll = function(update) { + // Loading may take a while when resources are fetched from remote + // servers. We do not want the user to force reload while we are reloading. + uDom('body').toggleClass('busy', true); + // Reload blacklists var switches = []; - var lis = $('#blacklists .blacklistDetails'); + var lis = uDom('#lists .listDetails'); var i = lis.length; var path; while ( i-- ) { - path = $(lis[i]).children('a').attr('href'); + path = lis + .subset(i) + .descendants('a') + .attr('href'); switches.push({ location: path, - off: $(lis[i]).children('input').prop('checked') === false + off: lis.subset(i).descendants('input').prop('checked') === false }); } messaging.tell({ - what: 'reloadPresetBlacklists', - switches: switches + what: 'reloadHostsFiles', + switches: switches, + update: update }); - $('#blacklistsApply').attr('disabled', true ); -} + cacheWasPurged = false; +}; /******************************************************************************/ -$(function() { - $('#blacklists').on('change', '.blacklistDetails', selectedBlacklistsChanged); - $('#blacklistsApply').on('click', blacklistsApplyHandler); +var buttonApplyHandler = function() { + reloadAll(false); + uDom('#buttonApply').toggleClass('enabled', false); +}; + +/******************************************************************************/ + +var buttonUpdateHandler = function() { + if ( needUpdate ) { + reloadAll(true); + } +}; + +/******************************************************************************/ + +var buttonPurgeAllHandler = function() { + var onCompleted = function() { + renderBlacklists(); + }; + messaging.ask({ what: 'purgeAllCaches' }, onCompleted); +}; + +/******************************************************************************/ + +var autoUpdateCheckboxChanged = function() { + messaging.tell({ + what: 'userSettings', + name: 'autoUpdate', + value: this.checked + }); +}; + +/******************************************************************************/ + +var renderExternalLists = function() { + var onReceived = function(details) { + uDom('#externalHostsFiles').val(details); + externalHostsFiles = details; + }; + messaging.ask({ what: 'userSettings', name: 'externalHostsFiles' }, onReceived); +}; + +/******************************************************************************/ + +var externalListsChangeHandler = function() { + uDom('#externalListsParse').prop( + 'disabled', + this.value.trim() === externalHostsFiles + ); +}; + +/******************************************************************************/ + +var externalListsApplyHandler = function() { + externalHostsFiles = uDom('#externalHostsFiles').val(); + messaging.tell({ + what: 'userSettings', + name: 'externalHostsFiles', + value: externalHostsFiles + }); renderBlacklists(); + uDom('#externalListsParse').prop('disabled', true); +}; + +/******************************************************************************/ + +uDom.onLoad(function() { + uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); + uDom('#buttonApply').on('click', buttonApplyHandler); + uDom('#buttonUpdate').on('click', buttonUpdateHandler); + uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler); + uDom('#lists').on('change', '.listDetails > input', onListCheckboxChanged); + uDom('#lists').on('click', '.listDetails > a:nth-of-type(1)', onListLinkClicked); + uDom('#lists').on('click', 'span.purge', onPurgeClicked); + uDom('#externalHostsFiles').on('input', externalListsChangeHandler); + uDom('#externalListsParse').on('click', externalListsApplyHandler); + + renderBlacklists(); + renderExternalLists(); }); /******************************************************************************/ diff --git a/src/js/httpsb.js b/src/js/httpsb.js index 89af978..20eab7e 100644 --- a/src/js/httpsb.js +++ b/src/js/httpsb.js @@ -31,7 +31,7 @@ µm.pMatrix.setSwitch('chrome-scheme', false); µm.pMatrix.setSwitch(µm.behindTheSceneScope, false); µm.pMatrix.setSwitch('opera-scheme', false); - µm.pMatrix.setCell('*', '*', '*', µm.Matrix.Green); + µm.pMatrix.setCell('*', '*', '*', µm.Matrix.Red); µm.pMatrix.setCell('*', '*', 'css', µm.Matrix.Green); µm.pMatrix.setCell('*', '*', 'image', µm.Matrix.Green); µm.pMatrix.setCell('*', '*', 'frame', µm.Matrix.Red); diff --git a/src/js/liquid-dict.js b/src/js/liquid-dict.js index f12b16e..257e3b3 100644 --- a/src/js/liquid-dict.js +++ b/src/js/liquid-dict.js @@ -28,6 +28,7 @@ var LiquidDict = function() { this.dict = {}; this.count = 0; + this.duplicateCount = 0; this.bucketCount = 0; this.frozenBucketCount = 0; @@ -160,6 +161,7 @@ LiquidDict.prototype.add = function(word) { this.count += 1; return true; } + this.duplicateCount += 1; return false; }; @@ -181,6 +183,7 @@ LiquidDict.prototype.freeze = function() { LiquidDict.prototype.reset = function() { this.dict = {}; this.count = 0; + this.duplicateCount = 0; this.bucketCount = 0; this.frozenBucketCount = 0; }; diff --git a/src/js/messaging-handlers.js b/src/js/messaging-handlers.js index 2a10108..7c4f3d2 100644 --- a/src/js/messaging-handlers.js +++ b/src/js/messaging-handlers.js @@ -474,15 +474,46 @@ var onMessage = function(request, sender, callback) { /******************************************************************************/ /******************************************************************************/ -// ubiquitous-rules.js +// hosts-files.js (function() { +var µm = µMatrix; + +/******************************************************************************/ + +var getLists = function(callback) { + var r = { + available: null, + cache: null, + current: µm.liveHostsFiles, + blockedHostnameCount: µm.ubiquitousBlacklist.count, + autoUpdate: µm.userSettings.autoUpdate + }; + var onMetadataReady = function(entries) { + r.cache = entries; + callback(r); + }; + var onAvailableHostsFilesReady = function(lists) { + r.available = lists; + µm.assets.metadata(onMetadataReady); + }; + µm.getAvailableHostsFiles(onAvailableHostsFilesReady); +}; + +/******************************************************************************/ + var onMessage = function(request, sender, callback) { var µm = µMatrix; // Async switch ( request.what ) { + case 'getLists': + return getLists(callback); + + case 'purgeAllCaches': + return µm.assets.purgeAll(callback); + default: break; } @@ -491,6 +522,10 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { + case 'purgeCache': + µm.assets.purge(request.path); + break; + default: return µm.messaging.defaultHandler(request, sender, callback); } @@ -498,7 +533,7 @@ var onMessage = function(request, sender, callback) { callback(response); }; -µMatrix.messaging.listen('ubiquitous-rules.js', onMessage); +µMatrix.messaging.listen('hosts-files.js', onMessage); })(); @@ -600,12 +635,6 @@ var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { - case 'getAssetUpdaterList': - return µm.assetUpdater.getList(callback); - - case 'launchAssetUpdater': - return µm.assetUpdater.update(request.list, callback); - case 'readUserSettings': return chrome.storage.local.get(µm.userSettings, callback); @@ -617,10 +646,6 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { - case 'loadUpdatableAssets': - response = µm.loadUpdatableAssets(); - break; - case 'getSomeStats': response = { storageQuota: µm.storageQuota, diff --git a/src/js/messaging.js b/src/js/messaging.js index b6e906e..c8ac45a 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -138,8 +138,8 @@ var onMessage = function(request, port) { function defaultHandler(request, sender, callback) { // Async switch ( request.what ) { - case 'loadUbiquitousAllowRules': - return µm.loadUbiquitousWhitelists(); + case 'getAssetContent': + return µm.assets.getLocal(request.url, callback); default: break; @@ -165,8 +165,8 @@ function defaultHandler(request, sender, callback) { µm.utils.gotoURL(request); break; - case 'reloadPresetBlacklists': - µm.reloadPresetBlacklists(request.switches); + case 'reloadHostsFiles': + µm.reloadHostsFiles(request.switches); break; case 'userSettings': diff --git a/src/js/storage.js b/src/js/storage.js index bf649be..8751308 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -91,134 +91,181 @@ /******************************************************************************/ -µMatrix.loadUbiquitousBlacklists = function() { - var µm = µMatrix; - var blacklists; - var blacklistLoadCount; - var obsoleteBlacklists = []; +µMatrix.getAvailableHostsFiles = function(callback) { + var availableHostsFiles = {}; + var redirections = {}; + var µm = this; - var removeObsoleteBlacklistsHandler = function(store) { - if ( !store.remoteBlacklists ) { - return; + // selected lists + var onSelectedHostsFilesLoaded = function(store) { + var lists = store.liveHostsFiles; + var locations = Object.keys(lists); + var oldLocation, newLocation; + var availableEntry, storedEntry; + + while ( oldLocation = locations.pop() ) { + newLocation = redirections[oldLocation] || oldLocation; + availableEntry = availableHostsFiles[newLocation]; + if ( availableEntry === undefined ) { + continue; + } + storedEntry = lists[oldLocation]; + availableEntry.off = storedEntry.off || false; + µm.assets.setHomeURL(newLocation, availableEntry.homeURL); + if ( storedEntry.entryCount !== undefined ) { + availableEntry.entryCount = storedEntry.entryCount; + } + if ( storedEntry.entryUsedCount !== undefined ) { + availableEntry.entryUsedCount = storedEntry.entryUsedCount; + } + // This may happen if the list name was pulled from the list content + if ( availableEntry.title === '' && storedEntry.title !== '' ) { + availableEntry.title = storedEntry.title; + } } - var location; - while ( location = obsoleteBlacklists.pop() ) { - delete store.remoteBlacklists[location]; - } - chrome.storage.local.set(store); + callback(availableHostsFiles); }; - var removeObsoleteBlacklists = function() { - if ( obsoleteBlacklists.length === 0 ) { - return; + // built-in lists + var onBuiltinHostsFilesLoaded = function(details) { + var location, locations; + try { + locations = JSON.parse(details.content); + } catch (e) { + locations = {}; } + var hostsFileEntry; + for ( location in locations ) { + if ( locations.hasOwnProperty(location) === false ) { + continue; + } + hostsFileEntry = locations[location]; + availableHostsFiles['assets/thirdparties/' + location] = hostsFileEntry; + if ( hostsFileEntry.old !== undefined ) { + redirections[hostsFileEntry.old] = location; + delete hostsFileEntry.old; + } + } + + // Now get user's selection of lists chrome.storage.local.get( - { 'remoteBlacklists': µm.remoteBlacklists }, - removeObsoleteBlacklistsHandler + { 'liveHostsFiles': availableHostsFiles }, + onSelectedHostsFilesLoaded ); }; - var mergeBlacklist = function(details) { - µm.mergeUbiquitousBlacklist(details); - blacklistLoadCount -= 1; - if ( blacklistLoadCount === 0 ) { - loadBlacklistsEnd(); - } - }; - - var loadBlacklistsEnd = function() { - µm.ubiquitousBlacklist.freeze(); - removeObsoleteBlacklists(); - µm.messaging.announce({ what: 'loadUbiquitousBlacklistCompleted' }); - }; - - var loadBlacklistsStart = function(store) { - // rhill 2013-12-10: set all existing entries to `false`. - µm.ubiquitousBlacklist.reset(); - blacklists = store.remoteBlacklists; - var blacklistLocations = Object.keys(store.remoteBlacklists); - - blacklistLoadCount = blacklistLocations.length; - if ( blacklistLoadCount === 0 ) { - loadBlacklistsEnd(); - return; - } - - // Load each preset blacklist which is not disabled. - var location; - while ( location = blacklistLocations.pop() ) { - // If loaded list location is not part of default list locations, - // remove its entry from local storage. - if ( !µm.remoteBlacklists[location] ) { - obsoleteBlacklists.push(location); - blacklistLoadCount -= 1; - continue; - } - // https://github.com/gorhill/httpswitchboard/issues/218 - // Transfer potentially existing list title into restored list data. - if ( store.remoteBlacklists[location].title !== µm.remoteBlacklists[location].title ) { - store.remoteBlacklists[location].title = µm.remoteBlacklists[location].title; - } - // Store details of this preset blacklist - µm.remoteBlacklists[location] = store.remoteBlacklists[location]; - // rhill 2013-12-09: - // Ignore list if disabled - // https://github.com/gorhill/httpswitchboard/issues/78 - if ( store.remoteBlacklists[location].off ) { - blacklistLoadCount -= 1; - continue; - } - µm.assets.get(location, mergeBlacklist); - } - }; - - var onListOfBlockListsLoaded = function(details) { - // Initialize built-in list of 3rd-party block lists. - var lists = JSON.parse(details.content); - for ( var location in lists ) { - if ( lists.hasOwnProperty(location) === false ) { - continue; - } - µm.remoteBlacklists['assets/thirdparties/' + location] = lists[location]; - } - // Now get user's selection of list of block lists. - chrome.storage.local.get( - { 'remoteBlacklists': µm.remoteBlacklists }, - loadBlacklistsStart - ); - }; - - // Reset list of 3rd-party block lists. - for ( var location in this.remoteBlacklists ) { - if ( location.indexOf('assets/thirdparties/') === 0 ) { - delete this.remoteBlacklists[location]; + // permanent hosts files + var location; + var lists = this.permanentHostsFiles; + for ( location in lists ) { + if ( lists.hasOwnProperty(location) === false ) { + continue; } + availableHostsFiles[location] = lists[location]; } - // Get new list of 3rd-party block lists. - this.assets.get('assets/umatrix/ubiquitous-block-lists.json', onListOfBlockListsLoaded); + // custom lists + var c; + var locations = this.userSettings.externalHostsFiles.split('\n'); + for ( var i = 0; i < locations.length; i++ ) { + location = locations[i].trim(); + c = location.charAt(0); + if ( location === '' || c === '!' || c === '#' ) { + continue; + } + // Coarse validation + if ( /[^0-9A-Za-z!*'();:@&=+$,\/?%#\[\]_.~-]/.test(location) ) { + continue; + } + availableHostsFiles[location] = { + title: '', + external: true + }; + } + + // get built-in block lists. + this.assets.get('assets/umatrix/hosts-files.json', onBuiltinHostsFilesLoaded); }; /******************************************************************************/ -µMatrix.mergeUbiquitousBlacklist = function(details) { - // console.log('storage.js > mergeUbiquitousBlacklist from "%s": "%s..."', details.path, details.content.slice(0, 40)); +µMatrix.loadHostsFiles = function(callback) { + var µm = µMatrix; + var hostsFileLoadCount; + + if ( typeof callback !== 'function' ) { + callback = this.noopFunc; + } + + var loadHostsFilesEnd = function() { + µm.ubiquitousBlacklist.freeze(); + chrome.storage.local.set({ 'liveHostsFiles': µm.liveHostsFiles }); + µm.messaging.announce({ what: 'loadHostsFilesCompleted' }); + callback(); + }; + + var mergeHostsFile = function(details) { + µm.mergeHostsFile(details); + hostsFileLoadCount -= 1; + if ( hostsFileLoadCount === 0 ) { + loadHostsFilesEnd(); + } + }; + + var loadHostsFilesStart = function(hostsFiles) { + µm.liveHostsFiles = hostsFiles; + µm.ubiquitousBlacklist.reset(); + var locations = Object.keys(hostsFiles); + hostsFileLoadCount = locations.length; + if ( hostsFileLoadCount === 0 ) { + loadHostsFilesEnd(); + return; + } + + // Load all hosts file which are not disabled. + var location; + while ( location = locations.pop() ) { + if ( hostsFiles[location].off ) { + hostsFileLoadCount -= 1; + continue; + } + µm.assets.get(location, mergeHostsFile); + } + }; + + this.getAvailableHostsFiles(loadHostsFilesStart); +}; + +/******************************************************************************/ + +µMatrix.mergeHostsFile = function(details) { + // console.log('storage.js > mergeHostsFile from "%s": "%s..."', details.path, details.content.slice(0, 40)); + + var usedCount = this.ubiquitousBlacklist.count; + var duplicateCount = this.ubiquitousBlacklist.duplicateCount; + + this.mergeHostsFileContent(details.content); + + usedCount = this.ubiquitousBlacklist.count - usedCount; + duplicateCount = this.ubiquitousBlacklist.duplicateCount - duplicateCount; + + var hostsFilesMeta = this.liveHostsFiles[details.path]; + hostsFilesMeta.entryCount = usedCount + duplicateCount; + hostsFilesMeta.entryUsedCount = usedCount; +}; + +/******************************************************************************/ + +µMatrix.mergeHostsFileContent = function(rawText) { + // console.log('storage.js > mergeHostsFileContent from "%s": "%s..."', details.path, details.content.slice(0, 40)); - var rawText = details.content; var rawEnd = rawText.length; - - // rhill 2013-10-21: No need to prefix with '* ', the hostname is just what - // we need for preset blacklists. The prefix '* ' is ONLY needed when - // used as a filter in temporary blacklist. - var ubiquitousBlacklist = this.ubiquitousBlacklist; - var thisListCount = 0; - var thisListUsedCount = 0; var reLocalhost = /(^|\s)(localhost\.localdomain|localhost|local|broadcasthost|0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)(?=\s|$)/g; var reAsciiSegment = /^[\x21-\x7e]+$/; var matches; var lineBeg = 0, lineEnd; - var line, c; + var line; while ( lineBeg < rawEnd ) { lineEnd = rawText.indexOf('\n', lineBeg); @@ -235,20 +282,10 @@ line = rawText.slice(lineBeg, lineEnd).trim(); lineBeg = lineEnd + 1; - // Strip comments - c = line.charAt(0); - if ( c === '!' || c === '[' ) { - continue; - } - - if ( c === '#' ) { - continue; - } - // https://github.com/gorhill/httpswitchboard/issues/15 // Ensure localhost et al. don't end up in the ubiquitous blacklist. line = line - .replace(/\s+#.*$/, '') + .replace(/#.*$/, '') .toLowerCase() .replace(reLocalhost, '') .trim(); @@ -273,16 +310,8 @@ continue; } - thisListCount++; - if ( ubiquitousBlacklist.add(line) ) { - thisListUsedCount++; - } + ubiquitousBlacklist.add(line); } - - // For convenience, store the number of entries for this - // blacklist, user might be happy to know this information. - this.remoteBlacklists[details.path].entryCount = thisListCount; - this.remoteBlacklists[details.path].entryUsedCount = thisListUsedCount; }; /******************************************************************************/ @@ -290,26 +319,23 @@ // `switches` contains the preset blacklists for which the switch must be // revisited. -µMatrix.reloadPresetBlacklists = function(switches) { - var presetBlacklists = this.remoteBlacklists; +µMatrix.reloadHostsFiles = function(switches) { + var liveHostsFiles = this.liveHostsFiles; // Toggle switches var i = switches.length; while ( i-- ) { - if ( !presetBlacklists[switches[i].location] ) { + if ( !liveHostsFiles[switches[i].location] ) { continue; } - presetBlacklists[switches[i].location].off = !!switches[i].off; + liveHostsFiles[switches[i].location].off = !!switches[i].off; } // Save switch states chrome.storage.local.set( - { 'remoteBlacklists': presetBlacklists }, - this.getBytesInUse.bind(this) + { 'liveHostsFiles': liveHostsFiles }, + this.loadHostsFiles.bind(this) ); - - // Now force reload - this.loadUbiquitousBlacklists(); }; /******************************************************************************/ @@ -333,7 +359,7 @@ // Load updatable assets µMatrix.loadUpdatableAssets = function() { - this.loadUbiquitousBlacklists(); + this.loadHostsFiles(); this.loadPublicSuffixList(); }; @@ -342,12 +368,9 @@ // Load all µMatrix.load = function() { - // user this.loadUserSettings(); this.loadMatrix(); - - // load updatable assets -- after updating them if needed - this.assetUpdater.update(null, this.loadUpdatableAssets.bind(this)); + this.loadUpdatableAssets(); this.getBytesInUse(); }; diff --git a/src/js/traffic.js b/src/js/traffic.js index c886431..15b1ea5 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -883,7 +883,7 @@ chrome.webRequest.onBeforeRequest.addListener( [ "blocking" ] ); -console.log('HTTP Switchboard> Beginning to intercept net requests at %s', (new Date()).toISOString()); +console.log('µMatrix > Beginning to intercept net requests at %s', (new Date()).toISOString()); chrome.webRequest.onBeforeSendHeaders.addListener( onBeforeSendHeadersHandler, diff --git a/src/js/udom.js b/src/js/udom.js index 38b95e4..2317a0e 100644 --- a/src/js/udom.js +++ b/src/js/udom.js @@ -166,6 +166,18 @@ var isDescendantOf = function(descendant, ancestor) { /******************************************************************************/ +var nodeInNodeList = function(node, nodeList) { + var i = nodeList.length; + while ( i-- ) { + if ( nodeList[i] === node ) { + return true; + } + } + return false; +}; + +/******************************************************************************/ + var doesMatchSelector = function(node, selector) { if ( !node ) { return false; @@ -599,8 +611,13 @@ DOMList.prototype.toggleClass = function(className, targetState) { var makeEventHandler = function(selector, callback) { return function(event) { - if ( doesMatchSelector(event.target, selector) ) { - callback.call(event.target, event); + var dispatcher = event.currentTarget; + if ( !dispatcher || typeof dispatcher.querySelectorAll !== 'function' ) { + return; + } + var receiver = event.target; + if ( nodeInNodeList(receiver, dispatcher.querySelectorAll(selector)) ) { + callback.call(receiver, event); } }; }; @@ -609,14 +626,13 @@ DOMList.prototype.on = function(etype, selector, callback) { if ( typeof selector === 'function' ) { callback = selector; selector = undefined; + } else { + callback = makeEventHandler(selector, callback); } + var i = this.nodes.length; while ( i-- ) { - if ( selector !== undefined ) { - this.nodes[i].addEventListener(etype, makeEventHandler(selector, callback), true); - } else { - this.nodes[i].addEventListener(etype, callback); - } + this.nodes[i].addEventListener(etype, callback, selector !== undefined); } return this; }; diff --git a/tools/_locales/de/messages.json b/tools/_locales/de/messages.json index 92ad0b2..8535986 100644 --- a/tools/_locales/de/messages.json +++ b/tools/_locales/de/messages.json @@ -409,14 +409,6 @@ }, - "ubiquitousWhatIsThisHeader" : { - "message": "Was ist das?", - "description": "English: What is this?" - }, - "ubiquitousWhatIsThisPrompt" : { - "message": "“Omnipräsente Regeln” sind Regeln, die überall gelten, d.h. in allen Geltungsbereichen.", - "description": "English: “Ubiquitous rules” are rules which applies everywhere, i.e. in all scopes." - }, "ubiquitousListsOfBlockedHostsPrompt1" : { "message": "Alle Listen blockierter Hostnamen werden als omnipräsente Regeln geladen, womit diese Hostnamen in allen Geltungsbereichen auf der Blacklist stehen.", "description": "English: All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes." diff --git a/tools/_locales/en/messages.json b/tools/_locales/en/messages.json index 7459ff1..56cdb0f 100644 --- a/tools/_locales/en/messages.json +++ b/tools/_locales/en/messages.json @@ -24,7 +24,7 @@ "description": "appears as tab name in dashboard." }, "ubiquitousRulesPageName" : { - "message": "Ubiquitous rules", + "message": "Hosts files", "description": "appears as tab name in dashboard." }, "aboutPageName": { @@ -409,57 +409,53 @@ }, - "ubiquitousWhatIsThisHeader" : { - "message": "What is this?", - "description": "English: What is this?" + "hostsFilesPrompt" : { + "message": "All hostnames in a hosts file are loaded as blacklisted hostnames in the global scope.", + "description": "English: All hostnames in a hosts file are loaded as blacklisted hostnames in the global scope." }, - "ubiquitousWhatIsThisPrompt" : { - "message": "“Ubiquitous rules” are rules which apply everywhere, i.e. in all scopes. A ubiquitous rule can be overridden by any scoped rule for the same element.", - "description": "English: “Ubiquitous rules” are rules which apply everywhere, i.e. in all scopes. A ubiquitous rule can be overridden by any scoped rule for the same element." + "hostsFilesStats" : { + "message": "{{blockedHostnameCount}} distinct blocked hostnames from:", + "description": "English: {{blockedHostnameCount}} distinct blocked hostnames from:" }, - "ubiquitousListsOfBlockedHostsPrompt1" : { - "message": "All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes.", - "description": "English: All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes." - }, - "ubiquitousListsOfBlockedHostsPrompt2" : { - "message": "{{ubiquitousBlacklistCount}} distinct blocked hostnames from:", - "description": "English: {{ubiquitousBlacklistCount}} distinct blocked hostnames from:" - }, - "ubiquitousListsOfBlockedHostsPerListStats" : { + "hostsFilesPerFileStats" : { "message": "{{used}} used out of {{total}}", "description": "English: {{used}} used out of {{total}}" }, - "ubiquitousListsOfBlockedHostsHeader" : { - "message": "Lists of blocked hosts", - "description": "English: Lists of blocked hosts" - }, - "userUbiquitousBlacklistHeader" : { - "message": "Your block rules", - "description": "English: Your block rules" - }, - "userUbiquitousWhitelistHeader" : { - "message": "Your allow rules", - "description": "English: Your allow rules" - }, - "ubiquitousApplyChanges" : { + "hostsFilesApplyChanges" : { "message": "Apply changes", "description": "English: Apply changes" }, - "ubiquitousFormatHint" : { - "message": "One rule per line. A rule can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘#’ will be ignored.", - "description": "English: One rule per line. A rule can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘#’ will be ignored." + "hostsFilesAutoUpdatePrompt":{ + "message":"Auto-update filter lists.", + "description":"English: Auto-update filter lists." }, - "ubiquitousAllowFormatHint" : { - "message": "One rule per line. A rule can be a plain hostname, or an Adblock Plus-compatible exception filter (prefixed with ‘@@’). Lines prefixed with ‘#’ will be ignored.", - "description": "English: One rule per line. A rule can be a plain hostname, or an Adblock Plus-compatible exception filter (prefixed with ‘@@’). Lines prefixed with ‘#’ will be ignored." + "hostsFilesUpdateNow":{ + "message":"Update now", + "description":"English: Update now" }, - "ubiquitousImport" : { - "message": "Import and append", - "description": "English: Import and append" + "hostsFilesPurgeAll":{ + "message":"Purge all caches", + "description":"English: Purge all caches" }, - "ubiquitousExport" : { - "message": "Export", - "description": "English: Export" + "hostsFilesExternalListsHint":{ + "message":"One URL per line. Lines prefixed with ‘!’ will be ignored. Invalid URLs will be silently ignored.", + "description":"English: One URL per line. Lines prefixed with ‘!’ will be ignored. Invalid URLs will be silently ignored." + }, + "hostsFilesExternalListsParse":{ + "message":"Parse", + "description":"English: Parse" + }, + "hostsFilesExternalListPurge":{ + "message":"purge cache", + "description":"English: purge cache" + }, + "hostsFilesExternalListNew":{ + "message":"new version available", + "description":"English: new version available" + }, + "hostsFilesExternalListObsolete":{ + "message":"outdated", + "description":"English: outdated" }, diff --git a/tools/_locales/fr/messages.json b/tools/_locales/fr/messages.json index 3b464d7..6c6b68b 100644 --- a/tools/_locales/fr/messages.json +++ b/tools/_locales/fr/messages.json @@ -409,14 +409,6 @@ }, - "ubiquitousWhatIsThisHeader" : { - "message": "De quoi s'agit-il ?", - "description": "English: What is this?" - }, - "ubiquitousWhatIsThisPrompt" : { - "message": "Les “règles à portée universelle” sont des règles qui s'appliquent dans TOUS les contextes.", - "description": "English: “Ubiquitous rules” are rules which applies everywhere, i.e. in all scopes." - }, "ubiquitousListsOfBlockedHostsPrompt1" : { "message": "Tous les hôtes des listes prédéfinies sont traités comme étant des règles universelles. Elles sont intégrées au sein de l'extension et peuvent être mises à jour dans l'onglet À propos.", "description": "English: All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes." diff --git a/tools/_locales/ru/messages.json b/tools/_locales/ru/messages.json index 68c672d..c0cb4bd 100644 --- a/tools/_locales/ru/messages.json +++ b/tools/_locales/ru/messages.json @@ -409,14 +409,6 @@ }, - "ubiquitousWhatIsThisHeader" : { - "message": "Что это?", - "description": "English: What is this?" - }, - "ubiquitousWhatIsThisPrompt" : { - "message": "“Глобальные правила” правила, применяемые везде, во всех областях.", - "description": "English: “Ubiquitous rules” are rules which applies everywhere, i.e. in all scopes." - }, "ubiquitousListsOfBlockedHostsPrompt1" : { "message": "Все списки заблокированных хостов загружаются, как глобальные правила, в связи с чем их владельцы находятся в черном списке для всех областей.", "description": "English: All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes." diff --git a/tools/_locales/zh_CN/messages.json b/tools/_locales/zh_CN/messages.json index 65bb7af..082ae0d 100644 --- a/tools/_locales/zh_CN/messages.json +++ b/tools/_locales/zh_CN/messages.json @@ -409,14 +409,6 @@ }, - "ubiquitousWhatIsThisHeader" : { - "message": "这是什么?", - "description": "English: What is this?" - }, - "ubiquitousWhatIsThisPrompt" : { - "message": "“普适规则”是应用到所有地方,即所有作用域,的规则。一条普适规则可以被任何针对同一元素的作用域规则所覆盖。", - "description": "English: “Ubiquitous rules” are rules which apply everywhere, i.e. in all scopes. A ubiquitous rule can be overridden by any scoped rule for the same element." - }, "ubiquitousListsOfBlockedHostsPrompt1" : { "message": "所有屏蔽站点的列表都作为普适规则被加载,所以这些站点将在所有作用域的黑名单中。", "description": "English: All lists of blocked hosts are loaded as ubiquitous rules, hence these hosts are blacklisted in all scopes."