+
-
+
+
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 updateLocalChecksums = function() {
+ var localChecksums = [];
+ var entries = repoMetadata.entries;
+ var entry;
+ for ( var path in entries ) {
+ if ( entries.hasOwnProperty(path) === false ) {
+ continue;
+ }
+ entry = entries[path];
+ if ( entry.localChecksum !== '' ) {
+ localChecksums.push(entry.localChecksum + ' ' + path);
+ }
+ }
+ cachedAssetsManager.save('assets/checksums.txt', localChecksums.join('\n'));
+};
-var cacheSynchronized = false;
+/******************************************************************************/
-var synchronizeCache = function() {
- if ( cacheSynchronized ) {
+// 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;
}
- cacheSynchronized = true;
- var onLastVersionRead = function(store) {
- var currentVersion = chrome.runtime.getManifest().version;
- var lastVersion = store.extensionLastVersion || '0.0.0.0';
- if ( currentVersion === lastVersion ) {
+ lastRepoMetaTimestamp = Date.now();
+
+ var localChecksums;
+ var repoChecksums;
+
+ var checksumsReceived = function() {
+ if ( localChecksums === undefined || repoChecksums === undefined ) {
return;
}
- chrome.storage.local.set({ 'extensionLastVersion': currentVersion });
- cachedAssetsManager.remove(/assets\/(umatrix|thirdparties)\//);
+ // 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);
+ }
};
- chrome.storage.local.get('extensionLastVersion', onLastVersionRead);
+ 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()');
- reportBack(this.responseText);
+ var onInstallFileLoaded = function() {
this.onload = this.onerror = null;
+ //console.log('assets.js > readLocalFile("%s") / onInstallFileLoaded()', path);
+ reportBack(this.responseText);
};
- var onLocalFileError = function(ev) {
- console.error('assets.js > readLocalFile() / onLocalFileError("%s")', path);
- reportBack('', 'Error');
+ var onInstallFileError = function() {
this.onload = this.onerror = null;
+ console.error('assets.js > readLocalFile("%s") / onInstallFileError()', path);
+ reportBack('', 'Error');
};
- 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 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 onInstallFileLoaded = function() {
+ this.onload = this.onerror = null;
+ //console.log('assets.js > readRepoOnlyAsset("%s") / onInstallFileLoaded()', path);
+ reportBack(this.responseText);
+ };
- var reportBackError = function() {
- callback({
- 'path': targetPath,
- 'error': 'Error'
- });
+ var onInstallFileError = function() {
+ this.onload = this.onerror = null;
+ console.error('assets.js > readRepoOnlyAsset("%s") / onInstallFileError()', path);
+ reportBack('', 'Error');
};
- var onRemoteFileLoaded = function() {
+ 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;
- }
- var location;
- while ( location = obsoleteBlacklists.pop() ) {
- delete store.remoteBlacklists[location];
+ // 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;
+ }
}
- 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();
+ // permanent hosts files
+ var location;
+ var lists = this.permanentHostsFiles;
+ for ( location in lists ) {
+ if ( lists.hasOwnProperty(location) === false ) {
+ continue;
}
- };
+ availableHostsFiles[location] = lists[location];
+ }
+
+ // 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.loadHostsFiles = function(callback) {
+ var µm = µMatrix;
+ var hostsFileLoadCount;
+
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
- var loadBlacklistsEnd = function() {
+ var loadHostsFilesEnd = function() {
µm.ubiquitousBlacklist.freeze();
- removeObsoleteBlacklists();
- µm.messaging.announce({ what: 'loadUbiquitousBlacklistCompleted' });
+ chrome.storage.local.set({ 'liveHostsFiles': µm.liveHostsFiles });
+ µm.messaging.announce({ what: 'loadHostsFilesCompleted' });
+ callback();
};
- 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);
+ var mergeHostsFile = function(details) {
+ µm.mergeHostsFile(details);
+ hostsFileLoadCount -= 1;
+ if ( hostsFileLoadCount === 0 ) {
+ loadHostsFilesEnd();
+ }
+ };
- blacklistLoadCount = blacklistLocations.length;
- if ( blacklistLoadCount === 0 ) {
- loadBlacklistsEnd();
+ var loadHostsFilesStart = function(hostsFiles) {
+ µm.liveHostsFiles = hostsFiles;
+ µm.ubiquitousBlacklist.reset();
+ var locations = Object.keys(hostsFiles);
+ hostsFileLoadCount = locations.length;
+ if ( hostsFileLoadCount === 0 ) {
+ loadHostsFilesEnd();
return;
}
- // Load each preset blacklist which is not disabled.
+ // Load all hosts file which are 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;
+ while ( location = locations.pop() ) {
+ if ( hostsFiles[location].off ) {
+ hostsFileLoadCount -= 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);
+ µm.assets.get(location, mergeHostsFile);
}
};
- 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
- );
- };
+ this.getAvailableHostsFiles(loadHostsFilesStart);
+};
- // Reset list of 3rd-party block lists.
- for ( var location in this.remoteBlacklists ) {
- if ( location.indexOf('assets/thirdparties/') === 0 ) {
- delete this.remoteBlacklists[location];
- }
- }
+/******************************************************************************/
- // Get new list of 3rd-party block lists.
- this.assets.get('assets/umatrix/ubiquitous-block-lists.json', onListOfBlockListsLoaded);
+µ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.mergeUbiquitousBlacklist = function(details) {
- // console.log('storage.js > mergeUbiquitousBlacklist from "%s": "%s..."', details.path, details.content.slice(0, 40));
+µ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"
+ "hostsFilesApplyChanges" : {
+ "message": "Apply changes",
+ "description": "English: Apply changes"
},
- "userUbiquitousBlacklistHeader" : {
- "message": "Your block rules",
- "description": "English: Your block rules"
+ "hostsFilesAutoUpdatePrompt":{
+ "message":"Auto-update filter lists.",
+ "description":"English: Auto-update filter lists."
},
- "userUbiquitousWhitelistHeader" : {
- "message": "Your allow rules",
- "description": "English: Your allow rules"
+ "hostsFilesUpdateNow":{
+ "message":"Update now",
+ "description":"English: Update now"
},
- "ubiquitousApplyChanges" : {
- "message": "Apply changes",
- "description": "English: Apply changes"
+ "hostsFilesPurgeAll":{
+ "message":"Purge all caches",
+ "description":"English: Purge all caches"
+ },
+ "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."
},
- "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."
+ "hostsFilesExternalListsParse":{
+ "message":"Parse",
+ "description":"English: Parse"
},
- "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."
+ "hostsFilesExternalListPurge":{
+ "message":"purge cache",
+ "description":"English: purge cache"
},
- "ubiquitousImport" : {
- "message": "Import and append",
- "description": "English: Import and append"
+ "hostsFilesExternalListNew":{
+ "message":"new version available",
+ "description":"English: new version available"
},
- "ubiquitousExport" : {
- "message": "Export",
- "description": "English: Export"
+ "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."
|