diff --git a/src/js/assets.js b/src/js/assets.js
index 0458813..2808488 100644
--- a/src/js/assets.js
+++ b/src/js/assets.js
@@ -1,7 +1,7 @@
/*******************************************************************************
- µMatrix - a browser extension to black/white list requests.
- Copyright (C) 2013-2015 Raymond Hill
+ uMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2013-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -237,6 +237,11 @@ var registerAssetSource = function(assetKey, dict) {
entry[prop] = dict[prop];
}
}
+ // `content` property => `type` property
+ if ( entry.type === undefined && entry.content !== undefined ) {
+ entry.type = entry.content;
+ entry.content = undefined;
+ }
var contentURL = dict.contentURL;
if ( contentURL !== undefined ) {
if ( typeof contentURL === 'string' ) {
@@ -337,6 +342,15 @@ var getAssetSourceRegistry = function(callback) {
assetSourceRegistryStatus = [ callback ];
var registryReady = function() {
+ // `content` property => `type` property
+ for ( let key in assetSourceRegistry ) {
+ let entry = assetSourceRegistry[key];
+ if ( entry.type === undefined && entry.content !== undefined ) {
+ entry.type = entry.content;
+ entry.content = undefined;
+ }
+ }
+
var callers = assetSourceRegistryStatus;
assetSourceRegistryStatus = 'ready';
var fn;
@@ -822,7 +836,15 @@ var updateNext = function() {
if ( cacheEntry && (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now ) {
continue;
}
- if ( fireNotification('before-asset-updated', { assetKey: assetKey }) ) {
+ if (
+ fireNotification(
+ 'before-asset-updated',
+ {
+ assetKey: assetKey,
+ type: assetEntry.type
+ }
+ )
+ ) {
return assetKey;
}
garbageCollectOne(assetKey);
diff --git a/src/js/background.js b/src/js/background.js
index eef4523..d5d282e 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -177,14 +177,17 @@ return {
deleteUnusedSessionCookiesAfter: 60,
deleteLocalStorage: false,
displayTextSize: '14px',
- externalHostsFiles: '',
+ externalHostsFiles: [],
+ externalRecipeFiles: [],
iconBadgeEnabled: false,
maxLoggedRequests: 1000,
popupCollapseAllDomains: false,
popupCollapseBlacklistedDomains: false,
popupScopeLevel: 'domain',
processHyperlinkAuditing: true,
- processReferer: false
+ processReferer: false,
+ selectedHostsFiles: [ '' ],
+ selectedRecipeFiles: [ '' ]
},
rawSettingsDefault: rawSettingsDefault,
@@ -202,8 +205,7 @@ return {
pslAssetKey: 'public_suffix_list.dat',
// list of live hosts files
- liveHostsFiles: {
- },
+ liveHostsFiles: new Map(),
// urls stats are kept on the back burner while waiting to be reactivated
// in a tab or another.
diff --git a/src/js/hosts-files.js b/src/js/hosts-files.js
index 42e3fea..e0f4d34 100644
--- a/src/js/hosts-files.js
+++ b/src/js/hosts-files.js
@@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a Chromium browser extension to black/white list requests.
- Copyright (C) 2014-2017 Raymond Hill
+ Copyright (C) 2014-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -32,7 +32,7 @@
var listDetails = {},
lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate'),
hostsFilesSettingsHash,
- reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/;
+ reValidExternalList = /^[a-z-]+:\/\/\S*\/\S+$/m;
/******************************************************************************/
@@ -67,15 +67,15 @@ var renderHostsFiles = function(soft) {
reExternalHostFile = /^https?:/;
// Assemble a pretty list name if possible
- var listNameFromListKey = function(listKey) {
- var list = listDetails.current[listKey] || listDetails.available[listKey];
- var listTitle = list ? list.title : '';
+ var listNameFromListKey = function(collection, listKey) {
+ let list = collection.get(listKey);
+ let listTitle = list ? list.title : '';
if ( listTitle === '' ) { return listKey; }
return listTitle;
};
- var liFromListEntry = function(listKey, li) {
- var entry = listDetails.available[listKey],
+ var liFromListEntry = function(collection, listKey, li) {
+ var entry = collection.get(listKey),
elem;
if ( !li ) {
li = listEntryTemplate.clone().nodeAt(0);
@@ -83,43 +83,31 @@ var renderHostsFiles = function(soft) {
if ( li.getAttribute('data-listkey') !== listKey ) {
li.setAttribute('data-listkey', listKey);
elem = li.querySelector('input[type="checkbox"]');
- elem.checked = entry.off !== true;
+ elem.checked = entry.selected === true;
elem = li.querySelector('a:nth-of-type(1)');
elem.setAttribute('href', 'asset-viewer.html?url=' + encodeURI(listKey));
elem.setAttribute('type', 'text/html');
- elem.textContent = listNameFromListKey(listKey);
+ elem.textContent = listNameFromListKey(collection, listKey);
li.classList.remove('toRemove');
- if ( entry.supportName ) {
- li.classList.add('support');
- elem = li.querySelector('a.support');
- elem.setAttribute('href', entry.supportURL);
- elem.setAttribute('title', entry.supportName);
- } else {
- li.classList.remove('support');
- }
- if ( entry.external ) {
- li.classList.add('external');
- } else {
- li.classList.remove('external');
- }
- if ( entry.instructionURL ) {
- li.classList.add('mustread');
- elem = li.querySelector('a.mustread');
- elem.setAttribute('href', entry.instructionURL);
- } else {
- li.classList.remove('mustread');
+ elem = li.querySelector('a.support');
+ if ( entry.supportURL ) {
+ elem.setAttribute(
+ 'href',
+ entry.supportURL ? entry.supportURL : ''
+ );
}
+ li.classList.toggle('external', entry.external === true);
}
// https://github.com/gorhill/uBlock/issues/1429
if ( !soft ) {
elem = li.querySelector('input[type="checkbox"]');
- elem.checked = entry.off !== true;
+ elem.checked = entry.selected === true;
}
elem = li.querySelector('span.counts');
var text = '';
if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) {
text = listStatsTemplate
- .replace('{{used}}', renderNumber(entry.off ? 0 : entry.entryUsedCount))
+ .replace('{{used}}', renderNumber(entry.selected ? entry.entryUsedCount : 0))
.replace('{{total}}', renderNumber(entry.entryCount));
}
elem.textContent = text;
@@ -142,38 +130,56 @@ var renderHostsFiles = function(soft) {
li.classList.remove('discard');
return li;
};
-
- var onListsReceived = function(details) {
- // Before all, set context vars
- listDetails = details;
-
- // Incremental rendering: this will allow us to easily discard unused
- // DOM list entries.
- uDom('#lists .listEntry').addClass('discard');
-
- var availableLists = details.available,
- listKeys = Object.keys(details.available);
+ var onRenderAssetFiles = function(collection, listSelector) {
+ var assetKeys = Array.from(collection.keys());
// Sort works this way:
// - Send /^https?:/ items at the end (custom hosts file URL)
- listKeys.sort(function(a, b) {
- var ta = availableLists[a].title || a,
- tb = availableLists[b].title || b;
+ assetKeys.sort(function(a, b) {
+ var ta = collection.get(a).title || a,
+ tb = collection.get(b).title || b;
if ( reExternalHostFile.test(ta) === reExternalHostFile.test(tb) ) {
return ta.localeCompare(tb);
}
return reExternalHostFile.test(tb) ? -1 : 1;
});
- var ulList = document.querySelector('#lists');
- for ( var i = 0; i < listKeys.length; i++ ) {
- var liEntry = liFromListEntry(listKeys[i], ulList.children[i]);
+ let ulList = document.querySelector(listSelector),
+ liImport = ulList.querySelector('.importURL');
+ if ( liImport.parentNode !== null ) {
+ liImport.parentNode.removeChild(liImport);
+ }
+ for ( let i = 0; i < assetKeys.length; i++ ) {
+ let liEntry = liFromListEntry(
+ collection,
+ assetKeys[i],
+ ulList.children[i]
+ );
if ( liEntry.parentElement === null ) {
ulList.appendChild(liEntry);
}
}
- uDom('#lists .listEntry.discard').remove();
+ ulList.appendChild(liImport);
+ };
+
+ var onAssetDataReceived = function(details) {
+ // Preprocess.
+ details.hosts = new Map(details.hosts);
+ details.recipes = new Map(details.recipes);
+
+ // Before all, set context vars
+ listDetails = details;
+
+ // Incremental rendering: this will allow us to easily discard unused
+ // DOM list entries.
+ uDom('#hosts .listEntry:not(.importURL)').addClass('discard');
+
+ onRenderAssetFiles(details.hosts, '#hosts');
+ onRenderAssetFiles(details.recipes, '#recipes');
+
+ uDom('.listEntry.discard').remove();
+
uDom('#listsOfBlockedHostsPrompt').text(
vAPI.i18n('hostsFilesStats').replace(
'{{blockedHostnameCount}}',
@@ -188,21 +194,25 @@ var renderHostsFiles = function(soft) {
renderWidgets();
};
- vAPI.messaging.send('hosts-files.js', { what: 'getLists' }, onListsReceived);
+ vAPI.messaging.send(
+ 'hosts-files.js',
+ { what: 'getAssets' },
+ onAssetDataReceived
+ );
};
/******************************************************************************/
var renderWidgets = function() {
- uDom('#buttonUpdate').toggleClass('disabled', document.querySelector('body:not(.updating) #lists .listEntry.obsolete > input[type="checkbox"]:checked') === null);
- uDom('#buttonPurgeAll').toggleClass('disabled', document.querySelector('#lists .listEntry.cached') === null);
+ uDom('#buttonUpdate').toggleClass('disabled', document.querySelector('body:not(.updating) .assets .listEntry.obsolete > input[type="checkbox"]:checked') === null);
+ uDom('#buttonPurgeAll').toggleClass('disabled', document.querySelector('.assets .listEntry.cached') === null);
uDom('#buttonApply').toggleClass('disabled', hostsFilesSettingsHash === hashFromCurrentFromSettings());
};
/******************************************************************************/
var updateAssetStatus = function(details) {
- var li = document.querySelector('#lists .listEntry[data-listkey="' + details.key + '"]');
+ var li = document.querySelector('.assets .listEntry[data-listkey="' + details.key + '"]');
if ( li === null ) { return; }
li.classList.toggle('failed', !!details.failed);
li.classList.toggle('obsolete', !details.cached);
@@ -229,19 +239,21 @@ var updateAssetStatus = function(details) {
var hashFromCurrentFromSettings = function() {
var hash = [],
listHash = [],
- listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'),
- liEntry,
+ listEntries = document.querySelectorAll('.assets .listEntry[data-listkey]:not(.toRemove)'),
i = listEntries.length;
while ( i-- ) {
- liEntry = listEntries[i];
+ let liEntry = listEntries[i];
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
listHash.push(liEntry.getAttribute('data-listkey'));
}
}
+ let textarea1 = document.querySelector('.assets .importURL > input[type="checkbox"]:checked ~ textarea');
+ let textarea2 = document.querySelector('#recipes .importURL > input[type="checkbox"]:checked ~ textarea');
hash.push(
listHash.sort().join(),
- reValidExternalList.test(document.getElementById('externalHostsFiles').value),
- document.querySelector('#lists .listEntry.toRemove') !== null
+ textarea1 !== null && reValidExternalList.test(textarea1.value),
+ textarea2 !== null && reValidExternalList.test(textarea2.value),
+ document.querySelector('.listEntry.toRemove') !== null
);
return hash.join();
};
@@ -254,7 +266,7 @@ var onHostsFilesSettingsChanged = function() {
/******************************************************************************/
-var onRemoveExternalHostsFile = function(ev) {
+var onRemoveExternalAsset = function(ev) {
var liEntry = uDom(this).ancestors('[data-listkey]'),
listKey = liEntry.attr('data-listkey');
if ( listKey ) {
@@ -283,39 +295,50 @@ var onPurgeClicked = function() {
/******************************************************************************/
-var selectHostsFiles = function(callback) {
- // Hosts files to select
- var toSelect = [],
- liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'),
- i = liEntries.length,
- liEntry;
- while ( i-- ) {
- liEntry = liEntries[i];
- if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
- toSelect.push(liEntry.getAttribute('data-listkey'));
+var selectAssets = function(callback) {
+ var prepareChanges = function(listSelector) {
+ var out = {
+ toSelect: [],
+ toImport: '',
+ toRemove: []
+ };
+
+ let root = document.querySelector(listSelector);
+
+ let liEntries = root.querySelectorAll('.listEntry[data-listkey]:not(.toRemove)'),
+ i = liEntries.length;
+ while ( i-- ) {
+ let liEntry = liEntries[i];
+ if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
+ out.toSelect.push(liEntry.getAttribute('data-listkey'));
+ }
}
- }
- // External hosts files to remove
- var toRemove = [];
- liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]');
- i = liEntries.length;
- while ( i-- ) {
- toRemove.push(liEntries[i].getAttribute('data-listkey'));
- }
+ // External hosts files to remove
+ liEntries = root.querySelectorAll('.listEntry.toRemove[data-listkey]');
+ i = liEntries.length;
+ while ( i-- ) {
+ out.toRemove.push(liEntries[i].getAttribute('data-listkey'));
+ }
- // External hosts files to import
- var externalListsElem = document.getElementById('externalHostsFiles'),
- toImport = externalListsElem.value.trim();
- externalListsElem.value = '';
+ // External hosts files to import
+ let input = root.querySelector('.importURL > input[type="checkbox"]:checked');
+ if ( input !== null ) {
+ let textarea = root.querySelector('.importURL textarea');
+ out.toImport = textarea.value.trim();
+ textarea.value = '';
+ input.checked = false;
+ }
+
+ return out;
+ };
vAPI.messaging.send(
'hosts-files.js',
{
- what: 'selectHostsFiles',
- toSelect: toSelect,
- toImport: toImport,
- toRemove: toRemove
+ what: 'selectAssets',
+ hosts: prepareChanges('#hosts'),
+ recipes: prepareChanges('#recipes')
},
callback
);
@@ -327,8 +350,13 @@ var selectHostsFiles = function(callback) {
var buttonApplyHandler = function() {
uDom('#buttonApply').removeClass('enabled');
- selectHostsFiles(function() {
- vAPI.messaging.send('hosts-files.js', { what: 'reloadHostsFiles' });
+ selectAssets(function(response) {
+ if ( response && response.hostsChanged ) {
+ vAPI.messaging.send('hosts-files.js', { what: 'reloadHostsFiles' });
+ }
+ if ( response && response.recipesChanged ) {
+ vAPI.messaging.send('hosts-files.js', { what: 'reloadRecipeFiles' });
+ }
});
renderWidgets();
};
@@ -337,7 +365,7 @@ var buttonApplyHandler = function() {
var buttonUpdateHandler = function() {
uDom('#buttonUpdate').removeClass('enabled');
- selectHostsFiles(function() {
+ selectAssets(function() {
document.body.classList.add('updating');
vAPI.messaging.send('hosts-files.js', { what: 'forceUpdateAssets' });
renderWidgets();
@@ -377,10 +405,10 @@ uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
uDom('#buttonApply').on('click', buttonApplyHandler);
uDom('#buttonUpdate').on('click', buttonUpdateHandler);
uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
-uDom('#lists').on('change', '.listEntry > input', onHostsFilesSettingsChanged);
-uDom('#lists').on('click', '.listEntry > a.remove', onRemoveExternalHostsFile);
-uDom('#lists').on('click', 'span.cache', onPurgeClicked);
-uDom('#externalHostsFiles').on('input', onHostsFilesSettingsChanged);
+uDom('.assets').on('change', '.listEntry > input', onHostsFilesSettingsChanged);
+uDom('.assets').on('input', '.listEntry.importURL textarea', onHostsFilesSettingsChanged);
+uDom('.assets').on('click', '.listEntry > a.remove', onRemoveExternalAsset);
+uDom('.assets').on('click', 'span.cache', onPurgeClicked);
renderHostsFiles();
diff --git a/src/js/messaging.js b/src/js/messaging.js
index 86ba338..df72435 100644
--- a/src/js/messaging.js
+++ b/src/js/messaging.js
@@ -41,8 +41,8 @@ function onMessage(request, sender, callback) {
µm.assets.get(request.url, { dontCache: true }, callback);
return;
- case 'selectHostsFiles':
- µm.selectHostsFiles(request, callback);
+ case 'selectAssets':
+ µm.selectAssets(request, callback);
return;
default:
@@ -319,6 +319,14 @@ var matrixSnapshotFromTabId = function(details, callback) {
var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
+ case 'fetchRecipes':
+ µm.recipeManager.fetch(
+ request.srcHostname,
+ request.desHostnames,
+ callback
+ );
+ return;
+
case 'matrixSnapshot':
matrixSnapshotFromTabId(request, callback);
return;
@@ -331,6 +339,14 @@ var onMessage = function(request, sender, callback) {
var response;
switch ( request.what ) {
+ case 'applyRecipe':
+ µm.recipeManager.apply(request);
+ break;
+
+ case 'fetchRecipeCommitStatuses':
+ response = µm.recipeManager.commitStatuses(request);
+ break;
+
case 'toggleMatrixSwitch':
µm.tMatrix.setSwitchZ(
request.switchName,
@@ -713,41 +729,26 @@ var µm = µMatrix;
/******************************************************************************/
-var prepEntries = function(entries) {
- var µmuri = µm.URI;
- var entry;
- for ( var k in entries ) {
- if ( entries.hasOwnProperty(k) === false ) {
- continue;
- }
- entry = entries[k];
- if ( typeof entry.homeURL === 'string' ) {
- entry.homeHostname = µmuri.hostnameFromURI(entry.homeURL);
- entry.homeDomain = µmuri.domainFromHostname(entry.homeHostname);
- }
- }
-};
-
-/******************************************************************************/
-
-var getLists = function(callback) {
+var getAssets = function(callback) {
var r = {
autoUpdate: µm.userSettings.autoUpdate,
- available: null,
- cache: null,
- current: µm.liveHostsFiles,
- blockedHostnameCount: µm.ubiquitousBlacklist.count
+ blockedHostnameCount: µm.ubiquitousBlacklist.count,
+ hosts: null,
+ recipes: null,
+ cache: null
};
var onMetadataReady = function(entries) {
r.cache = entries;
- prepEntries(r.cache);
callback(r);
};
- var onAvailableHostsFilesReady = function(lists) {
- r.available = lists;
- prepEntries(r.available);
+ var onAvailableRecipeFilesReady = function(collection) {
+ r.recipes = Array.from(collection);
µm.assets.metadata(onMetadataReady);
};
+ var onAvailableHostsFilesReady = function(collection) {
+ r.hosts = Array.from(collection);
+ µm.getAvailableRecipeFiles(onAvailableRecipeFilesReady);
+ };
µm.getAvailableHostsFiles(onAvailableHostsFilesReady);
};
@@ -758,8 +759,8 @@ var onMessage = function(request, sender, callback) {
// Async
switch ( request.what ) {
- case 'getLists':
- return getLists(callback);
+ case 'getAssets':
+ return getAssets(callback);
default:
break;
@@ -805,7 +806,7 @@ var µm = µMatrix;
/******************************************************************************/
var restoreUserData = function(userData) {
- var countdown = 4;
+ var countdown = 0;
var onCountdown = function() {
countdown -= 1;
if ( countdown === 0 ) {
@@ -814,11 +815,18 @@ var restoreUserData = function(userData) {
};
var onAllRemoved = function() {
+ let µm = µMatrix;
+ countdown += 1;
vAPI.storage.set(userData.settings, onCountdown);
- vAPI.storage.set({ userMatrix: userData.rules }, onCountdown);
- vAPI.storage.set({ liveHostsFiles: userData.hostsFiles }, onCountdown);
+ countdown += 1;
+ let bin = { userMatrix: userData.rules };
+ if ( userData.hostsFiles instanceof Object ) {
+ bin.liveHostsFiles = userData.hostsFiles;
+ }
+ vAPI.storage.set(bin, onCountdown);
if ( userData.rawSettings instanceof Object ) {
- µMatrix.saveRawSettings(userData.rawSettings, onCountdown);
+ countdown += 1;
+ µm.saveRawSettings(userData.rawSettings, onCountdown);
}
};
@@ -856,7 +864,6 @@ var onMessage = function(request, sender, callback) {
when: Date.now(),
settings: µm.userSettings,
rules: µm.pMatrix.toString(),
- hostsFiles: µm.liveHostsFiles,
rawSettings: µm.rawSettings
};
break;
diff --git a/src/js/popup.js b/src/js/popup.js
index 8395bdb..32a71b2 100644
--- a/src/js/popup.js
+++ b/src/js/popup.js
@@ -1058,6 +1058,7 @@ var makeMenu = function() {
initScopeCell();
updateMatrixButtons();
resizePopup();
+ recipeManager.fetch();
};
/******************************************************************************/
@@ -1279,16 +1280,6 @@ function updateMatrixButtons() {
/******************************************************************************/
-function revertAll() {
- var request = {
- what: 'revertTemporaryMatrix'
- };
- vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
- dropDownMenuHide();
-}
-
-/******************************************************************************/
-
function buttonReloadHandler(ev) {
vAPI.messaging.send('popup.js', {
what: 'forceReloadTab',
@@ -1299,6 +1290,126 @@ function buttonReloadHandler(ev) {
/******************************************************************************/
+let recipeManager = (function() {
+ let recipes = [];
+ let reScopeAlias = /(^|\s+)_(\s+|$)/g;
+
+ function createEntry(name, ruleset, parent) {
+ let li = document.querySelector('#templates li.recipe')
+ .cloneNode(true);
+ li.querySelector('.name').textContent = name;
+ li.querySelector('.ruleset').textContent = ruleset;
+ if ( parent ) {
+ parent.appendChild(li);
+ }
+ return li;
+ }
+
+ function apply(ev) {
+ if ( ev.target.classList.contains('expander') ) {
+ ev.currentTarget.classList.toggle('expanded');
+ return;
+ }
+ let root = ev.currentTarget;
+ let ruleset = root.querySelector('.ruleset');
+ let commit = ev.target.classList.contains('committer');
+ vAPI.messaging.send(
+ 'popup.js',
+ {
+ what: 'applyRecipe',
+ ruleset: ruleset.textContent,
+ commit: commit
+ },
+ updateMatrixSnapshot
+ );
+ if ( commit ) {
+ root.classList.remove('mustCommit');
+ }
+ //dropDownMenuHide();
+ }
+
+ function show(details) {
+ let root = document.querySelector('#dropDownMenuRecipes .dropdown-menu');
+ let ul = document.createElement('ul');
+ for ( let recipe of details.recipes ) {
+ let li = createEntry(
+ recipe.name,
+ recipe.ruleset.replace(reScopeAlias, '$1' + details.scope + '$2'),
+ ul
+ );
+ li.classList.toggle('mustCommit', recipe.mustCommit === true);
+ li.addEventListener('click', apply);
+ }
+ root.replaceChild(ul, root.querySelector('ul'));
+ dropDownMenuShow(uDom.nodeFromId('buttonRecipes'));
+ }
+
+ function beforeShow() {
+ if ( recipes.length === 0 ) { return; }
+ vAPI.messaging.send(
+ 'popup.js',
+ {
+ what: 'fetchRecipeCommitStatuses',
+ scope: matrixSnapshot.scope,
+ recipes: recipes
+ },
+ show
+ );
+ }
+
+ function fetch() {
+ let onResponse = function(response) {
+ recipes = Array.isArray(response) ? response : [];
+ let button = uDom.nodeFromId('buttonRecipes');
+ if ( recipes.length === 0 ) {
+ button.classList.add('disabled');
+ return;
+ }
+ button.classList.remove('disabled');
+ button.querySelector('span.badge').textContent = recipes.length;
+ };
+
+ let desHostnames = [];
+ for ( let hostname in matrixSnapshot.rows ) {
+ if ( matrixSnapshot.rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ let row = matrixSnapshot.rows[hostname];
+ if ( row.domain === matrixSnapshot.domain ) { continue; }
+ if ( row.counts[0] !== 0 || row.domain === hostname ) {
+ desHostnames.push(hostname);
+ }
+ }
+
+ vAPI.messaging.send('popup.js',
+ {
+ what: 'fetchRecipes',
+ srcHostname: matrixSnapshot.hostname,
+ desHostnames: desHostnames
+ },
+ onResponse
+ );
+ }
+
+ return {
+ fetch: fetch,
+ show: beforeShow,
+ apply: apply
+ };
+})();
+
+/******************************************************************************/
+
+function revertAll() {
+ vAPI.messaging.send(
+ 'popup.js',
+ { what: 'revertTemporaryMatrix' },
+ updateMatrixSnapshot
+ );
+}
+
+/******************************************************************************/
+
function mouseenterMatrixCellHandler(ev) {
matrixCellHotspots.appendTo(ev.target);
}
@@ -1324,8 +1435,7 @@ function gotoExtensionURL(ev) {
/******************************************************************************/
-function dropDownMenuShow(ev) {
- var button = ev.target;
+function dropDownMenuShow(button) {
var menuOverlay = document.getElementById(button.getAttribute('data-dropdown-menu'));
var butnRect = button.getBoundingClientRect();
var viewRect = document.body.getBoundingClientRect();
@@ -1512,15 +1622,20 @@ uDom('body')
.on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
uDom('#specificScope').on('click', selectSpecificScope);
uDom('#globalScope').on('click', selectGlobalScope);
+uDom('#buttonMtxSwitches').on('click', function(ev) {
+ dropDownMenuShow(ev.target);
+});
uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
uDom('#buttonPersist').on('click', persistMatrix);
uDom('#buttonRevertScope').on('click', revertMatrix);
+uDom('#buttonRecipes').on('click', function() {
+ recipeManager.show();
+});
+
uDom('#buttonRevertAll').on('click', revertAll);
uDom('#buttonReload').on('click', buttonReloadHandler);
uDom('.extensionURL').on('click', gotoExtensionURL);
-
-uDom('body').on('click', '[data-dropdown-menu]', dropDownMenuShow);
uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);
uDom('#matList').on('click', '.g4Meta', function(ev) {
diff --git a/src/js/recipe-manager.js b/src/js/recipe-manager.js
new file mode 100644
index 0000000..d9fe48f
--- /dev/null
+++ b/src/js/recipe-manager.js
@@ -0,0 +1,217 @@
+/*******************************************************************************
+
+ uMatrix - a Chromium browser extension to black/white list requests.
+ Copyright (C) 2018 Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global punycode */
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.recipeManager = (function() {
+ let rawRecipes = [];
+ let recipeIdGenerator = 1;
+ let recipeBook = new Map();
+ let reValidRecipeFile = /^! uMatrix: Ruleset recipes [0-9.]+\n/;
+ let reNoUnicode = /^[\x00-\x7F]$/;
+
+ var authorFromHeader = function(header) {
+ let match = /^! +maintainer: +([^\n]+)/im.exec(header);
+ return match !== null ? match[1].trim() : '';
+ };
+
+ var conditionMatch = function(condition, srcHostname, desHostnames) {
+ let i = condition.indexOf(' ');
+ if ( i === -1 ) { return false; }
+ let hn = condition.slice(0, i).trim();
+ if ( hn !== '*' && srcHostname.endsWith(hn) === false ) {
+ return false;
+ }
+ hn = condition.slice(i + 1).trim();
+ if ( hn === '*' ) { return true; }
+ for ( let desHostname of desHostnames ) {
+ if ( desHostname.endsWith(hn) ) { return true; }
+ }
+ return false;
+ };
+
+ var toASCII = function(rule) {
+ if ( reNoUnicode.test(rule) ) { return rule; }
+ let parts = rule.split(/\s+/);
+ for ( let i = 0; i < parts.length; i++ ) {
+ parts[i] = punycode.toASCII(parts[i]);
+ }
+ return parts.join(' ');
+ };
+
+ var compareLength = function(a, b) {
+ return b.length - a.length;
+ };
+
+ var getTokens = function(s) {
+ let tokens = s.match(/[a-z0-9]+/gi);
+ if ( tokens === null ) { return []; }
+ return tokens;
+ };
+
+ var addRecipe = function(recipe) {
+ let tokens = getTokens(recipe.condition);
+ tokens.sort(compareLength);
+ let token = tokens[0];
+ let recipes = recipeBook.get(token);
+ if ( recipes === undefined ) {
+ recipeBook.set(token, recipes = []);
+ }
+ recipes.push(recipe);
+ };
+
+ var fromString = function(raw) {
+ var recipeName,
+ recipeCondition,
+ recipeRuleset;
+ let rawHeader = raw.slice(0, 1024);
+ if ( reValidRecipeFile.test(rawHeader) === false ) { return; }
+ let maintainer = authorFromHeader(rawHeader);
+ let lineIter = new µMatrix.LineIterator(raw);
+ for (;;) {
+ let line = lineIter.next().trim();
+ if ( line.length === 0 ) {
+ if (
+ recipeName !== undefined &&
+ recipeCondition !== undefined &&
+ recipeRuleset.length !== 0
+ ) {
+ addRecipe({
+ id: recipeIdGenerator++,
+ name: recipeName,
+ maintainer: maintainer,
+ condition: recipeCondition,
+ ruleset: recipeRuleset
+ });
+ }
+ recipeName = undefined;
+ }
+ if ( lineIter.eot() && recipeName === undefined ) { break; }
+ if ( line.length === 0 ) { continue; }
+ let c = line.charCodeAt(0);
+ if ( c === 0x23 /* '#' */ || c === 0x21 /* '!' */ ) { continue; }
+ if ( recipeName === undefined ) {
+ recipeName = line;
+ recipeCondition = undefined;
+ continue;
+ }
+ if ( recipeCondition === undefined ) {
+ recipeCondition = toASCII(line);
+ recipeRuleset = '';
+ continue;
+ }
+ if ( recipeRuleset.length !== 0 ) {
+ recipeRuleset += '\n';
+ }
+ recipeRuleset += toASCII(line);
+ }
+ };
+
+ var fromPendingStrings = function() {
+ if ( rawRecipes.length === 0 ) { return; }
+ for ( var raw of rawRecipes ) {
+ fromString(raw);
+ }
+ rawRecipes = [];
+ };
+
+ return {
+ apply: function(details) {
+ let µm = µMatrix;
+ let tMatrix = µm.tMatrix;
+ let pMatrix = µm.pMatrix;
+ let mustPersist = false;
+ for ( let rule of details.ruleset.split('\n') ) {
+ let parts = rule.split(/\s+/);
+ let action = tMatrix.evaluateCellZXY(parts[0], parts[1], parts[2]);
+ if ( action === 1 ) {
+ tMatrix.whitelistCell(parts[0], parts[1], parts[2]);
+ }
+ if ( details.commit !== true ) { continue; }
+ action = pMatrix.evaluateCellZXY(parts[0], parts[1], parts[2]);
+ if ( action === 1 ) {
+ pMatrix.whitelistCell(parts[0], parts[1], parts[2]);
+ mustPersist = true;
+ }
+ }
+ if ( mustPersist ) {
+ µm.saveMatrix();
+ }
+ },
+ fetch: function(srcHostname, desHostnames, callback) {
+ fromPendingStrings();
+ let out = [];
+ let fetched = new Set();
+ let tokens = getTokens(srcHostname + ' ' + desHostnames.join(' '));
+ for ( let token of tokens ) {
+ let recipes = recipeBook.get(token);
+ if ( recipes === undefined ) { continue; }
+ for ( let recipe of recipes ) {
+ if ( fetched.has(recipe.id) ) { continue; }
+ if (
+ conditionMatch(
+ recipe.condition,
+ srcHostname,
+ desHostnames
+ )
+ ) {
+ out.push(recipe);
+ fetched.add(recipe.id);
+ }
+ }
+ }
+ callback(out);
+ },
+ commitStatuses: function(details) {
+ let matrix = µMatrix.pMatrix;
+ for ( let recipe of details.recipes ) {
+ let ruleIter = new µMatrix.LineIterator(recipe.ruleset);
+ while ( ruleIter.eot() === false ) {
+ let parts = ruleIter.next().split(/\s+/);
+ if (
+ matrix.evaluateCellZXY(
+ details.scope,
+ parts[1],
+ parts[2]
+ ) === 1
+ ) {
+ recipe.mustCommit = true;
+ break;
+ }
+ }
+ }
+ return details;
+ },
+ fromString: function(raw) {
+ rawRecipes.push(raw);
+ },
+ reset: function() {
+ rawRecipes.length = 0;
+ recipeBook.clear();
+ }
+ };
+})();
+
+/******************************************************************************/
diff --git a/src/js/start.js b/src/js/start.js
index c7388b0..4d053a8 100644
--- a/src/js/start.js
+++ b/src/js/start.js
@@ -76,6 +76,7 @@ var onTabsReady = function(tabs) {
var onUserSettingsLoaded = function() {
µm.loadHostsFiles();
+ µm.loadRecipes();
};
/******************************************************************************/
diff --git a/src/js/storage.js b/src/js/storage.js
index f66d2c9..052ae6b 100644
--- a/src/js/storage.js
+++ b/src/js/storage.js
@@ -54,14 +54,94 @@
callback = this.noopFunc;
}
- var settingsLoaded = function(store) {
- // console.log('storage.js > loaded user settings');
-
- µm.userSettings = store;
+ var onAvailableRulesetFilesReady = function(availableRulesetFiles) {
+ let selectedAssetKeys = new Set();
+ for ( let entry of availableRulesetFiles ) {
+ let assetKey = entry[0];
+ let assetLang = entry[1].lang;
+ if ( assetLang === undefined ) {
+ selectedAssetKeys.add(assetKey);
+ continue;
+ }
+ for ( let lang of navigator.languages ) {
+ if ( assetLang.indexOf(lang) !== -1 ) {
+ selectedAssetKeys.add(assetKey);
+ break;
+ }
+ }
+ }
+ µm.userSettings.selectedRecipeFiles = Array.from(selectedAssetKeys);
+ vAPI.storage.set({
+ selectedRecipeFiles: µm.userSettings.selectedRecipeFiles
+ });
+ callback(µm.userSettings);
+ };
+ var initializeSelectedRulesetFiles = function() {
+ if (
+ µm.userSettings.selectedRecipeFiles.length === 1 &&
+ µm.userSettings.selectedRecipeFiles[0] === ''
+ ) {
+ µm.getAvailableRecipeFiles(onAvailableRulesetFilesReady);
+ return;
+ }
callback(µm.userSettings);
};
+ var onAvailableHostsFilesReady = function(availableHostFiles) {
+ µm.userSettings.selectedHostsFiles =
+ Array.from(availableHostFiles.keys());
+ vAPI.storage.set({
+ selectedHostsFiles: µm.userSettings.selectedHostsFiles
+ });
+ initializeSelectedRulesetFiles();
+ };
+
+ var migrateSelectedHostsFiles = function(bin) {
+ if (
+ bin instanceof Object === false ||
+ bin.liveHostsFiles instanceof Object === false
+ ) {
+ µm.getAvailableHostsFiles(onAvailableHostsFilesReady);
+ return;
+ }
+ let selectedHostsFiles = new Set();
+ for ( let entry of µm.toMap(bin.liveHostsFiles) ) {
+ if ( entry[1].off !== true ) {
+ selectedHostsFiles.add(entry[0]);
+ }
+ }
+ µm.userSettings.selectedHostsFiles = Array.from(selectedHostsFiles);
+ vAPI.storage.set({
+ selectedHostsFiles: µm.userSettings.selectedHostsFiles
+ });
+ initializeSelectedRulesetFiles();
+ };
+
+ var initializeSelectedHostsFiles = function() {
+ // Backward-compatibility: populate the new list selection array with
+ // existing data.
+ if (
+ µm.userSettings.selectedHostsFiles.length === 1 &&
+ µm.userSettings.selectedHostsFiles[0] === ''
+ ) {
+ vAPI.storage.get('liveHostsFiles', migrateSelectedHostsFiles);
+ return;
+ }
+ initializeSelectedRulesetFiles();
+ };
+
+ var settingsLoaded = function(store) {
+ µm.userSettings = store;
+ if ( typeof µm.userSettings.externalHostsFiles === 'string' ) {
+ µm.userSettings.externalHostsFiles =
+ µm.userSettings.externalHostsFiles.length !== 0 ?
+ µm.userSettings.externalHostsFiles.split('\n') :
+ [];
+ }
+ initializeSelectedHostsFiles();
+ };
+
vAPI.storage.get(this.userSettings, settingsLoaded);
};
@@ -161,13 +241,10 @@
/******************************************************************************/
-// save white/blacklist
µMatrix.saveMatrix = function() {
µMatrix.XAL.keyvalSetOne('userMatrix', this.pMatrix.toString());
};
-/******************************************************************************/
-
µMatrix.loadMatrix = function(callback) {
if ( typeof callback !== 'function' ) {
callback = this.noopFunc;
@@ -185,72 +262,95 @@
/******************************************************************************/
-µMatrix.listKeysFromCustomHostsFiles = function(raw) {
+µMatrix.loadRecipes = function(reset, callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
+
+ let µm = this,
+ countdownCount = µm.userSettings.selectedRecipeFiles.length;
+
+ if ( reset ) {
+ µm.recipeManager.reset();
+ }
+
+ var onLoaded = function(details) {
+ if ( details.content ) {
+ µm.recipeManager.fromString(details.content);
+ }
+ countdownCount -= 1;
+ if ( countdownCount === 0 ) {
+ callback();
+ }
+ };
+
+ for ( let assetKey of µm.userSettings.selectedRecipeFiles ) {
+ this.assets.get(assetKey, onLoaded);
+ }
+};
+
+/******************************************************************************/
+
+µMatrix.assetKeysFromImportedAssets = function(raw) {
var out = new Set(),
reIgnore = /^[!#]/,
- reValid = /^[a-z-]+:\/\/\S+/,
- lineIter = new this.LineIterator(raw),
- location;
+ reValid = /^[a-z-]+:\/\/\S+\/./,
+ lineIter = new this.LineIterator(raw);
while ( lineIter.eot() === false ) {
- location = lineIter.next().trim();
+ let location = lineIter.next().trim();
if ( reIgnore.test(location) || !reValid.test(location) ) { continue; }
out.add(location);
}
- return this.setToArray(out);
+ return out;
};
/******************************************************************************/
µMatrix.getAvailableHostsFiles = function(callback) {
var µm = this,
- availableHostsFiles = {};
+ availableHostsFiles = new Map();
// Custom filter lists.
- var importedListKeys = this.listKeysFromCustomHostsFiles(µm.userSettings.externalHostsFiles),
- i = importedListKeys.length,
- listKey, entry;
- while ( i-- ) {
- listKey = importedListKeys[i];
- entry = {
- content: 'filters',
- contentURL: listKey,
+ var importedListKeys = new Set(µm.userSettings.externalHostsFiles);
+
+ for ( let assetKey of importedListKeys ) {
+ let entry = {
+ type: 'filters',
+ contentURL: assetKey,
external: true,
submitter: 'user',
- title: listKey
+ title: assetKey
};
- availableHostsFiles[listKey] = entry;
- this.assets.registerAssetSource(listKey, entry);
+ this.assets.registerAssetSource(assetKey, entry);
+ availableHostsFiles.set(assetKey, entry);
}
- // selected lists
- var onSelectedHostsFilesLoaded = function(bin) {
- // Now get user's selection of lists
- for ( var assetKey in bin.liveHostsFiles ) {
- var availableEntry = availableHostsFiles[assetKey];
- if ( availableEntry === undefined ) { continue; }
- var liveEntry = bin.liveHostsFiles[assetKey];
- availableEntry.off = liveEntry.off || false;
- if ( liveEntry.entryCount !== undefined ) {
- availableEntry.entryCount = liveEntry.entryCount;
- }
- if ( liveEntry.entryUsedCount !== undefined ) {
- availableEntry.entryUsedCount = liveEntry.entryUsedCount;
- }
- // This may happen if the list name was pulled from the list content
- if ( availableEntry.title === '' && liveEntry.title !== undefined ) {
- availableEntry.title = liveEntry.title;
+ // Populate available lists with useful data.
+ var onHostsFilesDataReady = function(bin) {
+ if ( bin && bin.liveHostsFiles ) {
+ for ( let entry of µm.toMap(bin.liveHostsFiles) ) {
+ let assetKey = entry[0];
+ let availableAsset = availableHostsFiles.get(assetKey);
+ if ( availableAsset === undefined ) { continue; }
+ let liveAsset = entry[1];
+ if ( liveAsset.entryCount !== undefined ) {
+ availableAsset.entryCount = liveAsset.entryCount;
+ }
+ if ( liveAsset.entryUsedCount !== undefined ) {
+ availableAsset.entryUsedCount = liveAsset.entryUsedCount;
+ }
+ // This may happen if the list name was pulled from the list content
+ if ( availableAsset.title === '' && liveAsset.title !== undefined ) {
+ availableAsset.title = liveAsset.title;
+ }
}
}
- // Remove unreferenced imported filter lists.
- var dict = new Set(importedListKeys);
- for ( assetKey in availableHostsFiles ) {
- var entry = availableHostsFiles[assetKey];
- if ( entry.submitter !== 'user' ) { continue; }
- if ( dict.has(assetKey) ) { continue; }
- delete availableHostsFiles[assetKey];
- µm.assets.unregisterAssetSource(assetKey);
- µm.assets.remove(assetKey);
+ for ( let asseyKey of µm.userSettings.selectedHostsFiles ) {
+ let asset = availableHostsFiles.get(asseyKey);
+ if ( asset !== undefined ) {
+ asset.selected = true;
+ }
}
callback(availableHostsFiles);
@@ -258,18 +358,22 @@
// built-in lists
var onBuiltinHostsFilesLoaded = function(entries) {
- for ( var assetKey in entries ) {
+ for ( let assetKey in entries ) {
if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
- entry = entries[assetKey];
- if ( entry.content !== 'filters' ) { continue; }
- availableHostsFiles[assetKey] = objectAssign({}, entry);
+ let entry = entries[assetKey];
+ if ( entry.type !== 'filters' ) { continue; }
+ if (
+ entry.submitter === 'user' &&
+ importedListKeys.has(assetKey) === false
+ ) {
+ µm.assets.unregisterAssetSource(assetKey);
+ µm.assets.remove(assetKey);
+ continue;
+ }
+ availableHostsFiles.set(assetKey, objectAssign({}, entry));
}
- // Now get user's selection of lists
- vAPI.storage.get(
- { 'liveHostsFiles': availableHostsFiles },
- onSelectedHostsFilesLoaded
- );
+ vAPI.storage.get('liveHostsFiles', onHostsFilesDataReady);
};
this.assets.metadata(onBuiltinHostsFilesLoaded);
@@ -277,6 +381,56 @@
/******************************************************************************/
+µMatrix.getAvailableRecipeFiles = function(callback) {
+ var µm = this,
+ availableRecipeFiles = new Map();
+
+ // Imported recipe resources.
+ var importedResourceKeys = new Set(µm.userSettings.externalRecipeFiles);
+
+ for ( let assetKey of importedResourceKeys ) {
+ let entry = {
+ type: 'recipes',
+ contentURL: assetKey,
+ external: true,
+ submitter: 'user',
+ title: assetKey
+ };
+ this.assets.registerAssetSource(assetKey, entry);
+ availableRecipeFiles.set(assetKey, entry);
+ }
+
+ var onBuiltinRecipeFilesLoaded = function(entries) {
+ for ( let assetKey in entries ) {
+ if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
+ let entry = entries[assetKey];
+ if ( entry.type !== 'recipes' ) { continue; }
+ if (
+ entry.submitter === 'user' &&
+ importedResourceKeys.has(assetKey) === false
+ ) {
+ µm.assets.unregisterAssetSource(assetKey);
+ µm.assets.remove(assetKey);
+ continue;
+ }
+ availableRecipeFiles.set(assetKey, objectAssign({}, entry));
+ }
+
+ for ( let asseyKey of µm.userSettings.selectedRecipeFiles ) {
+ let asset = availableRecipeFiles.get(asseyKey);
+ if ( asset !== undefined ) {
+ asset.selected = true;
+ }
+ }
+
+ callback(availableRecipeFiles);
+ };
+
+ this.assets.metadata(onBuiltinRecipeFilesLoaded);
+};
+
+/******************************************************************************/
+
µMatrix.loadHostsFiles = function(callback) {
var µm = µMatrix;
var hostsFileLoadCount;
@@ -287,7 +441,7 @@
var loadHostsFilesEnd = function() {
µm.ubiquitousBlacklist.freeze();
- vAPI.storage.set({ 'liveHostsFiles': µm.liveHostsFiles });
+ vAPI.storage.set({ liveHostsFiles: Array.from(µm.liveHostsFiles) });
vAPI.messaging.broadcast({ what: 'loadHostsFilesCompleted' });
µm.getBytesInUse();
callback();
@@ -304,17 +458,11 @@
var loadHostsFilesStart = function(hostsFiles) {
µm.liveHostsFiles = hostsFiles;
µm.ubiquitousBlacklist.reset();
- var locations = Object.keys(hostsFiles);
- hostsFileLoadCount = locations.length;
+ hostsFileLoadCount = µm.userSettings.selectedHostsFiles.length;
// 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);
+ for ( let assetKey of µm.userSettings.selectedHostsFiles ) {
+ µm.assets.get(assetKey, mergeHostsFile);
}
// https://github.com/gorhill/uMatrix/issues/2
@@ -338,9 +486,9 @@
usedCount = this.ubiquitousBlacklist.count - usedCount;
duplicateCount = this.ubiquitousBlacklist.duplicateCount - duplicateCount;
- var hostsFilesMeta = this.liveHostsFiles[details.assetKey];
- hostsFilesMeta.entryCount = usedCount + duplicateCount;
- hostsFilesMeta.entryUsedCount = usedCount;
+ let hostsFileMeta = this.liveHostsFiles.get(details.assetKey);
+ hostsFileMeta.entryCount = usedCount + duplicateCount;
+ hostsFileMeta.entryUsedCount = usedCount;
};
/******************************************************************************/
@@ -402,99 +550,123 @@
/******************************************************************************/
-// `switches` contains the filter lists for which the switch must be revisited.
-
-µMatrix.selectHostsFiles = function(details, callback) {
- var µm = this,
- externalHostsFiles = this.userSettings.externalHostsFiles,
- i, n, assetKey;
+µMatrix.selectAssets = function(details, callback) {
+ var µm = this;
- // Hosts file to select
- if ( Array.isArray(details.toSelect) ) {
- for ( assetKey in this.liveHostsFiles ) {
- if ( this.liveHostsFiles.hasOwnProperty(assetKey) === false ) {
- continue;
- }
- if ( details.toSelect.indexOf(assetKey) !== -1 ) {
- this.liveHostsFiles[assetKey].off = false;
- } else if ( details.merge !== true ) {
- this.liveHostsFiles[assetKey].off = true;
+ var applyAssetSelection = function(
+ metadata,
+ details,
+ propSelectedAssetKeys,
+ propImportedAssetKeys
+ ) {
+ let µmus = µm.userSettings;
+ let selectedAssetKeys = new Set();
+ let importedAssetKeys = new Set(µmus[propImportedAssetKeys]);
+
+ if ( Array.isArray(details.toSelect) ) {
+ for ( let assetKey of details.toSelect ) {
+ if ( metadata.has(assetKey) ) {
+ selectedAssetKeys.add(assetKey);
+ }
}
}
- }
- // Imported hosts files to remove
- if ( Array.isArray(details.toRemove) ) {
- var removeURLFromHaystack = function(haystack, needle) {
- return haystack.replace(
- new RegExp(
- '(^|\\n)' +
- needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
- '(\\n|$)', 'g'),
- '\n'
- ).trim();
- };
- for ( i = 0, n = details.toRemove.length; i < n; i++ ) {
- assetKey = details.toRemove[i];
- delete this.liveHostsFiles[assetKey];
- externalHostsFiles = removeURLFromHaystack(externalHostsFiles, assetKey);
- this.assets.remove(assetKey);
+ if ( Array.isArray(details.toRemove) ) {
+ for ( let assetKey of details.toRemove ) {
+ importedAssetKeys.delete(assetKey);
+ µm.assets.remove(assetKey);
+ }
}
- }
- // Hosts file to import
- if ( typeof details.toImport === 'string' ) {
+ // Hosts file to import
// https://github.com/gorhill/uBlock/issues/1181
- // Try mapping the URL of an imported filter list to the assetKey of an
- // existing stock list.
- var assetKeyFromURL = function(url) {
- var needle = url.replace(/^https?:/, '');
- var assets = µm.liveHostsFiles, asset;
- for ( var assetKey in assets ) {
- asset = assets[assetKey];
- if ( asset.content !== 'filters' ) { continue; }
- if ( typeof asset.contentURL === 'string' ) {
- if ( asset.contentURL.endsWith(needle) ) { return assetKey; }
- continue;
- }
- if ( Array.isArray(asset.contentURL) === false ) { continue; }
- for ( i = 0, n = asset.contentURL.length; i < n; i++ ) {
- if ( asset.contentURL[i].endsWith(needle) ) {
- return assetKey;
+ // Try mapping the URL of an imported filter list to the assetKey of
+ // an existing stock list.
+ if ( typeof details.toImport === 'string' ) {
+ var assetKeyFromURL = function(url) {
+ var needle = url.replace(/^https?:/, '');
+ for ( let entry of metadata ) {
+ let asset = entry[1];
+ if ( asset.type === 'internal' ) { continue; }
+ let assetKey = entry[0];
+ if ( typeof asset.contentURL === 'string' ) {
+ if ( asset.contentURL.endsWith(needle) ) { return assetKey; }
+ continue;
+ }
+ if ( Array.isArray(asset.contentURL) === false ) { continue; }
+ for ( let i = 0, n = asset.contentURL.length; i < n; i++ ) {
+ if ( asset.contentURL[i].endsWith(needle) ) {
+ return assetKey;
+ }
}
}
- }
- return url;
- };
- var importedSet = new Set(this.listKeysFromCustomHostsFiles(externalHostsFiles)),
- toImportSet = new Set(this.listKeysFromCustomHostsFiles(details.toImport)),
- iter = toImportSet.values();
- for (;;) {
- var entry = iter.next();
- if ( entry.done ) { break; }
- if ( importedSet.has(entry.value) ) { continue; }
- assetKey = assetKeyFromURL(entry.value);
- if ( assetKey === entry.value ) {
- importedSet.add(entry.value);
- }
- this.liveHostsFiles[assetKey] = {
- content: 'filters',
- contentURL: [ assetKey ],
- title: assetKey
+ return url;
};
+ var toImport = µm.assetKeysFromImportedAssets(details.toImport);
+ for ( let url of toImport ) {
+ if ( importedAssetKeys.has(url) ) { continue; }
+ let assetKey = assetKeyFromURL(url);
+ if ( assetKey === url ) {
+ importedAssetKeys.add(assetKey);
+ }
+ selectedAssetKeys.add(assetKey);
+ }
}
- externalHostsFiles = this.setToArray(importedSet).sort().join('\n');
- }
- if ( externalHostsFiles !== this.userSettings.externalHostsFiles ) {
- this.userSettings.externalHostsFiles = externalHostsFiles;
- vAPI.storage.set({ externalHostsFiles: externalHostsFiles });
- }
- vAPI.storage.set({ 'liveHostsFiles': this.liveHostsFiles });
+ let bin = {},
+ changed = false;
- if ( typeof callback === 'function' ) {
- callback();
- }
+ selectedAssetKeys = Array.from(selectedAssetKeys).sort();
+ µmus[propSelectedAssetKeys].sort();
+ if ( selectedAssetKeys.join() !== µmus[propSelectedAssetKeys].join() ) {
+ µmus[propSelectedAssetKeys] = selectedAssetKeys;
+ bin[propSelectedAssetKeys] = selectedAssetKeys;
+ changed = true;
+ }
+
+ importedAssetKeys = Array.from(importedAssetKeys).sort();
+ µmus[propImportedAssetKeys].sort();
+ if ( importedAssetKeys.join() !== µmus[propImportedAssetKeys].join() ) {
+ µmus[propImportedAssetKeys] = importedAssetKeys;
+ bin[propImportedAssetKeys] = importedAssetKeys;
+ changed = true;
+ }
+
+ if ( changed ) {
+ vAPI.storage.set(bin);
+ }
+
+ return changed;
+ };
+
+ var onMetadataReady = function(response) {
+ let metadata = µm.toMap(response);
+ let hostsChanged = applyAssetSelection(
+ metadata,
+ details.hosts,
+ 'selectedHostsFiles',
+ 'externalHostsFiles'
+ );
+ let recipesChanged = applyAssetSelection(
+ metadata,
+ details.recipes,
+ 'selectedRecipeFiles',
+ 'externalRecipeFiles'
+ );
+ if ( recipesChanged ) {
+ µm.recipeManager.reset();
+ µm.loadRecipes(true);
+ }
+
+ if ( typeof callback === 'function' ) {
+ callback({
+ hostsChanged: hostsChanged,
+ recipesChanged: recipesChanged
+ });
+ }
+ };
+
+ this.assets.metadata(onMetadataReady);
};
/******************************************************************************/
@@ -533,6 +705,7 @@
timer = undefined;
}
if ( updateDelay === 0 ) {
+ this.assets.updateStop();
next = 0;
return;
}
@@ -556,9 +729,13 @@
µMatrix.assetObserver = function(topic, details) {
// Do not update filter list if not in use.
if ( topic === 'before-asset-updated' ) {
+ let µmus = this.userSettings;
if (
- this.liveHostsFiles.hasOwnProperty(details.assetKey) === false ||
- this.liveHostsFiles[details.assetKey].off !== true
+ details.type === 'internal' ||
+ details.type === 'filters' &&
+ µmus.selectedHostsFiles.indexOf(details.assetKey) !== -1 ||
+ details.type === 'recipes' &&
+ µmus.selectedRecipeFiles.indexOf(details.assetKey) !== -1
) {
return true;
}
@@ -586,7 +763,20 @@
// Reload all filter lists if needed.
if ( topic === 'after-assets-updated' ) {
- if ( details.assetKeys.length !== 0 ) {
+ if (
+ this.arraysIntersect(
+ details.assetKeys,
+ this.userSettings.selectedRecipeFiles
+ )
+ ) {
+ this.loadRecipes(true);
+ }
+ if (
+ this.arraysIntersect(
+ details.assetKeys,
+ this.userSettings.selectedHostsFiles
+ )
+ ) {
this.loadHostsFiles();
}
if ( this.userSettings.autoUpdate ) {
@@ -600,15 +790,4 @@
});
return;
}
-
- // New asset source became available, if it's a filter list, should we
- // auto-select it?
- if ( topic === 'builtin-asset-source-added' ) {
- if ( details.entry.content === 'filters' ) {
- if ( details.entry.off !== true ) {
- this.saveSelectedFilterLists([ details.assetKey ], true);
- }
- }
- return;
- }
};
diff --git a/src/js/usersettings.js b/src/js/usersettings.js
index 50fedca..303f4d3 100644
--- a/src/js/usersettings.js
+++ b/src/js/usersettings.js
@@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a Chromium browser extension to black/white list requests.
- Copyright (C) 2014-2017 Raymond Hill
+ Copyright (C) 2014-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -49,7 +49,9 @@
// Post-change
switch ( name ) {
-
+ case 'autoUpdate':
+ this.scheduleAssetUpdater(value === true ? 120000 : 0);
+ break;
default:
break;
}
diff --git a/src/js/utils.js b/src/js/utils.js
index 5c3d683..3db5081 100644
--- a/src/js/utils.js
+++ b/src/js/utils.js
@@ -1,7 +1,7 @@
/*******************************************************************************
uMatrix - a Chromium browser extension to black/white list requests.
- Copyright (C) 2014-2017 Raymond Hill
+ Copyright (C) 2014-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -103,3 +103,32 @@
};
/******************************************************************************/
+
+µMatrix.toMap = function(input) {
+ if ( input instanceof Map ) {
+ return input;
+ }
+ if ( Array.isArray(input) ) {
+ return new Map(input);
+ }
+ let out = new Map();
+ if ( input instanceof Object ) {
+ for ( let key in input ) {
+ if ( input.hasOwnProperty(key) ) {
+ out.set(key, input[key]);
+ }
+ }
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+µMatrix.arraysIntersect = function(a1, a2) {
+ for ( let v of a1 ) {
+ if ( a2.indexOf(v) !== -1 ) { return true; }
+ }
+ return false;
+};
+
+/******************************************************************************/
diff --git a/src/popup.html b/src/popup.html
index 0eed6cf..1a15cee 100644
--- a/src/popup.html
+++ b/src/popup.html
@@ -11,29 +11,6 @@
-
-
uMatrix
@@ -44,12 +21,15 @@
+
+
+
-
+
-
+