Browse Source

first draft toward fixing #30

pull/2/head
Raymond Hill 7 years ago
parent
commit
2cf4a57bf4
No known key found for this signature in database GPG Key ID: 25E1490B761470C2
  1. 22
      src/_locales/en/messages.json
  2. 1
      src/background.html
  3. 2
      src/css/dashboard-common.css
  4. 40
      src/css/hosts-files.css
  5. 102
      src/css/popup.css
  6. 1
      src/css/user-rules.css
  7. 27
      src/hosts-files.html
  8. 28
      src/js/assets.js
  9. 10
      src/js/background.js
  10. 190
      src/js/hosts-files.js
  11. 77
      src/js/messaging.js
  12. 143
      src/js/popup.js
  13. 217
      src/js/recipe-manager.js
  14. 1
      src/js/start.js
  15. 465
      src/js/storage.js
  16. 6
      src/js/usersettings.js
  17. 31
      src/js/utils.js
  18. 56
      src/popup.html
  19. 3
      tools/make-assets.sh

22
src/_locales/en/messages.json

@ -32,7 +32,7 @@
"description": "a tab in dashboard"
},
"ubiquitousRulesPageName" : {
"message": "Hosts files",
"message": "Assets",
"description": "a tab in dashboard"
},
"rawSettingsPageName": {
@ -479,6 +479,10 @@
},
"assetsHostsSection" : {
"message": "Hosts files",
"description": "header to identify the hosts files section"
},
"hostsFilesPrompt" : {
"message": "All hostnames in a hosts file are loaded as blacklisted hostnames in the global scope.",
"description": ""
@ -500,7 +504,7 @@
"description": ""
},
"hostsFilesAutoUpdatePrompt":{
"message":"Auto-update hosts files.",
"message":"Auto-update assets.",
"description":""
},
"hostsFilesUpdateNow":{
@ -512,7 +516,7 @@
"description":""
},
"hostsFilesExternalListsHint":{
"message":"One URL per line. Lines prefixed with ‘#’ will be ignored. Invalid URLs will be silently ignored.",
"message":"Import external assets here:\nOne URL per line; invalid URLs will be silently ignored.",
"description":""
},
"hostsFilesExternalListsParse":{
@ -531,6 +535,18 @@
"message":"outdated",
"description":""
},
"assetsRecipesSection" : {
"message": "Ruleset recipes",
"description": "header to identify the ruleset files section"
},
"assetsRecipesSummary" : {
"message": "Ruleset recipes are imported from the popup panel <em>on demand</em>, i.e. <b>only</b> through user interaction.",
"description": ""
},
"assetsImportLabel" : {
"message": "Import...",
"description": ""
},
"rawSettingsWarning" : {
"message": "Warning! Change these raw configuration settings at your own risk.",

1
src/background.html

@ -16,6 +16,7 @@
<script src="js/usersettings.js"></script>
<script src="js/liquid-dict.js"></script>
<script src="js/matrix.js"></script>
<script src="js/recipe-manager.js"></script>
<script src="js/utils.js"></script>
<script src="js/assets.js"></script>
<script src="js/httpsb.js"></script>

2
src/css/dashboard-common.css

@ -3,7 +3,7 @@ body {
color: #000;
margin: 0;
padding: 0 0.5em 0 0.5em;
font: 15px/1.4 sans-serif;
font: 14px/1.4 sans-serif;
}
body > *:first-child {
margin-top: 0;

40
src/css/hosts-files.css

@ -18,6 +18,19 @@ ul#options {
ul#options li {
margin-bottom: 0.5em;
}
.assets {
border: 1px solid #ccc;
margin: 0.5em 0 0 0;
padding: 0;
}
.assets > div:first-of-type {
background-color: #eee;
margin: 0;
padding: 0.25em 0.5em;
}
.assets > div + div {
padding: 0.5em 1em;
}
ul#lists {
margin: 0.5em 0 0 0;
padding: 0;
@ -40,7 +53,6 @@ li.listEntry > * {
unicode-bidi: embed;
}
li.listEntry > a:nth-of-type(2) {
font-size: 13px;
opacity: 0.5;
}
li.listEntry.toRemove > input[type="checkbox"] {
@ -59,9 +71,12 @@ li.listEntry > .fa {
li.listEntry > a.fa:hover {
opacity: 1;
}
li.listEntry.support > a.support {
li.listEntry > a.support {
display: inline-block;
}
li.listEntry > a.support[href=""] {
display: none;
}
li.listEntry > a.remove,
li.listEntry > a.remove:visited {
color: darkred;
@ -124,19 +139,20 @@ body.updating li.listEntry.obsolete > input[type="checkbox"]:checked ~ span.upda
.dim {
opacity: 0.5;
}
#externalLists {
margin: 2em auto 0 auto;
}
body[dir="ltr"] #externalListsDiv {
margin-left: 1em;
li.listEntry.importURL > input[type="checkbox"] ~ textarea {
display: none;
margin-left: 1.6em;
}
body[dir="rtl"] #externalListsDiv {
margin-right: 1em;
li.listEntry.importURL > input[type="checkbox"]:checked ~ textarea {
display: block;
}
#externalHostsFiles {
li.listEntry.importURL > textarea {
border: 1px solid #ddd;
box-sizing: border-box;
display: block;
font-size: smaller;
width: 100%;
height: 12em;
width: calc(100% - 4em);
height: 6em;
resize: none;
white-space: pre;
}

102
src/css/popup.css

@ -210,10 +210,6 @@ body .toolbar button.fa {
display: block;
}
#buttonReload {
margin-left: 1em;
}
button > span.badge {
padding: 1px 1px;
display: inline-block;
@ -232,66 +228,66 @@ button.disabled > span.badge {
left: 10vw;
width: 80vw;
}
.presetInfo {
margin: 0.25em 0.5em;
text-align: center;
#dropDownMenuRecipes > .dropdown-menu > ul {
max-height: 70vh;
min-width: 50vw;
overflow: auto;
}
.presetEntry {
margin: 0.25em 0.25em;
border-radius: 3px;
padding: 0.5em;
display: inline-block;
cursor: pointer;
background-color: #eee;
#dropDownMenuRecipes li > ul {
margin-left: 1em;
padding: 0;
}
.presetEntry .fa {
margin-right: 0.25em;
font-size: 110%;
.recipe {
cursor: pointer;
list-style-type: none;
white-space: nowrap;
}
.presetEntry:hover {
background-color: #80e2ff;
.recipe > div {
align-items: baseline;
display: flex;
justify-content: space-between;
}
#presetMore > *:first-child {
margin: 0;
padding: 0;
text-align: center;
.recipe > div > span {
color: #888;
cursor: pointer;
font-size: 13px;
}
#presetMore > *:first-child + div {
margin: 0.25em 0 0 0;
padding: 0.25em 0 0 0;
display: none;
text-align: center;
.recipe > div > span:hover {
color: #000;
}
#presetMore > *:first-child + div.show {
display: block;
.recipe .expander {
display: inline-block;
padding: 0.4em;
width: 0.8em;
}
#presetMore > *:first-child + div > * {
vertical-align: middle;
.recipe .expander::before {
content: '\2BC8';
}
#presetMoreRecipe {
border: 1px solid #aaa;
width: 75%;
height: 4em;
overflow: hidden;
resize: none;
font-size: 10px;
color: #888;
.recipe.expanded .expander::before {
content: '\2BC6';
}
.recipe .name {
color: #000;
flex-grow: 1;
}
#presetMoreRecipe.bad {
border: 1px solid #fcc;
color: #aaa;
.recipe .committer {
display: none;
font-size: 120%;
padding: 0.4em;
width: 0.8em;
}
#presetMoreWrite.bad {
visibility: hidden;
.recipe.mustCommit .committer {
display: inline;
}
/* I think this is obsolete */
.dropdown-menu > li > a > i {
padding: 0 6px;
font-size: 1.2em;
.recipe:hover {
background-color: #eef;
}
.recipe .ruleset {
display: none;
font: smaller monospace;
padding: 0 0.5em 0.5em 2em;
white-space: pre;
}
.recipe.expanded .ruleset {
display: block;
}
body .toolbar .scope {
@ -347,7 +343,7 @@ body .toolbar #specificScope > span:first-of-type {
body .toolbar #globalScope {
justify-content: center;
margin-left: 1px;
width: 1.8em;
width: 1.6em;
}
body .toolbar #globalScope.on {
background-color: #000;

1
src/css/user-rules.css

@ -18,7 +18,6 @@ div > p:last-child {
padding: 0;
position: relative;
vertical-align: top;
white-space: normal;
width: calc(50% - 2px);
}
#diff > .pane > div {

27
src/hosts-files.html

@ -10,7 +10,6 @@
<body>
<p data-i18n="hostsFilesPrompt"></p>
<p>
<button id="buttonApply" class="custom important reloadAll disabled" data-i18n="hostsFilesApplyChanges"></button>
<button id="buttonUpdate" class="custom important reloadAll disabled" data-i18n="hostsFilesUpdateNow"></button>
@ -18,14 +17,28 @@
</p>
<ul id="options">
<li><input type="checkbox" id="autoUpdate"><label data-i18n="hostsFilesAutoUpdatePrompt" for="autoUpdate"></label>
<li><span id="listsOfBlockedHostsPrompt"></span>
</ul>
<ul id="lists">
</ul>
<div class="assets">
<div data-i18n="assetsHostsSection"></div>
<div>
<p data-i18n="hostsFilesPrompt"></p>
<p id="listsOfBlockedHostsPrompt"></p>
<ul id="hosts">
<li class="listEntry importURL"><input type="checkbox" id="importHosts"><label for="importHosts"data-i18n="assetsImportLabel"></label><!--
--><textarea dir="ltr" spellcheck="false" placeholder="hostsFilesExternalListsHint"></textarea>
</ul>
</div>
</div>
<div id="externalLists">
<p data-i18n="hostsFilesExternalListsHint" style="margin: 0 0 0.25em 0; font-size: 13px;"></p>
<textarea id="externalHostsFiles" dir="ltr" spellcheck="false"></textarea>
<div class="assets">
<div data-i18n="assetsRecipesSection"></div>
<div>
<p data-i18n="assetsRecipesSummary"></p>
<ul id="recipes">
<li class="listEntry importURL"><input type="checkbox" id="importRecipes"><label for="importRecipes"data-i18n="assetsImportLabel"></label><!--
--><textarea dir="ltr" spellcheck="false" placeholder="hostsFilesExternalListsHint"></textarea>
</ul>
</div>
</div>
<div id="templates" style="display: none;">

28
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);

10
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.

190
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');
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;
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-- ) {
liEntry = liEntries[i];
let liEntry = liEntries[i];
if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
toSelect.push(liEntry.getAttribute('data-listkey'));
out.toSelect.push(liEntry.getAttribute('data-listkey'));
}
}
// External hosts files to remove
var toRemove = [];
liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]');
liEntries = root.querySelectorAll('.listEntry.toRemove[data-listkey]');
i = liEntries.length;
while ( i-- ) {
toRemove.push(liEntries[i].getAttribute('data-listkey'));
out.toRemove.push(liEntries[i].getAttribute('data-listkey'));
}
// External hosts files to import
var externalListsElem = document.getElementById('externalHostsFiles'),
toImport = externalListsElem.value.trim();
externalListsElem.value = '';
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() {
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();

77
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;

143
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) {

217
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();
}
};
})();
/******************************************************************************/

1
src/js/start.js

@ -76,6 +76,7 @@ var onTabsReady = function(tabs) {
var onUserSettingsLoaded = function() {
µm.loadHostsFiles();
µm.loadRecipes();
};
/******************************************************************************/

465
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;
// 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 ( liveEntry.entryUsedCount !== undefined ) {
availableEntry.entryUsedCount = liveEntry.entryUsedCount;
if ( liveAsset.entryUsedCount !== undefined ) {
availableAsset.entryUsedCount = liveAsset.entryUsedCount;
}
// This may happen if the list name was pulled from the list content
if ( availableEntry.title === '' && liveEntry.title !== undefined ) {
availableEntry.title = liveEntry.title;
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,63 +550,51 @@
/******************************************************************************/
// `switches` contains the filter lists for which the switch must be revisited.
µMatrix.selectAssets = function(details, callback) {
var µm = this;
µMatrix.selectHostsFiles = function(details, callback) {
var µm = this,
externalHostsFiles = this.userSettings.externalHostsFiles,
i, n, assetKey;
var applyAssetSelection = function(
metadata,
details,
propSelectedAssetKeys,
propImportedAssetKeys
) {
let µmus = µm.userSettings;
let selectedAssetKeys = new Set();
let importedAssetKeys = new Set(µmus[propImportedAssetKeys]);
// 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;
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);
for ( let assetKey of details.toRemove ) {
importedAssetKeys.delete(assetKey);
µm.assets.remove(assetKey);
}
}
// Hosts file to import
if ( typeof details.toImport === 'string' ) {
// https://github.com/gorhill/uBlock/issues/1181
// Try mapping the URL of an imported filter list to the assetKey of an
// existing stock list.
// 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?:/, '');
var assets = µm.liveHostsFiles, asset;
for ( var assetKey in assets ) {
asset = assets[assetKey];
if ( asset.content !== 'filters' ) { continue; }
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 ( i = 0, n = asset.contentURL.length; i < n; i++ ) {
for ( let i = 0, n = asset.contentURL.length; i < n; i++ ) {
if ( asset.contentURL[i].endsWith(needle) ) {
return assetKey;
}
@ -466,35 +602,71 @@
}
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
};
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 });
let bin = {},
changed = false;
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);
}
vAPI.storage.set({ 'liveHostsFiles': this.liveHostsFiles });
if ( typeof callback === 'function' ) {
callback();
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;
}
};

6
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;
}

31
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;
};
/******************************************************************************/

56
src/popup.html

@ -11,29 +11,6 @@
<body>
<div id="templates" style="display:none">
<div class="groupSeparator"></div>
<div class="domainSeparator"></div>
<div class="matRow"><div class="matCell"><b> </b> </div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div></div>
<div id="cellHotspots"><div id="whitelist"></div><div id="blacklist"></div><div id="domainOnly"><span class="fa"></span></div></div>
<!-- Use once min supported browser version allows for use of CSS variables
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<symbol id="toggleButton" viewBox="0 0 152 96">
<g>
<path d="m 48,24 a 24,24 0 0 0 -24,24 24,24 0 0 0 24,24 l 48,0 A 24,24 0 0 0 120,48 24,24 0 0 0 96,24 l -48,0 z" style="opacity:1;fill:#bbb;fill-opacity:1;stroke:none;" />
<g style="display:var(--off);">
<ellipse style="fill:#bbb;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="48" ry="48" />
<ellipse style="opacity:1;fill:#fff;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="40" ry="40" />
<ellipse style="display:var(--dot);fill:#bbb;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="12" ry="12" /></g>
<g style="display:var(--on);">
<ellipse style="opacity:1;fill:#444;fill-opacity:1;stroke:none;" cx="104" cy="48" rx="48" ry="48" />
<ellipse style="display:var(--dot);fill:#bbb;fill-opacity:1;stroke:none;" cx="104" cy="48" rx="12" ry="12" /></g>
</g>
</symbol>
</svg>
-->
</div>
<div class="paneHead">
<a id="gotoDashboard" class="extensionURL" href="#" data-extension-url="dashboard.html" data-i18n-tip="matrixDashboardMenuEntry">uMatrix <span id="version"> </span><span class="fa">&#xf013;</span></a>
<div id="toolbarContainer">
@ -44,6 +21,9 @@
<button id="buttonMtxSwitches" type="button" class="fa scopeRel" tabindex="-1" data-dropdown-menu="dropDownMenuSwitches">&#xf142;<span class="badge"></span></button>
<button id="buttonPersist" type="button" class="fa scopeRel tip-anchor-left" data-i18n-tip="matrixPersistButtonTip">&#xf023;<span class="badge"></span></button>
<button id="buttonRevertScope" type="button" class="fa scopeRel tip-anchor-left" tabindex="-1" data-i18n-tip="matrixRevertButtonTip">&#xf12d;</button>
<button id="buttonRecipes" type="button" class="fa scopeRel tip-anchor-right" data-dropdown-menu="dropDownMenuRecipes">&#xf12e;<span class="badge"></span></button>
</div>
<div class="toolbar">
<button id="buttonReload" type="button" class="fa tip-anchor-left" data-i18n-tip="matrixReloadButton">&#xf021;</button>
</div>
<div class="toolbar">
@ -94,6 +74,12 @@
</div>
</div>
<div id="dropDownMenuRecipes" class="dropdown-menu-capture">
<div class="dropdown-menu">
<ul></ul>
</div>
</div>
<div id="noTabFound"></div>
<!-- Convenient to auto-fetch locale strings used in scripts -->
@ -104,6 +90,30 @@
<div style="clear: both; height: 0px;"></div>
<div id="templates" style="display:none">
<div class="groupSeparator"></div>
<div class="domainSeparator"></div>
<div class="matRow"><div class="matCell"><b> </b> </div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div><div class="matCell">&nbsp;</div></div>
<div id="cellHotspots"><div id="whitelist"></div><div id="blacklist"></div><div id="domainOnly"><span class="fa"></span></div></div>
<!-- Use once min supported browser version allows for use of CSS variables
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<symbol id="toggleButton" viewBox="0 0 152 96">
<g>
<path d="m 48,24 a 24,24 0 0 0 -24,24 24,24 0 0 0 24,24 l 48,0 A 24,24 0 0 0 120,48 24,24 0 0 0 96,24 l -48,0 z" style="opacity:1;fill:#bbb;fill-opacity:1;stroke:none;" />
<g style="display:var(--off);">
<ellipse style="fill:#bbb;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="48" ry="48" />
<ellipse style="opacity:1;fill:#fff;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="40" ry="40" />
<ellipse style="display:var(--dot);fill:#bbb;fill-opacity:1;stroke:none;" cx="48" cy="48" rx="12" ry="12" /></g>
<g style="display:var(--on);">
<ellipse style="opacity:1;fill:#444;fill-opacity:1;stroke:none;" cx="104" cy="48" rx="48" ry="48" />
<ellipse style="display:var(--dot);fill:#bbb;fill-opacity:1;stroke:none;" cx="104" cy="48" rx="12" ry="12" /></g>
</g>
</symbol>
</svg>
-->
<li class="recipe"><div><span class="expander"></span><span class="name"></span><span class="fa committer">&#xf13e;</span></div><div class="ruleset"></div></li>
</div>
<script src="lib/punycode.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>

3
tools/make-assets.sh

@ -8,6 +8,7 @@ printf "*** Packaging assets in $DES... "
rm -rf $DES
mkdir $DES
cp ./assets/assets.json $DES/
if [ -n "${TRAVIS_TAG}" ]; then
@ -24,5 +25,7 @@ cp -R ../uAssets/thirdparties/publicsuffix.org $DES/thirdparties/
cp -R ../uAssets/thirdparties/someonewhocares.org $DES/thirdparties/
cp -R ../uAssets/thirdparties/winhelp2002.mvps.org $DES/thirdparties/
cp -R ../uAssets/thirdparties/www.malwaredomainlist.com $DES/thirdparties/
mkdir $DES/umatrix
cp -R ../uAssets/recipes/* $DES/umatrix/
echo "done."
Loading…
Cancel
Save