aboutsummaryrefslogtreecommitdiffstats
path: root/js
diff options
context:
space:
mode:
authorAlessio Vanni <vannilla@firemail.cc>2019-02-19 21:06:09 +0100
committerAlessio Vanni <vannilla@firemail.cc>2019-02-19 21:06:09 +0100
commitfe2f8acc8210c2ddead4621797b47106a9b38f5b (patch)
tree5fb103d45d7e4345f56fc068ce8173b82fa7051f /js
downloadematrix-fe2f8acc8210c2ddead4621797b47106a9b38f5b.tar.lz
ematrix-fe2f8acc8210c2ddead4621797b47106a9b38f5b.tar.xz
ematrix-fe2f8acc8210c2ddead4621797b47106a9b38f5b.zip
Fork uMatrix
Pretty much just changing the name and the copyright.
Diffstat (limited to 'js')
-rw-r--r--js/about.js146
-rw-r--r--js/asset-viewer.js46
-rw-r--r--js/assets.js911
-rw-r--r--js/background.js246
-rw-r--r--js/browsercache.js65
-rw-r--r--js/cloud-ui.js214
-rw-r--r--js/contentscript-start.js97
-rw-r--r--js/contentscript.js541
-rw-r--r--js/cookies.js552
-rw-r--r--js/dashboard-common.js41
-rw-r--r--js/dashboard.js56
-rw-r--r--js/hosts-files.js391
-rw-r--r--js/httpsb.js212
-rw-r--r--js/i18n.js209
-rw-r--r--js/liquid-dict.js203
-rw-r--r--js/logger-ui.js908
-rw-r--r--js/logger.js93
-rw-r--r--js/main-blocked.js176
-rw-r--r--js/matrix.js873
-rw-r--r--js/messaging.js963
-rw-r--r--js/pagestats.js274
-rw-r--r--js/polyfill.js96
-rw-r--r--js/popup.js1538
-rw-r--r--js/profiler.js63
-rw-r--r--js/raw-settings.js116
-rw-r--r--js/settings.js195
-rw-r--r--js/start.js108
-rw-r--r--js/storage.js615
-rw-r--r--js/tab.js710
-rw-r--r--js/traffic.js444
-rw-r--r--js/udom.js729
-rw-r--r--js/uritools.js537
-rw-r--r--js/user-rules.js341
-rw-r--r--js/usersettings.js59
-rw-r--r--js/utils.js106
-rw-r--r--js/vapi-background.js3466
-rw-r--r--js/vapi-client.js226
-rw-r--r--js/vapi-common.js192
-rw-r--r--js/vapi-popup.js23
-rw-r--r--js/xal.js72
40 files changed, 16853 insertions, 0 deletions
diff --git a/js/about.js b/js/about.js
new file mode 100644
index 0000000..acaffa3
--- /dev/null
+++ b/js/about.js
@@ -0,0 +1,146 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+uDom.onLoad(function() {
+
+/******************************************************************************/
+
+var backupUserDataToFile = function() {
+ var userDataReady = function(userData) {
+ vAPI.download({
+ 'url': 'data:text/plain,' + encodeURIComponent(JSON.stringify(userData, null, 2)),
+ 'filename': uDom('[data-i18n="aboutBackupFilename"]').text()
+ });
+ };
+
+ vAPI.messaging.send('about.js', { what: 'getAllUserData' }, userDataReady);
+};
+
+/******************************************************************************/
+
+function restoreUserDataFromFile() {
+ var validateBackup = function(s) {
+ var userData = null;
+ try {
+ userData = JSON.parse(s);
+ }
+ catch (e) {
+ userData = null;
+ }
+ if ( userData === null ) {
+ return null;
+ }
+ if (
+ typeof userData !== 'object' ||
+ typeof userData.version !== 'string' ||
+ typeof userData.when !== 'number' ||
+ typeof userData.settings !== 'object' ||
+ typeof userData.rules !== 'string' ||
+ typeof userData.hostsFiles !== 'object'
+ ) {
+ return null;
+ }
+ return userData;
+ };
+
+ var fileReaderOnLoadHandler = function() {
+ var userData = validateBackup(this.result);
+ if ( !userData ) {
+ window.alert(uDom('[data-i18n="aboutRestoreError"]').text());
+ return;
+ }
+ var time = new Date(userData.when);
+ var msg = uDom('[data-i18n="aboutRestoreConfirm"]').text()
+ .replace('{{time}}', time.toLocaleString());
+ var proceed = window.confirm(msg);
+ if ( proceed ) {
+ vAPI.messaging.send(
+ 'about.js',
+ { what: 'restoreAllUserData', userData: userData }
+ );
+ }
+ };
+
+ var file = this.files[0];
+ if ( file === undefined || file.name === '' ) {
+ return;
+ }
+ if ( file.type.indexOf('text') !== 0 ) {
+ return;
+ }
+ var fr = new FileReader();
+ fr.onload = fileReaderOnLoadHandler;
+ fr.readAsText(file);
+}
+
+/******************************************************************************/
+
+var startRestoreFilePicker = function() {
+ var input = document.getElementById('restoreFilePicker');
+ // Reset to empty string, this will ensure an change event is properly
+ // triggered if the user pick a file, even if it is the same as the last
+ // one picked.
+ input.value = '';
+ input.click();
+};
+
+/******************************************************************************/
+
+var resetUserData = function() {
+ var proceed = window.confirm(uDom('[data-i18n="aboutResetConfirm"]').text());
+ if ( proceed ) {
+ vAPI.messaging.send('about.js', { what: 'resetAllUserData' });
+ }
+};
+
+/******************************************************************************/
+
+(function() {
+ var renderStats = function(details) {
+ document.getElementById('aboutVersion').textContent = details.version;
+ var template = uDom('[data-i18n="aboutStorageUsed"]').text();
+ var storageUsed = '?';
+ if ( typeof details.storageUsed === 'number' ) {
+ storageUsed = details.storageUsed.toLocaleString();
+ }
+ document.getElementById('aboutStorageUsed').textContent =
+ template.replace('{{storageUsed}}', storageUsed);
+ };
+ vAPI.messaging.send('about.js', { what: 'getSomeStats' }, renderStats);
+})();
+
+/******************************************************************************/
+
+uDom('#backupUserDataButton').on('click', backupUserDataToFile);
+uDom('#restoreUserDataButton').on('click', startRestoreFilePicker);
+uDom('#restoreFilePicker').on('change', restoreUserDataFromFile);
+uDom('#resetUserDataButton').on('click', resetUserData);
+
+/******************************************************************************/
+
+});
diff --git a/js/asset-viewer.js b/js/asset-viewer.js
new file mode 100644
index 0000000..97a1fb8
--- /dev/null
+++ b/js/asset-viewer.js
@@ -0,0 +1,46 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+ var onAssetContentReceived = function(details) {
+ document.getElementById('content').textContent =
+ details && (details.content || '');
+ };
+
+ var q = window.location.search;
+ var matches = q.match(/^\?url=([^&]+)/);
+ if ( !matches || matches.length !== 2 ) {
+ return;
+ }
+
+ vAPI.messaging.send(
+ 'asset-viewer.js',
+ { what : 'getAssetContent', url: matches[1] },
+ onAssetContentReceived
+ );
+
+})();
diff --git a/js/assets.js b/js/assets.js
new file mode 100644
index 0000000..c3fb85f
--- /dev/null
+++ b/js/assets.js
@@ -0,0 +1,911 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2013-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.assets = (function() {
+
+/******************************************************************************/
+
+var reIsExternalPath = /^(?:[a-z-]+):\/\//,
+ errorCantConnectTo = vAPI.i18n('errorCantConnectTo'),
+ noopfunc = function(){};
+
+var api = {
+};
+
+/******************************************************************************/
+
+var observers = [];
+
+api.addObserver = function(observer) {
+ if ( observers.indexOf(observer) === -1 ) {
+ observers.push(observer);
+ }
+};
+
+api.removeObserver = function(observer) {
+ var pos;
+ while ( (pos = observers.indexOf(observer)) !== -1 ) {
+ observers.splice(pos, 1);
+ }
+};
+
+var fireNotification = function(topic, details) {
+ var result;
+ for ( var i = 0; i < observers.length; i++ ) {
+ if ( observers[i](topic, details) === false ) {
+ result = false;
+ }
+ }
+ return result;
+};
+
+/******************************************************************************/
+
+api.fetchText = function(url, onLoad, onError) {
+ var actualUrl = reIsExternalPath.test(url) ? url : vAPI.getURL(url);
+
+ if ( typeof onError !== 'function' ) {
+ onError = onLoad;
+ }
+
+ // https://github.com/gorhill/uMatrix/issues/15
+ var onResponseReceived = function() {
+ this.onload = this.onerror = this.ontimeout = null;
+ // xhr for local files gives status 0, but actually succeeds
+ var details = {
+ url: url,
+ content: '',
+ statusCode: this.status || 200,
+ statusText: this.statusText || ''
+ };
+ if ( details.statusCode < 200 || details.statusCode >= 300 ) {
+ return onError.call(null, details);
+ }
+ // consider an empty result to be an error
+ if ( stringIsNotEmpty(this.responseText) === false ) {
+ return onError.call(null, details);
+ }
+ // we never download anything else than plain text: discard if response
+ // appears to be a HTML document: could happen when server serves
+ // some kind of error page I suppose
+ var text = this.responseText.trim();
+ if ( text.startsWith('<') && text.endsWith('>') ) {
+ return onError.call(null, details);
+ }
+ details.content = this.responseText;
+ return onLoad.call(null, details);
+ };
+
+ var onErrorReceived = function() {
+ this.onload = this.onerror = this.ontimeout = null;
+ µMatrix.logger.writeOne('', 'error', errorCantConnectTo.replace('{{msg}}', actualUrl));
+ onError.call(null, { url: url, content: '' });
+ };
+
+ // Be ready for thrown exceptions:
+ // I am pretty sure it used to work, but now using a URL such as
+ // `file:///` on Chromium 40 results in an exception being thrown.
+ var xhr = new XMLHttpRequest();
+ try {
+ xhr.open('get', actualUrl, true);
+ xhr.timeout = 30000;
+ xhr.onload = onResponseReceived;
+ xhr.onerror = onErrorReceived;
+ xhr.ontimeout = onErrorReceived;
+ xhr.responseType = 'text';
+ xhr.send();
+ } catch (e) {
+ onErrorReceived.call(xhr);
+ }
+};
+
+/*******************************************************************************
+
+ TODO(seamless migration):
+ This block of code will be removed when I am confident all users have
+ moved to a version of uBO which does not require the old way of caching
+ assets.
+
+ api.listKeyAliases: a map of old asset keys to new asset keys.
+
+ migrate(): to seamlessly migrate the old cache manager to the new one:
+ - attempt to preserve and move content of cached assets to new locations;
+ - removes all traces of now obsolete cache manager entries in cacheStorage.
+
+ This code will typically execute only once, when the newer version of uBO
+ is first installed and executed.
+
+**/
+
+api.listKeyAliases = {
+ "assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat": "public_suffix_list.dat",
+ "assets/thirdparties/hosts-file.net/ad-servers": "hphosts",
+ "assets/thirdparties/www.malwaredomainlist.com/hostslist/hosts.txt": "malware-0",
+ "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains": "malware-1",
+ "assets/thirdparties/pgl.yoyo.org/as/serverlist": "plowe-0",
+ "assets/thirdparties/someonewhocares.org/hosts/hosts": "dpollock-0",
+ "assets/thirdparties/winhelp2002.mvps.org/hosts.txt": "mvps-0"
+};
+
+var migrate = function(callback) {
+ var entries,
+ moveCount = 0,
+ toRemove = [];
+
+ var countdown = function(change) {
+ moveCount -= (change || 0);
+ if ( moveCount !== 0 ) { return; }
+ vAPI.cacheStorage.remove(toRemove);
+ saveAssetCacheRegistry();
+ callback();
+ };
+
+ var onContentRead = function(oldKey, newKey, bin) {
+ var content = bin && bin['cached_asset_content://' + oldKey] || undefined;
+ if ( content ) {
+ assetCacheRegistry[newKey] = {
+ readTime: Date.now(),
+ writeTime: entries[oldKey]
+ };
+ if ( reIsExternalPath.test(oldKey) ) {
+ assetCacheRegistry[newKey].remoteURL = oldKey;
+ }
+ bin = {};
+ bin['cache/' + newKey] = content;
+ vAPI.cacheStorage.set(bin);
+ }
+ countdown(1);
+ };
+
+ var onEntries = function(bin) {
+ entries = bin && bin.cached_asset_entries;
+ if ( !entries ) { return callback(); }
+ if ( bin && bin.assetCacheRegistry ) {
+ assetCacheRegistry = bin.assetCacheRegistry;
+ }
+ var aliases = api.listKeyAliases;
+ for ( var oldKey in entries ) {
+ var newKey = aliases[oldKey];
+ if ( !newKey && /^https?:\/\//.test(oldKey) ) {
+ newKey = oldKey;
+ }
+ if ( newKey ) {
+ vAPI.cacheStorage.get(
+ 'cached_asset_content://' + oldKey,
+ onContentRead.bind(null, oldKey, newKey)
+ );
+ moveCount += 1;
+ }
+ toRemove.push('cached_asset_content://' + oldKey);
+ }
+ toRemove.push('cached_asset_entries', 'extensionLastVersion');
+ countdown();
+ };
+
+ vAPI.cacheStorage.get(
+ [ 'cached_asset_entries', 'assetCacheRegistry' ],
+ onEntries
+ );
+};
+
+/*******************************************************************************
+
+ The purpose of the asset source registry is to keep key detail information
+ about an asset:
+ - Where to load it from: this may consist of one or more URLs, either local
+ or remote.
+ - After how many days an asset should be deemed obsolete -- i.e. in need of
+ an update.
+ - The origin and type of an asset.
+ - The last time an asset was registered.
+
+**/
+
+var assetSourceRegistryStatus,
+ assetSourceRegistry = Object.create(null);
+
+var registerAssetSource = function(assetKey, dict) {
+ var entry = assetSourceRegistry[assetKey] || {};
+ for ( var prop in dict ) {
+ if ( dict.hasOwnProperty(prop) === false ) { continue; }
+ if ( dict[prop] === undefined ) {
+ delete entry[prop];
+ } else {
+ entry[prop] = dict[prop];
+ }
+ }
+ var contentURL = dict.contentURL;
+ if ( contentURL !== undefined ) {
+ if ( typeof contentURL === 'string' ) {
+ contentURL = entry.contentURL = [ contentURL ];
+ } else if ( Array.isArray(contentURL) === false ) {
+ contentURL = entry.contentURL = [];
+ }
+ var remoteURLCount = 0;
+ for ( var i = 0; i < contentURL.length; i++ ) {
+ if ( reIsExternalPath.test(contentURL[i]) ) {
+ remoteURLCount += 1;
+ }
+ }
+ entry.hasLocalURL = remoteURLCount !== contentURL.length;
+ entry.hasRemoteURL = remoteURLCount !== 0;
+ } else if ( entry.contentURL === undefined ) {
+ entry.contentURL = [];
+ }
+ if ( typeof entry.updateAfter !== 'number' ) {
+ entry.updateAfter = 13;
+ }
+ if ( entry.submitter ) {
+ entry.submitTime = Date.now(); // To detect stale entries
+ }
+ assetSourceRegistry[assetKey] = entry;
+};
+
+var unregisterAssetSource = function(assetKey) {
+ assetCacheRemove(assetKey);
+ delete assetSourceRegistry[assetKey];
+};
+
+var saveAssetSourceRegistry = (function() {
+ var timer;
+ var save = function() {
+ timer = undefined;
+ vAPI.cacheStorage.set({ assetSourceRegistry: assetSourceRegistry });
+ };
+ return function(lazily) {
+ if ( timer !== undefined ) {
+ clearTimeout(timer);
+ }
+ if ( lazily ) {
+ timer = vAPI.setTimeout(save, 500);
+ } else {
+ save();
+ }
+ };
+})();
+
+var updateAssetSourceRegistry = function(json, silent) {
+ var newDict;
+ try {
+ newDict = JSON.parse(json);
+ } catch (ex) {
+ }
+ if ( newDict instanceof Object === false ) { return; }
+
+ var oldDict = assetSourceRegistry,
+ assetKey;
+
+ // Remove obsolete entries (only those which were built-in).
+ for ( assetKey in oldDict ) {
+ if (
+ newDict[assetKey] === undefined &&
+ oldDict[assetKey].submitter === undefined
+ ) {
+ unregisterAssetSource(assetKey);
+ }
+ }
+ // Add/update existing entries. Notify of new asset sources.
+ for ( assetKey in newDict ) {
+ if ( oldDict[assetKey] === undefined && !silent ) {
+ fireNotification(
+ 'builtin-asset-source-added',
+ { assetKey: assetKey, entry: newDict[assetKey] }
+ );
+ }
+ registerAssetSource(assetKey, newDict[assetKey]);
+ }
+ saveAssetSourceRegistry();
+};
+
+var getAssetSourceRegistry = function(callback) {
+ // Already loaded.
+ if ( assetSourceRegistryStatus === 'ready' ) {
+ callback(assetSourceRegistry);
+ return;
+ }
+
+ // Being loaded.
+ if ( Array.isArray(assetSourceRegistryStatus) ) {
+ assetSourceRegistryStatus.push(callback);
+ return;
+ }
+
+ // Not loaded: load it.
+ assetSourceRegistryStatus = [ callback ];
+
+ var registryReady = function() {
+ var callers = assetSourceRegistryStatus;
+ assetSourceRegistryStatus = 'ready';
+ var fn;
+ while ( (fn = callers.shift()) ) {
+ fn(assetSourceRegistry);
+ }
+ };
+
+ // First-install case.
+ var createRegistry = function() {
+ api.fetchText(
+ µMatrix.assetsBootstrapLocation || 'assets/assets.json',
+ function(details) {
+ updateAssetSourceRegistry(details.content, true);
+ registryReady();
+ }
+ );
+ };
+
+ vAPI.cacheStorage.get('assetSourceRegistry', function(bin) {
+ if ( !bin || !bin.assetSourceRegistry ) {
+ createRegistry();
+ return;
+ }
+ assetSourceRegistry = bin.assetSourceRegistry;
+ registryReady();
+ });
+};
+
+api.registerAssetSource = function(assetKey, details) {
+ getAssetSourceRegistry(function() {
+ registerAssetSource(assetKey, details);
+ saveAssetSourceRegistry(true);
+ });
+};
+
+api.unregisterAssetSource = function(assetKey) {
+ getAssetSourceRegistry(function() {
+ unregisterAssetSource(assetKey);
+ saveAssetSourceRegistry(true);
+ });
+};
+
+/*******************************************************************************
+
+ The purpose of the asset cache registry is to keep track of all assets
+ which have been persisted into the local cache.
+
+**/
+
+var assetCacheRegistryStatus,
+ assetCacheRegistryStartTime = Date.now(),
+ assetCacheRegistry = {};
+
+var getAssetCacheRegistry = function(callback) {
+ // Already loaded.
+ if ( assetCacheRegistryStatus === 'ready' ) {
+ callback(assetCacheRegistry);
+ return;
+ }
+
+ // Being loaded.
+ if ( Array.isArray(assetCacheRegistryStatus) ) {
+ assetCacheRegistryStatus.push(callback);
+ return;
+ }
+
+ // Not loaded: load it.
+ assetCacheRegistryStatus = [ callback ];
+
+ var registryReady = function() {
+ var callers = assetCacheRegistryStatus;
+ assetCacheRegistryStatus = 'ready';
+ var fn;
+ while ( (fn = callers.shift()) ) {
+ fn(assetCacheRegistry);
+ }
+ };
+
+ var migrationDone = function() {
+ vAPI.cacheStorage.get('assetCacheRegistry', function(bin) {
+ if ( bin && bin.assetCacheRegistry ) {
+ assetCacheRegistry = bin.assetCacheRegistry;
+ }
+ registryReady();
+ });
+ };
+
+ migrate(migrationDone);
+};
+
+var saveAssetCacheRegistry = (function() {
+ var timer;
+ var save = function() {
+ timer = undefined;
+ vAPI.cacheStorage.set({ assetCacheRegistry: assetCacheRegistry });
+ };
+ return function(lazily) {
+ if ( timer !== undefined ) { clearTimeout(timer); }
+ if ( lazily ) {
+ timer = vAPI.setTimeout(save, 500);
+ } else {
+ save();
+ }
+ };
+})();
+
+var assetCacheRead = function(assetKey, callback) {
+ var internalKey = 'cache/' + assetKey;
+
+ var reportBack = function(content, err) {
+ var details = { assetKey: assetKey, content: content };
+ if ( err ) { details.error = err; }
+ callback(details);
+ };
+
+ var onAssetRead = function(bin) {
+ if ( !bin || !bin[internalKey] ) {
+ return reportBack('', 'E_NOTFOUND');
+ }
+ var entry = assetCacheRegistry[assetKey];
+ if ( entry === undefined ) {
+ return reportBack('', 'E_NOTFOUND');
+ }
+ entry.readTime = Date.now();
+ saveAssetCacheRegistry(true);
+ reportBack(bin[internalKey]);
+ };
+
+ var onReady = function() {
+ vAPI.cacheStorage.get(internalKey, onAssetRead);
+ };
+
+ getAssetCacheRegistry(onReady);
+};
+
+var assetCacheWrite = function(assetKey, details, callback) {
+ var internalKey = 'cache/' + assetKey;
+ var content = '';
+ if ( typeof details === 'string' ) {
+ content = details;
+ } else if ( details instanceof Object ) {
+ content = details.content || '';
+ }
+
+ if ( content === '' ) {
+ return assetCacheRemove(assetKey, callback);
+ }
+
+ var reportBack = function(content) {
+ var details = { assetKey: assetKey, content: content };
+ if ( typeof callback === 'function' ) {
+ callback(details);
+ }
+ fireNotification('after-asset-updated', details);
+ };
+
+ var onReady = function() {
+ var entry = assetCacheRegistry[assetKey];
+ if ( entry === undefined ) {
+ entry = assetCacheRegistry[assetKey] = {};
+ }
+ entry.writeTime = entry.readTime = Date.now();
+ if ( details instanceof Object && typeof details.url === 'string' ) {
+ entry.remoteURL = details.url;
+ }
+ var bin = { assetCacheRegistry: assetCacheRegistry };
+ bin[internalKey] = content;
+ vAPI.cacheStorage.set(bin);
+ reportBack(content);
+ };
+ getAssetCacheRegistry(onReady);
+};
+
+var assetCacheRemove = function(pattern, callback) {
+ var onReady = function() {
+ var cacheDict = assetCacheRegistry,
+ removedEntries = [],
+ removedContent = [];
+ for ( var assetKey in cacheDict ) {
+ if ( pattern instanceof RegExp && !pattern.test(assetKey) ) {
+ continue;
+ }
+ if ( typeof pattern === 'string' && assetKey !== pattern ) {
+ continue;
+ }
+ removedEntries.push(assetKey);
+ removedContent.push('cache/' + assetKey);
+ delete cacheDict[assetKey];
+ }
+ if ( removedContent.length !== 0 ) {
+ vAPI.cacheStorage.remove(removedContent);
+ var bin = { assetCacheRegistry: assetCacheRegistry };
+ vAPI.cacheStorage.set(bin);
+ }
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ for ( var i = 0; i < removedEntries.length; i++ ) {
+ fireNotification('after-asset-updated', { assetKey: removedEntries[i] });
+ }
+ };
+
+ getAssetCacheRegistry(onReady);
+};
+
+var assetCacheMarkAsDirty = function(pattern, exclude, callback) {
+ var onReady = function() {
+ var cacheDict = assetCacheRegistry,
+ cacheEntry,
+ mustSave = false;
+ for ( var assetKey in cacheDict ) {
+ if ( pattern instanceof RegExp ) {
+ if ( pattern.test(assetKey) === false ) { continue; }
+ } else if ( typeof pattern === 'string' ) {
+ if ( assetKey !== pattern ) { continue; }
+ } else if ( Array.isArray(pattern) ) {
+ if ( pattern.indexOf(assetKey) === -1 ) { continue; }
+ }
+ if ( exclude instanceof RegExp ) {
+ if ( exclude.test(assetKey) ) { continue; }
+ } else if ( typeof exclude === 'string' ) {
+ if ( assetKey === exclude ) { continue; }
+ } else if ( Array.isArray(exclude) ) {
+ if ( exclude.indexOf(assetKey) !== -1 ) { continue; }
+ }
+ cacheEntry = cacheDict[assetKey];
+ if ( !cacheEntry.writeTime ) { continue; }
+ cacheDict[assetKey].writeTime = 0;
+ mustSave = true;
+ }
+ if ( mustSave ) {
+ var bin = { assetCacheRegistry: assetCacheRegistry };
+ vAPI.cacheStorage.set(bin);
+ }
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ };
+ if ( typeof exclude === 'function' ) {
+ callback = exclude;
+ exclude = undefined;
+ }
+ getAssetCacheRegistry(onReady);
+};
+
+/******************************************************************************/
+
+var stringIsNotEmpty = function(s) {
+ return typeof s === 'string' && s !== '';
+};
+
+/******************************************************************************/
+
+api.get = function(assetKey, options, callback) {
+ if ( typeof options === 'function' ) {
+ callback = options;
+ options = {};
+ } else if ( typeof callback !== 'function' ) {
+ callback = noopfunc;
+ }
+
+ var assetDetails = {},
+ contentURLs,
+ contentURL;
+
+ var reportBack = function(content, err) {
+ var details = { assetKey: assetKey, content: content };
+ if ( err ) {
+ details.error = assetDetails.lastError = err;
+ } else {
+ assetDetails.lastError = undefined;
+ }
+ callback(details);
+ };
+
+ var onContentNotLoaded = function() {
+ var isExternal;
+ while ( (contentURL = contentURLs.shift()) ) {
+ isExternal = reIsExternalPath.test(contentURL);
+ if ( isExternal === false || assetDetails.hasLocalURL !== true ) {
+ break;
+ }
+ }
+ if ( !contentURL ) {
+ return reportBack('', 'E_NOTFOUND');
+ }
+ api.fetchText(contentURL, onContentLoaded, onContentNotLoaded);
+ };
+
+ var onContentLoaded = function(details) {
+ if ( stringIsNotEmpty(details.content) === false ) {
+ onContentNotLoaded();
+ return;
+ }
+ if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) {
+ assetCacheWrite(assetKey, {
+ content: details.content,
+ url: contentURL
+ });
+ }
+ reportBack(details.content);
+ };
+
+ var onCachedContentLoaded = function(details) {
+ if ( details.content !== '' ) {
+ return reportBack(details.content);
+ }
+ getAssetSourceRegistry(function(registry) {
+ assetDetails = registry[assetKey] || {};
+ if ( typeof assetDetails.contentURL === 'string' ) {
+ contentURLs = [ assetDetails.contentURL ];
+ } else if ( Array.isArray(assetDetails.contentURL) ) {
+ contentURLs = assetDetails.contentURL.slice(0);
+ } else {
+ contentURLs = [];
+ }
+ onContentNotLoaded();
+ });
+ };
+
+ assetCacheRead(assetKey, onCachedContentLoaded);
+};
+
+/******************************************************************************/
+
+var getRemote = function(assetKey, callback) {
+ var assetDetails = {},
+ contentURLs,
+ contentURL;
+
+ var reportBack = function(content, err) {
+ var details = { assetKey: assetKey, content: content };
+ if ( err ) {
+ details.error = assetDetails.lastError = err;
+ } else {
+ assetDetails.lastError = undefined;
+ }
+ callback(details);
+ };
+
+ var onRemoteContentLoaded = function(details) {
+ if ( stringIsNotEmpty(details.content) === false ) {
+ registerAssetSource(assetKey, { error: { time: Date.now(), error: 'No content' } });
+ tryLoading();
+ return;
+ }
+ assetCacheWrite(assetKey, {
+ content: details.content,
+ url: contentURL
+ });
+ registerAssetSource(assetKey, { error: undefined });
+ reportBack(details.content);
+ };
+
+ var onRemoteContentError = function(details) {
+ var text = details.statusText;
+ if ( details.statusCode === 0 ) {
+ text = 'network error';
+ }
+ registerAssetSource(assetKey, { error: { time: Date.now(), error: text } });
+ tryLoading();
+ };
+
+ var tryLoading = function() {
+ while ( (contentURL = contentURLs.shift()) ) {
+ if ( reIsExternalPath.test(contentURL) ) { break; }
+ }
+ if ( !contentURL ) {
+ return reportBack('', 'E_NOTFOUND');
+ }
+ api.fetchText(contentURL, onRemoteContentLoaded, onRemoteContentError);
+ };
+
+ getAssetSourceRegistry(function(registry) {
+ assetDetails = registry[assetKey] || {};
+ if ( typeof assetDetails.contentURL === 'string' ) {
+ contentURLs = [ assetDetails.contentURL ];
+ } else if ( Array.isArray(assetDetails.contentURL) ) {
+ contentURLs = assetDetails.contentURL.slice(0);
+ } else {
+ contentURLs = [];
+ }
+ tryLoading();
+ });
+};
+
+/******************************************************************************/
+
+api.put = function(assetKey, content, callback) {
+ assetCacheWrite(assetKey, content, callback);
+};
+
+/******************************************************************************/
+
+api.metadata = function(callback) {
+ var assetRegistryReady = false,
+ cacheRegistryReady = false;
+
+ var onReady = function() {
+ var assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)),
+ cacheDict = assetCacheRegistry,
+ assetEntry, cacheEntry,
+ now = Date.now(), obsoleteAfter;
+ for ( var assetKey in assetDict ) {
+ assetEntry = assetDict[assetKey];
+ cacheEntry = cacheDict[assetKey];
+ if ( cacheEntry ) {
+ assetEntry.cached = true;
+ assetEntry.writeTime = cacheEntry.writeTime;
+ obsoleteAfter = cacheEntry.writeTime + assetEntry.updateAfter * 86400000;
+ assetEntry.obsolete = obsoleteAfter < now;
+ assetEntry.remoteURL = cacheEntry.remoteURL;
+ } else {
+ assetEntry.writeTime = 0;
+ obsoleteAfter = 0;
+ assetEntry.obsolete = true;
+ }
+ }
+ callback(assetDict);
+ };
+
+ getAssetSourceRegistry(function() {
+ assetRegistryReady = true;
+ if ( cacheRegistryReady ) { onReady(); }
+ });
+
+ getAssetCacheRegistry(function() {
+ cacheRegistryReady = true;
+ if ( assetRegistryReady ) { onReady(); }
+ });
+};
+
+/******************************************************************************/
+
+api.purge = assetCacheMarkAsDirty;
+
+api.remove = function(pattern, callback) {
+ assetCacheRemove(pattern, callback);
+};
+
+api.rmrf = function() {
+ assetCacheRemove(/./);
+};
+
+/******************************************************************************/
+
+// Asset updater area.
+var updaterStatus,
+ updaterTimer,
+ updaterAssetDelayDefault = 120000,
+ updaterAssetDelay = updaterAssetDelayDefault,
+ updaterUpdated = [],
+ updaterFetched = new Set();
+
+var updateFirst = function() {
+ updaterStatus = 'updating';
+ updaterFetched.clear();
+ updaterUpdated = [];
+ fireNotification('before-assets-updated');
+ updateNext();
+};
+
+var updateNext = function() {
+ var assetDict, cacheDict;
+
+ // This will remove a cached asset when it's no longer in use.
+ var garbageCollectOne = function(assetKey) {
+ var cacheEntry = cacheDict[assetKey];
+ if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) {
+ assetCacheRemove(assetKey);
+ }
+ };
+
+ var findOne = function() {
+ var now = Date.now(),
+ assetEntry, cacheEntry;
+ for ( var assetKey in assetDict ) {
+ assetEntry = assetDict[assetKey];
+ if ( assetEntry.hasRemoteURL !== true ) { continue; }
+ if ( updaterFetched.has(assetKey) ) { continue; }
+ cacheEntry = cacheDict[assetKey];
+ if ( cacheEntry && (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now ) {
+ continue;
+ }
+ if ( fireNotification('before-asset-updated', { assetKey: assetKey }) !== false ) {
+ return assetKey;
+ }
+ garbageCollectOne(assetKey);
+ }
+ };
+
+ var updatedOne = function(details) {
+ if ( details.content !== '' ) {
+ updaterUpdated.push(details.assetKey);
+ if ( details.assetKey === 'assets.json' ) {
+ updateAssetSourceRegistry(details.content);
+ }
+ } else {
+ fireNotification('asset-update-failed', { assetKey: details.assetKey });
+ }
+ if ( findOne() !== undefined ) {
+ vAPI.setTimeout(updateNext, updaterAssetDelay);
+ } else {
+ updateDone();
+ }
+ };
+
+ var updateOne = function() {
+ var assetKey = findOne();
+ if ( assetKey === undefined ) {
+ return updateDone();
+ }
+ updaterFetched.add(assetKey);
+ getRemote(assetKey, updatedOne);
+ };
+
+ getAssetSourceRegistry(function(dict) {
+ assetDict = dict;
+ if ( !cacheDict ) { return; }
+ updateOne();
+ });
+
+ getAssetCacheRegistry(function(dict) {
+ cacheDict = dict;
+ if ( !assetDict ) { return; }
+ updateOne();
+ });
+};
+
+var updateDone = function() {
+ var assetKeys = updaterUpdated.slice(0);
+ updaterFetched.clear();
+ updaterUpdated = [];
+ updaterStatus = undefined;
+ updaterAssetDelay = updaterAssetDelayDefault;
+ fireNotification('after-assets-updated', { assetKeys: assetKeys });
+};
+
+api.updateStart = function(details) {
+ var oldUpdateDelay = updaterAssetDelay,
+ newUpdateDelay = details.delay || updaterAssetDelayDefault;
+ updaterAssetDelay = Math.min(oldUpdateDelay, newUpdateDelay);
+ if ( updaterStatus !== undefined ) {
+ if ( newUpdateDelay < oldUpdateDelay ) {
+ clearTimeout(updaterTimer);
+ updaterTimer = vAPI.setTimeout(updateNext, updaterAssetDelay);
+ }
+ return;
+ }
+ updateFirst();
+};
+
+api.updateStop = function() {
+ if ( updaterTimer ) {
+ clearTimeout(updaterTimer);
+ updaterTimer = undefined;
+ }
+ if ( updaterStatus !== undefined ) {
+ updateDone();
+ }
+};
+
+/******************************************************************************/
+
+return api;
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/background.js b/js/background.js
new file mode 100644
index 0000000..1a46616
--- /dev/null
+++ b/js/background.js
@@ -0,0 +1,246 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+var µMatrix = (function() { // jshint ignore:line
+
+/******************************************************************************/
+
+var oneSecond = 1000;
+var oneMinute = 60 * oneSecond;
+var oneHour = 60 * oneMinute;
+var oneDay = 24 * oneHour;
+
+/******************************************************************************/
+/******************************************************************************/
+
+var _RequestStats = function() {
+ this.reset();
+};
+
+_RequestStats.prototype.reset = function() {
+ this.all =
+ this.doc =
+ this.frame =
+ this.script =
+ this.css =
+ this.image =
+ this.media =
+ this.xhr =
+ this.other =
+ this.cookie = 0;
+};
+
+/******************************************************************************/
+
+var RequestStats = function() {
+ this.allowed = new _RequestStats();
+ this.blocked = new _RequestStats();
+};
+
+RequestStats.prototype.reset = function() {
+ this.blocked.reset();
+ this.allowed.reset();
+};
+
+RequestStats.prototype.record = function(type, blocked) {
+ // Remember: always test against **false**
+ if ( blocked !== false ) {
+ this.blocked[type] += 1;
+ this.blocked.all += 1;
+ } else {
+ this.allowed[type] += 1;
+ this.allowed.all += 1;
+ }
+};
+
+var requestStatsFactory = function() {
+ return new RequestStats();
+};
+
+/*******************************************************************************
+
+ SVG-based icons below were extracted from
+ fontawesome-webfont.svg v4.7. Excerpt of copyright notice at
+ the top of the file:
+
+ > Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016
+ > By ,,,
+ > Copyright Dave Gandy 2016. All rights reserved.
+
+ Excerpt of the license information in the fontawesome CSS
+ file bundled with the package:
+
+ > Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ > License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+
+ Font icons:
+ - glyph-name: "external_link"
+
+*/
+
+var rawSettingsDefault = {
+ disableCSPReportInjection: false,
+ placeholderBackground:
+ [
+ 'url("data:image/png;base64,',
+ 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAK',
+ 'CAAAAACoWZBhAAAABGdBTUEAALGPC/xh',
+ 'BQAAAAJiS0dEAP+Hj8y/AAAAB3RJTUUH',
+ '3wwIAAgyL/YaPAAAACJJREFUCFtjfMbO',
+ 'AAQ/gZiFnQPEBAEmGIMIJgtIL8QEgtoA',
+ 'In4D/96X1KAAAAAldEVYdGRhdGU6Y3Jl',
+ 'YXRlADIwMTUtMTItMDhUMDA6MDg6NTAr',
+ 'MDM6MDAasuuJAAAAJXRFWHRkYXRlOm1v',
+ 'ZGlmeQAyMDE1LTEyLTA4VDAwOjA4OjUw',
+ 'KzAzOjAwa+9TNQAAAABJRU5ErkJggg==',
+ '") ',
+ 'repeat scroll #fff'
+ ].join(''),
+ placeholderBorder: '1px solid rgba(0, 0, 0, 0.1)',
+ imagePlaceholder: true,
+ imagePlaceholderBackground: 'default',
+ imagePlaceholderBorder: 'default',
+ framePlaceholder: true,
+ framePlaceholderDocument:
+ [
+ '<html><head>',
+ '<meta charset="utf-8">',
+ '<style>',
+ 'body { ',
+ 'background: {{bg}};',
+ 'color: gray;',
+ 'font: 12px sans-serif;',
+ 'margin: 0;',
+ 'overflow: hidden;',
+ 'padding: 2px;',
+ 'white-space: nowrap;',
+ '}',
+ 'a { ',
+ 'color: inherit;',
+ 'padding: 0 3px;',
+ 'text-decoration: none;',
+ '}',
+ 'svg {',
+ 'display: inline-block;',
+ 'fill: gray;',
+ 'height: 12px;',
+ 'vertical-align: bottom;',
+ 'width: 12px;',
+ '}',
+ '</style></head><body>',
+ '<span><a href="{{url}}" title="{{url}}" target="_blank">',
+ '<svg viewBox="0 0 1792 1792"><path transform="scale(1,-1) translate(0,-1536)" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" /></svg>',
+ '</a>{{url}}</span>',
+ '</body></html>'
+ ].join(''),
+ framePlaceholderBackground: 'default',
+};
+
+/******************************************************************************/
+
+return {
+ onBeforeStartQueue: [],
+
+ userSettings: {
+ alwaysDetachLogger: false,
+ autoUpdate: false,
+ clearBrowserCache: true,
+ clearBrowserCacheAfter: 60,
+ cloudStorageEnabled: false,
+ collapseBlacklisted: true,
+ collapseBlocked: false,
+ colorBlindFriendly: false,
+ deleteCookies: false,
+ deleteUnusedSessionCookies: false,
+ deleteUnusedSessionCookiesAfter: 60,
+ deleteLocalStorage: false,
+ displayTextSize: '14px',
+ externalHostsFiles: '',
+ iconBadgeEnabled: false,
+ maxLoggedRequests: 1000,
+ popupCollapseAllDomains: false,
+ popupCollapseBlacklistedDomains: false,
+ popupScopeLevel: 'domain',
+ processHyperlinkAuditing: true,
+ processReferer: false
+ },
+
+ rawSettingsDefault: rawSettingsDefault,
+ rawSettings: Object.assign({}, rawSettingsDefault),
+ rawSettingsWriteTime: 0,
+
+ clearBrowserCacheCycle: 0,
+ cspNoInlineScript: "script-src 'unsafe-eval' blob: *",
+ cspNoInlineStyle: "style-src blob: *",
+ cspNoWorker: undefined,
+ updateAssetsEvery: 11 * oneDay + 1 * oneHour + 1 * oneMinute + 1 * oneSecond,
+ firstUpdateAfter: 11 * oneMinute,
+ nextUpdateAfter: 11 * oneHour,
+ assetsBootstrapLocation: 'assets/assets.json',
+ pslAssetKey: 'public_suffix_list.dat',
+
+ // list of live hosts files
+ liveHostsFiles: {
+ },
+
+ // urls stats are kept on the back burner while waiting to be reactivated
+ // in a tab or another.
+ pageStores: {},
+ pageStoresToken: 0,
+ pageStoreCemetery: {},
+
+ // page url => permission scope
+ tMatrix: null,
+ pMatrix: null,
+
+ ubiquitousBlacklist: null,
+
+ // various stats
+ requestStatsFactory: requestStatsFactory,
+ requestStats: requestStatsFactory(),
+ cookieRemovedCounter: 0,
+ localStorageRemovedCounter: 0,
+ cookieHeaderFoiledCounter: 0,
+ refererHeaderFoiledCounter: 0,
+ hyperlinkAuditingFoiledCounter: 0,
+ browserCacheClearedCounter: 0,
+ storageUsed: 0,
+
+ // record what the browser is doing behind the scene
+ behindTheSceneScope: 'behind-the-scene',
+
+ noopFunc: function(){},
+
+ // so that I don't have to care for last comma
+ dummy: 0
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
diff --git a/js/browsercache.js b/js/browsercache.js
new file mode 100644
index 0000000..3316a63
--- /dev/null
+++ b/js/browsercache.js
@@ -0,0 +1,65 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2015-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global µMatrix */
+
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+// Browser data jobs
+
+var clearCache = function() {
+ vAPI.setTimeout(clearCache, 15 * 60 * 1000);
+
+ var µm = µMatrix;
+ if ( !µm.userSettings.clearBrowserCache ) {
+ return;
+ }
+
+ µm.clearBrowserCacheCycle -= 15;
+ if ( µm.clearBrowserCacheCycle > 0 ) {
+ return;
+ }
+
+ vAPI.browserData.clearCache();
+
+ µm.clearBrowserCacheCycle = µm.userSettings.clearBrowserCacheAfter;
+ µm.browserCacheClearedCounter++;
+
+ // TODO: i18n
+ µm.logger.writeOne('', 'info', vAPI.i18n('loggerEntryBrowserCacheCleared'));
+
+ //console.debug('clearBrowserCacheCallback()> vAPI.browserData.clearCache() called');
+};
+
+vAPI.setTimeout(clearCache, 15 * 60 * 1000);
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/cloud-ui.js b/js/cloud-ui.js
new file mode 100644
index 0000000..a017ae9
--- /dev/null
+++ b/js/cloud-ui.js
@@ -0,0 +1,214 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2015-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uBlock
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+self.cloud = {
+ options: {},
+ datakey: '',
+ data: undefined,
+ onPush: null,
+ onPull: null
+};
+
+/******************************************************************************/
+
+var widget = uDom.nodeFromId('cloudWidget');
+if ( widget === null ) {
+ return;
+}
+
+self.cloud.datakey = widget.getAttribute('data-cloud-entry') || '';
+if ( self.cloud.datakey === '' ) {
+ return;
+}
+
+/******************************************************************************/
+
+var onCloudDataReceived = function(entry) {
+ if ( typeof entry !== 'object' || entry === null ) {
+ return;
+ }
+
+ self.cloud.data = entry.data;
+
+ uDom.nodeFromId('cloudPull').removeAttribute('disabled');
+ uDom.nodeFromId('cloudPullAndMerge').removeAttribute('disabled');
+
+ var timeOptions = {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ timeZoneName: 'short'
+ };
+
+ var time = new Date(entry.tstamp);
+ widget.querySelector('span').textContent =
+ entry.source + '\n' +
+ time.toLocaleString('fullwide', timeOptions);
+};
+
+/******************************************************************************/
+
+var fetchCloudData = function() {
+ vAPI.messaging.send(
+ 'cloud-ui.js',
+ {
+ what: 'cloudPull',
+ datakey: self.cloud.datakey
+ },
+ onCloudDataReceived
+ );
+};
+
+/******************************************************************************/
+
+var pushData = function() {
+ if ( typeof self.cloud.onPush !== 'function' ) {
+ return;
+ }
+ vAPI.messaging.send(
+ 'cloud-ui.js',
+ {
+ what: 'cloudPush',
+ datakey: self.cloud.datakey,
+ data: self.cloud.onPush()
+ },
+ fetchCloudData
+ );
+};
+
+/******************************************************************************/
+
+var pullData = function(ev) {
+ if ( typeof self.cloud.onPull === 'function' ) {
+ self.cloud.onPull(self.cloud.data, ev.shiftKey);
+ }
+};
+
+/******************************************************************************/
+
+var pullAndMergeData = function() {
+ if ( typeof self.cloud.onPull === 'function' ) {
+ self.cloud.onPull(self.cloud.data, true);
+ }
+};
+
+/******************************************************************************/
+
+var openOptions = function() {
+ var input = uDom.nodeFromId('cloudDeviceName');
+ input.value = self.cloud.options.deviceName;
+ input.setAttribute('placeholder', self.cloud.options.defaultDeviceName);
+ uDom.nodeFromId('cloudOptions').classList.add('show');
+};
+
+/******************************************************************************/
+
+var closeOptions = function(ev) {
+ var root = uDom.nodeFromId('cloudOptions');
+ if ( ev.target !== root ) {
+ return;
+ }
+ root.classList.remove('show');
+};
+
+/******************************************************************************/
+
+var submitOptions = function() {
+ var onOptions = function(options) {
+ if ( typeof options !== 'object' || options === null ) {
+ return;
+ }
+ self.cloud.options = options;
+ };
+
+ vAPI.messaging.send('cloud-ui.js', {
+ what: 'cloudSetOptions',
+ options: {
+ deviceName: uDom.nodeFromId('cloudDeviceName').value
+ }
+ }, onOptions);
+ uDom.nodeFromId('cloudOptions').classList.remove('show');
+};
+
+/******************************************************************************/
+
+var onInitialize = function(options) {
+ if ( typeof options !== 'object' || options === null ) {
+ return;
+ }
+
+ if ( !options.enabled ) {
+ return;
+ }
+ self.cloud.options = options;
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', 'cloud-ui.html', true);
+ xhr.overrideMimeType('text/html;charset=utf-8');
+ xhr.responseType = 'text';
+ xhr.onload = function() {
+ this.onload = null;
+ var parser = new DOMParser(),
+ parsed = parser.parseFromString(this.responseText, 'text/html'),
+ fromParent = parsed.body;
+ while ( fromParent.firstElementChild !== null ) {
+ widget.appendChild(
+ document.adoptNode(fromParent.firstElementChild)
+ );
+ }
+
+ vAPI.i18n.render(widget);
+ widget.classList.remove('hide');
+
+ uDom('#cloudPush').on('click', pushData);
+ uDom('#cloudPull').on('click', pullData);
+ uDom('#cloudPullAndMerge').on('click', pullAndMergeData);
+ uDom('#cloudCog').on('click', openOptions);
+ uDom('#cloudOptions').on('click', closeOptions);
+ uDom('#cloudOptionsSubmit').on('click', submitOptions);
+
+ fetchCloudData();
+ };
+ xhr.send();
+};
+
+vAPI.messaging.send('cloud-ui.js', { what: 'cloudGetOptions' }, onInitialize);
+
+/******************************************************************************/
+
+// https://www.youtube.com/watch?v=aQFp67VoiDA
+
+})();
diff --git a/js/contentscript-start.js b/js/contentscript-start.js
new file mode 100644
index 0000000..c449c55
--- /dev/null
+++ b/js/contentscript-start.js
@@ -0,0 +1,97 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2017-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Injected into content pages
+
+(function() {
+
+ if ( typeof vAPI !== 'object' ) { return; }
+
+ vAPI.selfWorkerSrcReported = vAPI.selfWorkerSrcReported || false;
+
+ var reGoodWorkerSrc = /(?:child|worker)-src[^;,]+?'none'/;
+
+ var handler = function(ev) {
+ if (
+ ev.isTrusted !== true ||
+ ev.originalPolicy.includes('report-uri about:blank') === false
+ ) {
+ return false;
+ }
+
+ // Firefox and Chromium differs in how they fill the
+ // 'effectiveDirective' property.
+ if (
+ ev.effectiveDirective.startsWith('worker-src') === false &&
+ ev.effectiveDirective.startsWith('child-src') === false
+ ) {
+ return false;
+ }
+
+ // Further validate that the policy violation is relevant to uMatrix:
+ // the event still could have been fired as a result of a CSP header
+ // not injected by uMatrix.
+ if ( reGoodWorkerSrc.test(ev.originalPolicy) === false ) {
+ return false;
+ }
+
+ // We do not want to report internal resources more than once.
+ // However, we do want to report external resources each time.
+ // TODO: this could eventually lead to duplicated reports for external
+ // resources if another extension uses the same approach as
+ // uMatrix. Think about what could be done to avoid duplicate
+ // reports.
+ if ( ev.blockedURI.includes('://') === false ) {
+ if ( vAPI.selfWorkerSrcReported ) { return true; }
+ vAPI.selfWorkerSrcReported = true;
+ }
+
+ vAPI.messaging.send(
+ 'contentscript.js',
+ {
+ what: 'securityPolicyViolation',
+ directive: 'worker-src',
+ blockedURI: ev.blockedURI,
+ documentURI: ev.documentURI,
+ blocked: ev.disposition === 'enforce'
+ }
+ );
+
+ return true;
+ };
+
+ document.addEventListener(
+ 'securitypolicyviolation',
+ function(ev) {
+ if ( !handler(ev) ) { return; }
+ ev.stopPropagation();
+ ev.preventDefault();
+ },
+ true
+ );
+
+})();
diff --git a/js/contentscript.js b/js/contentscript.js
new file mode 100644
index 0000000..dcdd473
--- /dev/null
+++ b/js/contentscript.js
@@ -0,0 +1,541 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global HTMLDocument, XMLDocument */
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Injected into content pages
+
+(function() {
+
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/464
+// https://github.com/gorhill/uMatrix/issues/621
+if (
+ document instanceof HTMLDocument === false &&
+ document instanceof XMLDocument === false
+) {
+ return;
+}
+
+// This can also happen (for example if script injected into a `data:` URI doc)
+if ( !window.location ) {
+ return;
+}
+
+// This can happen
+if ( typeof vAPI !== 'object' ) {
+ //console.debug('contentscript.js > vAPI not found');
+ return;
+}
+
+// https://github.com/chrisaljoudi/uBlock/issues/456
+// Already injected?
+if ( vAPI.contentscriptEndInjected ) {
+ //console.debug('contentscript.js > content script already injected');
+ return;
+}
+vAPI.contentscriptEndInjected = true;
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Executed only once.
+
+(function() {
+ var localStorageHandler = function(mustRemove) {
+ if ( mustRemove ) {
+ window.localStorage.clear();
+ window.sessionStorage.clear();
+ }
+ };
+
+ // Check with extension whether local storage must be emptied
+ // rhill 2014-03-28: we need an exception handler in case 3rd-party access
+ // to site data is disabled.
+ // https://github.com/gorhill/httpswitchboard/issues/215
+ try {
+ var hasLocalStorage =
+ window.localStorage && window.localStorage.length !== 0;
+ var hasSessionStorage =
+ window.sessionStorage && window.sessionStorage.length !== 0;
+ if ( hasLocalStorage || hasSessionStorage ) {
+ vAPI.messaging.send('contentscript.js', {
+ what: 'contentScriptHasLocalStorage',
+ originURL: window.location.origin
+ }, localStorageHandler);
+ }
+
+ // TODO: indexedDB
+ //if ( window.indexedDB && !!window.indexedDB.webkitGetDatabaseNames ) {
+ // var db = window.indexedDB.webkitGetDatabaseNames().onsuccess = function(sender) {
+ // console.debug('webkitGetDatabaseNames(): result=%o', sender.target.result);
+ // };
+ //}
+
+ // TODO: Web SQL
+ // if ( window.openDatabase ) {
+ // Sad:
+ // "There is no way to enumerate or delete the databases available for an origin from this API."
+ // Ref.: http://www.w3.org/TR/webdatabase/#databases
+ // }
+ }
+ catch (e) {
+ }
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/gorhill/uMatrix/issues/45
+
+var collapser = (function() {
+ var resquestIdGenerator = 1,
+ processTimer,
+ toProcess = [],
+ toFilter = [],
+ toCollapse = new Map(),
+ cachedBlockedMap,
+ cachedBlockedMapHash,
+ cachedBlockedMapTimer,
+ reURLPlaceholder = /\{\{url\}\}/g;
+ var src1stProps = {
+ 'embed': 'src',
+ 'iframe': 'src',
+ 'img': 'src',
+ 'object': 'data'
+ };
+ var src2ndProps = {
+ 'img': 'srcset'
+ };
+ var tagToTypeMap = {
+ embed: 'media',
+ iframe: 'frame',
+ img: 'image',
+ object: 'media'
+ };
+ var cachedBlockedSetClear = function() {
+ cachedBlockedMap =
+ cachedBlockedMapHash =
+ cachedBlockedMapTimer = undefined;
+ };
+
+ // https://github.com/chrisaljoudi/uBlock/issues/174
+ // Do not remove fragment from src URL
+ var onProcessed = function(response) {
+ if ( !response ) { // This happens if uBO is disabled or restarted.
+ toCollapse.clear();
+ return;
+ }
+
+ var targets = toCollapse.get(response.id);
+ if ( targets === undefined ) { return; }
+ toCollapse.delete(response.id);
+ if ( cachedBlockedMapHash !== response.hash ) {
+ cachedBlockedMap = new Map(response.blockedResources);
+ cachedBlockedMapHash = response.hash;
+ if ( cachedBlockedMapTimer !== undefined ) {
+ clearTimeout(cachedBlockedMapTimer);
+ }
+ cachedBlockedMapTimer = vAPI.setTimeout(cachedBlockedSetClear, 30000);
+ }
+ if ( cachedBlockedMap === undefined || cachedBlockedMap.size === 0 ) {
+ return;
+ }
+
+ var placeholders = response.placeholders,
+ tag, prop, src, collapsed, docurl, replaced;
+
+ for ( var target of targets ) {
+ tag = target.localName;
+ prop = src1stProps[tag];
+ if ( prop === undefined ) { continue; }
+ src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) {
+ prop = src2ndProps[tag];
+ if ( prop === undefined ) { continue; }
+ src = target[prop];
+ if ( typeof src !== 'string' || src.length === 0 ) { continue; }
+ }
+ collapsed = cachedBlockedMap.get(tagToTypeMap[tag] + ' ' + src);
+ if ( collapsed === undefined ) { continue; }
+ if ( collapsed ) {
+ target.style.setProperty('display', 'none', 'important');
+ target.hidden = true;
+ continue;
+ }
+ switch ( tag ) {
+ case 'iframe':
+ if ( placeholders.frame !== true ) { break; }
+ docurl =
+ 'data:text/html,' +
+ encodeURIComponent(
+ placeholders.frameDocument.replace(
+ reURLPlaceholder,
+ src
+ )
+ );
+ replaced = false;
+ // Using contentWindow.location prevent tainting browser
+ // history -- i.e. breaking back button (seen on Chromium).
+ if ( target.contentWindow ) {
+ try {
+ target.contentWindow.location.replace(docurl);
+ replaced = true;
+ } catch(ex) {
+ }
+ }
+ if ( !replaced ) {
+ target.setAttribute('src', docurl);
+ }
+ break;
+ case 'img':
+ if ( placeholders.image !== true ) { break; }
+ target.style.setProperty('display', 'inline-block');
+ target.style.setProperty('min-width', '20px', 'important');
+ target.style.setProperty('min-height', '20px', 'important');
+ target.style.setProperty(
+ 'border',
+ placeholders.imageBorder,
+ 'important'
+ );
+ target.style.setProperty(
+ 'background',
+ placeholders.imageBackground,
+ 'important'
+ );
+ break;
+ }
+ }
+ };
+
+ var send = function() {
+ processTimer = undefined;
+ toCollapse.set(resquestIdGenerator, toProcess);
+ var msg = {
+ what: 'lookupBlockedCollapsibles',
+ id: resquestIdGenerator,
+ toFilter: toFilter,
+ hash: cachedBlockedMapHash
+ };
+ vAPI.messaging.send('contentscript.js', msg, onProcessed);
+ toProcess = [];
+ toFilter = [];
+ resquestIdGenerator += 1;
+ };
+
+ var process = function(delay) {
+ if ( toProcess.length === 0 ) { return; }
+ if ( delay === 0 ) {
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ }
+ send();
+ } else if ( processTimer === undefined ) {
+ processTimer = vAPI.setTimeout(send, delay || 47);
+ }
+ };
+
+ var add = function(target) {
+ toProcess.push(target);
+ };
+
+ var addMany = function(targets) {
+ var i = targets.length;
+ while ( i-- ) {
+ toProcess.push(targets[i]);
+ }
+ };
+
+ var iframeSourceModified = function(mutations) {
+ var i = mutations.length;
+ while ( i-- ) {
+ addIFrame(mutations[i].target, true);
+ }
+ process();
+ };
+ var iframeSourceObserver;
+ var iframeSourceObserverOptions = {
+ attributes: true,
+ attributeFilter: [ 'src' ]
+ };
+
+ var addIFrame = function(iframe, dontObserve) {
+ // https://github.com/gorhill/uBlock/issues/162
+ // Be prepared to deal with possible change of src attribute.
+ if ( dontObserve !== true ) {
+ if ( iframeSourceObserver === undefined ) {
+ iframeSourceObserver = new MutationObserver(iframeSourceModified);
+ }
+ iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
+ }
+ var src = iframe.src;
+ if ( src === '' || typeof src !== 'string' ) { return; }
+ if ( src.startsWith('http') === false ) { return; }
+ toFilter.push({ type: 'frame', url: iframe.src });
+ add(iframe);
+ };
+
+ var addIFrames = function(iframes) {
+ var i = iframes.length;
+ while ( i-- ) {
+ addIFrame(iframes[i]);
+ }
+ };
+
+ var addNodeList = function(nodeList) {
+ var node,
+ i = nodeList.length;
+ while ( i-- ) {
+ node = nodeList[i];
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( node.localName === 'iframe' ) {
+ addIFrame(node);
+ }
+ if ( node.childElementCount !== 0 ) {
+ addIFrames(node.querySelectorAll('iframe'));
+ }
+ }
+ };
+
+ var onResourceFailed = function(ev) {
+ if ( tagToTypeMap[ev.target.localName] !== undefined ) {
+ add(ev.target);
+ process();
+ }
+ };
+ document.addEventListener('error', onResourceFailed, true);
+
+ vAPI.shutdown.add(function() {
+ document.removeEventListener('error', onResourceFailed, true);
+ if ( iframeSourceObserver !== undefined ) {
+ iframeSourceObserver.disconnect();
+ iframeSourceObserver = undefined;
+ }
+ if ( processTimer !== undefined ) {
+ clearTimeout(processTimer);
+ processTimer = undefined;
+ }
+ });
+
+ return {
+ addMany: addMany,
+ addIFrames: addIFrames,
+ addNodeList: addNodeList,
+ process: process
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Observe changes in the DOM
+
+// Added node lists will be cumulated here before being processed
+
+(function() {
+ // This fixes http://acid3.acidtests.org/
+ if ( !document.body ) { return; }
+
+ var addedNodeLists = [];
+ var addedNodeListsTimer;
+
+ var treeMutationObservedHandler = function() {
+ addedNodeListsTimer = undefined;
+ var i = addedNodeLists.length;
+ while ( i-- ) {
+ collapser.addNodeList(addedNodeLists[i]);
+ }
+ collapser.process();
+ addedNodeLists = [];
+ };
+
+ // https://github.com/gorhill/uBlock/issues/205
+ // Do not handle added node directly from within mutation observer.
+ var treeMutationObservedHandlerAsync = function(mutations) {
+ var iMutation = mutations.length,
+ nodeList;
+ while ( iMutation-- ) {
+ nodeList = mutations[iMutation].addedNodes;
+ if ( nodeList.length !== 0 ) {
+ addedNodeLists.push(nodeList);
+ }
+ }
+ if ( addedNodeListsTimer === undefined ) {
+ addedNodeListsTimer = vAPI.setTimeout(treeMutationObservedHandler, 47);
+ }
+ };
+
+ // https://github.com/gorhill/httpswitchboard/issues/176
+ var treeObserver = new MutationObserver(treeMutationObservedHandlerAsync);
+ treeObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ vAPI.shutdown.add(function() {
+ if ( addedNodeListsTimer !== undefined ) {
+ clearTimeout(addedNodeListsTimer);
+ addedNodeListsTimer = undefined;
+ }
+ if ( treeObserver !== null ) {
+ treeObserver.disconnect();
+ treeObserver = undefined;
+ }
+ addedNodeLists = [];
+ });
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Executed only once.
+//
+// https://github.com/gorhill/httpswitchboard/issues/25
+//
+// https://github.com/gorhill/httpswitchboard/issues/131
+// Looks for inline javascript also in at least one a[href] element.
+//
+// https://github.com/gorhill/uMatrix/issues/485
+// Mind "on..." attributes.
+//
+// https://github.com/gorhill/uMatrix/issues/924
+// Report inline styles.
+
+(function() {
+ if (
+ document.querySelector('script:not([src])') !== null ||
+ document.querySelector('a[href^="javascript:"]') !== null ||
+ document.querySelector('[onabort],[onblur],[oncancel],[oncanplay],[oncanplaythrough],[onchange],[onclick],[onclose],[oncontextmenu],[oncuechange],[ondblclick],[ondrag],[ondragend],[ondragenter],[ondragexit],[ondragleave],[ondragover],[ondragstart],[ondrop],[ondurationchange],[onemptied],[onended],[onerror],[onfocus],[oninput],[oninvalid],[onkeydown],[onkeypress],[onkeyup],[onload],[onloadeddata],[onloadedmetadata],[onloadstart],[onmousedown],[onmouseenter],[onmouseleave],[onmousemove],[onmouseout],[onmouseover],[onmouseup],[onwheel],[onpause],[onplay],[onplaying],[onprogress],[onratechange],[onreset],[onresize],[onscroll],[onseeked],[onseeking],[onselect],[onshow],[onstalled],[onsubmit],[onsuspend],[ontimeupdate],[ontoggle],[onvolumechange],[onwaiting],[onafterprint],[onbeforeprint],[onbeforeunload],[onhashchange],[onlanguagechange],[onmessage],[onoffline],[ononline],[onpagehide],[onpageshow],[onrejectionhandled],[onpopstate],[onstorage],[onunhandledrejection],[onunload],[oncopy],[oncut],[onpaste]') !== null
+ ) {
+ vAPI.messaging.send('contentscript.js', {
+ what: 'securityPolicyViolation',
+ directive: 'script-src',
+ documentURI: window.location.href
+ });
+ }
+
+ if ( document.querySelector('style,[style]') !== null ) {
+ vAPI.messaging.send('contentscript.js', {
+ what: 'securityPolicyViolation',
+ directive: 'style-src',
+ documentURI: window.location.href
+ });
+ }
+
+ collapser.addMany(document.querySelectorAll('img'));
+ collapser.addIFrames(document.querySelectorAll('iframe'));
+ collapser.process();
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Executed only once.
+
+// https://github.com/gorhill/uMatrix/issues/232
+// Force `display` property, Firefox is still affected by the issue.
+
+(function() {
+ var noscripts = document.querySelectorAll('noscript');
+ if ( noscripts.length === 0 ) { return; }
+
+ var redirectTimer,
+ reMetaContent = /^\s*(\d+)\s*;\s*url=(['"]?)([^'"]+)\2/i,
+ reSafeURL = /^https?:\/\//;
+
+ var autoRefresh = function(root) {
+ var meta = root.querySelector('meta[http-equiv="refresh"][content]');
+ if ( meta === null ) { return; }
+ var match = reMetaContent.exec(meta.getAttribute('content'));
+ if ( match === null || match[3].trim() === '' ) { return; }
+ var url = new URL(match[3], document.baseURI);
+ if ( reSafeURL.test(url.href) === false ) { return; }
+ redirectTimer = setTimeout(
+ function() {
+ location.assign(url.href);
+ },
+ parseInt(match[1], 10) * 1000 + 1
+ );
+ meta.parentNode.removeChild(meta);
+ };
+
+ var morphNoscript = function(from) {
+ if ( /^application\/(?:xhtml\+)?xml/.test(document.contentType) ) {
+ var to = document.createElement('span');
+ while ( from.firstChild !== null ) {
+ to.appendChild(from.firstChild);
+ }
+ return to;
+ }
+ var parser = new DOMParser();
+ var doc = parser.parseFromString(
+ '<span>' + from.textContent + '</span>',
+ 'text/html'
+ );
+ return document.adoptNode(doc.querySelector('span'));
+ };
+
+ var renderNoscriptTags = function(response) {
+ if ( response !== true ) { return; }
+ var parent, span;
+ for ( var noscript of noscripts ) {
+ parent = noscript.parentNode;
+ if ( parent === null ) { continue; }
+ span = morphNoscript(noscript);
+ span.style.setProperty('display', 'inline', 'important');
+ if ( redirectTimer === undefined ) {
+ autoRefresh(span);
+ }
+ parent.replaceChild(span, noscript);
+ }
+ };
+
+ vAPI.messaging.send(
+ 'contentscript.js',
+ { what: 'mustRenderNoscriptTags?' },
+ renderNoscriptTags
+ );
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.messaging.send(
+ 'contentscript.js',
+ { what: 'shutdown?' },
+ function(response) {
+ if ( response === true ) {
+ vAPI.shutdown.exec();
+ }
+ }
+);
+
+/******************************************************************************/
+/******************************************************************************/
+
+})();
diff --git a/js/cookies.js b/js/cookies.js
new file mode 100644
index 0000000..7626ad0
--- /dev/null
+++ b/js/cookies.js
@@ -0,0 +1,552 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2013-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+// rhill 2013-12-14: the whole cookie management has been rewritten so as
+// to avoid having to call chrome API whenever a single cookie changes, and
+// to record cookie for a web page *only* when its value changes.
+// https://github.com/gorhill/httpswitchboard/issues/79
+
+"use strict";
+
+/******************************************************************************/
+
+// Isolate from global namespace
+
+// Use cached-context approach rather than object-based approach, as details
+// of the implementation do not need to be visible
+
+µMatrix.cookieHunter = (function() {
+
+/******************************************************************************/
+
+var µm = µMatrix;
+
+var recordPageCookiesQueue = new Map();
+var removePageCookiesQueue = new Map();
+var removeCookieQueue = new Set();
+var cookieDict = new Map();
+var cookieEntryJunkyard = [];
+var processRemoveQueuePeriod = 2 * 60 * 1000;
+var processCleanPeriod = 10 * 60 * 1000;
+var processPageRecordQueueTimer = null;
+var processPageRemoveQueueTimer = null;
+
+/******************************************************************************/
+
+var CookieEntry = function(cookie) {
+ this.usedOn = new Set();
+ this.init(cookie);
+};
+
+CookieEntry.prototype.init = function(cookie) {
+ this.secure = cookie.secure;
+ this.session = cookie.session;
+ this.anySubdomain = cookie.domain.charAt(0) === '.';
+ this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain;
+ this.domain = µm.URI.domainFromHostname(this.hostname) || this.hostname;
+ this.path = cookie.path;
+ this.name = cookie.name;
+ this.value = cookie.value;
+ this.tstamp = Date.now();
+ this.usedOn.clear();
+ return this;
+};
+
+// Release anything which may consume too much memory
+
+CookieEntry.prototype.dispose = function() {
+ this.hostname = '';
+ this.domain = '';
+ this.path = '';
+ this.name = '';
+ this.value = '';
+ this.usedOn.clear();
+ return this;
+};
+
+/******************************************************************************/
+
+var addCookieToDict = function(cookie) {
+ var cookieKey = cookieKeyFromCookie(cookie),
+ cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) {
+ cookieEntry = cookieEntryJunkyard.pop();
+ if ( cookieEntry ) {
+ cookieEntry.init(cookie);
+ } else {
+ cookieEntry = new CookieEntry(cookie);
+ }
+ cookieDict.set(cookieKey, cookieEntry);
+ }
+ return cookieEntry;
+};
+
+/******************************************************************************/
+
+var addCookiesToDict = function(cookies) {
+ var i = cookies.length;
+ while ( i-- ) {
+ addCookieToDict(cookies[i]);
+ }
+};
+
+/******************************************************************************/
+
+var removeCookieFromDict = function(cookieKey) {
+ var cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) { return false; }
+ cookieDict.delete(cookieKey);
+ if ( cookieEntryJunkyard.length < 25 ) {
+ cookieEntryJunkyard.push(cookieEntry.dispose());
+ }
+ return true;
+};
+
+/******************************************************************************/
+
+var cookieKeyBuilder = [
+ '', // 0 = scheme
+ '://',
+ '', // 2 = domain
+ '', // 3 = path
+ '{',
+ '', // 5 = persistent or session
+ '-cookie:',
+ '', // 7 = name
+ '}'
+];
+
+var cookieKeyFromCookie = function(cookie) {
+ var cb = cookieKeyBuilder;
+ cb[0] = cookie.secure ? 'https' : 'http';
+ cb[2] = cookie.domain.charAt(0) === '.' ? cookie.domain.slice(1) : cookie.domain;
+ cb[3] = cookie.path;
+ cb[5] = cookie.session ? 'session' : 'persistent';
+ cb[7] = cookie.name;
+ return cb.join('');
+};
+
+var cookieKeyFromCookieURL = function(url, type, name) {
+ var µmuri = µm.URI.set(url);
+ var cb = cookieKeyBuilder;
+ cb[0] = µmuri.scheme;
+ cb[2] = µmuri.hostname;
+ cb[3] = µmuri.path;
+ cb[5] = type;
+ cb[7] = name;
+ return cb.join('');
+};
+
+/******************************************************************************/
+
+var cookieURLFromCookieEntry = function(entry) {
+ if ( !entry ) {
+ return '';
+ }
+ return (entry.secure ? 'https://' : 'http://') + entry.hostname + entry.path;
+};
+
+/******************************************************************************/
+
+var cookieMatchDomains = function(cookieKey, allHostnamesString) {
+ var cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) { return false; }
+ if ( allHostnamesString.indexOf(' ' + cookieEntry.hostname + ' ') < 0 ) {
+ if ( !cookieEntry.anySubdomain ) {
+ return false;
+ }
+ if ( allHostnamesString.indexOf('.' + cookieEntry.hostname + ' ') < 0 ) {
+ return false;
+ }
+ }
+ return true;
+};
+
+/******************************************************************************/
+
+// Look for cookies to record for a specific web page
+
+var recordPageCookiesAsync = function(pageStats) {
+ // Store the page stats objects so that it doesn't go away
+ // before we handle the job.
+ // rhill 2013-10-19: pageStats could be nil, for example, this can
+ // happens if a file:// ... makes an xmlHttpRequest
+ if ( !pageStats ) {
+ return;
+ }
+ recordPageCookiesQueue.set(pageStats.pageUrl, pageStats);
+ if ( processPageRecordQueueTimer === null ) {
+ processPageRecordQueueTimer = vAPI.setTimeout(processPageRecordQueue, 1000);
+ }
+};
+
+/******************************************************************************/
+
+var cookieLogEntryBuilder = [
+ '',
+ '{',
+ '',
+ '-cookie:',
+ '',
+ '}'
+];
+
+var recordPageCookie = function(pageStore, cookieKey) {
+ if ( vAPI.isBehindTheSceneTabId(pageStore.tabId) ) { return; }
+
+ var cookieEntry = cookieDict.get(cookieKey);
+ var pageHostname = pageStore.pageHostname;
+ var block = µm.mustBlock(pageHostname, cookieEntry.hostname, 'cookie');
+
+ cookieLogEntryBuilder[0] = cookieURLFromCookieEntry(cookieEntry);
+ cookieLogEntryBuilder[2] = cookieEntry.session ? 'session' : 'persistent';
+ cookieLogEntryBuilder[4] = encodeURIComponent(cookieEntry.name);
+
+ var cookieURL = cookieLogEntryBuilder.join('');
+
+ // rhill 2013-11-20:
+ // https://github.com/gorhill/httpswitchboard/issues/60
+ // Need to URL-encode cookie name
+ pageStore.recordRequest('cookie', cookieURL, block);
+ µm.logger.writeOne(pageStore.tabId, 'net', pageHostname, cookieURL, 'cookie', block);
+
+ cookieEntry.usedOn.add(pageHostname);
+
+ // rhill 2013-11-21:
+ // https://github.com/gorhill/httpswitchboard/issues/65
+ // Leave alone cookies from behind-the-scene requests if
+ // behind-the-scene processing is disabled.
+ if ( !block ) {
+ return;
+ }
+ if ( !µm.userSettings.deleteCookies ) {
+ return;
+ }
+ removeCookieAsync(cookieKey);
+};
+
+/******************************************************************************/
+
+// Look for cookies to potentially remove for a specific web page
+
+var removePageCookiesAsync = function(pageStats) {
+ // Hold onto pageStats objects so that it doesn't go away
+ // before we handle the job.
+ // rhill 2013-10-19: pageStats could be nil, for example, this can
+ // happens if a file:// ... makes an xmlHttpRequest
+ if ( !pageStats ) {
+ return;
+ }
+ removePageCookiesQueue.set(pageStats.pageUrl, pageStats);
+ if ( processPageRemoveQueueTimer === null ) {
+ processPageRemoveQueueTimer = vAPI.setTimeout(processPageRemoveQueue, 15 * 1000);
+ }
+};
+
+/******************************************************************************/
+
+// Candidate for removal
+
+var removeCookieAsync = function(cookieKey) {
+ removeCookieQueue.add(cookieKey);
+};
+
+/******************************************************************************/
+
+var chromeCookieRemove = function(cookieEntry, name) {
+ var url = cookieURLFromCookieEntry(cookieEntry);
+ if ( url === '' ) {
+ return;
+ }
+ var sessionCookieKey = cookieKeyFromCookieURL(url, 'session', name);
+ var persistCookieKey = cookieKeyFromCookieURL(url, 'persistent', name);
+ var callback = function(details) {
+ var success = !!details;
+ var template = success ? i18nCookieDeleteSuccess : i18nCookieDeleteFailure;
+ if ( removeCookieFromDict(sessionCookieKey) ) {
+ if ( success ) {
+ µm.cookieRemovedCounter += 1;
+ }
+ µm.logger.writeOne('', 'info', 'cookie', template.replace('{{value}}', sessionCookieKey));
+ }
+ if ( removeCookieFromDict(persistCookieKey) ) {
+ if ( success ) {
+ µm.cookieRemovedCounter += 1;
+ }
+ µm.logger.writeOne('', 'info', 'cookie', template.replace('{{value}}', persistCookieKey));
+ }
+ };
+
+ vAPI.cookies.remove({ url: url, name: name }, callback);
+};
+
+var i18nCookieDeleteSuccess = vAPI.i18n('loggerEntryCookieDeleted');
+var i18nCookieDeleteFailure = vAPI.i18n('loggerEntryDeleteCookieError');
+
+/******************************************************************************/
+
+var processPageRecordQueue = function() {
+ processPageRecordQueueTimer = null;
+
+ for ( var pageStore of recordPageCookiesQueue.values() ) {
+ findAndRecordPageCookies(pageStore);
+ }
+ recordPageCookiesQueue.clear();
+};
+
+/******************************************************************************/
+
+var processPageRemoveQueue = function() {
+ processPageRemoveQueueTimer = null;
+
+ for ( var pageStore of removePageCookiesQueue.values() ) {
+ findAndRemovePageCookies(pageStore);
+ }
+ removePageCookiesQueue.clear();
+};
+
+/******************************************************************************/
+
+// Effectively remove cookies.
+
+var processRemoveQueue = function() {
+ var userSettings = µm.userSettings;
+ var deleteCookies = userSettings.deleteCookies;
+
+ // Session cookies which timestamp is *after* tstampObsolete will
+ // be left untouched
+ // https://github.com/gorhill/httpswitchboard/issues/257
+ var tstampObsolete = userSettings.deleteUnusedSessionCookies ?
+ Date.now() - userSettings.deleteUnusedSessionCookiesAfter * 60 * 1000 :
+ 0;
+
+ var srcHostnames;
+ var cookieEntry;
+
+ for ( var cookieKey of removeCookieQueue ) {
+ // rhill 2014-05-12: Apparently this can happen. I have to
+ // investigate how (A session cookie has same name as a
+ // persistent cookie?)
+ cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) { continue; }
+
+ // Delete obsolete session cookies: enabled.
+ if ( tstampObsolete !== 0 && cookieEntry.session ) {
+ if ( cookieEntry.tstamp < tstampObsolete ) {
+ chromeCookieRemove(cookieEntry, cookieEntry.name);
+ continue;
+ }
+ }
+
+ // Delete all blocked cookies: disabled.
+ if ( deleteCookies === false ) {
+ continue;
+ }
+
+ // Query scopes only if we are going to use them
+ if ( srcHostnames === undefined ) {
+ srcHostnames = µm.tMatrix.extractAllSourceHostnames();
+ }
+
+ // Ensure cookie is not allowed on ALL current web pages: It can
+ // happen that a cookie is blacklisted on one web page while
+ // being whitelisted on another (because of per-page permissions).
+ if ( canRemoveCookie(cookieKey, srcHostnames) ) {
+ chromeCookieRemove(cookieEntry, cookieEntry.name);
+ }
+ }
+
+ removeCookieQueue.clear();
+
+ vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
+};
+
+/******************************************************************************/
+
+// Once in a while, we go ahead and clean everything that might have been
+// left behind.
+
+// Remove only some of the cookies which are candidate for removal: who knows,
+// maybe a user has 1000s of cookies sitting in his browser...
+
+var processClean = function() {
+ var us = µm.userSettings;
+ if ( us.deleteCookies || us.deleteUnusedSessionCookies ) {
+ var cookieKeys = Array.from(cookieDict.keys()),
+ len = cookieKeys.length,
+ step, offset, n;
+ if ( len > 25 ) {
+ step = len / 25;
+ offset = Math.floor(Math.random() * len);
+ n = 25;
+ } else {
+ step = 1;
+ offset = 0;
+ n = len;
+ }
+ var i = offset;
+ while ( n-- ) {
+ removeCookieAsync(cookieKeys[Math.floor(i % len)]);
+ i += step;
+ }
+ }
+
+ vAPI.setTimeout(processClean, processCleanPeriod);
+};
+
+/******************************************************************************/
+
+var findAndRecordPageCookies = function(pageStore) {
+ for ( var cookieKey of cookieDict.keys() ) {
+ if ( cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
+ recordPageCookie(pageStore, cookieKey);
+ }
+ }
+};
+
+/******************************************************************************/
+
+var findAndRemovePageCookies = function(pageStore) {
+ for ( var cookieKey of cookieDict.keys() ) {
+ if ( cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
+ removeCookieAsync(cookieKey);
+ }
+ }
+};
+
+/******************************************************************************/
+
+var canRemoveCookie = function(cookieKey, srcHostnames) {
+ var cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) { return false; }
+
+ var cookieHostname = cookieEntry.hostname;
+ var srcHostname;
+
+ for ( srcHostname of cookieEntry.usedOn ) {
+ if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
+ return false;
+ }
+ }
+ // Maybe there is a scope in which the cookie is 1st-party-allowed.
+ // For example, if I am logged in into `github.com`, I do not want to be
+ // logged out just because I did not yet open a `github.com` page after
+ // re-starting the browser.
+ srcHostname = cookieHostname;
+ var pos;
+ for (;;) {
+ if ( srcHostnames.has(srcHostname) ) {
+ if ( µm.mustAllow(srcHostname, cookieHostname, 'cookie') ) {
+ return false;
+ }
+ }
+ if ( srcHostname === cookieEntry.domain ) {
+ break;
+ }
+ pos = srcHostname.indexOf('.');
+ if ( pos === -1 ) {
+ break;
+ }
+ srcHostname = srcHostname.slice(pos + 1);
+ }
+ return true;
+};
+
+/******************************************************************************/
+
+// Listen to any change in cookieland, we will update page stats accordingly.
+
+vAPI.cookies.onChanged = function(cookie) {
+ // rhill 2013-12-11: If cookie value didn't change, no need to record.
+ // https://github.com/gorhill/httpswitchboard/issues/79
+ var cookieKey = cookieKeyFromCookie(cookie);
+ var cookieEntry = cookieDict.get(cookieKey);
+ if ( cookieEntry === undefined ) {
+ cookieEntry = addCookieToDict(cookie);
+ } else {
+ cookieEntry.tstamp = Date.now();
+ if ( cookie.value === cookieEntry.value ) { return; }
+ cookieEntry.value = cookie.value;
+ }
+
+ // Go through all pages and update if needed, as one cookie can be used
+ // by many web pages, so they need to be recorded for all these pages.
+ var pageStores = µm.pageStores;
+ var pageStore;
+ for ( var tabId in pageStores ) {
+ if ( pageStores.hasOwnProperty(tabId) === false ) {
+ continue;
+ }
+ pageStore = pageStores[tabId];
+ if ( !cookieMatchDomains(cookieKey, pageStore.allHostnamesString) ) {
+ continue;
+ }
+ recordPageCookie(pageStore, cookieKey);
+ }
+};
+
+/******************************************************************************/
+
+// Listen to any change in cookieland, we will update page stats accordingly.
+
+vAPI.cookies.onRemoved = function(cookie) {
+ var cookieKey = cookieKeyFromCookie(cookie);
+ if ( removeCookieFromDict(cookieKey) ) {
+ µm.logger.writeOne('', 'info', 'cookie', i18nCookieDeleteSuccess.replace('{{value}}', cookieKey));
+ }
+};
+
+/******************************************************************************/
+
+// Listen to any change in cookieland, we will update page stats accordingly.
+
+vAPI.cookies.onAllRemoved = function() {
+ for ( var cookieKey of cookieDict.keys() ) {
+ if ( removeCookieFromDict(cookieKey) ) {
+ µm.logger.writeOne('', 'info', 'cookie', i18nCookieDeleteSuccess.replace('{{value}}', cookieKey));
+ }
+ }
+};
+
+/******************************************************************************/
+
+vAPI.cookies.getAll(addCookiesToDict);
+vAPI.cookies.start();
+
+vAPI.setTimeout(processRemoveQueue, processRemoveQueuePeriod);
+vAPI.setTimeout(processClean, processCleanPeriod);
+
+/******************************************************************************/
+
+// Expose only what is necessary
+
+return {
+ recordPageCookies: recordPageCookiesAsync,
+ removePageCookies: removePageCookiesAsync
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
diff --git a/js/dashboard-common.js b/js/dashboard-common.js
new file mode 100644
index 0000000..eecd668
--- /dev/null
+++ b/js/dashboard-common.js
@@ -0,0 +1,41 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/******************************************************************************/
+
+uDom.onLoad(function() {
+
+/******************************************************************************/
+
+// Open links in the proper window
+uDom('a').attr('target', '_blank');
+uDom('a[href*="dashboard.html"]').attr('target', '_parent');
+uDom('.whatisthis').on('click', function() {
+ uDom(this).parent()
+ .descendants('.whatisthis-expandable')
+ .toggleClass('whatisthis-expanded');
+});
+
+
+/******************************************************************************/
+
+});
diff --git a/js/dashboard.js b/js/dashboard.js
new file mode 100644
index 0000000..5ff6ebc
--- /dev/null
+++ b/js/dashboard.js
@@ -0,0 +1,56 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+ var loadDashboardPanel = function(hash) {
+ var button = uDom(hash);
+ var url = button.attr('data-dashboard-panel-url');
+ uDom('iframe').attr('src', url);
+ uDom('.tabButton').forEach(function(button){
+ button.toggleClass(
+ 'selected',
+ button.attr('data-dashboard-panel-url') === url
+ );
+ });
+ };
+
+ var onTabClickHandler = function() {
+ loadDashboardPanel(window.location.hash);
+ };
+
+ uDom.onLoad(function() {
+ window.addEventListener('hashchange', onTabClickHandler);
+ var hash = window.location.hash;
+ if ( hash.length < 2 ) {
+ hash = '#settings';
+ }
+ loadDashboardPanel(hash);
+ });
+
+})();
diff --git a/js/hosts-files.js b/js/hosts-files.js
new file mode 100644
index 0000000..a259240
--- /dev/null
+++ b/js/hosts-files.js
@@ -0,0 +1,391 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+var listDetails = {},
+ lastUpdateTemplateString = vAPI.i18n('hostsFilesLastUpdate'),
+ hostsFilesSettingsHash,
+ reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/;
+
+/******************************************************************************/
+
+vAPI.messaging.addListener(function onMessage(msg) {
+ switch ( msg.what ) {
+ case 'assetUpdated':
+ updateAssetStatus(msg);
+ break;
+ case 'assetsUpdated':
+ document.body.classList.remove('updating');
+ break;
+ case 'loadHostsFilesCompleted':
+ renderHostsFiles();
+ break;
+ default:
+ break;
+ }
+});
+
+/******************************************************************************/
+
+var renderNumber = function(value) {
+ return value.toLocaleString();
+};
+
+/******************************************************************************/
+
+var renderHostsFiles = function(soft) {
+ var listEntryTemplate = uDom('#templates .listEntry'),
+ listStatsTemplate = vAPI.i18n('hostsFilesPerFileStats'),
+ renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString,
+ 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 : '';
+ if ( listTitle === '' ) { return listKey; }
+ return listTitle;
+ };
+
+ var liFromListEntry = function(listKey, li) {
+ var entry = listDetails.available[listKey],
+ elem;
+ if ( !li ) {
+ li = listEntryTemplate.clone().nodeAt(0);
+ }
+ if ( li.getAttribute('data-listkey') !== listKey ) {
+ li.setAttribute('data-listkey', listKey);
+ elem = li.querySelector('input[type="checkbox"]');
+ elem.checked = entry.off !== true;
+ elem = li.querySelector('a:nth-of-type(1)');
+ elem.setAttribute('href', 'asset-viewer.html?url=' + encodeURI(listKey));
+ elem.setAttribute('type', 'text/html');
+ elem.textContent = listNameFromListKey(listKey);
+ li.classList.remove('toRemove');
+ if ( entry.supportName ) {
+ li.classList.add('support');
+ elem = li.querySelector('a.support');
+ elem.setAttribute('href', entry.supportURL);
+ elem.setAttribute('title', entry.supportName);
+ } else {
+ li.classList.remove('support');
+ }
+ if ( entry.external ) {
+ li.classList.add('external');
+ } else {
+ li.classList.remove('external');
+ }
+ if ( entry.instructionURL ) {
+ li.classList.add('mustread');
+ elem = li.querySelector('a.mustread');
+ elem.setAttribute('href', entry.instructionURL);
+ } else {
+ li.classList.remove('mustread');
+ }
+ }
+ // https://github.com/gorhill/uBlock/issues/1429
+ if ( !soft ) {
+ elem = li.querySelector('input[type="checkbox"]');
+ elem.checked = entry.off !== true;
+ }
+ elem = li.querySelector('span.counts');
+ var text = '';
+ if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) {
+ text = listStatsTemplate
+ .replace('{{used}}', renderNumber(entry.off ? 0 : entry.entryUsedCount))
+ .replace('{{total}}', renderNumber(entry.entryCount));
+ }
+ elem.textContent = text;
+ // https://github.com/chrisaljoudi/uBlock/issues/104
+ var asset = listDetails.cache[listKey] || {};
+ var remoteURL = asset.remoteURL;
+ li.classList.toggle(
+ 'unsecure',
+ typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0
+ );
+ li.classList.toggle('failed', asset.error !== undefined);
+ li.classList.toggle('obsolete', asset.obsolete === true);
+ li.classList.toggle('cached', asset.cached === true && asset.writeTime > 0);
+ if ( asset.cached ) {
+ li.querySelector('.status.cache').setAttribute(
+ 'title',
+ lastUpdateTemplateString.replace('{{ago}}', renderElapsedTimeToString(asset.writeTime))
+ );
+ }
+ 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);
+
+ // 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;
+ 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]);
+ if ( liEntry.parentElement === null ) {
+ ulList.appendChild(liEntry);
+ }
+ }
+
+ uDom('#lists .listEntry.discard').remove();
+ uDom('#listsOfBlockedHostsPrompt').text(
+ vAPI.i18n('hostsFilesStats').replace(
+ '{{blockedHostnameCount}}',
+ renderNumber(details.blockedHostnameCount)
+ )
+ );
+ uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true);
+
+ if ( !soft ) {
+ hostsFilesSettingsHash = hashFromCurrentFromSettings();
+ }
+ renderWidgets();
+ };
+
+ vAPI.messaging.send('hosts-files.js', { what: 'getLists' }, onListsReceived);
+};
+
+/******************************************************************************/
+
+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('#buttonApply').toggleClass('disabled', hostsFilesSettingsHash === hashFromCurrentFromSettings());
+};
+
+/******************************************************************************/
+
+var updateAssetStatus = function(details) {
+ var li = document.querySelector('#lists .listEntry[data-listkey="' + details.key + '"]');
+ if ( li === null ) { return; }
+ li.classList.toggle('failed', !!details.failed);
+ li.classList.toggle('obsolete', !details.cached);
+ li.classList.toggle('cached', !!details.cached);
+ if ( details.cached ) {
+ li.querySelector('.status.cache').setAttribute(
+ 'title',
+ lastUpdateTemplateString.replace(
+ '{{ago}}',
+ vAPI.i18n.renderElapsedTimeToString(Date.now())
+ )
+ );
+ }
+ renderWidgets();
+};
+
+/*******************************************************************************
+
+ Compute a hash from all the settings affecting how filter lists are loaded
+ in memory.
+
+**/
+
+var hashFromCurrentFromSettings = function() {
+ var hash = [],
+ listHash = [],
+ listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'),
+ liEntry,
+ i = listEntries.length;
+ while ( i-- ) {
+ liEntry = listEntries[i];
+ if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) {
+ listHash.push(liEntry.getAttribute('data-listkey'));
+ }
+ }
+ hash.push(
+ listHash.sort().join(),
+ reValidExternalList.test(document.getElementById('externalHostsFiles').value),
+ document.querySelector('#lists .listEntry.toRemove') !== null
+ );
+ return hash.join();
+};
+
+/******************************************************************************/
+
+var onHostsFilesSettingsChanged = function() {
+ renderWidgets();
+};
+
+/******************************************************************************/
+
+var onRemoveExternalHostsFile = function(ev) {
+ var liEntry = uDom(this).ancestors('[data-listkey]'),
+ listKey = liEntry.attr('data-listkey');
+ if ( listKey ) {
+ liEntry.toggleClass('toRemove');
+ renderWidgets();
+ }
+ ev.preventDefault();
+};
+
+/******************************************************************************/
+
+var onPurgeClicked = function() {
+ var button = uDom(this),
+ liEntry = button.ancestors('[data-listkey]'),
+ listKey = liEntry.attr('data-listkey');
+ if ( !listKey ) { return; }
+
+ vAPI.messaging.send('hosts-files.js', { what: 'purgeCache', assetKey: listKey });
+ liEntry.addClass('obsolete');
+ liEntry.removeClass('cached');
+
+ if ( liEntry.descendants('input').first().prop('checked') ) {
+ renderWidgets();
+ }
+};
+
+/******************************************************************************/
+
+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'));
+ }
+ }
+
+ // 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 import
+ var externalListsElem = document.getElementById('externalHostsFiles'),
+ toImport = externalListsElem.value.trim();
+ externalListsElem.value = '';
+
+ vAPI.messaging.send(
+ 'hosts-files.js',
+ {
+ what: 'selectHostsFiles',
+ toSelect: toSelect,
+ toImport: toImport,
+ toRemove: toRemove
+ },
+ callback
+ );
+
+ hostsFilesSettingsHash = hashFromCurrentFromSettings();
+};
+
+/******************************************************************************/
+
+var buttonApplyHandler = function() {
+ uDom('#buttonApply').removeClass('enabled');
+ selectHostsFiles(function() {
+ vAPI.messaging.send('hosts-files.js', { what: 'reloadHostsFiles' });
+ });
+ renderWidgets();
+};
+
+/******************************************************************************/
+
+var buttonUpdateHandler = function() {
+ uDom('#buttonUpdate').removeClass('enabled');
+ selectHostsFiles(function() {
+ document.body.classList.add('updating');
+ vAPI.messaging.send('hosts-files.js', { what: 'forceUpdateAssets' });
+ renderWidgets();
+ });
+ renderWidgets();
+};
+
+/******************************************************************************/
+
+var buttonPurgeAllHandler = function() {
+ uDom('#buttonPurgeAll').removeClass('enabled');
+ vAPI.messaging.send(
+ 'hosts-files.js',
+ { what: 'purgeAllCaches' },
+ function() {
+ renderHostsFiles(true);
+ }
+ );
+};
+
+/******************************************************************************/
+
+var autoUpdateCheckboxChanged = function() {
+ vAPI.messaging.send(
+ 'hosts-files.js',
+ {
+ what: 'userSettings',
+ name: 'autoUpdate',
+ value: this.checked
+ }
+ );
+};
+
+/******************************************************************************/
+
+uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
+uDom('#buttonApply').on('click', buttonApplyHandler);
+uDom('#buttonUpdate').on('click', buttonUpdateHandler);
+uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
+uDom('#lists').on('change', '.listEntry > input', onHostsFilesSettingsChanged);
+uDom('#lists').on('click', '.listEntry > a.remove', onRemoveExternalHostsFile);
+uDom('#lists').on('click', 'span.cache', onPurgeClicked);
+uDom('#externalHostsFiles').on('input', onHostsFilesSettingsChanged);
+
+renderHostsFiles();
+
+/******************************************************************************/
+
+})();
+
diff --git a/js/httpsb.js b/js/httpsb.js
new file mode 100644
index 0000000..d76371a
--- /dev/null
+++ b/js/httpsb.js
@@ -0,0 +1,212 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global chrome, µMatrix */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+ var µm = µMatrix;
+ µm.pMatrix = new µm.Matrix();
+ µm.pMatrix.setSwitch('matrix-off', 'about-scheme', 1);
+ µm.pMatrix.setSwitch('matrix-off', 'chrome-extension-scheme', 1);
+ µm.pMatrix.setSwitch('matrix-off', 'chrome-scheme', 1);
+ µm.pMatrix.setSwitch('matrix-off', 'moz-extension-scheme', 1);
+ µm.pMatrix.setSwitch('matrix-off', 'opera-scheme', 1);
+ // https://discourse.mozilla.org/t/support-umatrix/5131/157
+ µm.pMatrix.setSwitch('matrix-off', 'wyciwyg-scheme', 1);
+ µm.pMatrix.setSwitch('matrix-off', 'behind-the-scene', 1);
+ µm.pMatrix.setSwitch('referrer-spoof', 'behind-the-scene', 2);
+ µm.pMatrix.setSwitch('https-strict', 'behind-the-scene', 2);
+ // Global rules
+ µm.pMatrix.setSwitch('referrer-spoof', '*', 1);
+ µm.pMatrix.setSwitch('noscript-spoof', '*', 1);
+ µm.pMatrix.setCell('*', '*', '*', µm.Matrix.Red);
+ µm.pMatrix.setCell('*', '*', 'css', µm.Matrix.Green);
+ µm.pMatrix.setCell('*', '*', 'image', µm.Matrix.Green);
+ µm.pMatrix.setCell('*', '*', 'frame', µm.Matrix.Red);
+ // 1st-party rules
+ µm.pMatrix.setCell('*', '1st-party', '*', µm.Matrix.Green);
+ µm.pMatrix.setCell('*', '1st-party', 'frame', µm.Matrix.Green);
+
+ µm.tMatrix = new µm.Matrix();
+ µm.tMatrix.assign(µm.pMatrix);
+})();
+
+/******************************************************************************/
+
+µMatrix.hostnameFromURL = function(url) {
+ var hn = this.URI.hostnameFromURI(url);
+ return hn === '' ? '*' : hn;
+};
+
+µMatrix.scopeFromURL = µMatrix.hostnameFromURL;
+
+/******************************************************************************/
+
+µMatrix.evaluateURL = function(srcURL, desHostname, type) {
+ var srcHostname = this.URI.hostnameFromURI(srcURL);
+ return this.tMatrix.evaluateCellZXY(srcHostname, desHostname, type);
+};
+
+
+/******************************************************************************/
+
+// Whitelist something
+
+µMatrix.whitelistTemporarily = function(srcHostname, desHostname, type) {
+ this.tMatrix.whitelistCell(srcHostname, desHostname, type);
+};
+
+µMatrix.whitelistPermanently = function(srcHostname, desHostname, type) {
+ if ( this.pMatrix.whitelistCell(srcHostname, desHostname, type) ) {
+ this.saveMatrix();
+ }
+};
+
+/******************************************************************************/
+
+// Auto-whitelisting the `all` cell is a serious action, hence this will be
+// done only from within a scope.
+
+µMatrix.autoWhitelistAllTemporarily = function(pageURL) {
+ var srcHostname = this.URI.hostnameFromURI(pageURL);
+ if ( this.mustBlock(srcHostname, '*', '*') === false ) {
+ return false;
+ }
+ this.tMatrix.whitelistCell(srcHostname, '*', '*');
+ return true;
+};
+
+/******************************************************************************/
+
+// Blacklist something
+
+µMatrix.blacklistTemporarily = function(srcHostname, desHostname, type) {
+ this.tMatrix.blacklistCell(srcHostname, desHostname, type);
+};
+
+µMatrix.blacklistPermanently = function(srcHostname, desHostname, type) {
+ if ( this.pMatrix.blacklist(srcHostname, desHostname, type) ) {
+ this.saveMatrix();
+ }
+};
+
+/******************************************************************************/
+
+// Remove something from both black and white lists.
+
+µMatrix.graylistTemporarily = function(srcHostname, desHostname, type) {
+ this.tMatrix.graylistCell(srcHostname, desHostname, type);
+};
+
+µMatrix.graylistPermanently = function(srcHostname, desHostname, type) {
+ if ( this.pMatrix.graylistCell(srcHostname, desHostname, type) ) {
+ this.saveMatrix();
+ }
+};
+
+/******************************************************************************/
+
+// TODO: Should type be transposed by the caller or in place here? Not an
+// issue at this point but to keep in mind as this function is called
+// more and more from different places.
+
+µMatrix.filterRequest = function(fromURL, type, toURL) {
+ // Block request?
+ var srcHostname = this.hostnameFromURL(fromURL);
+ var desHostname = this.hostnameFromURL(toURL);
+
+ // If no valid hostname, use the hostname of the source.
+ // For example, this case can happen with data URI.
+ if ( desHostname === '' ) {
+ desHostname = srcHostname;
+ }
+
+ // Blocked by matrix filtering?
+ return this.mustBlock(srcHostname, desHostname, type);
+};
+
+/******************************************************************************/
+
+µMatrix.mustBlock = function(srcHostname, desHostname, type) {
+ return this.tMatrix.mustBlock(srcHostname, desHostname, type);
+};
+
+µMatrix.mustAllow = function(srcHostname, desHostname, type) {
+ return this.mustBlock(srcHostname, desHostname, type) === false;
+};
+
+/******************************************************************************/
+
+// Commit temporary permissions.
+
+µMatrix.commitPermissions = function(persist) {
+ this.pMatrix.assign(this.tMatrix);
+ if ( persist ) {
+ this.saveMatrix();
+ }
+};
+
+/******************************************************************************/
+
+// Reset all rules to their default state.
+
+µMatrix.revertAllRules = function() {
+ this.tMatrix.assign(this.pMatrix);
+};
+
+/******************************************************************************/
+
+µMatrix.turnOff = function() {
+ vAPI.app.start();
+};
+
+µMatrix.turnOn = function() {
+ vAPI.app.stop();
+};
+
+/******************************************************************************/
+
+µMatrix.formatCount = function(count) {
+ if ( typeof count !== 'number' ) {
+ return '';
+ }
+ var s = count.toFixed(0);
+ if ( count >= 1000 ) {
+ if ( count < 10000 ) {
+ s = '>' + s.slice(0,1) + 'K';
+ } else if ( count < 100000 ) {
+ s = s.slice(0,2) + 'K';
+ } else if ( count < 1000000 ) {
+ s = s.slice(0,3) + 'K';
+ } else if ( count < 10000000 ) {
+ s = s.slice(0,1) + 'M';
+ } else {
+ s = s.slice(0,-6) + 'M';
+ }
+ }
+ return s;
+};
+
diff --git a/js/i18n.js b/js/i18n.js
new file mode 100644
index 0000000..5bb854c
--- /dev/null
+++ b/js/i18n.js
@@ -0,0 +1,209 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global vAPI, uDom */
+
+/******************************************************************************/
+
+// This file should always be included at the end of the `body` tag, so as
+// to ensure all i18n targets are already loaded.
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/2084
+// Anything else than <a>, <b>, <code>, <em>, <i>, <input>, and <span> will
+// be rendered as plain text.
+// For <input>, only the type attribute is allowed.
+// For <a>, only href attribute must be present, and it MUST starts with
+// `https://`, and includes no single- or double-quotes.
+// No HTML entities are allowed, there is code to handle existing HTML
+// entities already present in translation files until they are all gone.
+
+var reSafeTags = /^([\s\S]*?)<(b|blockquote|code|em|i|kbd|span|sup)>(.+?)<\/\2>([\s\S]*)$/,
+ reSafeInput = /^([\s\S]*?)<(input type="[^"]+")>(.*?)([\s\S]*)$/,
+ reInput = /^input type=(['"])([a-z]+)\1$/,
+ reSafeLink = /^([\s\S]*?)<(a href=['"]https?:\/\/[^'" <>]+['"])>(.+?)<\/a>([\s\S]*)$/,
+ reLink = /^a href=(['"])(https?:\/\/[^'"]+)\1$/;
+
+var safeTextToTagNode = function(text) {
+ var matches, node;
+ if ( text.lastIndexOf('a ', 0) === 0 ) {
+ matches = reLink.exec(text);
+ if ( matches === null ) { return null; }
+ node = document.createElement('a');
+ node.setAttribute('href', matches[2]);
+ return node;
+ }
+ if ( text.lastIndexOf('input ', 0) === 0 ) {
+ matches = reInput.exec(text);
+ if ( matches === null ) { return null; }
+ node = document.createElement('input');
+ node.setAttribute('type', matches[2]);
+ return node;
+ }
+ // Firefox extension validator warns if using a variable as argument for
+ // document.createElement().
+ switch ( text ) {
+ case 'b':
+ return document.createElement('b');
+ case 'blockquote':
+ return document.createElement('blockquote');
+ case 'code':
+ return document.createElement('code');
+ case 'em':
+ return document.createElement('em');
+ case 'i':
+ return document.createElement('i');
+ case 'kbd':
+ return document.createElement('kbd');
+ case 'span':
+ return document.createElement('span');
+ case 'sup':
+ return document.createElement('sup');
+ default:
+ break;
+ }
+};
+
+var safeTextToTextNode = function(text) {
+ // TODO: remove once no more HTML entities in translation files.
+ if ( text.indexOf('&') !== -1 ) {
+ text = text.replace(/&ldquo;/g, '“')
+ .replace(/&rdquo;/g, '”')
+ .replace(/&lsquo;/g, '‘')
+ .replace(/&rsquo;/g, '’');
+ }
+ return document.createTextNode(text);
+};
+
+var safeTextToDOM = function(text, parent) {
+ if ( text === '' ) { return; }
+ // Fast path (most common).
+ if ( text.indexOf('<') === -1 ) {
+ return parent.appendChild(safeTextToTextNode(text));
+ }
+ // Slow path.
+ // `<p>` no longer allowed. Code below can be remove once all <p>'s are
+ // gone from translation files.
+ text = text.replace(/^<p>|<\/p>/g, '')
+ .replace(/<p>/g, '\n\n');
+ // Parse allowed HTML tags.
+ var matches,
+ matches1 = reSafeTags.exec(text),
+ matches2 = reSafeLink.exec(text);
+ if ( matches1 !== null && matches2 !== null ) {
+ matches = matches1.index < matches2.index ? matches1 : matches2;
+ } else if ( matches1 !== null ) {
+ matches = matches1;
+ } else if ( matches2 !== null ) {
+ matches = matches2;
+ } else {
+ matches = reSafeInput.exec(text);
+ }
+ if ( matches === null ) {
+ parent.appendChild(safeTextToTextNode(text));
+ return;
+ }
+ safeTextToDOM(matches[1], parent);
+ var node = safeTextToTagNode(matches[2]) || parent;
+ safeTextToDOM(matches[3], node);
+ parent.appendChild(node);
+ safeTextToDOM(matches[4], parent);
+};
+
+/******************************************************************************/
+
+// Helper to deal with the i18n'ing of HTML files.
+vAPI.i18n.render = function(context) {
+ var docu = document,
+ root = context || docu,
+ elems, n, i, elem, text;
+
+ elems = root.querySelectorAll('[data-i18n]');
+ n = elems.length;
+ for ( i = 0; i < n; i++ ) {
+ elem = elems[i];
+ text = vAPI.i18n(elem.getAttribute('data-i18n'));
+ if ( !text ) { continue; }
+ // TODO: remove once it's all replaced with <input type="...">
+ if ( text.indexOf('{') !== -1 ) {
+ text = text.replace(/\{\{input:([^}]+)\}\}/g, '<input type="$1">');
+ }
+ safeTextToDOM(text, elem);
+ }
+
+ uDom('[title]', context).forEach(function(elem) {
+ var title = vAPI.i18n(elem.attr('title'));
+ if ( title ) {
+ elem.attr('title', title);
+ }
+ });
+
+ uDom('[placeholder]', context).forEach(function(elem) {
+ elem.attr('placeholder', vAPI.i18n(elem.attr('placeholder')));
+ });
+
+ uDom('[data-i18n-tip]', context).forEach(function(elem) {
+ elem.attr(
+ 'data-tip',
+ vAPI.i18n(elem.attr('data-i18n-tip'))
+ .replace(/<br>/g, '\n')
+ .replace(/\n{3,}/g, '\n\n')
+ );
+ });
+};
+
+vAPI.i18n.render();
+
+/******************************************************************************/
+
+vAPI.i18n.renderElapsedTimeToString = function(tstamp) {
+ var value = (Date.now() - tstamp) / 60000;
+ if ( value < 2 ) {
+ return vAPI.i18n('elapsedOneMinuteAgo');
+ }
+ if ( value < 60 ) {
+ return vAPI.i18n('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
+ }
+ value /= 60;
+ if ( value < 2 ) {
+ return vAPI.i18n('elapsedOneHourAgo');
+ }
+ if ( value < 24 ) {
+ return vAPI.i18n('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
+ }
+ value /= 24;
+ if ( value < 2 ) {
+ return vAPI.i18n('elapsedOneDayAgo');
+ }
+ return vAPI.i18n('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/liquid-dict.js b/js/liquid-dict.js
new file mode 100644
index 0000000..92ca58c
--- /dev/null
+++ b/js/liquid-dict.js
@@ -0,0 +1,203 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/******************************************************************************/
+
+µMatrix.LiquidDict = (function() {
+
+/******************************************************************************/
+
+var LiquidDict = function() {
+ this.dict = {};
+ this.count = 0;
+ this.duplicateCount = 0;
+ this.bucketCount = 0;
+ this.frozenBucketCount = 0;
+
+ // Somewhat arbitrary: I need to come up with hard data to know at which
+ // point binary search is better than indexOf.
+ this.cutoff = 500;
+};
+
+/******************************************************************************/
+
+var meltBucket = function(ldict, len, bucket) {
+ ldict.frozenBucketCount -= 1;
+ var map = {};
+ if ( bucket.charAt(0) === ' ' ) {
+ bucket.trim().split(' ').map(function(k) {
+ map[k] = true;
+ });
+ } else {
+ var offset = 0;
+ while ( offset < bucket.length ) {
+ map[bucket.substring(offset, len)] = true;
+ offset += len;
+ }
+ }
+ return map;
+};
+
+/******************************************************************************/
+
+var melt = function(ldict) {
+ var buckets = ldict.dict;
+ var bucket;
+ for ( var key in buckets ) {
+ bucket = buckets[key];
+ if ( typeof bucket === 'string' ) {
+ buckets[key] = meltBucket(ldict, key.charCodeAt(0) & 0xFF, bucket);
+ }
+ }
+};
+
+/******************************************************************************/
+
+var freezeBucket = function(ldict, bucket) {
+ ldict.frozenBucketCount += 1;
+ var words = Object.keys(bucket);
+ var wordLen = words[0].length;
+ if ( wordLen * words.length < ldict.cutoff ) {
+ return ' ' + words.join(' ') + ' ';
+ }
+ return words.sort().join('');
+};
+
+/******************************************************************************/
+
+// How the key is derived dictates the number and size of buckets.
+//
+// http://jsperf.com/makekey-concat-vs-join/3
+//
+// Question: Why is using a prototyped function better than a standalone
+// helper function?
+
+LiquidDict.prototype.makeKey = function(word) {
+ var len = word.length;
+ if ( len > 255 ) {
+ len = 255;
+ }
+ var i = len >> 2;
+ return String.fromCharCode(
+ (word.charCodeAt( 0) & 0x03) << 14 |
+ (word.charCodeAt( i) & 0x03) << 12 |
+ (word.charCodeAt( i+i) & 0x03) << 10 |
+ (word.charCodeAt(i+i+i) & 0x03) << 8 |
+ len
+ );
+};
+
+/******************************************************************************/
+
+LiquidDict.prototype.test = function(word) {
+ var key = this.makeKey(word);
+ var bucket = this.dict[key];
+ if ( bucket === undefined ) {
+ return false;
+ }
+ if ( typeof bucket === 'object' ) {
+ return bucket[word] !== undefined;
+ }
+ if ( bucket.charAt(0) === ' ' ) {
+ return bucket.indexOf(' ' + word + ' ') >= 0;
+ }
+ // binary search
+ var len = word.length;
+ var left = 0;
+ // http://jsperf.com/or-vs-floor/3
+ var right = ~~(bucket.length / len + 0.5);
+ var i, needle;
+ while ( left < right ) {
+ i = left + right >> 1;
+ needle = bucket.substr( len * i, len );
+ if ( word < needle ) {
+ right = i;
+ } else if ( word > needle ) {
+ left = i + 1;
+ } else {
+ return true;
+ }
+ }
+ return false;
+};
+
+/******************************************************************************/
+
+LiquidDict.prototype.add = function(word) {
+ var key = this.makeKey(word);
+ if ( key === undefined ) {
+ return false;
+ }
+ var bucket = this.dict[key];
+ if ( bucket === undefined ) {
+ this.dict[key] = bucket = {};
+ this.bucketCount += 1;
+ bucket[word] = true;
+ this.count += 1;
+ return true;
+ } else if ( typeof bucket === 'string' ) {
+ this.dict[key] = bucket = meltBucket(this, word.len, bucket);
+ }
+ if ( bucket[word] === undefined ) {
+ bucket[word] = true;
+ this.count += 1;
+ return true;
+ }
+ this.duplicateCount += 1;
+ return false;
+};
+
+/******************************************************************************/
+
+LiquidDict.prototype.freeze = function() {
+ var buckets = this.dict;
+ var bucket;
+ for ( var key in buckets ) {
+ bucket = buckets[key];
+ if ( typeof bucket === 'object' ) {
+ buckets[key] = freezeBucket(this, bucket);
+ }
+ }
+};
+
+/******************************************************************************/
+
+LiquidDict.prototype.reset = function() {
+ this.dict = {};
+ this.count = 0;
+ this.duplicateCount = 0;
+ this.bucketCount = 0;
+ this.frozenBucketCount = 0;
+};
+
+/******************************************************************************/
+
+return LiquidDict;
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
+µMatrix.ubiquitousBlacklist = new µMatrix.LiquidDict();
+µMatrix.ubiquitousWhitelist = new µMatrix.LiquidDict();
diff --git a/js/logger-ui.js b/js/logger-ui.js
new file mode 100644
index 0000000..a3e9d9b
--- /dev/null
+++ b/js/logger-ui.js
@@ -0,0 +1,908 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2015-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/sessbench
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+var tbody = document.querySelector('#content tbody');
+var trJunkyard = [];
+var tdJunkyard = [];
+var firstVarDataCol = 2; // currently, column 2 (0-based index)
+var lastVarDataIndex = 3; // currently, d0-d3
+var maxEntries = 0;
+var noTabId = '';
+var allTabIds = {};
+var allTabIdsToken;
+var ownerId = Date.now();
+
+var emphasizeTemplate = document.querySelector('#emphasizeTemplate > span');
+var hiddenTemplate = document.querySelector('#hiddenTemplate > span');
+
+var prettyRequestTypes = {
+ 'main_frame': 'doc',
+ 'stylesheet': 'css',
+ 'sub_frame': 'frame',
+ 'xmlhttprequest': 'xhr'
+};
+
+var dontEmphasizeSet = new Set([
+ 'COOKIE',
+ 'CSP',
+ 'REFERER'
+]);
+
+/******************************************************************************/
+
+// Adjust top padding of content table, to match that of toolbar height.
+
+document.getElementById('content').style.setProperty(
+ 'margin-top',
+ document.getElementById('toolbar').clientHeight + 'px'
+);
+
+/******************************************************************************/
+
+var classNameFromTabId = function(tabId) {
+ if ( tabId === noTabId ) {
+ return 'tab_bts';
+ }
+ if ( tabId !== '' ) {
+ return 'tab_' + tabId;
+ }
+ return '';
+};
+
+/******************************************************************************/
+
+// Emphasize hostname and cookie name.
+
+var emphasizeCookie = function(s) {
+ var pnode = emphasizeHostname(s);
+ if ( pnode.childNodes.length !== 3 ) {
+ return pnode;
+ }
+ var prefix = '-cookie:';
+ var text = pnode.childNodes[2].textContent;
+ var beg = text.indexOf(prefix);
+ if ( beg === -1 ) {
+ return pnode;
+ }
+ beg += prefix.length;
+ var end = text.indexOf('}', beg);
+ if ( end === -1 ) {
+ return pnode;
+ }
+ var cnode = emphasizeTemplate.cloneNode(true);
+ cnode.childNodes[0].textContent = text.slice(0, beg);
+ cnode.childNodes[1].textContent = text.slice(beg, end);
+ cnode.childNodes[2].textContent = text.slice(end);
+ pnode.replaceChild(cnode.childNodes[0], pnode.childNodes[2]);
+ pnode.appendChild(cnode.childNodes[0]);
+ pnode.appendChild(cnode.childNodes[0]);
+ return pnode;
+};
+
+/******************************************************************************/
+
+// Emphasize hostname in URL.
+
+var emphasizeHostname = function(url) {
+ var hnbeg = url.indexOf('://');
+ if ( hnbeg === -1 ) {
+ return document.createTextNode(url);
+ }
+ hnbeg += 3;
+
+ var hnend = url.indexOf('/', hnbeg);
+ if ( hnend === -1 ) {
+ hnend = url.slice(hnbeg).search(/\?#/);
+ if ( hnend !== -1 ) {
+ hnend += hnbeg;
+ } else {
+ hnend = url.length;
+ }
+ }
+
+ var node = emphasizeTemplate.cloneNode(true);
+ node.childNodes[0].textContent = url.slice(0, hnbeg);
+ node.childNodes[1].textContent = url.slice(hnbeg, hnend);
+ node.childNodes[2].textContent = url.slice(hnend);
+ return node;
+};
+
+/******************************************************************************/
+
+var createCellAt = function(tr, index) {
+ var td = tr.cells[index];
+ var mustAppend = !td;
+ if ( mustAppend ) {
+ td = tdJunkyard.pop();
+ }
+ if ( td ) {
+ td.removeAttribute('colspan');
+ td.textContent = '';
+ } else {
+ td = document.createElement('td');
+ }
+ if ( mustAppend ) {
+ tr.appendChild(td);
+ }
+ return td;
+};
+
+/******************************************************************************/
+
+var createRow = function(layout) {
+ var tr = trJunkyard.pop();
+ if ( tr ) {
+ tr.className = '';
+ } else {
+ tr = document.createElement('tr');
+ }
+ for ( var index = 0; index < firstVarDataCol; index++ ) {
+ createCellAt(tr, index);
+ }
+ var i = 1, span = 1, td;
+ for (;;) {
+ td = createCellAt(tr, index);
+ if ( i === lastVarDataIndex ) {
+ break;
+ }
+ if ( layout.charAt(i) !== '1' ) {
+ span += 1;
+ } else {
+ if ( span !== 1 ) {
+ td.setAttribute('colspan', span);
+ }
+ index += 1;
+ span = 1;
+ }
+ i += 1;
+ }
+ if ( span !== 1 ) {
+ td.setAttribute('colspan', span);
+ }
+ index += 1;
+ while ( (td = tr.cells[index]) ) {
+ tdJunkyard.push(tr.removeChild(td));
+ }
+ return tr;
+};
+
+/******************************************************************************/
+
+var createHiddenTextNode = function(text) {
+ var node = hiddenTemplate.cloneNode(true);
+ node.textContent = text;
+ return node;
+};
+
+/******************************************************************************/
+
+var padTo2 = function(v) {
+ return v < 10 ? '0' + v : v;
+};
+
+/******************************************************************************/
+
+var createGap = function(tabId, url) {
+ var tr = createRow('1');
+ tr.classList.add('doc');
+ tr.classList.add('tab');
+ tr.classList.add('canMtx');
+ tr.classList.add('tab_' + tabId);
+ tr.cells[firstVarDataCol].textContent = url;
+ tbody.insertBefore(tr, tbody.firstChild);
+};
+
+/******************************************************************************/
+
+var renderLogEntry = function(entry) {
+ var tr;
+ var fvdc = firstVarDataCol;
+
+ switch ( entry.cat ) {
+ case 'error':
+ case 'info':
+ tr = createRow('1');
+ if ( entry.d0 === 'cookie' ) {
+ tr.cells[fvdc].appendChild(emphasizeCookie(entry.d1));
+ } else {
+ tr.cells[fvdc].textContent = entry.d0;
+ }
+ break;
+
+ case 'net':
+ tr = createRow('111');
+ tr.classList.add('canMtx');
+ // If the request is that of a root frame, insert a gap in the table
+ // in order to visually separate entries for different documents.
+ if ( entry.d2 === 'doc' && entry.tab !== noTabId ) {
+ createGap(entry.tab, entry.d1);
+ }
+ if ( entry.d3 ) {
+ tr.classList.add('blocked');
+ tr.cells[fvdc].textContent = '--';
+ } else {
+ tr.cells[fvdc].textContent = '';
+ }
+ tr.cells[fvdc+1].textContent = (prettyRequestTypes[entry.d2] || entry.d2);
+ if ( dontEmphasizeSet.has(entry.d2) ) {
+ tr.cells[fvdc+2].textContent = entry.d1;
+ } else if ( entry.d2 === 'cookie' ) {
+ tr.cells[fvdc+2].appendChild(emphasizeCookie(entry.d1));
+ } else {
+ tr.cells[fvdc+2].appendChild(emphasizeHostname(entry.d1));
+ }
+ break;
+
+ default:
+ tr = createRow('1');
+ tr.cells[fvdc].textContent = entry.d0;
+ break;
+ }
+
+ // Fields common to all rows.
+ var time = logDate;
+ time.setTime(entry.tstamp - logDateTimezoneOffset);
+ tr.cells[0].textContent = padTo2(time.getUTCHours()) + ':' +
+ padTo2(time.getUTCMinutes()) + ':' +
+ padTo2(time.getSeconds());
+
+ if ( entry.tab ) {
+ tr.classList.add('tab');
+ tr.classList.add(classNameFromTabId(entry.tab));
+ if ( entry.tab === noTabId ) {
+ tr.cells[1].appendChild(createHiddenTextNode('bts'));
+ }
+ }
+ if ( entry.cat !== '' ) {
+ tr.classList.add('cat_' + entry.cat);
+ }
+
+ rowFilterer.filterOne(tr, true);
+
+ tbody.insertBefore(tr, tbody.firstChild);
+};
+
+// Reuse date objects.
+var logDate = new Date(),
+ logDateTimezoneOffset = logDate.getTimezoneOffset() * 60000;
+
+/******************************************************************************/
+
+var renderLogEntries = function(response) {
+ var entries = response.entries;
+ if ( entries.length === 0 ) {
+ return;
+ }
+
+ // Preserve scroll position
+ var height = tbody.offsetHeight;
+
+ var tabIds = response.tabIds;
+ var n = entries.length;
+ var entry;
+ for ( var i = 0; i < n; i++ ) {
+ entry = entries[i];
+ // Unlikely, but it may happen
+ if ( entry.tab && tabIds.hasOwnProperty(entry.tab) === false ) {
+ continue;
+ }
+ renderLogEntry(entries[i]);
+ }
+
+ // Prevent logger from growing infinitely and eating all memory. For
+ // instance someone could forget that it is left opened for some
+ // dynamically refreshed pages.
+ truncateLog(maxEntries);
+
+ var yDelta = tbody.offsetHeight - height;
+ if ( yDelta === 0 ) {
+ return;
+ }
+
+ // Chromium:
+ // body.scrollTop = good value
+ // body.parentNode.scrollTop = 0
+ if ( document.body.scrollTop !== 0 ) {
+ document.body.scrollTop += yDelta;
+ return;
+ }
+
+ // Firefox:
+ // body.scrollTop = 0
+ // body.parentNode.scrollTop = good value
+ var parentNode = document.body.parentNode;
+ if ( parentNode && parentNode.scrollTop !== 0 ) {
+ parentNode.scrollTop += yDelta;
+ }
+};
+
+/******************************************************************************/
+
+var synchronizeTabIds = function(newTabIds) {
+ var oldTabIds = allTabIds;
+ var autoDeleteVoidRows = !!vAPI.localStorage.getItem('loggerAutoDeleteVoidRows');
+ var rowVoided = false;
+ var trs;
+ for ( var tabId in oldTabIds ) {
+ if ( oldTabIds.hasOwnProperty(tabId) === false ) {
+ continue;
+ }
+ if ( newTabIds.hasOwnProperty(tabId) ) {
+ continue;
+ }
+ // Mark or remove voided rows
+ trs = uDom('.tab_' + tabId);
+ if ( autoDeleteVoidRows ) {
+ toJunkyard(trs);
+ } else {
+ trs.removeClass('canMtx');
+ rowVoided = true;
+ }
+ // Remove popup if it is currently bound to a removed tab.
+ if ( tabId === popupManager.tabId ) {
+ popupManager.toggleOff();
+ }
+ }
+
+ var select = document.getElementById('pageSelector');
+ var selectValue = select.value;
+ var tabIds = Object.keys(newTabIds).sort(function(a, b) {
+ return newTabIds[a].localeCompare(newTabIds[b]);
+ });
+ var option;
+ for ( var i = 0, j = 2; i < tabIds.length; i++ ) {
+ tabId = tabIds[i];
+ if ( tabId === noTabId ) {
+ continue;
+ }
+ option = select.options[j];
+ j += 1;
+ if ( !option ) {
+ option = document.createElement('option');
+ select.appendChild(option);
+ }
+ option.textContent = newTabIds[tabId];
+ option.value = classNameFromTabId(tabId);
+ if ( option.value === selectValue ) {
+ option.setAttribute('selected', '');
+ } else {
+ option.removeAttribute('selected');
+ }
+ }
+ while ( j < select.options.length ) {
+ select.removeChild(select.options[j]);
+ }
+ if ( select.value !== selectValue ) {
+ select.selectedIndex = 0;
+ select.value = '';
+ select.options[0].setAttribute('selected', '');
+ pageSelectorChanged();
+ }
+
+ allTabIds = newTabIds;
+
+ return rowVoided;
+};
+
+/******************************************************************************/
+
+var truncateLog = function(size) {
+ if ( size === 0 ) {
+ size = 5000;
+ }
+ var tbody = document.querySelector('#content tbody');
+ size = Math.min(size, 10000);
+ var tr;
+ while ( tbody.childElementCount > size ) {
+ tr = tbody.lastElementChild;
+ trJunkyard.push(tbody.removeChild(tr));
+ }
+};
+
+/******************************************************************************/
+
+var onLogBufferRead = function(response) {
+ if ( !response || response.unavailable ) {
+ readLogBufferAsync();
+ return;
+ }
+
+ // This tells us the behind-the-scene tab id
+ noTabId = response.noTabId;
+
+ // This may have changed meanwhile
+ if ( response.maxLoggedRequests !== maxEntries ) {
+ maxEntries = response.maxLoggedRequests;
+ uDom('#maxEntries').val(maxEntries || '');
+ }
+
+ // Neuter rows for which a tab does not exist anymore
+ var rowVoided = false;
+ if ( response.tabIdsToken !== allTabIdsToken ) {
+ rowVoided = synchronizeTabIds(response.tabIds);
+ allTabIdsToken = response.tabIdsToken;
+ }
+
+ renderLogEntries(response);
+
+ if ( rowVoided ) {
+ uDom('#clean').toggleClass(
+ 'disabled',
+ tbody.querySelector('tr.tab:not(.canMtx)') === null
+ );
+ }
+
+ // Synchronize toolbar with content of log
+ uDom('#clear').toggleClass(
+ 'disabled',
+ tbody.querySelector('tr') === null
+ );
+
+ readLogBufferAsync();
+};
+
+/******************************************************************************/
+
+// This can be called only once, at init time. After that, this will be called
+// automatically. If called after init time, this will be messy, and this would
+// require a bit more code to ensure no multi time out events.
+
+var readLogBuffer = function() {
+ if ( ownerId === undefined ) { return; }
+ vAPI.messaging.send(
+ 'logger-ui.js',
+ { what: 'readMany', ownerId: ownerId },
+ onLogBufferRead
+ );
+};
+
+var readLogBufferAsync = function() {
+ if ( ownerId === undefined ) { return; }
+ vAPI.setTimeout(readLogBuffer, 1200);
+};
+
+/******************************************************************************/
+
+var pageSelectorChanged = function() {
+ var style = document.getElementById('tabFilterer');
+ var tabClass = document.getElementById('pageSelector').value;
+ var sheet = style.sheet;
+ while ( sheet.cssRules.length !== 0 ) {
+ sheet.deleteRule(0);
+ }
+ if ( tabClass !== '' ) {
+ sheet.insertRule(
+ '#content table tr:not(.' + tabClass + ') { display: none; }',
+ 0
+ );
+ }
+ uDom('#refresh').toggleClass(
+ 'disabled',
+ tabClass === '' || tabClass === 'tab_bts'
+ );
+};
+
+/******************************************************************************/
+
+var refreshTab = function() {
+ var tabClass = document.getElementById('pageSelector').value;
+ var matches = tabClass.match(/^tab_(.+)$/);
+ if ( matches === null ) {
+ return;
+ }
+ if ( matches[1] === 'bts' ) {
+ return;
+ }
+ vAPI.messaging.send(
+ 'logger-ui.js',
+ { what: 'forceReloadTab', tabId: matches[1] }
+ );
+};
+
+/******************************************************************************/
+
+var onMaxEntriesChanged = function() {
+ var raw = uDom(this).val();
+ try {
+ maxEntries = parseInt(raw, 10);
+ if ( isNaN(maxEntries) ) {
+ maxEntries = 0;
+ }
+ } catch (e) {
+ maxEntries = 0;
+ }
+
+ vAPI.messaging.send('logger-ui.js', {
+ what: 'userSettings',
+ name: 'maxLoggedRequests',
+ value: maxEntries
+ });
+
+ truncateLog(maxEntries);
+};
+
+/******************************************************************************/
+
+var rowFilterer = (function() {
+ var filters = [];
+
+ var parseInput = function() {
+ filters = [];
+
+ var rawPart, hardBeg, hardEnd;
+ var raw = uDom('#filterInput').val().trim();
+ var rawParts = raw.split(/\s+/);
+ var reStr, reStrs = [], not = false;
+ var n = rawParts.length;
+ for ( var i = 0; i < n; i++ ) {
+ rawPart = rawParts[i];
+ if ( rawPart.charAt(0) === '!' ) {
+ if ( reStrs.length === 0 ) {
+ not = true;
+ }
+ rawPart = rawPart.slice(1);
+ }
+ hardBeg = rawPart.charAt(0) === '|';
+ if ( hardBeg ) {
+ rawPart = rawPart.slice(1);
+ }
+ hardEnd = rawPart.slice(-1) === '|';
+ if ( hardEnd ) {
+ rawPart = rawPart.slice(0, -1);
+ }
+ if ( rawPart === '' ) {
+ continue;
+ }
+ // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
+ reStr = rawPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ if ( hardBeg ) {
+ reStr = '(?:^|\\s)' + reStr;
+ }
+ if ( hardEnd ) {
+ reStr += '(?:\\s|$)';
+ }
+ reStrs.push(reStr);
+ if ( i < (n - 1) && rawParts[i + 1] === '||' ) {
+ i += 1;
+ continue;
+ }
+ reStr = reStrs.length === 1 ? reStrs[0] : reStrs.join('|');
+ filters.push({
+ re: new RegExp(reStr, 'i'),
+ r: !not
+ });
+ reStrs = [];
+ not = false;
+ }
+ };
+
+ var filterOne = function(tr, clean) {
+ var ff = filters;
+ var fcount = ff.length;
+ if ( fcount === 0 && clean === true ) {
+ return;
+ }
+ // do not filter out doc boundaries, they help separate important
+ // section of log.
+ var cl = tr.classList;
+ if ( cl.contains('doc') ) {
+ return;
+ }
+ if ( fcount === 0 ) {
+ cl.remove('f');
+ return;
+ }
+ var cc = tr.cells;
+ var ccount = cc.length;
+ var hit, j, f;
+ // each filter expression must hit (implicit and-op)
+ // if...
+ // positive filter expression = there must one hit on any field
+ // negative filter expression = there must be no hit on all fields
+ for ( var i = 0; i < fcount; i++ ) {
+ f = ff[i];
+ hit = !f.r;
+ for ( j = 0; j < ccount; j++ ) {
+ if ( f.re.test(cc[j].textContent) ) {
+ hit = f.r;
+ break;
+ }
+ }
+ if ( !hit ) {
+ cl.add('f');
+ return;
+ }
+ }
+ cl.remove('f');
+ };
+
+ var filterAll = function() {
+ // Special case: no filter
+ if ( filters.length === 0 ) {
+ uDom('#content tr').removeClass('f');
+ return;
+ }
+ var tbody = document.querySelector('#content tbody');
+ var rows = tbody.rows;
+ var i = rows.length;
+ while ( i-- ) {
+ filterOne(rows[i]);
+ }
+ };
+
+ var onFilterChangedAsync = (function() {
+ var timer = null;
+ var commit = function() {
+ timer = null;
+ parseInput();
+ filterAll();
+ };
+ return function() {
+ if ( timer !== null ) {
+ clearTimeout(timer);
+ }
+ timer = vAPI.setTimeout(commit, 750);
+ };
+ })();
+
+ var onFilterButton = function() {
+ var cl = document.body.classList;
+ cl.toggle('f', cl.contains('f') === false);
+ };
+
+ uDom('#filterButton').on('click', onFilterButton);
+ uDom('#filterInput').on('input', onFilterChangedAsync);
+
+ return {
+ filterOne: filterOne,
+ filterAll: filterAll
+ };
+})();
+
+/******************************************************************************/
+
+var toJunkyard = function(trs) {
+ trs.remove();
+ var i = trs.length;
+ while ( i-- ) {
+ trJunkyard.push(trs.nodeAt(i));
+ }
+};
+
+/******************************************************************************/
+
+var clearBuffer = function() {
+ var tbody = document.querySelector('#content tbody');
+ var tr;
+ while ( tbody.firstChild !== null ) {
+ tr = tbody.lastElementChild;
+ trJunkyard.push(tbody.removeChild(tr));
+ }
+ uDom('#clear').addClass('disabled');
+ uDom('#clean').addClass('disabled');
+};
+
+/******************************************************************************/
+
+var cleanBuffer = function() {
+ var rows = uDom('#content tr.tab:not(.canMtx)').remove();
+ var i = rows.length;
+ while ( i-- ) {
+ trJunkyard.push(rows.nodeAt(i));
+ }
+ uDom('#clean').addClass('disabled');
+};
+
+/******************************************************************************/
+
+var toggleCompactView = function() {
+ document.body.classList.toggle('compactView');
+ uDom('#content table .vExpanded').removeClass('vExpanded');
+};
+
+var toggleCompactRow = function(ev) {
+ ev.target.parentElement.classList.toggle('vExpanded');
+};
+
+/******************************************************************************/
+
+var popupManager = (function() {
+ var realTabId = null;
+ var localTabId = null;
+ var container = null;
+ var popup = null;
+ var popupObserver = null;
+ var style = null;
+ var styleTemplate = [
+ 'tr:not(.tab_{{tabId}}) {',
+ 'cursor: not-allowed;',
+ 'opacity: 0.2;',
+ '}'
+ ].join('\n');
+
+ var resizePopup = function() {
+ if ( popup === null ) {
+ return;
+ }
+ var popupBody = popup.contentWindow.document.body;
+ if ( popupBody.clientWidth !== 0 && container.clientWidth !== popupBody.clientWidth ) {
+ container.style.setProperty('width', popupBody.clientWidth + 'px');
+ }
+ popup.style.removeProperty('height');
+ if ( popupBody.clientHeight !== 0 && popup.clientHeight !== popupBody.clientHeight ) {
+ popup.style.setProperty('height', popupBody.clientHeight + 'px');
+ }
+ var ph = document.documentElement.clientHeight;
+ var crect = container.getBoundingClientRect();
+ if ( crect.height > ph ) {
+ popup.style.setProperty('height', 'calc(' + ph + 'px - 1.8em)');
+ }
+ // Adjust width for presence/absence of vertical scroll bar which may
+ // have appeared as a result of last operation.
+ var cw = container.clientWidth;
+ var dw = popup.contentWindow.document.documentElement.clientWidth;
+ if ( cw !== dw ) {
+ container.style.setProperty('width', (2 * cw - dw) + 'px');
+ }
+ };
+
+ var toggleSize = function() {
+ container.classList.toggle('hide');
+ };
+
+ var onResizeRequested = function() {
+ var popupBody = popup.contentWindow.document.body;
+ if ( popupBody.hasAttribute('data-resize-popup') === false ) {
+ return;
+ }
+ popupBody.removeAttribute('data-resize-popup');
+ resizePopup();
+ };
+
+ var onLoad = function() {
+ resizePopup();
+ var popupBody = popup.contentDocument.body;
+ popupBody.removeAttribute('data-resize-popup');
+ popupObserver.observe(popupBody, {
+ attributes: true,
+ attributesFilter: [ 'data-resize-popup' ]
+ });
+ };
+
+ var toggleOn = function(td) {
+ var tr = td.parentNode;
+ var matches = tr.className.match(/(?:^| )tab_([^ ]+)/);
+ if ( matches === null ) {
+ return;
+ }
+ realTabId = localTabId = matches[1];
+ if ( localTabId === 'bts' ) {
+ realTabId = noTabId;
+ }
+
+ container = document.getElementById('popupContainer');
+
+ container.querySelector('div > span:nth-of-type(1)').addEventListener('click', toggleSize);
+ container.querySelector('div > span:nth-of-type(2)').addEventListener('click', toggleOff);
+
+ popup = document.createElement('iframe');
+ popup.addEventListener('load', onLoad);
+ popup.setAttribute('src', 'popup.html?tabId=' + realTabId);
+ popupObserver = new MutationObserver(onResizeRequested);
+ container.appendChild(popup);
+
+ style = document.getElementById('popupFilterer');
+ style.textContent = styleTemplate.replace('{{tabId}}', localTabId);
+
+ document.body.classList.add('popupOn');
+ };
+
+ var toggleOff = function() {
+ document.body.classList.remove('popupOn');
+
+ container.querySelector('div > span:nth-of-type(1)').removeEventListener('click', toggleSize);
+ container.querySelector('div > span:nth-of-type(2)').removeEventListener('click', toggleOff);
+ container.classList.remove('hide');
+
+ popup.removeEventListener('load', onLoad);
+ popupObserver.disconnect();
+ popupObserver = null;
+ popup.setAttribute('src', '');
+ container.removeChild(popup);
+ popup = null;
+
+ style.textContent = '';
+ style = null;
+
+ container = null;
+ realTabId = null;
+ };
+
+ var exports = {
+ toggleOn: function(ev) {
+ if ( realTabId === null ) {
+ toggleOn(ev.target);
+ }
+ },
+ toggleOff: function() {
+ if ( realTabId !== null ) {
+ toggleOff();
+ }
+ }
+ };
+
+ Object.defineProperty(exports, 'tabId', {
+ get: function() { return realTabId || 0; }
+ });
+
+ return exports;
+})();
+
+/******************************************************************************/
+
+var grabView = function() {
+ if ( ownerId === undefined ) {
+ ownerId = Date.now();
+ }
+ readLogBufferAsync();
+};
+
+var releaseView = function() {
+ if ( ownerId === undefined ) { return; }
+ vAPI.messaging.send(
+ 'logger-ui.js',
+ { what: 'releaseView', ownerId: ownerId }
+ );
+ ownerId = undefined;
+};
+
+window.addEventListener('pagehide', releaseView);
+window.addEventListener('pageshow', grabView);
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1398625
+window.addEventListener('beforeunload', releaseView);
+
+/******************************************************************************/
+
+readLogBuffer();
+
+uDom('#pageSelector').on('change', pageSelectorChanged);
+uDom('#refresh').on('click', refreshTab);
+uDom('#compactViewToggler').on('click', toggleCompactView);
+uDom('#clean').on('click', cleanBuffer);
+uDom('#clear').on('click', clearBuffer);
+uDom('#maxEntries').on('change', onMaxEntriesChanged);
+uDom('#content table').on('click', 'tr > td:nth-of-type(1)', toggleCompactRow);
+uDom('#content table').on('click', 'tr.canMtx > td:nth-of-type(2)', popupManager.toggleOn);
+
+/******************************************************************************/
+
+})();
diff --git a/js/logger.js b/js/logger.js
new file mode 100644
index 0000000..896fbdf
--- /dev/null
+++ b/js/logger.js
@@ -0,0 +1,93 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2015-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+µMatrix.logger = (function() {
+
+ var LogEntry = function(args) {
+ this.init(args);
+ };
+
+ LogEntry.prototype.init = function(args) {
+ this.tstamp = Date.now();
+ this.tab = args[0] || '';
+ this.cat = args[1] || '';
+ this.d0 = args[2];
+ this.d1 = args[3];
+ this.d2 = args[4];
+ this.d3 = args[5];
+ };
+
+ var buffer = null;
+ var lastReadTime = 0;
+ var writePtr = 0;
+
+ // After 60 seconds without being read, a buffer will be considered
+ // unused, and thus removed from memory.
+ var logBufferObsoleteAfter = 30 * 1000;
+
+ var janitor = function() {
+ if (
+ buffer !== null &&
+ lastReadTime < (Date.now() - logBufferObsoleteAfter)
+ ) {
+ buffer = null;
+ writePtr = 0;
+ api.ownerId = undefined;
+ }
+ if ( buffer !== null ) {
+ vAPI.setTimeout(janitor, logBufferObsoleteAfter);
+ }
+ };
+
+ var api = {
+ ownerId: undefined,
+ writeOne: function() {
+ if ( buffer === null ) { return; }
+ if ( writePtr === buffer.length ) {
+ buffer.push(new LogEntry(arguments));
+ } else {
+ buffer[writePtr].init(arguments);
+ }
+ writePtr += 1;
+ },
+ readAll: function(ownerId) {
+ this.ownerId = ownerId;
+ if ( buffer === null ) {
+ buffer = [];
+ vAPI.setTimeout(janitor, logBufferObsoleteAfter);
+ }
+ var out = buffer.slice(0, writePtr);
+ writePtr = 0;
+ lastReadTime = Date.now();
+ return out;
+ }
+ };
+
+ return api;
+})();
+
+/******************************************************************************/
diff --git a/js/main-blocked.js b/js/main-blocked.js
new file mode 100644
index 0000000..cb11910
--- /dev/null
+++ b/js/main-blocked.js
@@ -0,0 +1,176 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2015-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uBlock
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+var details = {};
+
+(function() {
+ var matches = /details=([^&]+)/.exec(window.location.search);
+ if ( matches === null ) { return; }
+ try {
+ details = JSON.parse(atob(matches[1]));
+ } catch(ex) {
+ }
+})();
+
+/******************************************************************************/
+
+uDom('.what').text(details.url);
+// uDom('#why').text(details.why.slice(3));
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uMatrix/issues/502
+// Code below originally imported from:
+// https://github.com/gorhill/uBlock/blob/master/src/js/document-blocked.js
+
+(function() {
+ if ( typeof URL !== 'function' ) { return; }
+
+ var reURL = /^https?:\/\//;
+
+ var liFromParam = function(name, value) {
+ if ( value === '' ) {
+ value = name;
+ name = '';
+ }
+ var li = document.createElement('li');
+ var span = document.createElement('span');
+ span.textContent = name;
+ li.appendChild(span);
+ if ( name !== '' && value !== '' ) {
+ li.appendChild(document.createTextNode(' = '));
+ }
+ span = document.createElement('span');
+ if ( reURL.test(value) ) {
+ var a = document.createElement('a');
+ a.href = a.textContent = value;
+ span.appendChild(a);
+ } else {
+ span.textContent = value;
+ }
+ li.appendChild(span);
+ return li;
+ };
+
+ var safeDecodeURIComponent = function(s) {
+ try {
+ s = decodeURIComponent(s);
+ } catch (ex) {
+ }
+ return s;
+ };
+
+ var renderParams = function(parentNode, rawURL) {
+ var a = document.createElement('a');
+ a.href = rawURL;
+ if ( a.search.length === 0 ) { return false; }
+
+ var pos = rawURL.indexOf('?');
+ var li = liFromParam(
+ vAPI.i18n('docblockedNoParamsPrompt'),
+ rawURL.slice(0, pos)
+ );
+ parentNode.appendChild(li);
+
+ var params = a.search.slice(1).split('&');
+ var param, name, value, ul;
+ for ( var i = 0; i < params.length; i++ ) {
+ param = params[i];
+ pos = param.indexOf('=');
+ if ( pos === -1 ) {
+ pos = param.length;
+ }
+ name = safeDecodeURIComponent(param.slice(0, pos));
+ value = safeDecodeURIComponent(param.slice(pos + 1));
+ li = liFromParam(name, value);
+ if ( reURL.test(value) ) {
+ ul = document.createElement('ul');
+ renderParams(ul, value);
+ li.appendChild(ul);
+ }
+ parentNode.appendChild(li);
+ }
+ return true;
+ };
+
+ if ( renderParams(uDom.nodeFromId('parsed'), details.url) === false ) {
+ return;
+ }
+
+ var toggler = document.createElement('span');
+ toggler.className = 'fa';
+ uDom('#theURL > p').append(toggler);
+
+ uDom(toggler).on('click', function() {
+ var collapsed = uDom.nodeFromId('theURL').classList.toggle('collapsed');
+ vAPI.localStorage.setItem(
+ 'document-blocked-collapse-url',
+ collapsed.toString()
+ );
+ });
+
+ uDom.nodeFromId('theURL').classList.toggle(
+ 'collapsed',
+ vAPI.localStorage.getItem('document-blocked-collapse-url') === 'true'
+ );
+})();
+
+/******************************************************************************/
+
+if ( window.history.length > 1 ) {
+ uDom('#back').on('click', function() { window.history.back(); });
+ uDom('#bye').css('display', 'none');
+} else {
+ uDom('#bye').on('click', function() { window.close(); });
+ uDom('#back').css('display', 'none');
+}
+
+/******************************************************************************/
+
+// See if the target hostname is still blacklisted, and if not, navigate to it.
+
+vAPI.messaging.send('main-blocked.js', {
+ what: 'mustBlock',
+ scope: details.hn,
+ hostname: details.hn,
+ type: 'doc'
+}, function(response) {
+ if ( response === false ) {
+ window.location.replace(details.url);
+ }
+});
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/matrix.js b/js/matrix.js
new file mode 100644
index 0000000..59eb84a
--- /dev/null
+++ b/js/matrix.js
@@ -0,0 +1,873 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global punycode */
+/* jshint bitwise: false */
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.Matrix = (function() {
+
+/******************************************************************************/
+
+var µm = µMatrix;
+var magicId = 'axyorpwxtmnf';
+var uniqueIdGenerator = 1;
+
+/******************************************************************************/
+
+var Matrix = function() {
+ this.id = uniqueIdGenerator++;
+ this.reset();
+ this.sourceRegister = '';
+ this.decomposedSourceRegister = [''];
+ this.specificityRegister = 0;
+};
+
+/******************************************************************************/
+
+Matrix.Transparent = 0;
+Matrix.Red = 1;
+Matrix.Green = 2;
+Matrix.Gray = 3;
+
+Matrix.Indirect = 0x00;
+Matrix.Direct = 0x80;
+
+Matrix.RedDirect = Matrix.Red | Matrix.Direct;
+Matrix.RedIndirect = Matrix.Red | Matrix.Indirect;
+Matrix.GreenDirect = Matrix.Green | Matrix.Direct;
+Matrix.GreenIndirect = Matrix.Green | Matrix.Indirect;
+Matrix.GrayDirect = Matrix.Gray | Matrix.Direct;
+Matrix.GrayIndirect = Matrix.Gray | Matrix.Indirect;
+
+/******************************************************************************/
+
+var typeBitOffsets = new Map([
+ [ '*', 0 ],
+ [ 'doc', 2 ],
+ [ 'cookie', 4 ],
+ [ 'css', 6 ],
+ [ 'image', 8 ],
+ [ 'media', 10 ],
+ [ 'script', 12 ],
+ [ 'xhr', 14 ],
+ [ 'frame', 16 ],
+ [ 'other', 18 ]
+]);
+
+var stateToNameMap = new Map([
+ [ 1, 'block' ],
+ [ 2, 'allow' ],
+ [ 3, 'inherit' ]
+]);
+
+var nameToStateMap = {
+ 'block': 1,
+ 'allow': 2,
+ 'noop': 2,
+ 'inherit': 3
+};
+
+var switchBitOffsets = new Map([
+ [ 'matrix-off', 0 ],
+ [ 'https-strict', 2 ],
+ /* 4 is now unused, formerly assigned to UA spoofing */
+ [ 'referrer-spoof', 6 ],
+ [ 'noscript-spoof', 8 ],
+ [ 'no-workers', 10 ]
+]);
+
+var switchStateToNameMap = new Map([
+ [ 1, 'true' ],
+ [ 2, 'false' ]
+]);
+
+var nameToSwitchStateMap = {
+ 'true': 1,
+ 'false': 2
+};
+
+/******************************************************************************/
+
+Matrix.columnHeaderIndices = (function() {
+ var out = new Map(),
+ i = 0;
+ for ( var type of typeBitOffsets.keys() ) {
+ out.set(type, i++);
+ }
+ return out;
+})();
+
+
+Matrix.switchNames = new Set(switchBitOffsets.keys());
+
+/******************************************************************************/
+
+// For performance purpose, as simple tests as possible
+var reHostnameVeryCoarse = /[g-z_-]/;
+var reIPv4VeryCoarse = /\.\d+$/;
+
+// http://tools.ietf.org/html/rfc5952
+// 4.3: "MUST be represented in lowercase"
+// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers
+
+var isIPAddress = function(hostname) {
+ if ( reHostnameVeryCoarse.test(hostname) ) {
+ return false;
+ }
+ if ( reIPv4VeryCoarse.test(hostname) ) {
+ return true;
+ }
+ return hostname.charAt(0) === '[';
+};
+
+/******************************************************************************/
+
+var toBroaderHostname = function(hostname) {
+ if ( hostname === '*' ) { return ''; }
+ if ( isIPAddress(hostname) ) {
+ return toBroaderIPAddress(hostname);
+ }
+ var pos = hostname.indexOf('.');
+ if ( pos === -1 ) {
+ return '*';
+ }
+ return hostname.slice(pos + 1);
+};
+
+var toBroaderIPAddress = function(ipaddress) {
+ // Can't broaden IPv6 (for now)
+ if ( ipaddress.charAt(0) === '[' ) {
+ return '*';
+ }
+ var pos = ipaddress.lastIndexOf('.');
+ return pos !== -1 ? ipaddress.slice(0, pos) : '*';
+};
+
+Matrix.toBroaderHostname = toBroaderHostname;
+
+/******************************************************************************/
+
+// Find out src-des relationship, using coarse-to-fine grained tests for
+// speed. If desHostname is 1st-party to srcHostname, the domain is returned,
+// otherwise the empty string.
+
+var extractFirstPartyDesDomain = function(srcHostname, desHostname) {
+ if ( srcHostname === '*' || desHostname === '*' || desHostname === '1st-party' ) {
+ return '';
+ }
+ var µmuri = µm.URI;
+ var srcDomain = µmuri.domainFromHostname(srcHostname) || srcHostname;
+ var desDomain = µmuri.domainFromHostname(desHostname) || desHostname;
+ return desDomain === srcDomain ? desDomain : '';
+};
+
+/******************************************************************************/
+
+Matrix.prototype.reset = function() {
+ this.switches = new Map();
+ this.rules = new Map();
+ this.rootValue = Matrix.RedIndirect;
+ this.modifiedTime = 0;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.decomposeSource = function(srcHostname) {
+ if ( srcHostname === this.sourceRegister ) { return; }
+ var hn = srcHostname;
+ this.decomposedSourceRegister[0] = this.sourceRegister = hn;
+ var i = 1;
+ for (;;) {
+ hn = toBroaderHostname(hn);
+ this.decomposedSourceRegister[i++] = hn;
+ if ( hn === '' ) { break; }
+ }
+};
+
+/******************************************************************************/
+
+// Copy another matrix to self. Do this incrementally to minimize impact on
+// a live matrix.
+
+Matrix.prototype.assign = function(other) {
+ var k, entry;
+ // Remove rules not in other
+ for ( k of this.rules.keys() ) {
+ if ( other.rules.has(k) === false ) {
+ this.rules.delete(k);
+ }
+ }
+ // Remove switches not in other
+ for ( k of this.switches.keys() ) {
+ if ( other.switches.has(k) === false ) {
+ this.switches.delete(k);
+ }
+ }
+ // Add/change rules in other
+ for ( entry of other.rules ) {
+ this.rules.set(entry[0], entry[1]);
+ }
+ // Add/change switches in other
+ for ( entry of other.switches ) {
+ this.switches.set(entry[0], entry[1]);
+ }
+ this.modifiedTime = other.modifiedTime;
+ return this;
+};
+
+// https://www.youtube.com/watch?v=e9RS4biqyAc
+
+/******************************************************************************/
+
+// If value is undefined, the switch is removed
+
+Matrix.prototype.setSwitch = function(switchName, srcHostname, newVal) {
+ var bitOffset = switchBitOffsets.get(switchName);
+ if ( bitOffset === undefined ) {
+ return false;
+ }
+ if ( newVal === this.evaluateSwitch(switchName, srcHostname) ) {
+ return false;
+ }
+ var bits = this.switches.get(srcHostname) || 0;
+ bits &= ~(3 << bitOffset);
+ bits |= newVal << bitOffset;
+ if ( bits === 0 ) {
+ this.switches.delete(srcHostname);
+ } else {
+ this.switches.set(srcHostname, bits);
+ }
+ this.modifiedTime = Date.now();
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) {
+ var bitOffset = typeBitOffsets.get(type),
+ k = srcHostname + ' ' + desHostname,
+ oldBitmap = this.rules.get(k);
+ if ( oldBitmap === undefined ) {
+ oldBitmap = 0;
+ }
+ var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset);
+ if ( newBitmap === oldBitmap ) {
+ return false;
+ }
+ if ( newBitmap === 0 ) {
+ this.rules.delete(k);
+ } else {
+ this.rules.set(k, newBitmap);
+ }
+ this.modifiedTime = Date.now();
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.blacklistCell = function(srcHostname, desHostname, type) {
+ var r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 1 ) {
+ return false;
+ }
+ this.setCell(srcHostname, desHostname, type, 0);
+ r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 1 ) {
+ return true;
+ }
+ this.setCell(srcHostname, desHostname, type, 1);
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.whitelistCell = function(srcHostname, desHostname, type) {
+ var r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 2 ) {
+ return false;
+ }
+ this.setCell(srcHostname, desHostname, type, 0);
+ r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 2 ) {
+ return true;
+ }
+ this.setCell(srcHostname, desHostname, type, 2);
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.graylistCell = function(srcHostname, desHostname, type) {
+ var r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 0 || r === 3 ) {
+ return false;
+ }
+ this.setCell(srcHostname, desHostname, type, 0);
+ r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 0 || r === 3 ) {
+ return true;
+ }
+ this.setCell(srcHostname, desHostname, type, 3);
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) {
+ var key = srcHostname + ' ' + desHostname;
+ var bitmap = this.rules.get(key);
+ if ( bitmap === undefined ) {
+ return 0;
+ }
+ return bitmap >> typeBitOffsets.get(type) & 3;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) {
+ this.decomposeSource(srcHostname);
+
+ var bitOffset = typeBitOffsets.get(type),
+ s, v, i = 0;
+ for (;;) {
+ s = this.decomposedSourceRegister[i++];
+ if ( s === '' ) { break; }
+ v = this.rules.get(s + ' ' + desHostname);
+ if ( v !== undefined ) {
+ v = v >> bitOffset & 3;
+ if ( v !== 0 ) {
+ return v;
+ }
+ }
+ }
+ // srcHostname is '*' at this point
+
+ // Preset blacklisted hostnames are blacklisted in global scope
+ if ( type === '*' && µm.ubiquitousBlacklist.test(desHostname) ) {
+ return 1;
+ }
+
+ // https://github.com/gorhill/uMatrix/issues/65
+ // Hardcoded global `doc` rule
+ if ( type === 'doc' && desHostname === '*' ) {
+ return 2;
+ }
+
+ return 0;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.evaluateCellZXY = function(srcHostname, desHostname, type) {
+ // Matrix filtering switch
+ this.specificityRegister = 0;
+ if ( this.evaluateSwitchZ('matrix-off', srcHostname) ) {
+ return Matrix.GreenIndirect;
+ }
+
+ // TODO: There are cells evaluated twice when the type is '*'. Unsure
+ // whether it's worth trying to avoid that, as this could introduce
+ // overhead which may not be gained back by skipping the redundant tests.
+ // And this happens *only* when building the matrix UI, not when
+ // evaluating net requests.
+
+ // Specific-hostname specific-type cell
+ this.specificityRegister = 1;
+ var r = this.evaluateCellZ(srcHostname, desHostname, type);
+ if ( r === 1 ) { return Matrix.RedDirect; }
+ if ( r === 2 ) { return Matrix.GreenDirect; }
+
+ // Specific-hostname any-type cell
+ this.specificityRegister = 2;
+ var rl = this.evaluateCellZ(srcHostname, desHostname, '*');
+ if ( rl === 1 ) { return Matrix.RedIndirect; }
+
+ var d = desHostname;
+ var firstPartyDesDomain = extractFirstPartyDesDomain(srcHostname, desHostname);
+
+ // Ancestor cells, up to 1st-party destination domain
+ if ( firstPartyDesDomain !== '' ) {
+ this.specificityRegister = 3;
+ for (;;) {
+ if ( d === firstPartyDesDomain ) { break; }
+ d = d.slice(d.indexOf('.') + 1);
+
+ // specific-hostname specific-type cell
+ r = this.evaluateCellZ(srcHostname, d, type);
+ if ( r === 1 ) { return Matrix.RedIndirect; }
+ if ( r === 2 ) { return Matrix.GreenIndirect; }
+ // Do not override a narrower rule
+ if ( rl !== 2 ) {
+ rl = this.evaluateCellZ(srcHostname, d, '*');
+ if ( rl === 1 ) { return Matrix.RedIndirect; }
+ }
+ }
+
+ // 1st-party specific-type cell: it's a special row, looked up only
+ // when destination is 1st-party to source.
+ r = this.evaluateCellZ(srcHostname, '1st-party', type);
+ if ( r === 1 ) { return Matrix.RedIndirect; }
+ if ( r === 2 ) { return Matrix.GreenIndirect; }
+ // Do not override narrower rule
+ if ( rl !== 2 ) {
+ rl = this.evaluateCellZ(srcHostname, '1st-party', '*');
+ if ( rl === 1 ) { return Matrix.RedIndirect; }
+ }
+ }
+
+ // Keep going, up to root
+ this.specificityRegister = 4;
+ for (;;) {
+ d = toBroaderHostname(d);
+ if ( d === '*' ) { break; }
+
+ // specific-hostname specific-type cell
+ r = this.evaluateCellZ(srcHostname, d, type);
+ if ( r === 1 ) { return Matrix.RedIndirect; }
+ if ( r === 2 ) { return Matrix.GreenIndirect; }
+ // Do not override narrower rule
+ if ( rl !== 2 ) {
+ rl = this.evaluateCellZ(srcHostname, d, '*');
+ if ( rl === 1 ) { return Matrix.RedIndirect; }
+ }
+ }
+
+ // Any-hostname specific-type cells
+ this.specificityRegister = 5;
+ r = this.evaluateCellZ(srcHostname, '*', type);
+ // Line below is strict-blocking
+ if ( r === 1 ) { return Matrix.RedIndirect; }
+ // Narrower rule wins
+ if ( rl === 2 ) { return Matrix.GreenIndirect; }
+ if ( r === 2 ) { return Matrix.GreenIndirect; }
+
+ // Any-hostname any-type cell
+ this.specificityRegister = 6;
+ r = this.evaluateCellZ(srcHostname, '*', '*');
+ if ( r === 1 ) { return Matrix.RedIndirect; }
+ if ( r === 2 ) { return Matrix.GreenIndirect; }
+ return this.rootValue;
+};
+
+// https://www.youtube.com/watch?v=4C5ZkwrnVfM
+
+/******************************************************************************/
+
+Matrix.prototype.evaluateRowZXY = function(srcHostname, desHostname) {
+ var out = [];
+ for ( var type of typeBitOffsets.keys() ) {
+ out.push(this.evaluateCellZXY(srcHostname, desHostname, type));
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.mustBlock = function(srcHostname, desHostname, type) {
+ return (this.evaluateCellZXY(srcHostname, desHostname, type) & 3) === Matrix.Red;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.srcHostnameFromRule = function(rule) {
+ return rule.slice(0, rule.indexOf(' '));
+};
+
+/******************************************************************************/
+
+Matrix.prototype.desHostnameFromRule = function(rule) {
+ return rule.slice(rule.indexOf(' ') + 1);
+};
+
+/******************************************************************************/
+
+Matrix.prototype.setSwitchZ = function(switchName, srcHostname, newState) {
+ var bitOffset = switchBitOffsets.get(switchName);
+ if ( bitOffset === undefined ) {
+ return false;
+ }
+ var state = this.evaluateSwitchZ(switchName, srcHostname);
+ if ( newState === state ) {
+ return false;
+ }
+ if ( newState === undefined ) {
+ newState = !state;
+ }
+ var bits = this.switches.get(srcHostname) || 0;
+ bits &= ~(3 << bitOffset);
+ if ( bits === 0 ) {
+ this.switches.delete(srcHostname);
+ } else {
+ this.switches.set(srcHostname, bits);
+ }
+ this.modifiedTime = Date.now();
+ state = this.evaluateSwitchZ(switchName, srcHostname);
+ if ( state === newState ) {
+ return true;
+ }
+ this.switches.set(srcHostname, bits | ((newState ? 1 : 2) << bitOffset));
+ return true;
+};
+
+/******************************************************************************/
+
+// 0 = inherit from broader scope, up to default state
+// 1 = non-default state
+// 2 = forced default state (to override a broader non-default state)
+
+Matrix.prototype.evaluateSwitch = function(switchName, srcHostname) {
+ var bits = this.switches.get(srcHostname) || 0;
+ if ( bits === 0 ) {
+ return 0;
+ }
+ var bitOffset = switchBitOffsets.get(switchName);
+ if ( bitOffset === undefined ) {
+ return 0;
+ }
+ return (bits >> bitOffset) & 3;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.evaluateSwitchZ = function(switchName, srcHostname) {
+ var bitOffset = switchBitOffsets.get(switchName);
+ if ( bitOffset === undefined ) { return false; }
+
+ this.decomposeSource(srcHostname);
+
+ var s, bits, i = 0;
+ for (;;) {
+ s = this.decomposedSourceRegister[i++];
+ if ( s === '' ) { break; }
+ bits = this.switches.get(s) || 0;
+ if ( bits !== 0 ) {
+ bits = bits >> bitOffset & 3;
+ if ( bits !== 0 ) {
+ return bits === 1;
+ }
+ }
+ }
+ return false;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.extractAllSourceHostnames = (function() {
+ var cachedResult = new Set();
+ var matrixId = 0;
+ var readTime = 0;
+
+ return function() {
+ if ( matrixId !== this.id || readTime !== this.modifiedTime ) {
+ cachedResult.clear();
+ for ( var rule of this.rules.keys() ) {
+ cachedResult.add(rule.slice(0, rule.indexOf(' ')));
+ }
+ matrixId = this.id;
+ readTime = this.modifiedTime;
+ }
+ return cachedResult;
+ };
+})();
+
+/******************************************************************************/
+
+Matrix.prototype.toString = function() {
+ var out = [];
+ var rule, type, switchName, val;
+ var srcHostname, desHostname;
+ for ( rule of this.rules.keys() ) {
+ srcHostname = this.srcHostnameFromRule(rule);
+ desHostname = this.desHostnameFromRule(rule);
+ for ( type of typeBitOffsets.keys() ) {
+ val = this.evaluateCell(srcHostname, desHostname, type);
+ if ( val === 0 ) { continue; }
+ out.push(
+ punycode.toUnicode(srcHostname) + ' ' +
+ punycode.toUnicode(desHostname) + ' ' +
+ type + ' ' +
+ stateToNameMap.get(val)
+ );
+ }
+ }
+ for ( srcHostname of this.switches.keys() ) {
+ for ( switchName of switchBitOffsets.keys() ) {
+ val = this.evaluateSwitch(switchName, srcHostname);
+ if ( val === 0 ) { continue; }
+ out.push(switchName + ': ' + srcHostname + ' ' + switchStateToNameMap.get(val));
+ }
+ }
+ return out.sort().join('\n');
+};
+
+/******************************************************************************/
+
+Matrix.prototype.fromString = function(text, append) {
+ var matrix = append ? this : new Matrix();
+ var textEnd = text.length;
+ var lineBeg = 0, lineEnd;
+ var line, pos;
+ var fields, fieldVal;
+ var switchName;
+ var srcHostname = '';
+ var desHostname = '';
+ var type, state;
+
+ while ( lineBeg < textEnd ) {
+ lineEnd = text.indexOf('\n', lineBeg);
+ if ( lineEnd < 0 ) {
+ lineEnd = text.indexOf('\r', lineBeg);
+ if ( lineEnd < 0 ) {
+ lineEnd = textEnd;
+ }
+ }
+ line = text.slice(lineBeg, lineEnd).trim();
+ lineBeg = lineEnd + 1;
+
+ pos = line.indexOf('# ');
+ if ( pos !== -1 ) {
+ line = line.slice(0, pos).trim();
+ }
+ if ( line === '' ) {
+ continue;
+ }
+
+ fields = line.split(/\s+/);
+
+ // Less than 2 fields makes no sense
+ if ( fields.length < 2 ) {
+ continue;
+ }
+
+ fieldVal = fields[0];
+
+ // Special directives:
+
+ // title
+ pos = fieldVal.indexOf('title:');
+ if ( pos !== -1 ) {
+ // TODO
+ continue;
+ }
+
+ // Name
+ pos = fieldVal.indexOf('name:');
+ if ( pos !== -1 ) {
+ // TODO
+ continue;
+ }
+
+ // Switch on/off
+
+ // `switch:` srcHostname state
+ // state = [`true`, `false`]
+ switchName = '';
+ if ( fieldVal === 'switch:' || fieldVal === 'matrix:' ) {
+ fieldVal = 'matrix-off:';
+ }
+ pos = fieldVal.indexOf(':');
+ if ( pos !== -1 ) {
+ switchName = fieldVal.slice(0, pos);
+ }
+ if ( switchBitOffsets.has(switchName) ) {
+ srcHostname = punycode.toASCII(fields[1]);
+
+ // No state field: reject
+ fieldVal = fields[2];
+ if ( fieldVal === null ) {
+ continue;
+ }
+ // Unknown state: reject
+ if ( nameToSwitchStateMap.hasOwnProperty(fieldVal) === false ) {
+ continue;
+ }
+
+ // Backward compatibility:
+ // `chromium-behind-the-scene` is now `behind-the-scene`
+ if ( srcHostname === 'chromium-behind-the-scene' ) {
+ srcHostname = 'behind-the-scene';
+ }
+
+ matrix.setSwitch(switchName, srcHostname, nameToSwitchStateMap[fieldVal]);
+ continue;
+ }
+
+ // Unknown directive
+ if ( fieldVal.endsWith(':') ) {
+ continue;
+ }
+
+ // Valid rule syntax:
+
+ // srcHostname desHostname [type [state]]
+ // type = a valid request type
+ // state = [`block`, `allow`, `inherit`]
+
+ // srcHostname desHostname type
+ // type = a valid request type
+ // state = `allow`
+
+ // srcHostname desHostname
+ // type = `*`
+ // state = `allow`
+
+ // Lines with invalid syntax silently ignored
+
+ srcHostname = punycode.toASCII(fields[0]);
+ desHostname = punycode.toASCII(fields[1]);
+
+ fieldVal = fields[2];
+
+ if ( fieldVal !== undefined ) {
+ type = fieldVal;
+ // https://github.com/gorhill/uMatrix/issues/759
+ // Backward compatibility.
+ if ( type === 'plugin' ) {
+ type = 'media';
+ }
+ // Unknown type: reject
+ if ( typeBitOffsets.has(type) === false ) {
+ continue;
+ }
+ } else {
+ type = '*';
+ }
+
+ fieldVal = fields[3];
+
+ if ( fieldVal !== undefined ) {
+ // Unknown state: reject
+ if ( nameToStateMap.hasOwnProperty(fieldVal) === false ) {
+ continue;
+ }
+ state = nameToStateMap[fieldVal];
+ } else {
+ state = 2;
+ }
+
+ matrix.setCell(srcHostname, desHostname, type, state);
+ }
+
+ if ( !append ) {
+ this.assign(matrix);
+ }
+
+ this.modifiedTime = Date.now();
+};
+
+/******************************************************************************/
+
+Matrix.prototype.toSelfie = function() {
+ return {
+ magicId: magicId,
+ switches: Array.from(this.switches),
+ rules: Array.from(this.rules)
+ };
+};
+
+/******************************************************************************/
+
+Matrix.prototype.fromSelfie = function(selfie) {
+ if ( selfie.magicId !== magicId ) { return false; }
+ this.switches = new Map(selfie.switches);
+ this.rules = new Map(selfie.rules);
+ this.modifiedTime = Date.now();
+ return true;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.diff = function(other, srcHostname, desHostnames) {
+ var out = [];
+ var desHostname, type;
+ var switchName, i, thisVal, otherVal;
+ for (;;) {
+ for ( switchName of switchBitOffsets.keys() ) {
+ thisVal = this.evaluateSwitch(switchName, srcHostname);
+ otherVal = other.evaluateSwitch(switchName, srcHostname);
+ if ( thisVal !== otherVal ) {
+ out.push({
+ 'what': switchName,
+ 'src': srcHostname
+ });
+ }
+ }
+ i = desHostnames.length;
+ while ( i-- ) {
+ desHostname = desHostnames[i];
+ for ( type of typeBitOffsets.keys() ) {
+ thisVal = this.evaluateCell(srcHostname, desHostname, type);
+ otherVal = other.evaluateCell(srcHostname, desHostname, type);
+ if ( thisVal === otherVal ) { continue; }
+ out.push({
+ 'what': 'rule',
+ 'src': srcHostname,
+ 'des': desHostname,
+ 'type': type
+ });
+ }
+ }
+ srcHostname = toBroaderHostname(srcHostname);
+ if ( srcHostname === '' ) {
+ break;
+ }
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+Matrix.prototype.applyDiff = function(diff, from) {
+ var changed = false;
+ var i = diff.length;
+ var action, val;
+ while ( i-- ) {
+ action = diff[i];
+ if ( action.what === 'rule' ) {
+ val = from.evaluateCell(action.src, action.des, action.type);
+ changed = this.setCell(action.src, action.des, action.type, val) || changed;
+ continue;
+ }
+ if ( switchBitOffsets.has(action.what) ) {
+ val = from.evaluateSwitch(action.what, action.src);
+ changed = this.setSwitch(action.what, action.src, val) || changed;
+ continue;
+ }
+ }
+ return changed;
+};
+
+/******************************************************************************/
+
+return Matrix;
+
+/******************************************************************************/
+
+// https://www.youtube.com/watch?v=wlNrQGmj6oQ
+
+})();
+
+/******************************************************************************/
diff --git a/js/messaging.js b/js/messaging.js
new file mode 100644
index 0000000..d5c472f
--- /dev/null
+++ b/js/messaging.js
@@ -0,0 +1,963 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Default handler
+
+(function() {
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+// Default is for commonly used message.
+
+function onMessage(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'getAssetContent':
+ µm.assets.get(request.url, { dontCache: true }, callback);
+ return;
+
+ case 'selectHostsFiles':
+ µm.selectHostsFiles(request, callback);
+ return;
+
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'forceReloadTab':
+ µm.forceReload(request.tabId, request.bypassCache);
+ break;
+
+ case 'forceUpdateAssets':
+ µm.scheduleAssetUpdater(0);
+ µm.assets.updateStart({ delay: 2000 });
+ break;
+
+ case 'getUserSettings':
+ response = {
+ userSettings: µm.userSettings,
+ matrixSwitches: {
+ 'https-strict': µm.pMatrix.evaluateSwitch('https-strict', '*') === 1,
+ 'referrer-spoof': µm.pMatrix.evaluateSwitch('referrer-spoof', '*') === 1,
+ 'noscript-spoof': µm.pMatrix.evaluateSwitch('noscript-spoof', '*') === 1
+ }
+ };
+ break;
+
+ case 'gotoExtensionURL':
+ µm.gotoExtensionURL(request);
+ break;
+
+ case 'gotoURL':
+ µm.gotoURL(request);
+ break;
+
+ case 'mustBlock':
+ response = µm.mustBlock(
+ request.scope,
+ request.hostname,
+ request.type
+ );
+ break;
+
+ case 'readRawSettings':
+ response = µm.stringFromRawSettings();
+ break;
+
+ case 'reloadHostsFiles':
+ µm.reloadHostsFiles();
+ break;
+
+ case 'setMatrixSwitch':
+ µm.tMatrix.setSwitch(request.switchName, '*', request.state);
+ if ( µm.pMatrix.setSwitch(request.switchName, '*', request.state) ) {
+ µm.saveMatrix();
+ }
+ break;
+
+ case 'userSettings':
+ if ( request.hasOwnProperty('value') === false ) {
+ request.value = undefined;
+ }
+ response = µm.changeUserSettings(request.name, request.value);
+ break;
+
+ case 'writeRawSettings':
+ µm.rawSettingsFromString(request.content);
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+}
+
+/******************************************************************************/
+
+vAPI.messaging.setup(onMessage);
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+(function() {
+
+// popup.js
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+// Constructor is faster than object literal
+
+var RowSnapshot = function(srcHostname, desHostname, desDomain) {
+ this.domain = desDomain;
+ this.temporary = µm.tMatrix.evaluateRowZXY(srcHostname, desHostname);
+ this.permanent = µm.pMatrix.evaluateRowZXY(srcHostname, desHostname);
+ this.counts = RowSnapshot.counts.slice();
+ this.totals = RowSnapshot.counts.slice();
+};
+
+RowSnapshot.counts = (function() {
+ var aa = [];
+ for ( var i = 0, n = µm.Matrix.columnHeaderIndices.size; i < n; i++ ) {
+ aa[i] = 0;
+ }
+ return aa;
+})();
+
+/******************************************************************************/
+
+var matrixSnapshot = function(pageStore, details) {
+ var µmuser = µm.userSettings;
+ var headerIndices = µm.Matrix.columnHeaderIndices;
+
+ var r = {
+ appVersion: vAPI.app.version,
+ blockedCount: pageStore.requestStats.blocked.all,
+ collapseAllDomains: µmuser.popupCollapseAllDomains,
+ collapseBlacklistedDomains: µmuser.popupCollapseBlacklistedDomains,
+ diff: [],
+ domain: pageStore.pageDomain,
+ has3pReferrer: pageStore.has3pReferrer,
+ hasMixedContent: pageStore.hasMixedContent,
+ hasNoscriptTags: pageStore.hasNoscriptTags,
+ hasWebWorkers: pageStore.hasWebWorkers,
+ headerIndices: Array.from(headerIndices),
+ hostname: pageStore.pageHostname,
+ mtxContentModified: pageStore.mtxContentModifiedTime !== details.mtxContentModifiedTime,
+ mtxCountModified: pageStore.mtxCountModifiedTime !== details.mtxCountModifiedTime,
+ mtxContentModifiedTime: pageStore.mtxContentModifiedTime,
+ mtxCountModifiedTime: pageStore.mtxCountModifiedTime,
+ pMatrixModified: µm.pMatrix.modifiedTime !== details.pMatrixModifiedTime,
+ pMatrixModifiedTime: µm.pMatrix.modifiedTime,
+ pSwitches: {},
+ rows: {},
+ rowCount: 0,
+ scope: '*',
+ tabId: pageStore.tabId,
+ tMatrixModified: µm.tMatrix.modifiedTime !== details.tMatrixModifiedTime,
+ tMatrixModifiedTime: µm.tMatrix.modifiedTime,
+ tSwitches: {},
+ url: pageStore.pageUrl,
+ userSettings: {
+ colorBlindFriendly: µmuser.colorBlindFriendly,
+ displayTextSize: µmuser.displayTextSize,
+ popupScopeLevel: µmuser.popupScopeLevel
+ }
+ };
+
+ if ( typeof details.scope === 'string' ) {
+ r.scope = details.scope;
+ } else if ( µmuser.popupScopeLevel === 'site' ) {
+ r.scope = r.hostname;
+ } else if ( µmuser.popupScopeLevel === 'domain' ) {
+ r.scope = r.domain;
+ }
+
+ for ( var switchName of µm.Matrix.switchNames ) {
+ r.tSwitches[switchName] = µm.tMatrix.evaluateSwitchZ(switchName, r.scope);
+ r.pSwitches[switchName] = µm.pMatrix.evaluateSwitchZ(switchName, r.scope);
+ }
+
+ // These rows always exist
+ r.rows['*'] = new RowSnapshot(r.scope, '*', '*');
+ r.rows['1st-party'] = new RowSnapshot(r.scope, '1st-party', '1st-party');
+ r.rowCount += 1;
+
+ var µmuri = µm.URI;
+ var reqType, reqHostname, reqDomain;
+ var desHostname;
+ var row, typeIndex;
+ var anyIndex = headerIndices.get('*');
+ var pos, count;
+
+ for ( var entry of pageStore.hostnameTypeCells ) {
+ pos = entry[0].indexOf(' ');
+ reqHostname = entry[0].slice(0, pos);
+ reqType = entry[0].slice(pos + 1);
+ // rhill 2013-10-23: hostname can be empty if the request is a data url
+ // https://github.com/gorhill/httpswitchboard/issues/26
+ if ( reqHostname === '' ) {
+ reqHostname = pageStore.pageHostname;
+ }
+ reqDomain = µmuri.domainFromHostname(reqHostname) || reqHostname;
+
+ // We want rows of self and ancestors
+ desHostname = reqHostname;
+ for (;;) {
+ // If row exists, ancestors exist
+ if ( r.rows.hasOwnProperty(desHostname) !== false ) { break; }
+ r.rows[desHostname] = new RowSnapshot(r.scope, desHostname, reqDomain);
+ r.rowCount += 1;
+ if ( desHostname === reqDomain ) { break; }
+ pos = desHostname.indexOf('.');
+ if ( pos === -1 ) { break; }
+ desHostname = desHostname.slice(pos + 1);
+ }
+
+ count = entry[1].size;
+ typeIndex = headerIndices.get(reqType);
+ row = r.rows[reqHostname];
+ row.counts[typeIndex] += count;
+ row.counts[anyIndex] += count;
+ row = r.rows[reqDomain];
+ row.totals[typeIndex] += count;
+ row.totals[anyIndex] += count;
+ row = r.rows['*'];
+ row.totals[typeIndex] += count;
+ row.totals[anyIndex] += count;
+ }
+
+ r.diff = µm.tMatrix.diff(µm.pMatrix, r.hostname, Object.keys(r.rows));
+
+ return r;
+};
+
+/******************************************************************************/
+
+var matrixSnapshotFromTabId = function(details, callback) {
+ var matrixSnapshotIf = function(tabId, details) {
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) {
+ callback('ENOTFOUND');
+ return;
+ }
+
+ // First verify whether we must return data or not.
+ if (
+ µm.tMatrix.modifiedTime === details.tMatrixModifiedTime &&
+ µm.pMatrix.modifiedTime === details.pMatrixModifiedTime &&
+ pageStore.mtxContentModifiedTime === details.mtxContentModifiedTime &&
+ pageStore.mtxCountModifiedTime === details.mtxCountModifiedTime
+ ) {
+ callback('ENOCHANGE');
+ return ;
+ }
+
+ callback(matrixSnapshot(pageStore, details));
+ };
+
+ // Specific tab id requested?
+ if ( details.tabId ) {
+ matrixSnapshotIf(details.tabId, details);
+ return;
+ }
+
+ // Fall back to currently active tab
+ var onTabReady = function(tab) {
+ if ( tab instanceof Object === false ) {
+ callback('ENOTFOUND');
+ return;
+ }
+
+ // Allow examination of behind-the-scene requests
+ var tabId = tab.url.lastIndexOf(vAPI.getURL('dashboard.html'), 0) !== 0 ?
+ tab.id :
+ vAPI.noTabId;
+ matrixSnapshotIf(tabId, details);
+ };
+
+ vAPI.tabs.get(null, onTabReady);
+};
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'matrixSnapshot':
+ matrixSnapshotFromTabId(request, callback);
+ return;
+
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'toggleMatrixSwitch':
+ µm.tMatrix.setSwitchZ(
+ request.switchName,
+ request.srcHostname,
+ µm.tMatrix.evaluateSwitchZ(request.switchName, request.srcHostname) === false
+ );
+ break;
+
+ case 'blacklistMatrixCell':
+ µm.tMatrix.blacklistCell(
+ request.srcHostname,
+ request.desHostname,
+ request.type
+ );
+ break;
+
+ case 'whitelistMatrixCell':
+ µm.tMatrix.whitelistCell(
+ request.srcHostname,
+ request.desHostname,
+ request.type
+ );
+ break;
+
+ case 'graylistMatrixCell':
+ µm.tMatrix.graylistCell(
+ request.srcHostname,
+ request.desHostname,
+ request.type
+ );
+ break;
+
+ case 'applyDiffToPermanentMatrix': // aka "persist"
+ if ( µm.pMatrix.applyDiff(request.diff, µm.tMatrix) ) {
+ µm.saveMatrix();
+ }
+ break;
+
+ case 'applyDiffToTemporaryMatrix': // aka "revert"
+ µm.tMatrix.applyDiff(request.diff, µm.pMatrix);
+ break;
+
+ case 'revertTemporaryMatrix':
+ µm.tMatrix.assign(µm.pMatrix);
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('popup.js', onMessage);
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// content scripts
+
+(function() {
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var foundInlineCode = function(tabId, pageStore, details, type) {
+ if ( pageStore === null ) { return; }
+
+ var pageHostname = pageStore.pageHostname,
+ µmuri = µm.URI.set(details.documentURI),
+ frameURL = µmuri.normalizedURI();
+
+ var blocked = details.blocked;
+ if ( blocked === undefined ) {
+ blocked = µm.mustBlock(pageHostname, µmuri.hostname, type);
+ }
+
+ var mapTo = {
+ css: 'style',
+ script: 'script'
+ };
+
+ // https://github.com/gorhill/httpswitchboard/issues/333
+ // Look-up here whether inline scripting is blocked for the frame.
+ var url = frameURL + '{inline_' + mapTo[type] + '}';
+ pageStore.recordRequest(type, url, blocked);
+ µm.logger.writeOne(tabId, 'net', pageHostname, url, type, blocked);
+};
+
+/******************************************************************************/
+
+var contentScriptLocalStorageHandler = function(tabId, originURL) {
+ var tabContext = µm.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) { return; }
+
+ var blocked = µm.mustBlock(
+ tabContext.rootHostname,
+ µm.URI.hostnameFromURI(originURL),
+ 'cookie'
+ );
+
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ if ( pageStore !== null ) {
+ var requestURL = originURL + '/{localStorage}';
+ pageStore.recordRequest('cookie', requestURL, blocked);
+ µm.logger.writeOne(tabId, 'net', tabContext.rootHostname, requestURL, 'cookie', blocked);
+ }
+
+ var removeStorage = blocked && µm.userSettings.deleteLocalStorage;
+ if ( removeStorage ) {
+ µm.localStorageRemovedCounter++;
+ }
+
+ return removeStorage;
+};
+
+/******************************************************************************/
+
+// Evaluate many URLs against the matrix.
+
+var lookupBlockedCollapsibles = function(tabId, requests) {
+ if ( placeholdersReadTime < µm.rawSettingsWriteTime ) {
+ placeholders = undefined;
+ }
+
+ if ( placeholders === undefined ) {
+ placeholders = {
+ frame: µm.rawSettings.framePlaceholder,
+ image: µm.rawSettings.imagePlaceholder
+ };
+ if ( placeholders.frame ) {
+ placeholders.frameDocument =
+ µm.rawSettings.framePlaceholderDocument.replace(
+ '{{bg}}',
+ µm.rawSettings.framePlaceholderBackground !== 'default' ?
+ µm.rawSettings.framePlaceholderBackground :
+ µm.rawSettings.placeholderBackground
+ );
+ }
+ if ( placeholders.image ) {
+ placeholders.imageBorder =
+ µm.rawSettings.imagePlaceholderBorder !== 'default' ?
+ µm.rawSettings.imagePlaceholderBorder :
+ µm.rawSettings.placeholderBorder;
+ placeholders.imageBackground =
+ µm.rawSettings.imagePlaceholderBackground !== 'default' ?
+ µm.rawSettings.imagePlaceholderBackground :
+ µm.rawSettings.placeholderBackground;
+ }
+ placeholdersReadTime = Date.now();
+ }
+
+ var response = {
+ blockedResources: [],
+ hash: requests.hash,
+ id: requests.id,
+ placeholders: placeholders
+ };
+
+ var tabContext = µm.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) {
+ return response;
+ }
+
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ if ( pageStore !== null ) {
+ pageStore.lookupBlockedCollapsibles(requests, response);
+ }
+
+ return response;
+};
+
+var placeholders,
+ placeholdersReadTime = 0;
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ var tabId = sender && sender.tab ? sender.tab.id || 0 : 0,
+ tabContext = µm.tabContextManager.lookup(tabId),
+ rootHostname = tabContext && tabContext.rootHostname,
+ pageStore = µm.pageStoreFromTabId(tabId);
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'contentScriptHasLocalStorage':
+ response = contentScriptLocalStorageHandler(tabId, request.originURL);
+ break;
+
+ case 'lookupBlockedCollapsibles':
+ response = lookupBlockedCollapsibles(tabId, request);
+ break;
+
+ case 'mustRenderNoscriptTags?':
+ if ( tabContext === null ) { break; }
+ response =
+ µm.tMatrix.mustBlock(rootHostname, rootHostname, 'script') &&
+ µm.tMatrix.evaluateSwitchZ('noscript-spoof', rootHostname);
+ if ( pageStore !== null ) {
+ pageStore.hasNoscriptTags = true;
+ }
+ // https://github.com/gorhill/uMatrix/issues/225
+ // A good place to force an update of the page title, as at
+ // this point the DOM has been loaded.
+ µm.updateTitle(tabId);
+ break;
+
+ case 'securityPolicyViolation':
+ if ( request.directive === 'worker-src' ) {
+ var url = µm.URI.hostnameFromURI(request.blockedURI) !== '' ?
+ request.blockedURI :
+ request.documentURI;
+ if ( pageStore !== null ) {
+ pageStore.hasWebWorkers = true;
+ pageStore.recordRequest('script', url, true);
+ }
+ if ( tabContext !== null ) {
+ µm.logger.writeOne(tabId, 'net', rootHostname, url, 'worker', request.blocked);
+ }
+ } else if ( request.directive === 'script-src' ) {
+ foundInlineCode(tabId, pageStore, request, 'script');
+ } else if ( request.directive === 'style-src' ) {
+ foundInlineCode(tabId, pageStore, request, 'css');
+ }
+ break;
+
+ case 'shutdown?':
+ if ( tabContext !== null ) {
+ response = µm.tMatrix.evaluateSwitchZ('matrix-off', rootHostname);
+ }
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('contentscript.js', onMessage);
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// cloud-ui.js
+
+(function() {
+
+/******************************************************************************/
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ case 'cloudGetOptions':
+ vAPI.cloud.getOptions(function(options) {
+ options.enabled = µm.userSettings.cloudStorageEnabled === true;
+ callback(options);
+ });
+ return;
+
+ case 'cloudSetOptions':
+ vAPI.cloud.setOptions(request.options, callback);
+ return;
+
+ case 'cloudPull':
+ return vAPI.cloud.pull(request.datakey, callback);
+
+ case 'cloudPush':
+ return vAPI.cloud.push(request.datakey, request.data, callback);
+
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ // For when cloud storage is disabled.
+ case 'cloudPull':
+ // fallthrough
+ case 'cloudPush':
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+/******************************************************************************/
+
+vAPI.messaging.listen('cloud-ui.js', onMessage);
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// user-rules.js
+
+(function() {
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'getUserRules':
+ response = {
+ temporaryRules: µm.tMatrix.toString(),
+ permanentRules: µm.pMatrix.toString()
+ };
+ break;
+
+ case 'setUserRules':
+ if ( typeof request.temporaryRules === 'string' ) {
+ µm.tMatrix.fromString(request.temporaryRules);
+ }
+ if ( typeof request.permanentRules === 'string' ) {
+ µm.pMatrix.fromString(request.permanentRules);
+ µm.saveMatrix();
+ }
+ response = {
+ temporaryRules: µm.tMatrix.toString(),
+ permanentRules: µm.pMatrix.toString()
+ };
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('user-rules.js', onMessage);
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// hosts-files.js
+
+(function() {
+
+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 r = {
+ autoUpdate: µm.userSettings.autoUpdate,
+ available: null,
+ cache: null,
+ current: µm.liveHostsFiles,
+ blockedHostnameCount: µm.ubiquitousBlacklist.count
+ };
+ var onMetadataReady = function(entries) {
+ r.cache = entries;
+ prepEntries(r.cache);
+ callback(r);
+ };
+ var onAvailableHostsFilesReady = function(lists) {
+ r.available = lists;
+ prepEntries(r.available);
+ µm.assets.metadata(onMetadataReady);
+ };
+ µm.getAvailableHostsFiles(onAvailableHostsFilesReady);
+};
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ var µm = µMatrix;
+
+ // Async
+ switch ( request.what ) {
+ case 'getLists':
+ return getLists(callback);
+
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'purgeCache':
+ µm.assets.purge(request.assetKey);
+ µm.assets.remove('compiled/' + request.assetKey);
+ break;
+
+ case 'purgeAllCaches':
+ if ( request.hard ) {
+ µm.assets.remove(/./);
+ } else {
+ µm.assets.purge(/./, 'public_suffix_list.dat');
+ }
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('hosts-files.js', onMessage);
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// about.js
+
+(function() {
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var restoreUserData = function(userData) {
+ var countdown = 4;
+ var onCountdown = function() {
+ countdown -= 1;
+ if ( countdown === 0 ) {
+ vAPI.app.restart();
+ }
+ };
+
+ var onAllRemoved = function() {
+ vAPI.storage.set(userData.settings, onCountdown);
+ vAPI.storage.set({ userMatrix: userData.rules }, onCountdown);
+ vAPI.storage.set({ liveHostsFiles: userData.hostsFiles }, onCountdown);
+ if ( userData.rawSettings instanceof Object ) {
+ µMatrix.saveRawSettings(userData.rawSettings, onCountdown);
+ }
+ };
+
+ // If we are going to restore all, might as well wipe out clean local
+ // storage
+ µm.XAL.keyvalRemoveAll(onAllRemoved);
+};
+
+/******************************************************************************/
+
+var resetUserData = function() {
+ var onAllRemoved = function() {
+ vAPI.app.restart();
+ };
+ µm.XAL.keyvalRemoveAll(onAllRemoved);
+};
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'getAllUserData':
+ response = {
+ app: vAPI.app.name,
+ version: vAPI.app.version,
+ when: Date.now(),
+ settings: µm.userSettings,
+ rules: µm.pMatrix.toString(),
+ hostsFiles: µm.liveHostsFiles,
+ rawSettings: µm.rawSettings
+ };
+ break;
+
+ case 'getSomeStats':
+ response = {
+ version: vAPI.app.version,
+ storageUsed: µm.storageUsed
+ };
+ break;
+
+ case 'restoreAllUserData':
+ restoreUserData(request.userData);
+ break;
+
+ case 'resetAllUserData':
+ resetUserData();
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('about.js', onMessage);
+
+/******************************************************************************/
+/******************************************************************************/
+
+// logger-ui.js
+
+(function() {
+
+/******************************************************************************/
+
+var µm = µMatrix,
+ loggerURL = vAPI.getURL('logger-ui.html');
+
+/******************************************************************************/
+
+var onMessage = function(request, sender, callback) {
+ // Async
+ switch ( request.what ) {
+ default:
+ break;
+ }
+
+ // Sync
+ var response;
+
+ switch ( request.what ) {
+ case 'readMany':
+ if (
+ µm.logger.ownerId !== undefined &&
+ request.ownerId !== µm.logger.ownerId
+ ) {
+ response = { unavailable: true };
+ break;
+ }
+ var tabIds = {};
+ for ( var tabId in µm.pageStores ) {
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) { continue; }
+ if ( pageStore.rawUrl.startsWith(loggerURL) ) { continue; }
+ tabIds[tabId] = pageStore.title || pageStore.rawUrl;
+ }
+ response = {
+ colorBlind: false,
+ entries: µm.logger.readAll(request.ownerId),
+ maxLoggedRequests: µm.userSettings.maxLoggedRequests,
+ noTabId: vAPI.noTabId,
+ tabIds: tabIds,
+ tabIdsToken: µm.pageStoresToken
+ };
+ break;
+
+ case 'releaseView':
+ if ( request.ownerId === µm.logger.ownerId ) {
+ µm.logger.ownerId = undefined;
+ }
+ break;
+
+ default:
+ return vAPI.messaging.UNHANDLED;
+ }
+
+ callback(response);
+};
+
+vAPI.messaging.listen('logger-ui.js', onMessage);
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/pagestats.js b/js/pagestats.js
new file mode 100644
index 0000000..37e94c7
--- /dev/null
+++ b/js/pagestats.js
@@ -0,0 +1,274 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2013-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.pageStoreFactory = (function() {
+
+/******************************************************************************/
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var BlockedCollapsibles = function() {
+ this.boundPruneAsyncCallback = this.pruneAsyncCallback.bind(this);
+ this.blocked = new Map();
+ this.hash = 0;
+ this.timer = null;
+};
+
+BlockedCollapsibles.prototype = {
+
+ shelfLife: 10 * 1000,
+
+ add: function(type, url, isSpecific) {
+ if ( this.blocked.size === 0 ) { this.pruneAsync(); }
+ var now = Date.now() / 1000 | 0;
+ // The following "trick" is to encode the specifity into the lsb of the
+ // time stamp so as to avoid to have to allocate a memory structure to
+ // store both time stamp and specificity.
+ if ( isSpecific ) {
+ now |= 0x00000001;
+ } else {
+ now &= 0xFFFFFFFE;
+ }
+ this.blocked.set(type + ' ' + url, now);
+ this.hash = now;
+ },
+
+ reset: function() {
+ this.blocked.clear();
+ this.hash = 0;
+ if ( this.timer !== null ) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+
+ pruneAsync: function() {
+ if ( this.timer === null ) {
+ this.timer = vAPI.setTimeout(
+ this.boundPruneAsyncCallback,
+ this.shelfLife * 2
+ );
+ }
+ },
+
+ pruneAsyncCallback: function() {
+ this.timer = null;
+ var obsolete = Date.now() - this.shelfLife;
+ for ( var entry of this.blocked ) {
+ if ( entry[1] <= obsolete ) {
+ this.blocked.delete(entry[0]);
+ }
+ }
+ if ( this.blocked.size !== 0 ) { this.pruneAsync(); }
+ }
+};
+
+/******************************************************************************/
+
+// Ref: Given a URL, returns a (somewhat) unique 32-bit value
+// Based on: FNV32a
+// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
+// The rest is custom, suited for uMatrix.
+
+var PageStore = function(tabContext) {
+ this.hostnameTypeCells = new Map();
+ this.domains = new Set();
+ this.blockedCollapsibles = new BlockedCollapsibles();
+ this.requestStats = µm.requestStatsFactory();
+ this.off = false;
+ this.init(tabContext);
+};
+
+PageStore.prototype = {
+
+ collapsibleTypes: new Set([ 'image' ]),
+ pageStoreJunkyard: [],
+
+ init: function(tabContext) {
+ this.tabId = tabContext.tabId;
+ this.rawUrl = tabContext.rawURL;
+ this.pageUrl = tabContext.normalURL;
+ this.pageHostname = tabContext.rootHostname;
+ this.pageDomain = tabContext.rootDomain;
+ this.title = '';
+ this.hostnameTypeCells.clear();
+ this.domains.clear();
+ this.allHostnamesString = ' ';
+ this.blockedCollapsibles.reset();
+ this.requestStats.reset();
+ this.distinctRequestCount = 0;
+ this.perLoadAllowedRequestCount = 0;
+ this.perLoadBlockedRequestCount = 0;
+ this.has3pReferrer = false;
+ this.hasMixedContent = false;
+ this.hasNoscriptTags = false;
+ this.hasWebWorkers = false;
+ this.incinerationTimer = null;
+ this.mtxContentModifiedTime = 0;
+ this.mtxCountModifiedTime = 0;
+ return this;
+ },
+
+ dispose: function() {
+ this.rawUrl = '';
+ this.pageUrl = '';
+ this.pageHostname = '';
+ this.pageDomain = '';
+ this.title = '';
+ this.hostnameTypeCells.clear();
+ this.domains.clear();
+ this.allHostnamesString = ' ';
+ this.blockedCollapsibles.reset();
+ if ( this.incinerationTimer !== null ) {
+ clearTimeout(this.incinerationTimer);
+ this.incinerationTimer = null;
+ }
+ if ( this.pageStoreJunkyard.length < 8 ) {
+ this.pageStoreJunkyard.push(this);
+ }
+ },
+
+ cacheBlockedCollapsible: function(type, url, specificity) {
+ if ( this.collapsibleTypes.has(type) ) {
+ this.blockedCollapsibles.add(
+ type,
+ url,
+ specificity !== 0 && specificity < 5
+ );
+ }
+ },
+
+ lookupBlockedCollapsibles: function(request, response) {
+ var tabContext = µm.tabContextManager.lookup(this.tabId);
+ if ( tabContext === null ) { return; }
+
+ var collapseBlacklisted = µm.userSettings.collapseBlacklisted,
+ collapseBlocked = µm.userSettings.collapseBlocked,
+ entry;
+
+ var blockedResources = response.blockedResources;
+
+ if (
+ Array.isArray(request.toFilter) &&
+ request.toFilter.length !== 0
+ ) {
+ var roothn = tabContext.rootHostname,
+ hnFromURI = µm.URI.hostnameFromURI,
+ tMatrix = µm.tMatrix;
+ for ( entry of request.toFilter ) {
+ if ( tMatrix.mustBlock(roothn, hnFromURI(entry.url), entry.type) === false ) {
+ continue;
+ }
+ blockedResources.push([
+ entry.type + ' ' + entry.url,
+ collapseBlocked ||
+ collapseBlacklisted && tMatrix.specificityRegister !== 0 &&
+ tMatrix.specificityRegister < 5
+ ]);
+ }
+ }
+
+ if ( this.blockedCollapsibles.hash === response.hash ) { return; }
+ response.hash = this.blockedCollapsibles.hash;
+
+ for ( entry of this.blockedCollapsibles.blocked ) {
+ blockedResources.push([
+ entry[0],
+ collapseBlocked || collapseBlacklisted && (entry[1] & 1) !== 0
+ ]);
+ }
+ },
+
+ recordRequest: function(type, url, block) {
+ // Store distinct network requests. This is used to:
+ // - remember which hostname/type were seen
+ // - count the number of distinct URLs for any given
+ // hostname-type pair
+ var hostname = µm.URI.hostnameFromURI(url),
+ key = hostname + ' ' + type,
+ uids = this.hostnameTypeCells.get(key);
+ if ( uids === undefined ) {
+ this.hostnameTypeCells.set(key, (uids = new Set()));
+ } else if ( uids.size > 99 ) {
+ return;
+ }
+ var uid = this.uidFromURL(url);
+ if ( uids.has(uid) ) { return; }
+ uids.add(uid);
+
+ // Count blocked/allowed requests
+ this.requestStats.record(type, block);
+
+ // https://github.com/gorhill/httpswitchboard/issues/306
+ // If it is recorded locally, record globally
+ µm.requestStats.record(type, block);
+ µm.updateBadgeAsync(this.tabId);
+
+ if ( block !== false ) {
+ this.perLoadBlockedRequestCount++;
+ } else {
+ this.perLoadAllowedRequestCount++;
+ }
+
+ this.distinctRequestCount++;
+ this.mtxCountModifiedTime = Date.now();
+
+ if ( this.domains.has(hostname) === false ) {
+ this.domains.add(hostname);
+ this.allHostnamesString += hostname + ' ';
+ this.mtxContentModifiedTime = Date.now();
+ }
+ },
+
+ uidFromURL: function(uri) {
+ var hint = 0x811c9dc5,
+ i = uri.length;
+ while ( i-- ) {
+ hint ^= uri.charCodeAt(i) | 0;
+ hint += (hint<<1) + (hint<<4) + (hint<<7) + (hint<<8) + (hint<<24) | 0;
+ hint >>>= 0;
+ }
+ return hint;
+ }
+};
+
+/******************************************************************************/
+
+return function pageStoreFactory(tabContext) {
+ var entry = PageStore.prototype.pageStoreJunkyard.pop();
+ if ( entry ) {
+ return entry.init(tabContext);
+ }
+ return new PageStore(tabContext);
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/polyfill.js b/js/polyfill.js
new file mode 100644
index 0000000..dea7e39
--- /dev/null
+++ b/js/polyfill.js
@@ -0,0 +1,96 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2017-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+
+ This file has been originally imported from:
+ https://github.com/gorhill/uBlock/tree/master/platform/chromium
+
+*/
+
+// For background page or non-background pages
+
+/* exported objectAssign */
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// As per MDN, Object.assign appeared first in Firefox 34.
+// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Browser_compatibility
+
+var objectAssign = Object.assign || function(target, source) {
+ var keys = Object.keys(source);
+ for ( var i = 0, n = keys.length, key; i < n; i++ ) {
+ key = keys[i];
+ target[key] = source[key];
+ }
+ return target;
+};
+
+/******************************************************************************/
+
+// Patching for Pale Moon which does not implement ES6 Set/Map.
+// Test for non-ES6 Set/Map: check if property `iterator` is present.
+// The code is strictly to satisfy uBO's core, not to be an accurate
+// implementation of ES6.
+
+if ( self.Set.prototype.iterator instanceof Function ) {
+ //console.log('Patching non-ES6 Set() to be more ES6-like.');
+ self.Set.prototype._values = self.Set.prototype.values;
+ self.Set.prototype.values = function() {
+ this._valueIter = this._values();
+ this.value = undefined;
+ this.done = false;
+ return this;
+ };
+ self.Set.prototype.next = function() {
+ try {
+ this.value = this._valueIter.next();
+ } catch (ex) {
+ this._valueIter = undefined;
+ this.value = undefined;
+ this.done = true;
+ }
+ return this;
+ };
+}
+
+if ( self.Map.prototype.iterator instanceof Function ) {
+ //console.log('Patching non-ES6 Map() to be more ES6-like.');
+ self.Map.prototype._entries = self.Map.prototype.entries;
+ self.Map.prototype.entries = function() {
+ this._entryIter = this._entries();
+ this.value = undefined;
+ this.done = false;
+ return this;
+ };
+ self.Map.prototype.next = function() {
+ try {
+ this.value = this._entryIter.next();
+ } catch (ex) {
+ this._entryIter = undefined;
+ this.value = undefined;
+ this.done = true;
+ }
+ return this;
+ };
+}
+
diff --git a/js/popup.js b/js/popup.js
new file mode 100644
index 0000000..98781b4
--- /dev/null
+++ b/js/popup.js
@@ -0,0 +1,1538 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global punycode, uDom */
+/* jshint esnext: true, bitwise: false */
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Stuff which is good to do very early so as to avoid visual glitches.
+
+(function() {
+ var paneContentPaddingTop = vAPI.localStorage.getItem('paneContentPaddingTop'),
+ touchDevice = vAPI.localStorage.getItem('touchDevice');
+
+ if ( typeof paneContentPaddingTop === 'string' ) {
+ document.querySelector('.paneContent').style.setProperty(
+ 'padding-top',
+ paneContentPaddingTop
+ );
+ }
+ if ( touchDevice === 'true' ) {
+ document.body.setAttribute('data-touch', 'true');
+ } else {
+ document.addEventListener('touchstart', function onTouched(ev) {
+ document.removeEventListener(ev.type, onTouched);
+ document.body.setAttribute('data-touch', 'true');
+ vAPI.localStorage.setItem('touchDevice', 'true');
+ resizePopup();
+ });
+ }
+})();
+
+var popupWasResized = function() {
+ document.body.setAttribute('data-resize-popup', '');
+};
+
+var resizePopup = (function() {
+ var timer;
+ var fix = function() {
+ timer = undefined;
+ var doc = document;
+ // Manually adjust the position of the main matrix according to the
+ // height of the toolbar/matrix header.
+ var paddingTop = (doc.querySelector('.paneHead').clientHeight + 2) + 'px',
+ paneContent = doc.querySelector('.paneContent');
+ if ( paddingTop !== paneContent.style.paddingTop ) {
+ paneContent.style.setProperty('padding-top', paddingTop);
+ vAPI.localStorage.setItem('paneContentPaddingTop', paddingTop);
+ }
+ document.body.classList.toggle(
+ 'hConstrained',
+ window.innerWidth < document.body.clientWidth
+ );
+ popupWasResized();
+ };
+ return function() {
+ if ( timer !== undefined ) {
+ clearTimeout(timer);
+ }
+ timer = vAPI.setTimeout(fix, 97);
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Must be consistent with definitions in matrix.js
+var Dark = 0x80;
+var Red = 1;
+var Green = 2;
+var DarkRed = Dark | Red;
+var DarkGreen = Dark | Green;
+
+var matrixSnapshot = {};
+var groupsSnapshot = [];
+var allHostnamesSnapshot = 'do not leave this initial string empty';
+
+var matrixCellHotspots = null;
+
+var matrixHeaderPrettyNames = {
+ 'all': '',
+ 'cookie': '',
+ 'css': '',
+ 'image': '',
+ 'media': '',
+ 'script': '',
+ 'xhr': '',
+ 'frame': '',
+ 'other': ''
+};
+
+var firstPartyLabel = '';
+var blacklistedHostnamesLabel = '';
+
+var expandosIdGenerator = 1;
+var nodeToExpandosMap = (function() {
+ if ( typeof window.Map === 'function' ) {
+ return new window.Map();
+ }
+})();
+
+var expandosFromNode = function(node) {
+ if (
+ node instanceof HTMLElement === false &&
+ typeof node.nodeAt === 'function'
+ ) {
+ node = node.nodeAt(0);
+ }
+ if ( nodeToExpandosMap ) {
+ var expandosId = node.getAttribute('data-expandos');
+ if ( !expandosId ) {
+ expandosId = '' + (expandosIdGenerator++);
+ node.setAttribute('data-expandos', expandosId);
+ }
+ var expandos = nodeToExpandosMap.get(expandosId);
+ if ( expandos === undefined ) {
+ nodeToExpandosMap.set(expandosId, (expandos = Object.create(null)));
+ }
+ return expandos;
+ }
+ return node;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+function getUserSetting(setting) {
+ return matrixSnapshot.userSettings[setting];
+ }
+
+function setUserSetting(setting, value) {
+ matrixSnapshot.userSettings[setting] = value;
+ vAPI.messaging.send('popup.js', {
+ what: 'userSettings',
+ name: setting,
+ value: value
+ });
+}
+
+/******************************************************************************/
+
+function getUISetting(setting) {
+ var r = vAPI.localStorage.getItem(setting);
+ if ( typeof r !== 'string' ) {
+ return undefined;
+ }
+ return JSON.parse(r);
+}
+
+function setUISetting(setting, value) {
+ vAPI.localStorage.setItem(
+ setting,
+ JSON.stringify(value)
+ );
+}
+
+/******************************************************************************/
+
+function updateMatrixSnapshot() {
+ matrixSnapshotPoller.pollNow();
+}
+
+/******************************************************************************/
+
+// For display purpose, create four distinct groups of rows:
+// 0th: literal "1st-party" row
+// 1st: page domain's related
+// 2nd: whitelisted
+// 3rd: graylisted
+// 4th: blacklisted
+
+function getGroupStats() {
+
+ // Try to not reshuffle groups around while popup is opened if
+ // no new hostname added.
+ var latestDomainListSnapshot = Object.keys(matrixSnapshot.rows).sort().join();
+ if ( latestDomainListSnapshot === allHostnamesSnapshot ) {
+ return groupsSnapshot;
+ }
+ allHostnamesSnapshot = latestDomainListSnapshot;
+
+ // First, group according to whether at least one node in the domain
+ // hierarchy is white or blacklisted
+ var pageDomain = matrixSnapshot.domain;
+ var rows = matrixSnapshot.rows;
+ var anyTypeOffset = matrixSnapshot.headerIndices.get('*');
+ var hostname, domain;
+ var row, color, count, groupIndex;
+ var domainToGroupMap = {};
+
+ // These have hard-coded position which cannot be overriden
+ domainToGroupMap['1st-party'] = 0;
+ domainToGroupMap[pageDomain] = 1;
+
+ // 1st pass: domain wins if it has an explicit rule or a count
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ if ( hostname === '*' || hostname === '1st-party' ) {
+ continue;
+ }
+ domain = rows[hostname].domain;
+ if ( domain === pageDomain || hostname !== domain ) {
+ continue;
+ }
+ row = rows[domain];
+ color = row.temporary[anyTypeOffset];
+ if ( color === DarkGreen ) {
+ domainToGroupMap[domain] = 2;
+ continue;
+ }
+ if ( color === DarkRed ) {
+ domainToGroupMap[domain] = 4;
+ continue;
+ }
+ count = row.counts[anyTypeOffset];
+ if ( count !== 0 ) {
+ domainToGroupMap[domain] = 3;
+ continue;
+ }
+ }
+ // 2nd pass: green wins
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ row = rows[hostname];
+ domain = row.domain;
+ if ( domainToGroupMap.hasOwnProperty(domain) ) {
+ continue;
+ }
+ color = row.temporary[anyTypeOffset];
+ if ( color === DarkGreen ) {
+ domainToGroupMap[domain] = 2;
+ }
+ }
+ // 3rd pass: gray with count wins
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ row = rows[hostname];
+ domain = row.domain;
+ if ( domainToGroupMap.hasOwnProperty(domain) ) {
+ continue;
+ }
+ color = row.temporary[anyTypeOffset];
+ count = row.counts[anyTypeOffset];
+ if ( color !== DarkRed && count !== 0 ) {
+ domainToGroupMap[domain] = 3;
+ }
+ }
+ // 4th pass: red wins whatever is left
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ row = rows[hostname];
+ domain = row.domain;
+ if ( domainToGroupMap.hasOwnProperty(domain) ) {
+ continue;
+ }
+ color = row.temporary[anyTypeOffset];
+ if ( color === DarkRed ) {
+ domainToGroupMap[domain] = 4;
+ }
+ }
+ // 5th pass: gray wins whatever is left
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ domain = rows[hostname].domain;
+ if ( domainToGroupMap.hasOwnProperty(domain) ) {
+ continue;
+ }
+ domainToGroupMap[domain] = 3;
+ }
+
+ // Last pass: put each domain in a group
+ var groups = [ {}, {}, {}, {}, {} ];
+ var group;
+ for ( hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ if ( hostname === '*' ) {
+ continue;
+ }
+ domain = rows[hostname].domain;
+ groupIndex = domainToGroupMap[domain];
+ group = groups[groupIndex];
+ if ( group.hasOwnProperty(domain) === false ) {
+ group[domain] = {};
+ }
+ group[domain][hostname] = true;
+ }
+
+ groupsSnapshot = groups;
+
+ return groups;
+}
+
+/******************************************************************************/
+
+// helpers
+
+function getTemporaryColor(hostname, type) {
+ return matrixSnapshot.rows[hostname].temporary[matrixSnapshot.headerIndices.get(type)];
+}
+
+function getPermanentColor(hostname, type) {
+ return matrixSnapshot.rows[hostname].permanent[matrixSnapshot.headerIndices.get(type)];
+}
+
+function addCellClass(cell, hostname, type) {
+ var cl = cell.classList;
+ cl.add('matCell');
+ cl.add('t' + getTemporaryColor(hostname, type).toString(16));
+ cl.add('p' + getPermanentColor(hostname, type).toString(16));
+}
+
+/******************************************************************************/
+
+// This is required for when we update the matrix while it is open:
+// the user might have collapsed/expanded one or more domains, and we don't
+// want to lose all his hardwork.
+
+function getCollapseState(domain) {
+ var states = getUISetting('popupCollapseSpecificDomains');
+ if ( typeof states === 'object' && states[domain] !== undefined ) {
+ return states[domain];
+ }
+ return matrixSnapshot.collapseAllDomains === true;
+}
+
+function toggleCollapseState(elem) {
+ if ( elem.ancestors('#matHead.collapsible').length > 0 ) {
+ toggleMainCollapseState(elem);
+ } else {
+ toggleSpecificCollapseState(elem);
+ }
+ popupWasResized();
+}
+
+function toggleMainCollapseState(uelem) {
+ var matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
+ var collapsed = matrixSnapshot.collapseAllDomains = matHead.hasClass('collapsed');
+ uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed);
+ setUserSetting('popupCollapseAllDomains', collapsed);
+
+ var specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {};
+ var domains = Object.keys(specificCollapseStates);
+ var i = domains.length;
+ var domain;
+ while ( i-- ) {
+ domain = domains[i];
+ if ( specificCollapseStates[domain] === collapsed ) {
+ delete specificCollapseStates[domain];
+ }
+ }
+ setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
+}
+
+function toggleSpecificCollapseState(uelem) {
+ // Remember collapse state forever, but only if it is different
+ // from main collapse switch.
+ var section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed'),
+ domain = expandosFromNode(section).domain,
+ collapsed = section.hasClass('collapsed'),
+ mainCollapseState = matrixSnapshot.collapseAllDomains === true,
+ specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {};
+ if ( collapsed !== mainCollapseState ) {
+ specificCollapseStates[domain] = collapsed;
+ setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
+ } else if ( specificCollapseStates[domain] !== undefined ) {
+ delete specificCollapseStates[domain];
+ setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
+ }
+}
+
+/******************************************************************************/
+
+// Update count value of matrix cells(s)
+
+function updateMatrixCounts() {
+ var matCells = uDom('.matrix .matRow.rw > .matCell'),
+ i = matCells.length,
+ matRow, matCell, count, counts,
+ headerIndices = matrixSnapshot.headerIndices,
+ rows = matrixSnapshot.rows,
+ expandos;
+ while ( i-- ) {
+ matCell = matCells.nodeAt(i);
+ expandos = expandosFromNode(matCell);
+ if ( expandos.hostname === '*' || expandos.reqType === '*' ) {
+ continue;
+ }
+ matRow = matCell.parentNode;
+ counts = matRow.classList.contains('meta') ? 'totals' : 'counts';
+ count = rows[expandos.hostname][counts][headerIndices.get(expandos.reqType)];
+ if ( count === expandos.count ) { continue; }
+ expandos.count = count;
+ matCell.textContent = cellTextFromCount(count);
+ }
+}
+
+function cellTextFromCount(count) {
+ if ( count === 0 ) { return '\u00A0'; }
+ if ( count < 100 ) { return count; }
+ return '99+';
+}
+
+/******************************************************************************/
+
+// Update color of matrix cells(s)
+// Color changes when rules change
+
+function updateMatrixColors() {
+ var cells = uDom('.matrix .matRow.rw > .matCell').removeClass(),
+ i = cells.length,
+ cell, expandos;
+ while ( i-- ) {
+ cell = cells.nodeAt(i);
+ expandos = expandosFromNode(cell);
+ addCellClass(cell, expandos.hostname, expandos.reqType);
+ }
+ popupWasResized();
+}
+
+/******************************************************************************/
+
+// Update behavior of matrix:
+// - Whether a section is collapsible or not. It is collapsible if:
+// - It has at least one subdomain AND
+// - There is no explicit rule anywhere in the subdomain cells AND
+// - It is not part of group 3 (blacklisted hostnames)
+
+function updateMatrixBehavior() {
+ matrixList = matrixList || uDom('#matList');
+ var sections = matrixList.descendants('.matSection');
+ var i = sections.length;
+ var section, subdomainRows, j, subdomainRow;
+ while ( i-- ) {
+ section = sections.at(i);
+ subdomainRows = section.descendants('.l2:not(.g4)');
+ j = subdomainRows.length;
+ while ( j-- ) {
+ subdomainRow = subdomainRows.at(j);
+ subdomainRow.toggleClass('collapsible', subdomainRow.descendants('.t81,.t82').length === 0);
+ }
+ section.toggleClass('collapsible', subdomainRows.filter('.collapsible').length > 0);
+ }
+}
+
+/******************************************************************************/
+
+// handle user interaction with filters
+
+function getCellAction(hostname, type, leaning) {
+ var temporaryColor = getTemporaryColor(hostname, type);
+ var hue = temporaryColor & 0x03;
+ // Special case: root toggle only between two states
+ if ( type === '*' && hostname === '*' ) {
+ return hue === Green ? 'blacklistMatrixCell' : 'whitelistMatrixCell';
+ }
+ // When explicitly blocked/allowed, can only graylist
+ var saturation = temporaryColor & 0x80;
+ if ( saturation === Dark ) {
+ return 'graylistMatrixCell';
+ }
+ return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell';
+}
+
+function handleFilter(button, leaning) {
+ // our parent cell knows who we are
+ var cell = button.ancestors('div.matCell'),
+ expandos = expandosFromNode(cell),
+ type = expandos.reqType,
+ desHostname = expandos.hostname;
+ // https://github.com/gorhill/uMatrix/issues/24
+ // No hostname can happen -- like with blacklist meta row
+ if ( desHostname === '' ) {
+ return;
+ }
+ var request = {
+ what: getCellAction(desHostname, type, leaning),
+ srcHostname: matrixSnapshot.scope,
+ desHostname: desHostname,
+ type: type
+ };
+ vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
+}
+
+function handleWhitelistFilter(button) {
+ handleFilter(button, 'whitelisting');
+}
+
+function handleBlacklistFilter(button) {
+ handleFilter(button, 'blacklisting');
+}
+
+/******************************************************************************/
+
+var matrixRowPool = [];
+var matrixSectionPool = [];
+var matrixGroupPool = [];
+var matrixRowTemplate = null;
+var matrixList = null;
+
+var startMatrixUpdate = function() {
+ matrixList = matrixList || uDom('#matList');
+ matrixList.detach();
+ var rows = matrixList.descendants('.matRow');
+ rows.detach();
+ matrixRowPool = matrixRowPool.concat(rows.toArray());
+ var sections = matrixList.descendants('.matSection');
+ sections.detach();
+ matrixSectionPool = matrixSectionPool.concat(sections.toArray());
+ var groups = matrixList.descendants('.matGroup');
+ groups.detach();
+ matrixGroupPool = matrixGroupPool.concat(groups.toArray());
+};
+
+var endMatrixUpdate = function() {
+ // https://github.com/gorhill/httpswitchboard/issues/246
+ // If the matrix has no rows, we need to insert a dummy one, invisible,
+ // to ensure the extension pop-up is properly sized. This is needed because
+ // the header pane's `position` property is `fixed`, which means it doesn't
+ // affect layout size, hence the matrix header row will be truncated.
+ if ( matrixSnapshot.rowCount <= 1 ) {
+ matrixList.append(createMatrixRow().css('visibility', 'hidden'));
+ }
+ updateMatrixBehavior();
+ matrixList.css('display', '');
+ matrixList.appendTo('.paneContent');
+};
+
+var createMatrixGroup = function() {
+ var group = matrixGroupPool.pop();
+ if ( group ) {
+ return uDom(group).removeClass().addClass('matGroup');
+ }
+ return uDom(document.createElement('div')).addClass('matGroup');
+};
+
+var createMatrixSection = function() {
+ var section = matrixSectionPool.pop();
+ if ( section ) {
+ return uDom(section).removeClass().addClass('matSection');
+ }
+ return uDom(document.createElement('div')).addClass('matSection');
+};
+
+var createMatrixRow = function() {
+ var row = matrixRowPool.pop();
+ if ( row ) {
+ row.style.visibility = '';
+ row = uDom(row);
+ row.descendants('.matCell').removeClass().addClass('matCell');
+ row.removeClass().addClass('matRow');
+ return row;
+ }
+ if ( matrixRowTemplate === null ) {
+ matrixRowTemplate = uDom('#templates .matRow');
+ }
+ return matrixRowTemplate.clone();
+};
+
+/******************************************************************************/
+
+function renderMatrixHeaderRow() {
+ var matHead = uDom('#matHead.collapsible');
+ matHead.toggleClass('collapsed', matrixSnapshot.collapseAllDomains === true);
+ var cells = matHead.descendants('.matCell'), cell, expandos;
+ cell = cells.nodeAt(0);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = '*';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', '*');
+ cell = cells.nodeAt(1);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'cookie';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'cookie');
+ cell = cells.nodeAt(2);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'css';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'css');
+ cell = cells.nodeAt(3);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'image';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'image');
+ cell = cells.nodeAt(4);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'media';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'media');
+ cell = cells.nodeAt(5);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'script';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'script');
+ cell = cells.nodeAt(6);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'xhr';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'xhr');
+ cell = cells.nodeAt(7);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'frame';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'frame');
+ cell = cells.nodeAt(8);
+ expandos = expandosFromNode(cell);
+ expandos.reqType = 'other';
+ expandos.hostname = '*';
+ addCellClass(cell, '*', 'other');
+ uDom('#matHead .matRow').css('display', '');
+}
+
+/******************************************************************************/
+
+function renderMatrixCellDomain(cell, domain) {
+ var expandos = expandosFromNode(cell);
+ expandos.hostname = domain;
+ expandos.reqType = '*';
+ addCellClass(cell.nodeAt(0), domain, '*');
+ var contents = cell.contents();
+ contents.nodeAt(0).textContent = domain === '1st-party' ?
+ firstPartyLabel :
+ punycode.toUnicode(domain);
+ contents.nodeAt(1).textContent = ' ';
+}
+
+function renderMatrixCellSubdomain(cell, domain, subomain) {
+ var expandos = expandosFromNode(cell);
+ expandos.hostname = subomain;
+ expandos.reqType = '*';
+ addCellClass(cell.nodeAt(0), subomain, '*');
+ var contents = cell.contents();
+ contents.nodeAt(0).textContent = punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.';
+ contents.nodeAt(1).textContent = punycode.toUnicode(domain);
+}
+
+function renderMatrixMetaCellDomain(cell, domain) {
+ var expandos = expandosFromNode(cell);
+ expandos.hostname = domain;
+ expandos.reqType = '*';
+ addCellClass(cell.nodeAt(0), domain, '*');
+ var contents = cell.contents();
+ contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain);
+ contents.nodeAt(1).textContent = ' ';
+}
+
+function renderMatrixCellType(cell, hostname, type, count) {
+ var node = cell.nodeAt(0),
+ expandos = expandosFromNode(node);
+ expandos.hostname = hostname;
+ expandos.reqType = type;
+ expandos.count = count;
+ addCellClass(node, hostname, type);
+ node.textContent = cellTextFromCount(count);
+}
+
+function renderMatrixCellTypes(cells, hostname, countName) {
+ var counts = matrixSnapshot.rows[hostname][countName];
+ var headerIndices = matrixSnapshot.headerIndices;
+ renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[headerIndices.get('cookie')]);
+ renderMatrixCellType(cells.at(2), hostname, 'css', counts[headerIndices.get('css')]);
+ renderMatrixCellType(cells.at(3), hostname, 'image', counts[headerIndices.get('image')]);
+ renderMatrixCellType(cells.at(4), hostname, 'media', counts[headerIndices.get('media')]);
+ renderMatrixCellType(cells.at(5), hostname, 'script', counts[headerIndices.get('script')]);
+ renderMatrixCellType(cells.at(6), hostname, 'xhr', counts[headerIndices.get('xhr')]);
+ renderMatrixCellType(cells.at(7), hostname, 'frame', counts[headerIndices.get('frame')]);
+ renderMatrixCellType(cells.at(8), hostname, 'other', counts[headerIndices.get('other')]);
+}
+
+/******************************************************************************/
+
+function makeMatrixRowDomain(domain) {
+ var matrixRow = createMatrixRow().addClass('rw');
+ var cells = matrixRow.descendants('.matCell');
+ renderMatrixCellDomain(cells.at(0), domain);
+ renderMatrixCellTypes(cells, domain, 'counts');
+ return matrixRow;
+}
+
+function makeMatrixRowSubdomain(domain, subdomain) {
+ var matrixRow = createMatrixRow().addClass('rw');
+ var cells = matrixRow.descendants('.matCell');
+ renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
+ renderMatrixCellTypes(cells, subdomain, 'counts');
+ return matrixRow;
+}
+
+function makeMatrixMetaRowDomain(domain) {
+ var matrixRow = createMatrixRow().addClass('rw');
+ var cells = matrixRow.descendants('.matCell');
+ renderMatrixMetaCellDomain(cells.at(0), domain);
+ renderMatrixCellTypes(cells, domain, 'totals');
+ return matrixRow;
+}
+
+/******************************************************************************/
+
+function renderMatrixMetaCellType(cell, count) {
+ // https://github.com/gorhill/uMatrix/issues/24
+ // Don't forget to reset cell properties
+ var node = cell.nodeAt(0),
+ expandos = expandosFromNode(node);
+ expandos.hostname = '';
+ expandos.reqType = '';
+ expandos.count = count;
+ cell.addClass('t1');
+ node.textContent = cellTextFromCount(count);
+}
+
+function makeMatrixMetaRow(totals) {
+ var headerIndices = matrixSnapshot.headerIndices,
+ matrixRow = createMatrixRow().at(0).addClass('ro'),
+ cells = matrixRow.descendants('.matCell'),
+ contents = cells.at(0).addClass('t81').contents(),
+ expandos = expandosFromNode(cells.nodeAt(0));
+ expandos.hostname = '';
+ expandos.reqType = '*';
+ contents.nodeAt(0).textContent = ' ';
+ contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace(
+ '{{count}}',
+ totals[headerIndices.get('*')].toLocaleString()
+ );
+ renderMatrixMetaCellType(cells.at(1), totals[headerIndices.get('cookie')]);
+ renderMatrixMetaCellType(cells.at(2), totals[headerIndices.get('css')]);
+ renderMatrixMetaCellType(cells.at(3), totals[headerIndices.get('image')]);
+ renderMatrixMetaCellType(cells.at(4), totals[headerIndices.get('media')]);
+ renderMatrixMetaCellType(cells.at(5), totals[headerIndices.get('script')]);
+ renderMatrixMetaCellType(cells.at(6), totals[headerIndices.get('xhr')]);
+ renderMatrixMetaCellType(cells.at(7), totals[headerIndices.get('frame')]);
+ renderMatrixMetaCellType(cells.at(8), totals[headerIndices.get('other')]);
+ return matrixRow;
+}
+
+/******************************************************************************/
+
+function computeMatrixGroupMetaStats(group) {
+ var headerIndices = matrixSnapshot.headerIndices,
+ anyTypeIndex = headerIndices.get('*'),
+ n = headerIndices.size,
+ totals = new Array(n),
+ i = n;
+ while ( i-- ) {
+ totals[i] = 0;
+ }
+ var rows = matrixSnapshot.rows, row;
+ for ( var hostname in rows ) {
+ if ( rows.hasOwnProperty(hostname) === false ) {
+ continue;
+ }
+ row = rows[hostname];
+ if ( group.hasOwnProperty(row.domain) === false ) {
+ continue;
+ }
+ if ( row.counts[anyTypeIndex] === 0 ) {
+ continue;
+ }
+ totals[0] += 1;
+ for ( i = 1; i < n; i++ ) {
+ totals[i] += row.counts[i];
+ }
+ }
+ return totals;
+}
+
+/******************************************************************************/
+
+// Compare hostname helper, to order hostname in a logical manner:
+// top-most < bottom-most, take into account whether IP address or
+// named hostname
+
+function hostnameCompare(a,b) {
+ // Normalize: most significant parts first
+ if ( !a.match(/^\d+(\.\d+){1,3}$/) ) {
+ var aa = a.split('.');
+ a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
+ }
+ if ( !b.match(/^\d+(\.\d+){1,3}$/) ) {
+ var bb = b.split('.');
+ b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
+ }
+ return a.localeCompare(b);
+}
+
+/******************************************************************************/
+
+function makeMatrixGroup0SectionDomain() {
+ return makeMatrixRowDomain('1st-party').addClass('g0 l1');
+}
+
+function makeMatrixGroup0Section() {
+ var domainDiv = createMatrixSection();
+ expandosFromNode(domainDiv).domain = '1st-party';
+ makeMatrixGroup0SectionDomain().appendTo(domainDiv);
+ return domainDiv;
+}
+
+function makeMatrixGroup0() {
+ // Show literal "1st-party" row only if there is
+ // at least one 1st-party hostname
+ if ( Object.keys(groupsSnapshot[1]).length === 0 ) {
+ return;
+ }
+ var groupDiv = createMatrixGroup().addClass('g0');
+ makeMatrixGroup0Section().appendTo(groupDiv);
+ groupDiv.appendTo(matrixList);
+}
+
+/******************************************************************************/
+
+function makeMatrixGroup1SectionDomain(domain) {
+ return makeMatrixRowDomain(domain)
+ .addClass('g1 l1');
+}
+
+function makeMatrixGroup1SectionSubomain(domain, subdomain) {
+ return makeMatrixRowSubdomain(domain, subdomain)
+ .addClass('g1 l2');
+}
+
+function makeMatrixGroup1SectionMetaDomain(domain) {
+ return makeMatrixMetaRowDomain(domain).addClass('g1 l1 meta');
+}
+
+function makeMatrixGroup1Section(hostnames) {
+ var domain = hostnames[0];
+ var domainDiv = createMatrixSection()
+ .toggleClass('collapsed', getCollapseState(domain));
+ expandosFromNode(domainDiv).domain = domain;
+ if ( hostnames.length > 1 ) {
+ makeMatrixGroup1SectionMetaDomain(domain)
+ .appendTo(domainDiv);
+ }
+ makeMatrixGroup1SectionDomain(domain)
+ .appendTo(domainDiv);
+ for ( var i = 1; i < hostnames.length; i++ ) {
+ makeMatrixGroup1SectionSubomain(domain, hostnames[i])
+ .appendTo(domainDiv);
+ }
+ return domainDiv;
+}
+
+function makeMatrixGroup1(group) {
+ var domains = Object.keys(group).sort(hostnameCompare);
+ if ( domains.length ) {
+ var groupDiv = createMatrixGroup().addClass('g1');
+ makeMatrixGroup1Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ for ( var i = 1; i < domains.length; i++ ) {
+ makeMatrixGroup1Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ }
+ groupDiv.appendTo(matrixList);
+ }
+}
+
+/******************************************************************************/
+
+function makeMatrixGroup2SectionDomain(domain) {
+ return makeMatrixRowDomain(domain)
+ .addClass('g2 l1');
+}
+
+function makeMatrixGroup2SectionSubomain(domain, subdomain) {
+ return makeMatrixRowSubdomain(domain, subdomain)
+ .addClass('g2 l2');
+}
+
+function makeMatrixGroup2SectionMetaDomain(domain) {
+ return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
+}
+
+function makeMatrixGroup2Section(hostnames) {
+ var domain = hostnames[0];
+ var domainDiv = createMatrixSection()
+ .toggleClass('collapsed', getCollapseState(domain));
+ expandosFromNode(domainDiv).domain = domain;
+ if ( hostnames.length > 1 ) {
+ makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
+ }
+ makeMatrixGroup2SectionDomain(domain)
+ .appendTo(domainDiv);
+ for ( var i = 1; i < hostnames.length; i++ ) {
+ makeMatrixGroup2SectionSubomain(domain, hostnames[i])
+ .appendTo(domainDiv);
+ }
+ return domainDiv;
+}
+
+function makeMatrixGroup2(group) {
+ var domains = Object.keys(group).sort(hostnameCompare);
+ if ( domains.length) {
+ var groupDiv = createMatrixGroup()
+ .addClass('g2');
+ makeMatrixGroup2Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ for ( var i = 1; i < domains.length; i++ ) {
+ makeMatrixGroup2Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ }
+ groupDiv.appendTo(matrixList);
+ }
+}
+
+/******************************************************************************/
+
+function makeMatrixGroup3SectionDomain(domain) {
+ return makeMatrixRowDomain(domain)
+ .addClass('g3 l1');
+}
+
+function makeMatrixGroup3SectionSubomain(domain, subdomain) {
+ return makeMatrixRowSubdomain(domain, subdomain)
+ .addClass('g3 l2');
+}
+
+function makeMatrixGroup3SectionMetaDomain(domain) {
+ return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
+}
+
+function makeMatrixGroup3Section(hostnames) {
+ var domain = hostnames[0];
+ var domainDiv = createMatrixSection()
+ .toggleClass('collapsed', getCollapseState(domain));
+ expandosFromNode(domainDiv).domain = domain;
+ if ( hostnames.length > 1 ) {
+ makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
+ }
+ makeMatrixGroup3SectionDomain(domain)
+ .appendTo(domainDiv);
+ for ( var i = 1; i < hostnames.length; i++ ) {
+ makeMatrixGroup3SectionSubomain(domain, hostnames[i])
+ .appendTo(domainDiv);
+ }
+ return domainDiv;
+}
+
+function makeMatrixGroup3(group) {
+ var domains = Object.keys(group).sort(hostnameCompare);
+ if ( domains.length) {
+ var groupDiv = createMatrixGroup()
+ .addClass('g3');
+ makeMatrixGroup3Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ for ( var i = 1; i < domains.length; i++ ) {
+ makeMatrixGroup3Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ }
+ groupDiv.appendTo(matrixList);
+ }
+}
+
+/******************************************************************************/
+
+function makeMatrixGroup4SectionDomain(domain) {
+ return makeMatrixRowDomain(domain)
+ .addClass('g4 l1');
+}
+
+function makeMatrixGroup4SectionSubomain(domain, subdomain) {
+ return makeMatrixRowSubdomain(domain, subdomain)
+ .addClass('g4 l2');
+}
+
+function makeMatrixGroup4Section(hostnames) {
+ var domain = hostnames[0];
+ var domainDiv = createMatrixSection();
+ expandosFromNode(domainDiv).domain = domain;
+ makeMatrixGroup4SectionDomain(domain)
+ .appendTo(domainDiv);
+ for ( var i = 1; i < hostnames.length; i++ ) {
+ makeMatrixGroup4SectionSubomain(domain, hostnames[i])
+ .appendTo(domainDiv);
+ }
+ return domainDiv;
+}
+
+function makeMatrixGroup4(group) {
+ var domains = Object.keys(group).sort(hostnameCompare);
+ if ( domains.length === 0 ) {
+ return;
+ }
+ var groupDiv = createMatrixGroup().addClass('g4');
+ createMatrixSection()
+ .addClass('g4Meta')
+ .toggleClass('g4Collapsed', !!matrixSnapshot.collapseBlacklistedDomains)
+ .appendTo(groupDiv);
+ makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
+ .appendTo(groupDiv);
+ makeMatrixGroup4Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ for ( var i = 1; i < domains.length; i++ ) {
+ makeMatrixGroup4Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
+ .appendTo(groupDiv);
+ }
+ groupDiv.appendTo(matrixList);
+}
+
+/******************************************************************************/
+
+var makeMenu = function() {
+ var groupStats = getGroupStats();
+
+ if ( Object.keys(groupStats).length === 0 ) { return; }
+
+ // https://github.com/gorhill/httpswitchboard/issues/31
+ if ( matrixCellHotspots ) {
+ matrixCellHotspots.detach();
+ }
+
+ renderMatrixHeaderRow();
+
+ startMatrixUpdate();
+ makeMatrixGroup0(groupStats[0]);
+ makeMatrixGroup1(groupStats[1]);
+ makeMatrixGroup2(groupStats[2]);
+ makeMatrixGroup3(groupStats[3]);
+ makeMatrixGroup4(groupStats[4]);
+ endMatrixUpdate();
+
+ initScopeCell();
+ updateMatrixButtons();
+ resizePopup();
+};
+
+/******************************************************************************/
+
+// Do all the stuff that needs to be done before building menu et al.
+
+function initMenuEnvironment() {
+ document.body.style.setProperty(
+ 'font-size',
+ getUserSetting('displayTextSize')
+ );
+ document.body.classList.toggle(
+ 'colorblind',
+ getUserSetting('colorBlindFriendly')
+ );
+ uDom.nodeFromId('version').textContent = matrixSnapshot.appVersion || '';
+
+ var prettyNames = matrixHeaderPrettyNames;
+ var keys = Object.keys(prettyNames);
+ var i = keys.length;
+ var cell, key, text;
+ while ( i-- ) {
+ key = keys[i];
+ cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
+ text = vAPI.i18n(key + 'PrettyName');
+ cell.text(text);
+ prettyNames[key] = text;
+ }
+
+ firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
+ blacklistedHostnamesLabel = uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
+}
+
+/******************************************************************************/
+
+// Create page scopes for the web page
+
+function selectGlobalScope() {
+ if ( matrixSnapshot.scope === '*' ) { return; }
+ matrixSnapshot.scope = '*';
+ document.body.classList.add('globalScope');
+ matrixSnapshot.tMatrixModifiedTime = undefined;
+ updateMatrixSnapshot();
+ dropDownMenuHide();
+}
+
+function selectSpecificScope(ev) {
+ var newScope = ev.target.getAttribute('data-scope');
+ if ( !newScope || matrixSnapshot.scope === newScope ) { return; }
+ document.body.classList.remove('globalScope');
+ matrixSnapshot.scope = newScope;
+ matrixSnapshot.tMatrixModifiedTime = undefined;
+ updateMatrixSnapshot();
+ dropDownMenuHide();
+}
+
+function initScopeCell() {
+ // It's possible there is no page URL at this point: some pages cannot
+ // be filtered by uMatrix.
+ if ( matrixSnapshot.url === '' ) { return; }
+ var specificScope = uDom.nodeFromId('specificScope');
+
+ while ( specificScope.firstChild !== null ) {
+ specificScope.removeChild(specificScope.firstChild);
+ }
+
+ // Fill in the scope menu entries
+ var pos = matrixSnapshot.domain.indexOf('.');
+ var tld, labels;
+ if ( pos === -1 ) {
+ tld = '';
+ labels = matrixSnapshot.hostname;
+ } else {
+ tld = matrixSnapshot.domain.slice(pos + 1);
+ labels = matrixSnapshot.hostname.slice(0, -tld.length);
+ }
+ var beg = 0, span, label;
+ while ( beg < labels.length ) {
+ pos = labels.indexOf('.', beg);
+ if ( pos === -1 ) {
+ pos = labels.length;
+ } else {
+ pos += 1;
+ }
+ label = document.createElement('span');
+ label.appendChild(
+ document.createTextNode(punycode.toUnicode(labels.slice(beg, pos)))
+ );
+ span = document.createElement('span');
+ span.setAttribute('data-scope', labels.slice(beg) + tld);
+ span.appendChild(label);
+ specificScope.appendChild(span);
+ beg = pos;
+ }
+ if ( tld !== '' ) {
+ label = document.createElement('span');
+ label.appendChild(document.createTextNode(punycode.toUnicode(tld)));
+ span = document.createElement('span');
+ span.setAttribute('data-scope', tld);
+ span.appendChild(label);
+ specificScope.appendChild(span);
+ }
+ updateScopeCell();
+}
+
+function updateScopeCell() {
+ var specificScope = uDom.nodeFromId('specificScope'),
+ isGlobal = matrixSnapshot.scope === '*';
+ document.body.classList.toggle('globalScope', isGlobal);
+ specificScope.classList.toggle('on', !isGlobal);
+ uDom.nodeFromId('globalScope').classList.toggle('on', isGlobal);
+ for ( var node of specificScope.children ) {
+ node.classList.toggle(
+ 'on',
+ !isGlobal &&
+ matrixSnapshot.scope.endsWith(node.getAttribute('data-scope'))
+ );
+ }
+}
+
+/******************************************************************************/
+
+function updateMatrixSwitches() {
+ var count = 0,
+ enabled,
+ switches = matrixSnapshot.tSwitches;
+ for ( var switchName in switches ) {
+ if ( switches.hasOwnProperty(switchName) === false ) { continue; }
+ enabled = switches[switchName];
+ if ( enabled && switchName !== 'matrix-off' ) {
+ count += 1;
+ }
+ uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', enabled);
+ }
+ uDom.nodeFromId('mtxSwitch_https-strict').classList.toggle(
+ 'relevant',
+ matrixSnapshot.hasMixedContent
+ );
+ uDom.nodeFromId('mtxSwitch_no-workers').classList.toggle(
+ 'relevant',
+ matrixSnapshot.hasWebWorkers
+ );
+ uDom.nodeFromId('mtxSwitch_referrer-spoof').classList.toggle(
+ 'relevant',
+ matrixSnapshot.has3pReferrer
+ );
+ uDom.nodeFromId('mtxSwitch_noscript-spoof').classList.toggle(
+ 'relevant',
+ matrixSnapshot.hasNoscriptTags
+ );
+ uDom.nodeFromSelector('#buttonMtxSwitches span.badge').textContent =
+ count.toLocaleString();
+ uDom.nodeFromSelector('#mtxSwitch_matrix-off span.badge').textContent =
+ matrixSnapshot.blockedCount.toLocaleString();
+ document.body.classList.toggle('powerOff', switches['matrix-off']);
+}
+
+function toggleMatrixSwitch(ev) {
+ if ( ev.target.localName === 'a' ) { return; }
+ var elem = ev.currentTarget;
+ var pos = elem.id.indexOf('_');
+ if ( pos === -1 ) { return; }
+ var switchName = elem.id.slice(pos + 1);
+ var request = {
+ what: 'toggleMatrixSwitch',
+ switchName: switchName,
+ srcHostname: matrixSnapshot.scope
+ };
+ vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
+}
+
+/******************************************************************************/
+
+function updatePersistButton() {
+ var diffCount = matrixSnapshot.diff.length;
+ var button = uDom('#buttonPersist');
+ button.contents()
+ .filter(function(){return this.nodeType===3;})
+ .first()
+ .text(diffCount > 0 ? '\uf13e' : '\uf023');
+ button.descendants('span.badge').text(diffCount > 0 ? diffCount : '');
+ var disabled = diffCount === 0;
+ button.toggleClass('disabled', disabled);
+ uDom('#buttonRevertScope').toggleClass('disabled', disabled);
+}
+
+/******************************************************************************/
+
+function persistMatrix() {
+ var request = {
+ what: 'applyDiffToPermanentMatrix',
+ diff: matrixSnapshot.diff
+ };
+ vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
+}
+
+/******************************************************************************/
+
+// rhill 2014-03-12: revert completely ALL changes related to the
+// current page, including scopes.
+
+function revertMatrix() {
+ var request = {
+ what: 'applyDiffToTemporaryMatrix',
+ diff: matrixSnapshot.diff
+ };
+ vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
+}
+
+/******************************************************************************/
+
+// Buttons which are affected by any changes in the matrix
+
+function updateMatrixButtons() {
+ updateScopeCell();
+ updateMatrixSwitches();
+ updatePersistButton();
+}
+
+/******************************************************************************/
+
+function revertAll() {
+ var request = {
+ what: 'revertTemporaryMatrix'
+ };
+ vAPI.messaging.send('popup.js', request, updateMatrixSnapshot);
+ dropDownMenuHide();
+}
+
+/******************************************************************************/
+
+function buttonReloadHandler(ev) {
+ vAPI.messaging.send('popup.js', {
+ what: 'forceReloadTab',
+ tabId: matrixSnapshot.tabId,
+ bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey
+ });
+}
+
+/******************************************************************************/
+
+function mouseenterMatrixCellHandler(ev) {
+ matrixCellHotspots.appendTo(ev.target);
+}
+
+function mouseleaveMatrixCellHandler() {
+ matrixCellHotspots.detach();
+}
+
+/******************************************************************************/
+
+function gotoExtensionURL(ev) {
+ var url = uDom(ev.currentTarget).attr('data-extension-url');
+ if ( url ) {
+ vAPI.messaging.send('popup.js', {
+ what: 'gotoExtensionURL',
+ url: url,
+ shiftKey: ev.shiftKey
+ });
+ }
+ dropDownMenuHide();
+ vAPI.closePopup();
+}
+
+/******************************************************************************/
+
+function dropDownMenuShow(ev) {
+ var button = ev.target;
+ var menuOverlay = document.getElementById(button.getAttribute('data-dropdown-menu'));
+ var butnRect = button.getBoundingClientRect();
+ var viewRect = document.body.getBoundingClientRect();
+ var butnNormalLeft = butnRect.left / (viewRect.width - butnRect.width);
+ menuOverlay.classList.add('show');
+ var menu = menuOverlay.querySelector('.dropdown-menu');
+ var menuRect = menu.getBoundingClientRect();
+ var menuLeft = butnNormalLeft * (viewRect.width - menuRect.width);
+ menu.style.left = menuLeft.toFixed(0) + 'px';
+ menu.style.top = butnRect.bottom + 'px';
+}
+
+function dropDownMenuHide() {
+ uDom('.dropdown-menu-capture').removeClass('show');
+}
+
+/******************************************************************************/
+
+var onMatrixSnapshotReady = function(response) {
+ if ( response === 'ENOTFOUND' ) {
+ uDom.nodeFromId('noTabFound').textContent =
+ vAPI.i18n('matrixNoTabFound');
+ document.body.classList.add('noTabFound');
+ return;
+ }
+
+ // Now that tabId and pageURL are set, we can build our menu
+ initMenuEnvironment();
+ makeMenu();
+
+ // After popup menu is built, check whether there is a non-empty matrix
+ if ( matrixSnapshot.url === '' ) {
+ uDom('#matHead').remove();
+ uDom('#toolbarContainer').remove();
+
+ // https://github.com/gorhill/httpswitchboard/issues/191
+ uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
+ uDom('#noNetTrafficPrompt').css('display', '');
+ }
+
+ // Create a hash to find out whether the reload button needs to be
+ // highlighted.
+ // TODO:
+};
+
+/******************************************************************************/
+
+var matrixSnapshotPoller = (function() {
+ var timer = null;
+
+ var preprocessMatrixSnapshot = function(snapshot) {
+ if ( Array.isArray(snapshot.headerIndices) ) {
+ snapshot.headerIndices = new Map(snapshot.headerIndices);
+ }
+ return snapshot;
+ };
+
+ var processPollResult = function(response) {
+ if ( typeof response !== 'object' ) {
+ return;
+ }
+ if (
+ response.mtxContentModified === false &&
+ response.mtxCountModified === false &&
+ response.pMatrixModified === false &&
+ response.tMatrixModified === false
+ ) {
+ return;
+ }
+ matrixSnapshot = preprocessMatrixSnapshot(response);
+
+ if ( response.mtxContentModified ) {
+ makeMenu();
+ return;
+ }
+ if ( response.mtxCountModified ) {
+ updateMatrixCounts();
+ }
+ if (
+ response.pMatrixModified ||
+ response.tMatrixModified ||
+ response.scopeModified
+ ) {
+ updateMatrixColors();
+ updateMatrixBehavior();
+ updateMatrixButtons();
+ }
+ };
+
+ var onPolled = function(response) {
+ processPollResult(response);
+ pollAsync();
+ };
+
+ var pollNow = function() {
+ unpollAsync();
+ vAPI.messaging.send('popup.js', {
+ what: 'matrixSnapshot',
+ tabId: matrixSnapshot.tabId,
+ scope: matrixSnapshot.scope,
+ mtxContentModifiedTime: matrixSnapshot.mtxContentModifiedTime,
+ mtxCountModifiedTime: matrixSnapshot.mtxCountModifiedTime,
+ mtxDiffCount: matrixSnapshot.diff.length,
+ pMatrixModifiedTime: matrixSnapshot.pMatrixModifiedTime,
+ tMatrixModifiedTime: matrixSnapshot.tMatrixModifiedTime,
+ }, onPolled);
+ };
+
+ var poll = function() {
+ timer = null;
+ pollNow();
+ };
+
+ var pollAsync = function() {
+ if ( timer !== null ) {
+ return;
+ }
+ if ( document.defaultView === null ) {
+ return;
+ }
+ timer = vAPI.setTimeout(poll, 1414);
+ };
+
+ var unpollAsync = function() {
+ if ( timer !== null ) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ (function() {
+ var tabId = matrixSnapshot.tabId;
+
+ // If no tab id yet, see if there is one specified in our URL
+ if ( tabId === undefined ) {
+ var matches = window.location.search.match(/(?:\?|&)tabId=([^&]+)/);
+ if ( matches !== null ) {
+ tabId = matches[1];
+ // No need for logger button when embedded in logger
+ uDom('[data-extension-url="logger-ui.html"]').remove();
+ }
+ }
+
+ var snapshotFetched = function(response) {
+ if ( typeof response === 'object' ) {
+ matrixSnapshot = preprocessMatrixSnapshot(response);
+ }
+ onMatrixSnapshotReady(response);
+ pollAsync();
+ };
+
+ vAPI.messaging.send('popup.js', {
+ what: 'matrixSnapshot',
+ tabId: tabId
+ }, snapshotFetched);
+ })();
+
+ return {
+ pollNow: pollNow
+ };
+})();
+
+/******************************************************************************/
+
+// Below is UI stuff which is not key to make the menu, so this can
+// be done without having to wait for a tab to be bound to the menu.
+
+// We reuse for all cells the one and only cell hotspots.
+uDom('#whitelist').on('click', function() {
+ handleWhitelistFilter(uDom(this));
+ return false;
+ });
+uDom('#blacklist').on('click', function() {
+ handleBlacklistFilter(uDom(this));
+ return false;
+ });
+uDom('#domainOnly').on('click', function() {
+ toggleCollapseState(uDom(this));
+ return false;
+ });
+matrixCellHotspots = uDom('#cellHotspots').detach();
+uDom('body')
+ .on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
+ .on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
+uDom('#specificScope').on('click', selectSpecificScope);
+uDom('#globalScope').on('click', selectGlobalScope);
+uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
+uDom('#buttonPersist').on('click', persistMatrix);
+uDom('#buttonRevertScope').on('click', revertMatrix);
+
+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) {
+ matrixSnapshot.collapseBlacklistedDomains =
+ ev.target.classList.toggle('g4Collapsed');
+ setUserSetting(
+ 'popupCollapseBlacklistedDomains',
+ matrixSnapshot.collapseBlacklistedDomains
+ );
+});
+
+/******************************************************************************/
+
+})();
diff --git a/js/profiler.js b/js/profiler.js
new file mode 100644
index 0000000..732403c
--- /dev/null
+++ b/js/profiler.js
@@ -0,0 +1,63 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/******************************************************************************/
+
+var quickProfiler = (function() {
+ var timer = performance;
+ var time = 0;
+ var count = 0;
+ var tstart = 0;
+ var lastlog = timer.now();
+ var prompt = '';
+ var reset = function() {
+ time = 0;
+ count = 0;
+ tstart = 0;
+ };
+ var avg = function() {
+ return count > 0 ? time / count : 0;
+ };
+ var start = function(s) {
+ prompt = s || '';
+ tstart = timer.now();
+ };
+ var stop = function(period) {
+ if ( period === undefined ) {
+ period = 10000;
+ }
+ var now = timer.now();
+ count += 1;
+ time += (now - tstart);
+ if ( (now - lastlog) >= period ) {
+ console.log('µMatrix> %s: %s ms (%d samples)', prompt, avg().toFixed(3), count);
+ lastlog = now;
+ }
+ };
+ return {
+ reset: reset,
+ start: start,
+ stop: stop
+ };
+})();
+
+/******************************************************************************/
diff --git a/js/raw-settings.js b/js/raw-settings.js
new file mode 100644
index 0000000..4abcd97
--- /dev/null
+++ b/js/raw-settings.js
@@ -0,0 +1,116 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2018-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uBlock
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+var messaging = vAPI.messaging;
+var cachedData = '';
+var rawSettingsInput = uDom.nodeFromId('rawSettings');
+
+/******************************************************************************/
+
+var hashFromRawSettings = function(raw) {
+ return raw.trim().replace(/\s+/g, '|');
+};
+
+/******************************************************************************/
+
+// This is to give a visual hint that the content of user blacklist has changed.
+
+var rawSettingsChanged = (function () {
+ var timer = null;
+
+ var handler = function() {
+ timer = null;
+ var changed =
+ hashFromRawSettings(rawSettingsInput.value) !== cachedData;
+ uDom.nodeFromId('rawSettingsApply').disabled = !changed;
+ };
+
+ return function() {
+ if ( timer !== null ) {
+ clearTimeout(timer);
+ }
+ timer = vAPI.setTimeout(handler, 100);
+ };
+})();
+
+/******************************************************************************/
+
+function renderRawSettings() {
+ var onRead = function(raw) {
+ cachedData = hashFromRawSettings(raw);
+ var pretty = [],
+ whitespaces = ' ',
+ lines = raw.split('\n'),
+ max = 0,
+ pos,
+ i, n = lines.length;
+ for ( i = 0; i < n; i++ ) {
+ pos = lines[i].indexOf(' ');
+ if ( pos > max ) {
+ max = pos;
+ }
+ }
+ for ( i = 0; i < n; i++ ) {
+ pos = lines[i].indexOf(' ');
+ pretty.push(whitespaces.slice(0, max - pos) + lines[i]);
+ }
+ rawSettingsInput.value = pretty.join('\n') + '\n';
+ rawSettingsChanged();
+ rawSettingsInput.focus();
+ };
+ messaging.send('dashboard', { what: 'readRawSettings' }, onRead);
+}
+
+/******************************************************************************/
+
+var applyChanges = function() {
+ messaging.send(
+ 'dashboard',
+ {
+ what: 'writeRawSettings',
+ content: rawSettingsInput.value
+ },
+ renderRawSettings
+ );
+};
+
+/******************************************************************************/
+
+// Handle user interaction
+uDom('#rawSettings').on('input', rawSettingsChanged);
+uDom('#rawSettingsApply').on('click', applyChanges);
+
+renderRawSettings();
+
+/******************************************************************************/
+
+})();
diff --git a/js/settings.js b/js/settings.js
new file mode 100644
index 0000000..bc11c7a
--- /dev/null
+++ b/js/settings.js
@@ -0,0 +1,195 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+var cachedSettings = {};
+
+/******************************************************************************/
+
+function changeUserSettings(name, value) {
+ vAPI.messaging.send('settings.js', {
+ what: 'userSettings',
+ name: name,
+ value: value
+ });
+}
+
+/******************************************************************************/
+
+function changeMatrixSwitch(name, state) {
+ vAPI.messaging.send('settings.js', {
+ what: 'setMatrixSwitch',
+ switchName: name,
+ state: state
+ });
+}
+
+/******************************************************************************/
+
+function onChangeValueHandler(elem, setting, min, max) {
+ var oldVal = cachedSettings.userSettings[setting];
+ var newVal = Math.round(parseFloat(elem.value));
+ if ( typeof newVal !== 'number' ) {
+ newVal = oldVal;
+ } else {
+ newVal = Math.max(newVal, min);
+ newVal = Math.min(newVal, max);
+ }
+ elem.value = newVal;
+ if ( newVal !== oldVal ) {
+ changeUserSettings(setting, newVal);
+ }
+}
+
+/******************************************************************************/
+
+function prepareToDie() {
+ onChangeValueHandler(
+ uDom.nodeFromId('deleteUnusedSessionCookiesAfter'),
+ 'deleteUnusedSessionCookiesAfter',
+ 15, 1440
+ );
+ onChangeValueHandler(
+ uDom.nodeFromId('clearBrowserCacheAfter'),
+ 'clearBrowserCacheAfter',
+ 15, 1440
+ );
+}
+
+/******************************************************************************/
+
+function onInputChanged(ev) {
+ var target = ev.target;
+
+ switch ( target.id ) {
+ case 'displayTextSize':
+ changeUserSettings('displayTextSize', target.value + 'px');
+ break;
+ case 'clearBrowserCache':
+ case 'cloudStorageEnabled':
+ case 'collapseBlacklisted':
+ case 'collapseBlocked':
+ case 'colorBlindFriendly':
+ case 'deleteCookies':
+ case 'deleteLocalStorage':
+ case 'deleteUnusedSessionCookies':
+ case 'iconBadgeEnabled':
+ case 'processHyperlinkAuditing':
+ changeUserSettings(target.id, target.checked);
+ break;
+ case 'noMixedContent':
+ case 'noscriptTagsSpoofed':
+ case 'processReferer':
+ changeMatrixSwitch(
+ target.getAttribute('data-matrix-switch'),
+ target.checked
+ );
+ break;
+ case 'deleteUnusedSessionCookiesAfter':
+ onChangeValueHandler(target, 'deleteUnusedSessionCookiesAfter', 15, 1440);
+ break;
+ case 'clearBrowserCacheAfter':
+ onChangeValueHandler(target, 'clearBrowserCacheAfter', 15, 1440);
+ break;
+ case 'popupScopeLevel':
+ changeUserSettings('popupScopeLevel', target.value);
+ break;
+ default:
+ break;
+ }
+
+ switch ( target.id ) {
+ case 'collapseBlocked':
+ synchronizeWidgets();
+ break;
+ default:
+ break;
+ }
+}
+
+/******************************************************************************/
+
+function synchronizeWidgets() {
+ var e1, e2;
+
+ e1 = uDom.nodeFromId('collapseBlocked');
+ e2 = uDom.nodeFromId('collapseBlacklisted');
+ if ( e1.checked ) {
+ e2.setAttribute('disabled', '');
+ } else {
+ e2.removeAttribute('disabled');
+ }
+}
+
+/******************************************************************************/
+
+vAPI.messaging.send(
+ 'settings.js',
+ { what: 'getUserSettings' },
+ function onSettingsReceived(settings) {
+ // Cache copy
+ cachedSettings = settings;
+
+ var userSettings = settings.userSettings;
+ var matrixSwitches = settings.matrixSwitches;
+
+ uDom('[data-setting-bool]').forEach(function(elem){
+ elem.prop('checked', userSettings[elem.prop('id')] === true);
+ });
+
+ uDom('[data-matrix-switch]').forEach(function(elem){
+ var switchName = elem.attr('data-matrix-switch');
+ if ( typeof switchName === 'string' && switchName !== '' ) {
+ elem.prop('checked', matrixSwitches[switchName] === true);
+ }
+ });
+
+ uDom.nodeFromId('displayTextSize').value =
+ parseInt(userSettings.displayTextSize, 10) || 14;
+
+ uDom.nodeFromId('popupScopeLevel').value = userSettings.popupScopeLevel;
+ uDom.nodeFromId('deleteUnusedSessionCookiesAfter').value =
+ userSettings.deleteUnusedSessionCookiesAfter;
+ uDom.nodeFromId('clearBrowserCacheAfter').value =
+ userSettings.clearBrowserCacheAfter;
+
+ synchronizeWidgets();
+
+ document.addEventListener('change', onInputChanged);
+
+ // https://github.com/gorhill/httpswitchboard/issues/197
+ uDom(window).on('beforeunload', prepareToDie);
+ }
+);
+
+/******************************************************************************/
+
+})();
diff --git a/js/start.js b/js/start.js
new file mode 100644
index 0000000..051e58f
--- /dev/null
+++ b/js/start.js
@@ -0,0 +1,108 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+// ORDER IS IMPORTANT
+
+/******************************************************************************/
+
+// Load everything
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+var µm = µMatrix;
+
+/******************************************************************************/
+
+var processCallbackQueue = function(queue, callback) {
+ var processOne = function() {
+ var fn = queue.pop();
+ if ( fn ) {
+ fn(processOne);
+ } else if ( typeof callback === 'function' ) {
+ callback();
+ }
+ };
+ processOne();
+};
+
+/******************************************************************************/
+
+var onAllDone = function() {
+ µm.webRequest.start();
+
+ µm.assets.addObserver(µm.assetObserver.bind(µm));
+ µm.scheduleAssetUpdater(µm.userSettings.autoUpdate ? 7 * 60 * 1000 : 0);
+
+ vAPI.cloud.start([ 'myRulesPane' ]);
+};
+
+/******************************************************************************/
+
+var onTabsReady = function(tabs) {
+ var tab;
+ var i = tabs.length;
+ // console.debug('start.js > binding %d tabs', i);
+ while ( i-- ) {
+ tab = tabs[i];
+ µm.tabContextManager.push(tab.id, tab.url, 'newURL');
+ }
+
+ onAllDone();
+};
+
+/******************************************************************************/
+
+var onUserSettingsLoaded = function() {
+ µm.loadHostsFiles();
+};
+
+/******************************************************************************/
+
+var onPSLReady = function() {
+ µm.loadUserSettings(onUserSettingsLoaded);
+ µm.loadRawSettings();
+ µm.loadMatrix();
+
+ // rhill 2013-11-24: bind behind-the-scene virtual tab/url manually, since the
+ // normal way forbid binding behind the scene tab.
+ // https://github.com/gorhill/httpswitchboard/issues/67
+ µm.pageStores[vAPI.noTabId] = µm.pageStoreFactory(µm.tabContextManager.mustLookup(vAPI.noTabId));
+ µm.pageStores[vAPI.noTabId].title = vAPI.i18n('statsPageDetailedBehindTheScenePage');
+
+ vAPI.tabs.getAll(onTabsReady);
+};
+
+/******************************************************************************/
+
+processCallbackQueue(µm.onBeforeStartQueue, function() {
+ µm.loadPublicSuffixList(onPSLReady);
+});
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/storage.js b/js/storage.js
new file mode 100644
index 0000000..c2ece8f
--- /dev/null
+++ b/js/storage.js
@@ -0,0 +1,615 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global objectAssign, punycode, publicSuffixList */
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.getBytesInUse = function() {
+ var µm = this;
+ var getBytesInUseHandler = function(bytesInUse) {
+ µm.storageUsed = bytesInUse;
+ };
+ // Not all WebExtension implementations support getBytesInUse().
+ if ( typeof vAPI.storage.getBytesInUse === 'function' ) {
+ vAPI.storage.getBytesInUse(null, getBytesInUseHandler);
+ } else {
+ µm.storageUsed = undefined;
+ }
+};
+
+/******************************************************************************/
+
+µMatrix.saveUserSettings = function() {
+ this.XAL.keyvalSetMany(
+ this.userSettings,
+ this.getBytesInUse.bind(this)
+ );
+};
+
+µMatrix.loadUserSettings = function(callback) {
+ var µm = this;
+
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
+
+ var settingsLoaded = function(store) {
+ // console.log('storage.js > loaded user settings');
+
+ µm.userSettings = store;
+
+ callback(µm.userSettings);
+ };
+
+ vAPI.storage.get(this.userSettings, settingsLoaded);
+};
+
+/******************************************************************************/
+
+µMatrix.loadRawSettings = function() {
+ var µm = this;
+
+ var onLoaded = function(bin) {
+ if ( !bin || bin.rawSettings instanceof Object === false ) { return; }
+ for ( var key of Object.keys(bin.rawSettings) ) {
+ if (
+ µm.rawSettings.hasOwnProperty(key) === false ||
+ typeof bin.rawSettings[key] !== typeof µm.rawSettings[key]
+ ) {
+ continue;
+ }
+ µm.rawSettings[key] = bin.rawSettings[key];
+ }
+ µm.rawSettingsWriteTime = Date.now();
+ };
+
+ vAPI.storage.get('rawSettings', onLoaded);
+};
+
+µMatrix.saveRawSettings = function(rawSettings, callback) {
+ var keys = Object.keys(rawSettings);
+ if ( keys.length === 0 ) {
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ return;
+ }
+ for ( var key of keys ) {
+ if (
+ this.rawSettingsDefault.hasOwnProperty(key) &&
+ typeof rawSettings[key] === typeof this.rawSettingsDefault[key]
+ ) {
+ this.rawSettings[key] = rawSettings[key];
+ }
+ }
+ vAPI.storage.set({ rawSettings: this.rawSettings }, callback);
+ this.rawSettingsWriteTime = Date.now();
+};
+
+µMatrix.rawSettingsFromString = function(raw) {
+ var result = {},
+ lineIter = new this.LineIterator(raw),
+ line, matches, name, value;
+ while ( lineIter.eot() === false ) {
+ line = lineIter.next().trim();
+ matches = /^(\S+)(\s+(.+))?$/.exec(line);
+ if ( matches === null ) { continue; }
+ name = matches[1];
+ if ( this.rawSettingsDefault.hasOwnProperty(name) === false ) {
+ continue;
+ }
+ value = (matches[2] || '').trim();
+ switch ( typeof this.rawSettingsDefault[name] ) {
+ case 'boolean':
+ if ( value === 'true' ) {
+ value = true;
+ } else if ( value === 'false' ) {
+ value = false;
+ } else {
+ value = this.rawSettingsDefault[name];
+ }
+ break;
+ case 'string':
+ if ( value === '' ) {
+ value = this.rawSettingsDefault[name];
+ }
+ break;
+ case 'number':
+ value = parseInt(value, 10);
+ if ( isNaN(value) ) {
+ value = this.rawSettingsDefault[name];
+ }
+ break;
+ default:
+ break;
+ }
+ if ( this.rawSettings[name] !== value ) {
+ result[name] = value;
+ }
+ }
+ this.saveRawSettings(result);
+};
+
+µMatrix.stringFromRawSettings = function() {
+ var out = [];
+ for ( var key of Object.keys(this.rawSettings).sort() ) {
+ out.push(key + ' ' + this.rawSettings[key]);
+ }
+ return out.join('\n');
+};
+
+/******************************************************************************/
+
+// save white/blacklist
+µMatrix.saveMatrix = function() {
+ µMatrix.XAL.keyvalSetOne('userMatrix', this.pMatrix.toString());
+};
+
+/******************************************************************************/
+
+µMatrix.loadMatrix = function(callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
+ var µm = this;
+ var onLoaded = function(bin) {
+ if ( bin.hasOwnProperty('userMatrix') ) {
+ µm.pMatrix.fromString(bin.userMatrix);
+ µm.tMatrix.assign(µm.pMatrix);
+ callback();
+ }
+ };
+ this.XAL.keyvalGetOne('userMatrix', onLoaded);
+};
+
+/******************************************************************************/
+
+µMatrix.listKeysFromCustomHostsFiles = function(raw) {
+ var out = new Set(),
+ reIgnore = /^[!#]/,
+ reValid = /^[a-z-]+:\/\/\S+/,
+ lineIter = new this.LineIterator(raw),
+ location;
+ while ( lineIter.eot() === false ) {
+ location = lineIter.next().trim();
+ if ( reIgnore.test(location) || !reValid.test(location) ) { continue; }
+ out.add(location);
+ }
+ return this.setToArray(out);
+};
+
+/******************************************************************************/
+
+µMatrix.getAvailableHostsFiles = function(callback) {
+ var µm = this,
+ availableHostsFiles = {};
+
+ // 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,
+ external: true,
+ submitter: 'user',
+ title: listKey
+ };
+ availableHostsFiles[listKey] = entry;
+ this.assets.registerAssetSource(listKey, 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;
+ }
+ }
+
+ // 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);
+ }
+
+ callback(availableHostsFiles);
+ };
+
+ // built-in lists
+ var onBuiltinHostsFilesLoaded = function(entries) {
+ for ( var assetKey in entries ) {
+ if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
+ entry = entries[assetKey];
+ if ( entry.content !== 'filters' ) { continue; }
+ availableHostsFiles[assetKey] = objectAssign({}, entry);
+ }
+
+ // Now get user's selection of lists
+ vAPI.storage.get(
+ { 'liveHostsFiles': availableHostsFiles },
+ onSelectedHostsFilesLoaded
+ );
+ };
+
+ this.assets.metadata(onBuiltinHostsFilesLoaded);
+};
+
+/******************************************************************************/
+
+µMatrix.loadHostsFiles = function(callback) {
+ var µm = µMatrix;
+ var hostsFileLoadCount;
+
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
+
+ var loadHostsFilesEnd = function() {
+ µm.ubiquitousBlacklist.freeze();
+ vAPI.storage.set({ 'liveHostsFiles': µm.liveHostsFiles });
+ vAPI.messaging.broadcast({ what: 'loadHostsFilesCompleted' });
+ µm.getBytesInUse();
+ callback();
+ };
+
+ var mergeHostsFile = function(details) {
+ µm.mergeHostsFile(details);
+ hostsFileLoadCount -= 1;
+ if ( hostsFileLoadCount === 0 ) {
+ loadHostsFilesEnd();
+ }
+ };
+
+ var loadHostsFilesStart = function(hostsFiles) {
+ µm.liveHostsFiles = hostsFiles;
+ µm.ubiquitousBlacklist.reset();
+ var locations = Object.keys(hostsFiles);
+ hostsFileLoadCount = locations.length;
+
+ // 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);
+ }
+
+ // https://github.com/gorhill/uMatrix/issues/2
+ if ( hostsFileLoadCount === 0 ) {
+ loadHostsFilesEnd();
+ return;
+ }
+ };
+
+ this.getAvailableHostsFiles(loadHostsFilesStart);
+};
+
+/******************************************************************************/
+
+µMatrix.mergeHostsFile = function(details) {
+ var usedCount = this.ubiquitousBlacklist.count;
+ var duplicateCount = this.ubiquitousBlacklist.duplicateCount;
+
+ this.mergeHostsFileContent(details.content);
+
+ usedCount = this.ubiquitousBlacklist.count - usedCount;
+ duplicateCount = this.ubiquitousBlacklist.duplicateCount - duplicateCount;
+
+ var hostsFilesMeta = this.liveHostsFiles[details.assetKey];
+ hostsFilesMeta.entryCount = usedCount + duplicateCount;
+ hostsFilesMeta.entryUsedCount = usedCount;
+};
+
+/******************************************************************************/
+
+µMatrix.mergeHostsFileContent = function(rawText) {
+ var rawEnd = rawText.length;
+ var ubiquitousBlacklist = this.ubiquitousBlacklist;
+ var reLocalhost = /(^|\s)(localhost\.localdomain|localhost|local|broadcasthost|0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)(?=\s|$)/g;
+ var reAsciiSegment = /^[\x21-\x7e]+$/;
+ var matches;
+ var lineBeg = 0, lineEnd;
+ var line;
+
+ while ( lineBeg < rawEnd ) {
+ lineEnd = rawText.indexOf('\n', lineBeg);
+ if ( lineEnd < 0 ) {
+ lineEnd = rawText.indexOf('\r', lineBeg);
+ if ( lineEnd < 0 ) {
+ lineEnd = rawEnd;
+ }
+ }
+
+ // rhill 2014-04-18: The trim is important here, as without it there
+ // could be a lingering `\r` which would cause problems in the
+ // following parsing code.
+ line = rawText.slice(lineBeg, lineEnd).trim();
+ lineBeg = lineEnd + 1;
+
+ // https://github.com/gorhill/httpswitchboard/issues/15
+ // Ensure localhost et al. don't end up in the ubiquitous blacklist.
+ line = line
+ .replace(/#.*$/, '')
+ .toLowerCase()
+ .replace(reLocalhost, '')
+ .trim();
+
+ // The filter is whatever sequence of printable ascii character without
+ // whitespaces
+ matches = reAsciiSegment.exec(line);
+ if ( !matches || matches.length === 0 ) {
+ continue;
+ }
+
+ // Bypass anomalies
+ // For example, when a filter contains whitespace characters, or
+ // whatever else outside the range of printable ascii characters.
+ if ( matches[0] !== line ) {
+ continue;
+ }
+
+ line = matches[0];
+ if ( line === '' ) {
+ continue;
+ }
+
+ ubiquitousBlacklist.add(line);
+ }
+};
+
+/******************************************************************************/
+
+// `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;
+
+ // 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;
+ }
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ // 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.
+ 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;
+ }
+ }
+ }
+ 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
+ };
+ }
+ 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 });
+
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+};
+
+/******************************************************************************/
+
+// `switches` contains the preset blacklists for which the switch must be
+// revisited.
+
+µMatrix.reloadHostsFiles = function() {
+ this.loadHostsFiles();
+};
+
+/******************************************************************************/
+
+µMatrix.loadPublicSuffixList = function(callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = this.noopFunc;
+ }
+
+ var applyPublicSuffixList = function(details) {
+ if ( !details.error ) {
+ publicSuffixList.parse(details.content, punycode.toASCII);
+ }
+ callback();
+ };
+
+ this.assets.get(this.pslAssetKey, applyPublicSuffixList);
+};
+
+/******************************************************************************/
+
+µMatrix.scheduleAssetUpdater = (function() {
+ var timer, next = 0;
+ return function(updateDelay) {
+ if ( timer ) {
+ clearTimeout(timer);
+ timer = undefined;
+ }
+ if ( updateDelay === 0 ) {
+ next = 0;
+ return;
+ }
+ var now = Date.now();
+ // Use the new schedule if and only if it is earlier than the previous
+ // one.
+ if ( next !== 0 ) {
+ updateDelay = Math.min(updateDelay, Math.max(next - now, 0));
+ }
+ next = now + updateDelay;
+ timer = vAPI.setTimeout(function() {
+ timer = undefined;
+ next = 0;
+ µMatrix.assets.updateStart({ delay: 120000 });
+ }, updateDelay);
+ };
+})();
+
+/******************************************************************************/
+
+µMatrix.assetObserver = function(topic, details) {
+ // Do not update filter list if not in use.
+ if ( topic === 'before-asset-updated' ) {
+ if (
+ this.liveHostsFiles.hasOwnProperty(details.assetKey) === false ||
+ this.liveHostsFiles[details.assetKey].off === true
+ ) {
+ return false;
+ }
+ return;
+ }
+
+ if ( topic === 'after-asset-updated' ) {
+ vAPI.messaging.broadcast({
+ what: 'assetUpdated',
+ key: details.assetKey,
+ cached: true
+ });
+ return;
+ }
+
+ // Update failed.
+ if ( topic === 'asset-update-failed' ) {
+ vAPI.messaging.broadcast({
+ what: 'assetUpdated',
+ key: details.assetKey,
+ failed: true
+ });
+ return;
+ }
+
+ // Reload all filter lists if needed.
+ if ( topic === 'after-assets-updated' ) {
+ if ( details.assetKeys.length !== 0 ) {
+ this.loadHostsFiles();
+ }
+ if ( this.userSettings.autoUpdate ) {
+ this.scheduleAssetUpdater(25200000);
+ } else {
+ this.scheduleAssetUpdater(0);
+ }
+ vAPI.messaging.broadcast({
+ what: 'assetsUpdated',
+ assetKeys: details.assetKeys
+ });
+ 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/js/tab.js b/js/tab.js
new file mode 100644
index 0000000..b0dd1ab
--- /dev/null
+++ b/js/tab.js
@@ -0,0 +1,710 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/******************************************************************************/
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+var µm = µMatrix;
+
+// https://github.com/gorhill/httpswitchboard/issues/303
+// Some kind of trick going on here:
+// Any scheme other than 'http' and 'https' is remapped into a fake
+// URL which trick the rest of µMatrix into being able to process an
+// otherwise unmanageable scheme. µMatrix needs web page to have a proper
+// hostname to work properly, so just like the 'behind-the-scene'
+// fake domain name, we map unknown schemes into a fake '{scheme}-scheme'
+// hostname. This way, for a specific scheme you can create scope with
+// rules which will apply only to that scheme.
+
+/******************************************************************************/
+/******************************************************************************/
+
+µm.normalizePageURL = function(tabId, pageURL) {
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return 'http://' + this.behindTheSceneScope + '/';
+ }
+
+ // If the URL is that of our "blocked page" document, return the URL of
+ // the blocked page.
+ if ( pageURL.lastIndexOf(vAPI.getURL('main-blocked.html'), 0) === 0 ) {
+ var matches = /main-blocked\.html\?details=([^&]+)/.exec(pageURL);
+ if ( matches && matches.length === 2 ) {
+ try {
+ var details = JSON.parse(atob(matches[1]));
+ pageURL = details.url;
+ } catch (e) {
+ }
+ }
+ }
+
+ var uri = this.URI.set(pageURL);
+ var scheme = uri.scheme;
+ if ( scheme === 'https' || scheme === 'http' ) {
+ return uri.normalizedURI();
+ }
+
+ var fakeHostname = scheme + '-scheme';
+
+ if ( uri.hostname !== '' ) {
+ fakeHostname = uri.hostname + '.' + fakeHostname;
+ } else if ( scheme === 'about' ) {
+ fakeHostname = uri.path + '.' + fakeHostname;
+ }
+
+ return 'http://' + fakeHostname + '/';
+};
+
+/******************************************************************************/
+/******************************************************************************
+
+To keep track from which context *exactly* network requests are made. This is
+often tricky for various reasons, and the challenge is not specific to one
+browser.
+
+The time at which a URL is assigned to a tab and the time when a network
+request for a root document is made must be assumed to be unrelated: it's all
+asynchronous. There is no guaranteed order in which the two events are fired.
+
+Also, other "anomalies" can occur:
+
+- a network request for a root document is fired without the corresponding
+tab being really assigned a new URL
+<https://github.com/chrisaljoudi/uBlock/issues/516>
+
+- a network request for a secondary resource is labeled with a tab id for
+which no root document was pulled for that tab.
+<https://github.com/chrisaljoudi/uBlock/issues/1001>
+
+- a network request for a secondary resource is made without the root
+document to which it belongs being formally bound yet to the proper tab id,
+causing a bad scope to be used for filtering purpose.
+<https://github.com/chrisaljoudi/uBlock/issues/1205>
+<https://github.com/chrisaljoudi/uBlock/issues/1140>
+
+So the solution here is to keep a lightweight data structure which only
+purpose is to keep track as accurately as possible of which root document
+belongs to which tab. That's the only purpose, and because of this, there are
+no restrictions for when the URL of a root document can be associated to a tab.
+
+Before, the PageStore object was trying to deal with this, but it had to
+enforce some restrictions so as to not descend into one of the above issues, or
+other issues. The PageStore object can only be associated with a tab for which
+a definitive navigation event occurred, because it collects information about
+what occurred in the tab (for example, the number of requests blocked for a
+page).
+
+The TabContext objects do not suffer this restriction, and as a result they
+offer the most reliable picture of which root document URL is really associated
+to which tab. Moreover, the TabObject can undo an association from a root
+document, and automatically re-associate with the next most recent. This takes
+care of <https://github.com/chrisaljoudi/uBlock/issues/516>.
+
+The PageStore object no longer cache the various information about which
+root document it is currently bound. When it needs to find out, it will always
+defer to the TabContext object, which will provide the real answer. This takes
+case of <https://github.com/chrisaljoudi/uBlock/issues/1205>. In effect, the
+master switch and dynamic filtering rules can be evaluated now properly even
+in the absence of a PageStore object, this was not the case before.
+
+Also, the TabContext object will try its best to find a good candidate root
+document URL for when none exists. This takes care of
+<https://github.com/chrisaljoudi/uBlock/issues/1001>.
+
+The TabContext manager is self-contained, and it takes care to properly
+housekeep itself.
+
+*/
+
+µm.tabContextManager = (function() {
+ var tabContexts = Object.create(null);
+
+ // https://github.com/chrisaljoudi/uBlock/issues/1001
+ // This is to be used as last-resort fallback in case a tab is found to not
+ // be bound while network requests are fired for the tab.
+ var mostRecentRootDocURL = '';
+ var mostRecentRootDocURLTimestamp = 0;
+
+ var gcPeriod = 31 * 60 * 1000; // every 31 minutes
+
+ // A pushed entry is removed from the stack unless it is committed with
+ // a set time.
+ var StackEntry = function(url, commit) {
+ this.url = url;
+ this.committed = commit;
+ this.tstamp = Date.now();
+ };
+
+ var TabContext = function(tabId) {
+ this.tabId = tabId;
+ this.stack = [];
+ this.rawURL =
+ this.normalURL =
+ this.scheme =
+ this.rootHostname =
+ this.rootDomain = '';
+ this.secure = false;
+ this.commitTimer = null;
+ this.gcTimer = null;
+
+ tabContexts[tabId] = this;
+ };
+
+ TabContext.prototype.destroy = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
+ return;
+ }
+ if ( this.gcTimer !== null ) {
+ clearTimeout(this.gcTimer);
+ this.gcTimer = null;
+ }
+ delete tabContexts[this.tabId];
+ };
+
+ TabContext.prototype.onTab = function(tab) {
+ if ( tab ) {
+ this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
+ } else {
+ this.destroy();
+ }
+ };
+
+ TabContext.prototype.onGC = function() {
+ this.gcTimer = null;
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
+ return;
+ }
+ vAPI.tabs.get(this.tabId, this.onTab.bind(this));
+ };
+
+ // https://github.com/gorhill/uBlock/issues/248
+ // Stack entries have to be committed to stick. Non-committed stack
+ // entries are removed after a set delay.
+ TabContext.prototype.onCommit = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
+ return;
+ }
+ this.commitTimer = null;
+ // Remove uncommitted entries at the top of the stack.
+ var i = this.stack.length;
+ while ( i-- ) {
+ if ( this.stack[i].committed ) {
+ break;
+ }
+ }
+ // https://github.com/gorhill/uBlock/issues/300
+ // If no committed entry was found, fall back on the bottom-most one
+ // as being the committed one by default.
+ if ( i === -1 && this.stack.length !== 0 ) {
+ this.stack[0].committed = true;
+ i = 0;
+ }
+ i += 1;
+ if ( i < this.stack.length ) {
+ this.stack.length = i;
+ this.update();
+ µm.bindTabToPageStats(this.tabId, 'newURL');
+ }
+ };
+
+ // This takes care of orphanized tab contexts. Can't be started for all
+ // contexts, as the behind-the-scene context is permanent -- so we do not
+ // want to flush it.
+ TabContext.prototype.autodestroy = function() {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) {
+ return;
+ }
+ this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod);
+ };
+
+ // Update just force all properties to be updated to match the most recent
+ // root URL.
+ TabContext.prototype.update = function() {
+ if ( this.stack.length === 0 ) {
+ this.rawURL = this.normalURL = this.scheme =
+ this.rootHostname = this.rootDomain = '';
+ this.secure = false;
+ return;
+ }
+ this.rawURL = this.stack[this.stack.length - 1].url;
+ this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL);
+ this.scheme = µm.URI.schemeFromURI(this.rawURL);
+ this.rootHostname = µm.URI.hostnameFromURI(this.normalURL);
+ this.rootDomain = µm.URI.domainFromHostname(this.rootHostname) || this.rootHostname;
+ this.secure = µm.URI.isSecureScheme(this.scheme);
+ };
+
+ // Called whenever a candidate root URL is spotted for the tab.
+ TabContext.prototype.push = function(url, context) {
+ if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { return; }
+ var committed = context !== undefined;
+ var count = this.stack.length;
+ var topEntry = this.stack[count - 1];
+ if ( topEntry && topEntry.url === url ) {
+ if ( committed ) {
+ topEntry.committed = true;
+ }
+ return;
+ }
+ if ( this.commitTimer !== null ) {
+ clearTimeout(this.commitTimer);
+ }
+ if ( committed ) {
+ this.stack = [new StackEntry(url, true)];
+ } else {
+ this.stack.push(new StackEntry(url));
+ this.commitTimer = vAPI.setTimeout(this.onCommit.bind(this), 1000);
+ }
+ this.update();
+ µm.bindTabToPageStats(this.tabId, context);
+ };
+
+ // These are to be used for the API of the tab context manager.
+
+ var push = function(tabId, url, context) {
+ var entry = tabContexts[tabId];
+ if ( entry === undefined ) {
+ entry = new TabContext(tabId);
+ entry.autodestroy();
+ }
+ entry.push(url, context);
+ mostRecentRootDocURL = url;
+ mostRecentRootDocURLTimestamp = Date.now();
+ return entry;
+ };
+
+ // Find a tab context for a specific tab. If none is found, attempt to
+ // fix this. When all fail, the behind-the-scene context is returned.
+ var mustLookup = function(tabId, url) {
+ var entry;
+ if ( url !== undefined ) {
+ entry = push(tabId, url);
+ } else {
+ entry = tabContexts[tabId];
+ }
+ if ( entry !== undefined ) {
+ return entry;
+ }
+ // https://github.com/chrisaljoudi/uBlock/issues/1025
+ // Google Hangout popup opens without a root frame. So for now we will
+ // just discard that best-guess root frame if it is too far in the
+ // future, at which point it ceases to be a "best guess".
+ if ( mostRecentRootDocURL !== '' && mostRecentRootDocURLTimestamp + 500 < Date.now() ) {
+ mostRecentRootDocURL = '';
+ }
+ // https://github.com/chrisaljoudi/uBlock/issues/1001
+ // Not a behind-the-scene request, yet no page store found for the
+ // tab id: we will thus bind the last-seen root document to the
+ // unbound tab. It's a guess, but better than ending up filtering
+ // nothing at all.
+ if ( mostRecentRootDocURL !== '' ) {
+ return push(tabId, mostRecentRootDocURL);
+ }
+ // If all else fail at finding a page store, re-categorize the
+ // request as behind-the-scene. At least this ensures that ultimately
+ // the user can still inspect/filter those net requests which were
+ // about to fall through the cracks.
+ // Example: Chromium + case #12 at
+ // http://raymondhill.net/ublock/popup.html
+ return tabContexts[vAPI.noTabId];
+ };
+
+ var lookup = function(tabId) {
+ return tabContexts[tabId] || null;
+ };
+
+ // Behind-the-scene tab context
+ (function() {
+ var entry = new TabContext(vAPI.noTabId);
+ entry.stack.push(new StackEntry('', true));
+ entry.rawURL = '';
+ entry.normalURL = µm.normalizePageURL(entry.tabId);
+ entry.rootHostname = µm.URI.hostnameFromURI(entry.normalURL);
+ entry.rootDomain = µm.URI.domainFromHostname(entry.rootHostname) || entry.rootHostname;
+ })();
+
+ // https://github.com/gorhill/uMatrix/issues/513
+ // Force a badge update here, it could happen that all the subsequent
+ // network requests are already in the page store, which would cause
+ // the badge to no be updated for these network requests.
+
+ vAPI.tabs.onNavigation = function(details) {
+ var tabId = details.tabId;
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
+ push(tabId, details.url, 'newURL');
+ µm.updateBadgeAsync(tabId);
+ };
+
+ // https://github.com/gorhill/uMatrix/issues/872
+ // `changeInfo.url` may not always be available (Firefox).
+
+ vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) {
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
+ if ( typeof tab.url !== 'string' || tab.url === '' ) { return; }
+ var url = changeInfo.url || tab.url;
+ if ( url ) {
+ push(tabId, url, 'updateURL');
+ }
+ };
+
+ vAPI.tabs.onClosed = function(tabId) {
+ µm.unbindTabFromPageStats(tabId);
+ var entry = tabContexts[tabId];
+ if ( entry instanceof TabContext ) {
+ entry.destroy();
+ }
+ };
+
+ return {
+ push: push,
+ lookup: lookup,
+ mustLookup: mustLookup
+ };
+})();
+
+vAPI.tabs.registerListeners();
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Create an entry for the tab if it doesn't exist
+
+µm.bindTabToPageStats = function(tabId, context) {
+ this.updateBadgeAsync(tabId);
+
+ // Do not create a page store for URLs which are of no interests
+ // Example: dev console
+ var tabContext = this.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) {
+ throw new Error('Unmanaged tab id: ' + tabId);
+ }
+
+ // rhill 2013-11-24: Never ever rebind behind-the-scene
+ // virtual tab.
+ // https://github.com/gorhill/httpswitchboard/issues/67
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return this.pageStores[tabId];
+ }
+
+ var normalURL = tabContext.normalURL;
+ var pageStore = this.pageStores[tabId] || null;
+
+ // The previous page URL, if any, associated with the tab
+ if ( pageStore !== null ) {
+ // No change, do not rebind
+ if ( pageStore.pageUrl === normalURL ) {
+ return pageStore;
+ }
+
+ // https://github.com/gorhill/uMatrix/issues/37
+ // Just rebind whenever possible: the URL changed, but the document
+ // maybe is the same.
+ // Example: Google Maps, Github
+ // https://github.com/gorhill/uMatrix/issues/72
+ // Need to double-check that the new scope is same as old scope
+ if ( context === 'updateURL' && pageStore.pageHostname === tabContext.rootHostname ) {
+ pageStore.rawURL = tabContext.rawURL;
+ pageStore.normalURL = normalURL;
+ this.updateTitle(tabId);
+ this.pageStoresToken = Date.now();
+ return pageStore;
+ }
+
+ // We won't be reusing this page store.
+ this.unbindTabFromPageStats(tabId);
+ }
+
+ // Try to resurrect first.
+ pageStore = this.resurrectPageStore(tabId, normalURL);
+ if ( pageStore === null ) {
+ pageStore = this.pageStoreFactory(tabContext);
+ }
+ this.pageStores[tabId] = pageStore;
+ this.updateTitle(tabId);
+ this.pageStoresToken = Date.now();
+
+ // console.debug('tab.js > bindTabToPageStats(): dispatching traffic in tab id %d to page store "%s"', tabId, pageUrl);
+
+ return pageStore;
+};
+
+/******************************************************************************/
+
+µm.unbindTabFromPageStats = function(tabId) {
+ // Never unbind behind-the-scene page store.
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return;
+ }
+
+ var pageStore = this.pageStores[tabId] || null;
+ if ( pageStore === null ) {
+ return;
+ }
+
+ delete this.pageStores[tabId];
+ this.pageStoresToken = Date.now();
+
+ if ( pageStore.incinerationTimer ) {
+ clearTimeout(pageStore.incinerationTimer);
+ pageStore.incinerationTimer = null;
+ }
+
+ if ( this.pageStoreCemetery.hasOwnProperty(tabId) === false ) {
+ this.pageStoreCemetery[tabId] = {};
+ }
+ var pageStoreCrypt = this.pageStoreCemetery[tabId];
+
+ var pageURL = pageStore.pageUrl;
+ pageStoreCrypt[pageURL] = pageStore;
+
+ pageStore.incinerationTimer = vAPI.setTimeout(
+ this.incineratePageStore.bind(this, tabId, pageURL),
+ 4 * 60 * 1000
+ );
+};
+
+/******************************************************************************/
+
+µm.resurrectPageStore = function(tabId, pageURL) {
+ if ( this.pageStoreCemetery.hasOwnProperty(tabId) === false ) {
+ return null;
+ }
+ var pageStoreCrypt = this.pageStoreCemetery[tabId];
+
+ if ( pageStoreCrypt.hasOwnProperty(pageURL) === false ) {
+ return null;
+ }
+
+ var pageStore = pageStoreCrypt[pageURL];
+
+ if ( pageStore.incinerationTimer !== null ) {
+ clearTimeout(pageStore.incinerationTimer);
+ pageStore.incinerationTimer = null;
+ }
+
+ delete pageStoreCrypt[pageURL];
+ if ( Object.keys(pageStoreCrypt).length === 0 ) {
+ delete this.pageStoreCemetery[tabId];
+ }
+
+ return pageStore;
+};
+
+/******************************************************************************/
+
+µm.incineratePageStore = function(tabId, pageURL) {
+ if ( this.pageStoreCemetery.hasOwnProperty(tabId) === false ) {
+ return;
+ }
+ var pageStoreCrypt = this.pageStoreCemetery[tabId];
+
+ if ( pageStoreCrypt.hasOwnProperty(pageURL) === false ) {
+ return;
+ }
+
+ var pageStore = pageStoreCrypt[pageURL];
+ if ( pageStore.incinerationTimer !== null ) {
+ clearTimeout(pageStore.incinerationTimer);
+ pageStore.incinerationTimer = null;
+ }
+
+ delete pageStoreCrypt[pageURL];
+ if ( Object.keys(pageStoreCrypt).length === 0 ) {
+ delete this.pageStoreCemetery[tabId];
+ }
+
+ pageStore.dispose();
+};
+
+/******************************************************************************/
+
+µm.pageStoreFromTabId = function(tabId) {
+ return this.pageStores[tabId] || null;
+};
+
+// Never return null
+µm.mustPageStoreFromTabId = function(tabId) {
+ return this.pageStores[tabId] || this.pageStores[vAPI.noTabId];
+};
+
+/******************************************************************************/
+
+µm.forceReload = function(tabId, bypassCache) {
+ vAPI.tabs.reload(tabId, bypassCache);
+};
+
+/******************************************************************************/
+
+// Update badge
+
+// rhill 2013-11-09: well this sucks, I can't update icon/badge
+// incrementally, as chromium overwrite the icon at some point without
+// notifying me, and this causes internal cached state to be out of sync.
+
+µm.updateBadgeAsync = (function() {
+ var tabIdToTimer = Object.create(null);
+
+ var updateBadge = function(tabId) {
+ delete tabIdToTimer[tabId];
+
+ var iconId = null;
+ var badgeStr = '';
+
+ var pageStore = this.pageStoreFromTabId(tabId);
+ if ( pageStore !== null ) {
+ var total = pageStore.perLoadAllowedRequestCount +
+ pageStore.perLoadBlockedRequestCount;
+ if ( total ) {
+ var squareSize = 19;
+ var greenSize = squareSize * Math.sqrt(pageStore.perLoadAllowedRequestCount / total);
+ iconId = greenSize < squareSize/2 ? Math.ceil(greenSize) : Math.floor(greenSize);
+ }
+ if ( this.userSettings.iconBadgeEnabled && pageStore.distinctRequestCount !== 0) {
+ badgeStr = this.formatCount(pageStore.distinctRequestCount);
+ }
+ }
+
+ vAPI.setIcon(tabId, iconId, badgeStr);
+ };
+
+ return function(tabId) {
+ if ( tabIdToTimer[tabId] ) {
+ return;
+ }
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return;
+ }
+ tabIdToTimer[tabId] = vAPI.setTimeout(updateBadge.bind(this, tabId), 750);
+ };
+})();
+
+/******************************************************************************/
+
+µm.updateTitle = (function() {
+ var tabIdToTimer = Object.create(null);
+ var tabIdToTryCount = Object.create(null);
+ var delay = 499;
+
+ var tryNoMore = function(tabId) {
+ delete tabIdToTryCount[tabId];
+ };
+
+ var tryAgain = function(tabId) {
+ var count = tabIdToTryCount[tabId];
+ if ( count === undefined ) {
+ return false;
+ }
+ if ( count === 1 ) {
+ delete tabIdToTryCount[tabId];
+ return false;
+ }
+ tabIdToTryCount[tabId] = count - 1;
+ tabIdToTimer[tabId] = vAPI.setTimeout(updateTitle.bind(µm, tabId), delay);
+ return true;
+ };
+
+ var onTabReady = function(tabId, tab) {
+ if ( !tab ) {
+ return tryNoMore(tabId);
+ }
+ var pageStore = this.pageStoreFromTabId(tabId);
+ if ( pageStore === null ) {
+ return tryNoMore(tabId);
+ }
+ if ( !tab.title && tryAgain(tabId) ) {
+ return;
+ }
+ // https://github.com/gorhill/uMatrix/issues/225
+ // Sometimes title changes while page is loading.
+ var settled = tab.title && tab.title === pageStore.title;
+ pageStore.title = tab.title || tab.url || '';
+ this.pageStoresToken = Date.now();
+ if ( settled || !tryAgain(tabId) ) {
+ tryNoMore(tabId);
+ }
+ };
+
+ var updateTitle = function(tabId) {
+ delete tabIdToTimer[tabId];
+ vAPI.tabs.get(tabId, onTabReady.bind(this, tabId));
+ };
+
+ return function(tabId) {
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ return;
+ }
+ if ( tabIdToTimer[tabId] ) {
+ clearTimeout(tabIdToTimer[tabId]);
+ }
+ tabIdToTimer[tabId] = vAPI.setTimeout(updateTitle.bind(this, tabId), delay);
+ tabIdToTryCount[tabId] = 5;
+ };
+})();
+
+/******************************************************************************/
+
+// Stale page store entries janitor
+// https://github.com/chrisaljoudi/uBlock/issues/455
+
+(function() {
+ var cleanupPeriod = 7 * 60 * 1000;
+ var cleanupSampleAt = 0;
+ var cleanupSampleSize = 11;
+
+ var cleanup = function() {
+ var vapiTabs = vAPI.tabs;
+ var tabIds = Object.keys(µm.pageStores).sort();
+ var checkTab = function(tabId) {
+ vapiTabs.get(tabId, function(tab) {
+ if ( !tab ) {
+ µm.unbindTabFromPageStats(tabId);
+ }
+ });
+ };
+ if ( cleanupSampleAt >= tabIds.length ) {
+ cleanupSampleAt = 0;
+ }
+ var tabId;
+ var n = Math.min(cleanupSampleAt + cleanupSampleSize, tabIds.length);
+ for ( var i = cleanupSampleAt; i < n; i++ ) {
+ tabId = tabIds[i];
+ if ( vAPI.isBehindTheSceneTabId(tabId) ) {
+ continue;
+ }
+ checkTab(tabId);
+ }
+ cleanupSampleAt = n;
+
+ vAPI.setTimeout(cleanup, cleanupPeriod);
+ };
+
+ vAPI.setTimeout(cleanup, cleanupPeriod);
+})();
+
+/******************************************************************************/
+
+})();
diff --git a/js/traffic.js b/js/traffic.js
new file mode 100644
index 0000000..3e6a6ac
--- /dev/null
+++ b/js/traffic.js
@@ -0,0 +1,444 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+// Start isolation from global scope
+
+µMatrix.webRequest = (function() {
+
+/******************************************************************************/
+
+// Intercept and filter web requests according to white and black lists.
+
+var onBeforeRootFrameRequestHandler = function(details) {
+ var µm = µMatrix;
+ var requestURL = details.url;
+ var requestHostname = µm.URI.hostnameFromURI(requestURL);
+ var tabId = details.tabId;
+
+ µm.tabContextManager.push(tabId, requestURL);
+
+ var tabContext = µm.tabContextManager.mustLookup(tabId);
+ var rootHostname = tabContext.rootHostname;
+
+ // Disallow request as per matrix?
+ var block = µm.mustBlock(rootHostname, requestHostname, 'doc');
+
+ var pageStore = µm.pageStoreFromTabId(tabId);
+ pageStore.recordRequest('doc', requestURL, block);
+ µm.logger.writeOne(tabId, 'net', rootHostname, requestURL, 'doc', block);
+
+ // Not blocked
+ if ( !block ) {
+ // rhill 2013-11-07: Senseless to do this for behind-the-scene requests.
+ µm.cookieHunter.recordPageCookies(pageStore);
+ return;
+ }
+
+ // Blocked
+ var query = btoa(JSON.stringify({
+ url: requestURL,
+ hn: requestHostname,
+ why: '?'
+ }));
+
+ vAPI.tabs.replace(tabId, vAPI.getURL('main-blocked.html?details=') + query);
+
+ return { cancel: true };
+};
+
+/******************************************************************************/
+
+// Intercept and filter web requests according to white and black lists.
+
+var onBeforeRequestHandler = function(details) {
+ var µm = µMatrix,
+ µmuri = µm.URI,
+ requestURL = details.url,
+ requestScheme = µmuri.schemeFromURI(requestURL);
+
+ if ( µmuri.isNetworkScheme(requestScheme) === false ) { return; }
+
+ var requestType = requestTypeNormalizer[details.type] || 'other';
+
+ // https://github.com/gorhill/httpswitchboard/issues/303
+ // Wherever the main doc comes from, create a receiver page URL: synthetize
+ // one if needed.
+ if ( requestType === 'doc' && details.parentFrameId === -1 ) {
+ return onBeforeRootFrameRequestHandler(details);
+ }
+
+ // Re-classify orphan HTTP requests as behind-the-scene requests. There is
+ // not much else which can be done, because there are URLs
+ // which cannot be handled by µMatrix, i.e. `opera://startpage`,
+ // as this would lead to complications with no obvious solution, like how
+ // to scope on unknown scheme? Etc.
+ // https://github.com/gorhill/httpswitchboard/issues/191
+ // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
+ var tabContext = µm.tabContextManager.mustLookup(details.tabId),
+ tabId = tabContext.tabId,
+ rootHostname = tabContext.rootHostname,
+ specificity = 0;
+
+ // Filter through matrix
+ var block = µm.tMatrix.mustBlock(
+ rootHostname,
+ µmuri.hostnameFromURI(requestURL),
+ requestType
+ );
+ if ( block ) {
+ specificity = µm.tMatrix.specificityRegister;
+ }
+
+ // Record request.
+ // https://github.com/gorhill/httpswitchboard/issues/342
+ // The way requests are handled now, it may happen at this point some
+ // processing has already been performed, and that a synthetic URL has
+ // been constructed for logging purpose. Use this synthetic URL if
+ // it is available.
+ var pageStore = µm.mustPageStoreFromTabId(tabId);
+
+ // Enforce strict secure connection?
+ if ( tabContext.secure && µmuri.isSecureScheme(requestScheme) === false ) {
+ pageStore.hasMixedContent = true;
+ if ( block === false ) {
+ block = µm.tMatrix.evaluateSwitchZ('https-strict', rootHostname);
+ }
+ }
+
+ pageStore.recordRequest(requestType, requestURL, block);
+ µm.logger.writeOne(tabId, 'net', rootHostname, requestURL, details.type, block);
+
+ if ( block ) {
+ pageStore.cacheBlockedCollapsible(requestType, requestURL, specificity);
+ return { 'cancel': true };
+ }
+};
+
+/******************************************************************************/
+
+// Sanitize outgoing headers as per user settings.
+
+var onBeforeSendHeadersHandler = function(details) {
+ var µm = µMatrix,
+ µmuri = µm.URI,
+ requestURL = details.url,
+ requestScheme = µmuri.schemeFromURI(requestURL);
+
+ // Ignore non-network schemes
+ if ( µmuri.isNetworkScheme(requestScheme) === false ) { return; }
+
+ // Re-classify orphan HTTP requests as behind-the-scene requests. There is
+ // not much else which can be done, because there are URLs
+ // which cannot be handled by HTTP Switchboard, i.e. `opera://startpage`,
+ // as this would lead to complications with no obvious solution, like how
+ // to scope on unknown scheme? Etc.
+ // https://github.com/gorhill/httpswitchboard/issues/191
+ // https://github.com/gorhill/httpswitchboard/issues/91#issuecomment-37180275
+ var tabId = details.tabId,
+ pageStore = µm.mustPageStoreFromTabId(tabId),
+ requestType = requestTypeNormalizer[details.type] || 'other',
+ requestHeaders = details.requestHeaders,
+ headerIndex, headerValue;
+
+ // https://github.com/gorhill/httpswitchboard/issues/342
+ // Is this hyperlink auditing?
+ // If yes, create a synthetic URL for reporting hyperlink auditing
+ // in request log. This way the user is better informed of what went
+ // on.
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing
+ //
+ // Target URL = the href of the link
+ // Doc URL = URL of the document containing the target URL
+ // Ping URLs = servers which will be told that user clicked target URL
+ //
+ // `Content-Type` = `text/ping` (always present)
+ // `Ping-To` = target URL (always present)
+ // `Ping-From` = doc URL
+ // `Referer` = doc URL
+ // request URL = URL which will receive the information
+ //
+ // With hyperlink-auditing, removing header(s) is pointless, the whole
+ // request must be cancelled.
+
+ headerIndex = headerIndexFromName('ping-to', requestHeaders);
+ if ( headerIndex !== -1 ) {
+ headerValue = requestHeaders[headerIndex].value;
+ if ( headerValue !== '' ) {
+ var block = µm.userSettings.processHyperlinkAuditing;
+ pageStore.recordRequest('other', requestURL + '{Ping-To:' + headerValue + '}', block);
+ µm.logger.writeOne(tabId, 'net', '', requestURL, 'ping', block);
+ if ( block ) {
+ µm.hyperlinkAuditingFoiledCounter += 1;
+ return { 'cancel': true };
+ }
+ }
+ }
+
+ // If we reach this point, request is not blocked, so what is left to do
+ // is to sanitize headers.
+
+ var rootHostname = pageStore.pageHostname,
+ requestHostname = µmuri.hostnameFromURI(requestURL),
+ modified = false;
+
+ // Process `Cookie` header.
+
+ headerIndex = headerIndexFromName('cookie', requestHeaders);
+ if (
+ headerIndex !== -1 &&
+ µm.mustBlock(rootHostname, requestHostname, 'cookie')
+ ) {
+ modified = true;
+ headerValue = requestHeaders[headerIndex].value;
+ requestHeaders.splice(headerIndex, 1);
+ µm.cookieHeaderFoiledCounter++;
+ if ( requestType === 'doc' ) {
+ µm.logger.writeOne(tabId, 'net', '', headerValue, 'COOKIE', true);
+ }
+ }
+
+ // Process `Referer` header.
+
+ // https://github.com/gorhill/httpswitchboard/issues/222#issuecomment-44828402
+
+ // https://github.com/gorhill/uMatrix/issues/320
+ // http://tools.ietf.org/html/rfc6454#section-7.3
+ // "The user agent MAY include an Origin header field in any HTTP
+ // "request.
+ // "The user agent MUST NOT include more than one Origin header field in
+ // "any HTTP request.
+ // "Whenever a user agent issues an HTTP request from a "privacy-
+ // "sensitive" context, the user agent MUST send the value "null" in the
+ // "Origin header field."
+
+ // https://github.com/gorhill/uMatrix/issues/358
+ // Do not spoof `Origin` header for the time being.
+
+ // https://github.com/gorhill/uMatrix/issues/773
+ // For non-GET requests, remove `Referer` header instead of spoofing it.
+
+ headerIndex = headerIndexFromName('referer', requestHeaders);
+ if ( headerIndex !== -1 ) {
+ headerValue = requestHeaders[headerIndex].value;
+ if ( headerValue !== '' ) {
+ var toDomain = µmuri.domainFromHostname(requestHostname);
+ if ( toDomain !== '' && toDomain !== µmuri.domainFromURI(headerValue) ) {
+ pageStore.has3pReferrer = true;
+ if ( µm.tMatrix.evaluateSwitchZ('referrer-spoof', rootHostname) ) {
+ modified = true;
+ var newValue;
+ if ( details.method === 'GET' ) {
+ newValue = requestHeaders[headerIndex].value =
+ requestScheme + '://' + requestHostname + '/';
+ } else {
+ requestHeaders.splice(headerIndex, 1);
+ }
+ µm.refererHeaderFoiledCounter++;
+ if ( requestType === 'doc' ) {
+ µm.logger.writeOne(tabId, 'net', '', headerValue, 'REFERER', true);
+ if ( newValue !== undefined ) {
+ µm.logger.writeOne(tabId, 'net', '', newValue, 'REFERER', false);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( modified ) {
+ return { requestHeaders: requestHeaders };
+ }
+};
+
+/******************************************************************************/
+
+// To prevent inline javascript from being executed.
+
+// Prevent inline scripting using `Content-Security-Policy`:
+// https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html
+
+// This fixes:
+// https://github.com/gorhill/httpswitchboard/issues/35
+
+var onHeadersReceived = function(details) {
+ // Ignore schemes other than 'http...'
+ var µm = µMatrix,
+ tabId = details.tabId,
+ requestURL = details.url,
+ requestType = requestTypeNormalizer[details.type] || 'other';
+
+ // https://github.com/gorhill/uMatrix/issues/145
+ // Check if the main_frame is a download
+ if ( requestType === 'doc' ) {
+ µm.tabContextManager.push(tabId, requestURL);
+ }
+
+ var tabContext = µm.tabContextManager.lookup(tabId);
+ if ( tabContext === null ) { return; }
+
+ var csp = [],
+ cspReport = [],
+ rootHostname = tabContext.rootHostname,
+ requestHostname = µm.URI.hostnameFromURI(requestURL);
+
+ // Inline script tags.
+ if ( µm.mustAllow(rootHostname, requestHostname, 'script' ) !== true ) {
+ csp.push(µm.cspNoInlineScript);
+ }
+
+ // Inline style tags.
+ if ( µm.mustAllow(rootHostname, requestHostname, 'css' ) !== true ) {
+ csp.push(µm.cspNoInlineStyle);
+ }
+
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1302667
+ var cspNoWorker = µm.cspNoWorker;
+ if ( cspNoWorker === undefined ) {
+ cspNoWorker = cspNoWorkerInit();
+ }
+
+ if ( µm.tMatrix.evaluateSwitchZ('no-workers', rootHostname) ) {
+ csp.push(cspNoWorker);
+ } else if ( µm.rawSettings.disableCSPReportInjection === false ) {
+ cspReport.push(cspNoWorker);
+ }
+
+ var headers = details.responseHeaders,
+ cspDirectives, i;
+
+ if ( csp.length !== 0 ) {
+ cspDirectives = csp.join(',');
+ i = headerIndexFromName('content-security-policy', headers);
+ if ( i !== -1 ) {
+ headers[i].value += ',' + cspDirectives;
+ } else {
+ headers.push({
+ name: 'Content-Security-Policy',
+ value: cspDirectives
+ });
+ }
+ if ( requestType === 'doc' ) {
+ µm.logger.writeOne(tabId, 'net', '', cspDirectives, 'CSP', false);
+ }
+ }
+
+ if ( cspReport.length !== 0 ) {
+ cspDirectives = cspReport.join(',');
+ i = headerIndexFromName('content-security-policy-report-only', headers);
+ if ( i !== -1 ) {
+ headers[i].value += ',' + cspDirectives;
+ } else {
+ headers.push({
+ name: 'Content-Security-Policy-Report-Only',
+ value: cspDirectives
+ });
+ }
+ }
+
+ return { responseHeaders: headers };
+};
+
+/******************************************************************************/
+
+var cspNoWorkerInit = function() {
+ if ( vAPI.webextFlavor === undefined ) {
+ return "child-src 'none'; frame-src data: blob: *; report-uri about:blank";
+ }
+ µMatrix.cspNoWorker = /^Mozilla-Firefox-5[67]/.test(vAPI.webextFlavor) ?
+ "child-src 'none'; frame-src data: blob: *; report-uri about:blank" :
+ "worker-src 'none'; report-uri about:blank" ;
+ return µMatrix.cspNoWorker;
+};
+
+/******************************************************************************/
+
+// Caller must ensure headerName is normalized to lower case.
+
+var headerIndexFromName = function(headerName, headers) {
+ var i = headers.length;
+ while ( i-- ) {
+ if ( headers[i].name.toLowerCase() === headerName ) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+/******************************************************************************/
+
+var requestTypeNormalizer = {
+ 'font' : 'css',
+ 'image' : 'image',
+ 'imageset' : 'image',
+ 'main_frame' : 'doc',
+ 'media' : 'media',
+ 'object' : 'media',
+ 'other' : 'other',
+ 'script' : 'script',
+ 'stylesheet' : 'css',
+ 'sub_frame' : 'frame',
+ 'websocket' : 'xhr',
+ 'xmlhttprequest': 'xhr'
+};
+
+/******************************************************************************/
+
+vAPI.net.onBeforeRequest = {
+ extra: [ 'blocking' ],
+ callback: onBeforeRequestHandler
+};
+
+vAPI.net.onBeforeSendHeaders = {
+ extra: [ 'blocking', 'requestHeaders' ],
+ callback: onBeforeSendHeadersHandler
+};
+
+vAPI.net.onHeadersReceived = {
+ urls: [ 'http://*/*', 'https://*/*' ],
+ types: [ 'main_frame', 'sub_frame' ],
+ extra: [ 'blocking', 'responseHeaders' ],
+ callback: onHeadersReceived
+};
+
+/******************************************************************************/
+
+var start = function() {
+ vAPI.net.registerListeners();
+};
+
+/******************************************************************************/
+
+return {
+ start: start
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
diff --git a/js/udom.js b/js/udom.js
new file mode 100644
index 0000000..19848aa
--- /dev/null
+++ b/js/udom.js
@@ -0,0 +1,729 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uBlock
+*/
+
+/* global DOMTokenList */
+/* exported uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+// It's just a silly, minimalist DOM framework: this allows me to not rely
+// on jQuery. jQuery contains way too much stuff than I need, and as per
+// Opera rules, I am not allowed to use a cut-down version of jQuery. So
+// the code here does *only* what I need, and nothing more, and with a lot
+// of assumption on passed parameters, etc. I grow it on a per-need-basis only.
+
+var uDom = (function() {
+
+/******************************************************************************/
+
+var DOMList = function() {
+ this.nodes = [];
+};
+
+/******************************************************************************/
+
+Object.defineProperty(
+ DOMList.prototype,
+ 'length',
+ {
+ get: function() {
+ return this.nodes.length;
+ }
+ }
+);
+
+/******************************************************************************/
+
+var DOMListFactory = function(selector, context) {
+ var r = new DOMList();
+ if ( typeof selector === 'string' ) {
+ selector = selector.trim();
+ if ( selector !== '' ) {
+ return addSelectorToList(r, selector, context);
+ }
+ }
+ if ( selector instanceof Node ) {
+ return addNodeToList(r, selector);
+ }
+ if ( selector instanceof NodeList ) {
+ return addNodeListToList(r, selector);
+ }
+ if ( selector instanceof DOMList ) {
+ return addListToList(r, selector);
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMListFactory.onLoad = function(callback) {
+ window.addEventListener('load', callback);
+};
+
+/******************************************************************************/
+
+DOMListFactory.nodeFromId = function(id) {
+ return document.getElementById(id);
+};
+
+DOMListFactory.nodeFromSelector = function(selector) {
+ return document.querySelector(selector);
+};
+
+/******************************************************************************/
+
+var addNodeToList = function(list, node) {
+ if ( node ) {
+ list.nodes.push(node);
+ }
+ return list;
+};
+
+/******************************************************************************/
+
+var addNodeListToList = function(list, nodelist) {
+ if ( nodelist ) {
+ var n = nodelist.length;
+ for ( var i = 0; i < n; i++ ) {
+ list.nodes.push(nodelist[i]);
+ }
+ }
+ return list;
+};
+
+/******************************************************************************/
+
+var addListToList = function(list, other) {
+ list.nodes = list.nodes.concat(other.nodes);
+ return list;
+};
+
+/******************************************************************************/
+
+var addSelectorToList = function(list, selector, context) {
+ var p = context || document;
+ var r = p.querySelectorAll(selector);
+ var n = r.length;
+ for ( var i = 0; i < n; i++ ) {
+ list.nodes.push(r[i]);
+ }
+ return list;
+};
+
+/******************************************************************************/
+
+var nodeInNodeList = function(node, nodeList) {
+ var i = nodeList.length;
+ while ( i-- ) {
+ if ( nodeList[i] === node ) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/******************************************************************************/
+
+var doesMatchSelector = function(node, selector) {
+ if ( !node ) {
+ return false;
+ }
+ if ( node.nodeType !== 1 ) {
+ return false;
+ }
+ if ( selector === undefined ) {
+ return true;
+ }
+ var parentNode = node.parentNode;
+ if ( !parentNode || !parentNode.setAttribute ) {
+ return false;
+ }
+ var doesMatch = false;
+ parentNode.setAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO', '');
+ var grandpaNode = parentNode.parentNode || document;
+ var nl = grandpaNode.querySelectorAll('[uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO] > ' + selector);
+ var i = nl.length;
+ while ( doesMatch === false && i-- ) {
+ doesMatch = nl[i] === node;
+ }
+ parentNode.removeAttribute('uDom-32kXc6xEZA7o73AMB8vLbLct1RZOkeoO');
+ return doesMatch;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.nodeAt = function(i) {
+ return this.nodes[i] || null;
+};
+
+DOMList.prototype.at = function(i) {
+ return addNodeToList(new DOMList(), this.nodes[i]);
+};
+
+/******************************************************************************/
+
+DOMList.prototype.toArray = function() {
+ return this.nodes.slice();
+};
+
+/******************************************************************************/
+
+DOMList.prototype.pop = function() {
+ return addNodeToList(new DOMList(), this.nodes.pop());
+};
+
+/******************************************************************************/
+
+DOMList.prototype.forEach = function(fn) {
+ var n = this.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ fn(this.at(i), i);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.subset = function(i, l) {
+ var r = new DOMList();
+ var n = l !== undefined ? l : this.nodes.length;
+ var j = Math.min(i + n, this.nodes.length);
+ if ( i < j ) {
+ r.nodes = this.nodes.slice(i, j);
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.first = function() {
+ return this.subset(0, 1);
+};
+
+/******************************************************************************/
+
+DOMList.prototype.next = function(selector) {
+ var r = new DOMList();
+ var n = this.nodes.length;
+ var node;
+ for ( var i = 0; i < n; i++ ) {
+ node = this.nodes[i];
+ while ( node.nextSibling !== null ) {
+ node = node.nextSibling;
+ if ( node.nodeType !== 1 ) {
+ continue;
+ }
+ if ( doesMatchSelector(node, selector) === false ) {
+ continue;
+ }
+ addNodeToList(r, node);
+ break;
+ }
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.parent = function() {
+ var r = new DOMList();
+ if ( this.nodes.length ) {
+ addNodeToList(r, this.nodes[0].parentNode);
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.filter = function(filter) {
+ var r = new DOMList();
+ var filterFunc;
+ if ( typeof filter === 'string' ) {
+ filterFunc = function() {
+ return doesMatchSelector(this, filter);
+ };
+ } else if ( typeof filter === 'function' ) {
+ filterFunc = filter;
+ } else {
+ filterFunc = function(){
+ return true;
+ };
+ }
+ var n = this.nodes.length;
+ var node;
+ for ( var i = 0; i < n; i++ ) {
+ node = this.nodes[i];
+ if ( filterFunc.apply(node) ) {
+ addNodeToList(r, node);
+ }
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+// TODO: Avoid possible duplicates
+
+DOMList.prototype.ancestors = function(selector) {
+ var r = new DOMList();
+ var n = this.nodes.length;
+ var node;
+ for ( var i = 0; i < n; i++ ) {
+ node = this.nodes[i].parentNode;
+ while ( node ) {
+ if ( doesMatchSelector(node, selector) ) {
+ addNodeToList(r, node);
+ }
+ node = node.parentNode;
+ }
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.descendants = function(selector) {
+ var r = new DOMList();
+ var n = this.nodes.length;
+ var nl;
+ for ( var i = 0; i < n; i++ ) {
+ nl = this.nodes[i].querySelectorAll(selector);
+ addNodeListToList(r, nl);
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.contents = function() {
+ var r = new DOMList();
+ var cnodes, cn, ci;
+ var n = this.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ cnodes = this.nodes[i].childNodes;
+ cn = cnodes.length;
+ for ( ci = 0; ci < cn; ci++ ) {
+ addNodeToList(r, cnodes.item(ci));
+ }
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.remove = function() {
+ var cn, p;
+ var i = this.nodes.length;
+ while ( i-- ) {
+ cn = this.nodes[i];
+ if ( (p = cn.parentNode) ) {
+ p.removeChild(cn);
+ }
+ }
+ return this;
+};
+
+DOMList.prototype.detach = DOMList.prototype.remove;
+
+/******************************************************************************/
+
+DOMList.prototype.empty = function() {
+ var node;
+ var i = this.nodes.length;
+ while ( i-- ) {
+ node = this.nodes[i];
+ while ( node.firstChild ) {
+ node.removeChild(node.firstChild);
+ }
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.append = function(selector, context) {
+ var p = this.nodes[0];
+ if ( p ) {
+ var c = DOMListFactory(selector, context);
+ var n = c.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ p.appendChild(c.nodes[i]);
+ }
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.prepend = function(selector, context) {
+ var p = this.nodes[0];
+ if ( p ) {
+ var c = DOMListFactory(selector, context);
+ var i = c.nodes.length;
+ while ( i-- ) {
+ p.insertBefore(c.nodes[i], p.firstChild);
+ }
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.appendTo = function(selector, context) {
+ var p = selector instanceof DOMListFactory ? selector : DOMListFactory(selector, context);
+ var n = p.length;
+ for ( var i = 0; i < n; i++ ) {
+ p.nodes[0].appendChild(this.nodes[i]);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.insertAfter = function(selector, context) {
+ if ( this.nodes.length === 0 ) {
+ return this;
+ }
+ var p = this.nodes[0].parentNode;
+ if ( !p ) {
+ return this;
+ }
+ var c = DOMListFactory(selector, context);
+ var n = c.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ p.appendChild(c.nodes[i]);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.insertBefore = function(selector, context) {
+ if ( this.nodes.length === 0 ) {
+ return this;
+ }
+ var referenceNodes = DOMListFactory(selector, context);
+ if ( referenceNodes.nodes.length === 0 ) {
+ return this;
+ }
+ var referenceNode = referenceNodes.nodes[0];
+ var parentNode = referenceNode.parentNode;
+ if ( !parentNode ) {
+ return this;
+ }
+ var n = this.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ parentNode.insertBefore(this.nodes[i], referenceNode);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.clone = function(notDeep) {
+ var r = new DOMList();
+ var n = this.nodes.length;
+ for ( var i = 0; i < n; i++ ) {
+ addNodeToList(r, this.nodes[i].cloneNode(!notDeep));
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.nthOfType = function() {
+ if ( this.nodes.length === 0 ) {
+ return 0;
+ }
+ var node = this.nodes[0];
+ var tagName = node.tagName;
+ var i = 1;
+ while ( node.previousElementSibling !== null ) {
+ node = node.previousElementSibling;
+ if ( typeof node.tagName !== 'string' ) {
+ continue;
+ }
+ if ( node.tagName !== tagName ) {
+ continue;
+ }
+ i++;
+ }
+ return i;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.attr = function(attr, value) {
+ var i = this.nodes.length;
+ if ( value === undefined && typeof attr !== 'object' ) {
+ return i ? this.nodes[0].getAttribute(attr) : undefined;
+ }
+ if ( typeof attr === 'object' ) {
+ var attrNames = Object.keys(attr);
+ var node, j, attrName;
+ while ( i-- ) {
+ node = this.nodes[i];
+ j = attrNames.length;
+ while ( j-- ) {
+ attrName = attrNames[j];
+ node.setAttribute(attrName, attr[attrName]);
+ }
+ }
+ } else {
+ while ( i-- ) {
+ this.nodes[i].setAttribute(attr, value);
+ }
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.prop = function(prop, value) {
+ var i = this.nodes.length;
+ if ( value === undefined ) {
+ return i !== 0 ? this.nodes[0][prop] : undefined;
+ }
+ while ( i-- ) {
+ this.nodes[i][prop] = value;
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.css = function(prop, value) {
+ var i = this.nodes.length;
+ if ( value === undefined ) {
+ return i ? this.nodes[0].style[prop] : undefined;
+ }
+ if ( value !== '' ) {
+ while ( i-- ) {
+ this.nodes[i].style.setProperty(prop, value);
+ }
+ return this;
+ }
+ while ( i-- ) {
+ this.nodes[i].style.removeProperty(prop);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.val = function(value) {
+ return this.prop('value', value);
+};
+
+/******************************************************************************/
+
+DOMList.prototype.html = function(html) {
+ var i = this.nodes.length;
+ if ( html === undefined ) {
+ return i ? this.nodes[0].innerHTML : '';
+ }
+ while ( i-- ) {
+ vAPI.insertHTML(this.nodes[i], html);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.text = function(text) {
+ var i = this.nodes.length;
+ if ( text === undefined ) {
+ return i ? this.nodes[0].textContent : '';
+ }
+ while ( i-- ) {
+ this.nodes[i].textContent = text;
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+var toggleClass = function(node, className, targetState) {
+ var tokenList = node.classList;
+ if ( tokenList instanceof DOMTokenList === false ) {
+ return;
+ }
+ var currentState = tokenList.contains(className);
+ var newState = targetState;
+ if ( newState === undefined ) {
+ newState = !currentState;
+ }
+ if ( newState === currentState ) {
+ return;
+ }
+ tokenList.toggle(className, newState);
+};
+
+/******************************************************************************/
+
+DOMList.prototype.hasClass = function(className) {
+ if ( !this.nodes.length ) {
+ return false;
+ }
+ var tokenList = this.nodes[0].classList;
+ return tokenList instanceof DOMTokenList &&
+ tokenList.contains(className);
+};
+DOMList.prototype.hasClassName = DOMList.prototype.hasClass;
+
+DOMList.prototype.addClass = function(className) {
+ return this.toggleClass(className, true);
+};
+
+DOMList.prototype.removeClass = function(className) {
+ if ( className !== undefined ) {
+ return this.toggleClass(className, false);
+ }
+ var i = this.nodes.length;
+ while ( i-- ) {
+ this.nodes[i].className = '';
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.toggleClass = function(className, targetState) {
+ if ( className.indexOf(' ') !== -1 ) {
+ return this.toggleClasses(className, targetState);
+ }
+ var i = this.nodes.length;
+ while ( i-- ) {
+ toggleClass(this.nodes[i], className, targetState);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.toggleClasses = function(classNames, targetState) {
+ var tokens = classNames.split(/\s+/);
+ var i = this.nodes.length;
+ var node, j;
+ while ( i-- ) {
+ node = this.nodes[i];
+ j = tokens.length;
+ while ( j-- ) {
+ toggleClass(node, tokens[j], targetState);
+ }
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+var listenerEntries = [];
+
+var ListenerEntry = function(target, type, capture, callback) {
+ this.target = target;
+ this.type = type;
+ this.capture = capture;
+ this.callback = callback;
+ target.addEventListener(type, callback, capture);
+};
+
+ListenerEntry.prototype.dispose = function() {
+ this.target.removeEventListener(this.type, this.callback, this.capture);
+ this.target = null;
+ this.callback = null;
+};
+
+/******************************************************************************/
+
+var makeEventHandler = function(selector, callback) {
+ return function(event) {
+ var dispatcher = event.currentTarget;
+ if ( !dispatcher || typeof dispatcher.querySelectorAll !== 'function' ) {
+ return;
+ }
+ var receiver = event.target;
+ if ( nodeInNodeList(receiver, dispatcher.querySelectorAll(selector)) ) {
+ callback.call(receiver, event);
+ }
+ };
+};
+
+DOMList.prototype.on = function(etype, selector, callback) {
+ if ( typeof selector === 'function' ) {
+ callback = selector;
+ selector = undefined;
+ } else {
+ callback = makeEventHandler(selector, callback);
+ }
+
+ var i = this.nodes.length;
+ while ( i-- ) {
+ listenerEntries.push(new ListenerEntry(this.nodes[i], etype, selector !== undefined, callback));
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+// TODO: Won't work for delegated handlers. Need to figure
+// what needs to be done.
+
+DOMList.prototype.off = function(evtype, callback) {
+ var i = this.nodes.length;
+ while ( i-- ) {
+ this.nodes[i].removeEventListener(evtype, callback);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+DOMList.prototype.trigger = function(etype) {
+ var ev = new CustomEvent(etype);
+ var i = this.nodes.length;
+ while ( i-- ) {
+ this.nodes[i].dispatchEvent(ev);
+ }
+ return this;
+};
+
+/******************************************************************************/
+
+// Cleanup
+
+var onBeforeUnload = function() {
+ var entry;
+ while ( (entry = listenerEntries.pop()) ) {
+ entry.dispose();
+ }
+ window.removeEventListener('beforeunload', onBeforeUnload);
+};
+
+window.addEventListener('beforeunload', onBeforeUnload);
+
+/******************************************************************************/
+
+return DOMListFactory;
+
+})();
diff --git a/js/uritools.js b/js/uritools.js
new file mode 100644
index 0000000..2e50fd2
--- /dev/null
+++ b/js/uritools.js
@@ -0,0 +1,537 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global publicSuffixList, punycode */
+
+'use strict';
+
+/*******************************************************************************
+
+RFC 3986 as reference: http://tools.ietf.org/html/rfc3986#appendix-A
+
+Naming convention from https://en.wikipedia.org/wiki/URI_scheme#Examples
+
+*/
+
+/******************************************************************************/
+
+µMatrix.URI = (function() {
+
+/******************************************************************************/
+
+// Favorite regex tool: http://regex101.com/
+
+// Ref: <http://tools.ietf.org/html/rfc3986#page-50>
+// I removed redundant capture groups: capture less = peform faster. See
+// <http://jsperf.com/old-uritools-vs-new-uritools>
+// Performance improvements welcomed.
+// jsperf: <http://jsperf.com/old-uritools-vs-new-uritools>
+var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
+
+// Derived
+var reSchemeFromURI = /^[^:\/?#]+:/;
+var reAuthorityFromURI = /^(?:[^:\/?#]+:)?(\/\/[^\/?#]+)/;
+var reOriginFromURI = /^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]+)/;
+var reCommonHostnameFromURL = /^https?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//;
+var rePathFromURI = /^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]*)?([^?#]*)/;
+var reMustNormalizeHostname = /[^0-9a-z._-]/;
+
+// These are to parse authority field, not parsed by above official regex
+// IPv6 is seen as an exception: a non-compatible IPv6 is first tried, and
+// if it fails, the IPv6 compatible regex istr used. This helps
+// peformance by avoiding the use of a too complicated regex first.
+
+// https://github.com/gorhill/httpswitchboard/issues/211
+// "While a hostname may not contain other characters, such as the
+// "underscore character (_), other DNS names may contain the underscore"
+var reHostPortFromAuthority = /^(?:[^@]*@)?([^:]*)(:\d*)?$/;
+var reIPv6PortFromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]*\])(:\d*)?$/i;
+
+var reHostFromNakedAuthority = /^[0-9a-z._-]+[0-9a-z]$/i;
+var reHostFromAuthority = /^(?:[^@]*@)?([^:]+)(?::\d*)?$/;
+var reIPv6FromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]+\])(?::\d*)?$/i;
+
+// Coarse (but fast) tests
+var reValidHostname = /^([a-z\d]+(-*[a-z\d]+)*)(\.[a-z\d]+(-*[a-z\d])*)*$/;
+var reIPAddressNaive = /^\d+\.\d+\.\d+\.\d+$|^\[[\da-zA-Z:]+\]$/;
+
+// Accurate tests
+// Source.: http://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp/5284410#5284410
+//var reIPv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)(\.|$)){4}/;
+
+// Source: http://forums.intermapper.com/viewtopic.php?p=1096#1096
+//var reIPv6 = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
+
+/******************************************************************************/
+
+var reset = function(o) {
+ o.scheme = '';
+ o.hostname = '';
+ o._ipv4 = undefined;
+ o._ipv6 = undefined;
+ o.port = '';
+ o.path = '';
+ o.query = '';
+ o.fragment = '';
+ return o;
+};
+
+var resetAuthority = function(o) {
+ o.hostname = '';
+ o._ipv4 = undefined;
+ o._ipv6 = undefined;
+ o.port = '';
+ return o;
+};
+
+/******************************************************************************/
+
+// This will be exported
+
+var URI = {
+ scheme: '',
+ authority: '',
+ hostname: '',
+ _ipv4: undefined,
+ _ipv6: undefined,
+ port: '',
+ domain: undefined,
+ path: '',
+ query: '',
+ fragment: '',
+ schemeBit: (1 << 0),
+ userBit: (1 << 1),
+ passwordBit: (1 << 2),
+ hostnameBit: (1 << 3),
+ portBit: (1 << 4),
+ pathBit: (1 << 5),
+ queryBit: (1 << 6),
+ fragmentBit: (1 << 7),
+ allBits: (0xFFFF)
+};
+
+URI.authorityBit = (URI.userBit | URI.passwordBit | URI.hostnameBit | URI.portBit);
+URI.normalizeBits = (URI.schemeBit | URI.hostnameBit | URI.pathBit | URI.queryBit);
+
+/******************************************************************************/
+
+// See: https://en.wikipedia.org/wiki/URI_scheme#Examples
+// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+//
+// foo://example.com:8042/over/there?name=ferret#nose
+// \_/ \______________/\_________/ \_________/ \__/
+// | | | | |
+// scheme authority path query fragment
+// | _____________________|__
+// / \ / \
+// urn:example:animal:ferret:nose
+
+URI.set = function(uri) {
+ if ( uri === undefined ) {
+ return reset(URI);
+ }
+ var matches = reRFC3986.exec(uri);
+ if ( !matches ) {
+ return reset(URI);
+ }
+ this.scheme = matches[1] !== undefined ? matches[1].slice(0, -1) : '';
+ this.authority = matches[2] !== undefined ? matches[2].slice(2).toLowerCase() : '';
+ this.path = matches[3] !== undefined ? matches[3] : '';
+
+ // <http://tools.ietf.org/html/rfc3986#section-6.2.3>
+ // "In general, a URI that uses the generic syntax for authority
+ // "with an empty path should be normalized to a path of '/'."
+ if ( this.authority !== '' && this.path === '' ) {
+ this.path = '/';
+ }
+ this.query = matches[4] !== undefined ? matches[4].slice(1) : '';
+ this.fragment = matches[5] !== undefined ? matches[5].slice(1) : '';
+
+ // Assume very simple authority, i.e. just a hostname (highest likelihood
+ // case for µMatrix)
+ if ( reHostFromNakedAuthority.test(this.authority) ) {
+ this.hostname = this.authority;
+ this.port = '';
+ return this;
+ }
+ // Authority contains more than just a hostname
+ matches = reHostPortFromAuthority.exec(this.authority);
+ if ( !matches ) {
+ matches = reIPv6PortFromAuthority.exec(this.authority);
+ if ( !matches ) {
+ return resetAuthority(URI);
+ }
+ }
+ this.hostname = matches[1] !== undefined ? matches[1] : '';
+ // http://en.wikipedia.org/wiki/FQDN
+ if ( this.hostname.slice(-1) === '.' ) {
+ this.hostname = this.hostname.slice(0, -1);
+ }
+ this.port = matches[2] !== undefined ? matches[2].slice(1) : '';
+ return this;
+};
+
+/******************************************************************************/
+
+// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+//
+// foo://example.com:8042/over/there?name=ferret#nose
+// \_/ \______________/\_________/ \_________/ \__/
+// | | | | |
+// scheme authority path query fragment
+// | _____________________|__
+// / \ / \
+// urn:example:animal:ferret:nose
+
+URI.assemble = function(bits) {
+ if ( bits === undefined ) {
+ bits = this.allBits;
+ }
+ var s = [];
+ if ( this.scheme && (bits & this.schemeBit) ) {
+ s.push(this.scheme, ':');
+ }
+ if ( this.hostname && (bits & this.hostnameBit) ) {
+ s.push('//', this.hostname);
+ }
+ if ( this.port && (bits & this.portBit) ) {
+ s.push(':', this.port);
+ }
+ if ( this.path && (bits & this.pathBit) ) {
+ s.push(this.path);
+ }
+ if ( this.query && (bits & this.queryBit) ) {
+ s.push('?', this.query);
+ }
+ if ( this.fragment && (bits & this.fragmentBit) ) {
+ s.push('#', this.fragment);
+ }
+ return s.join('');
+};
+
+/******************************************************************************/
+
+URI.originFromURI = function(uri) {
+ var matches = reOriginFromURI.exec(uri);
+ return matches !== null ? matches[0].toLowerCase() : '';
+};
+
+/******************************************************************************/
+
+URI.schemeFromURI = function(uri) {
+ var matches = reSchemeFromURI.exec(uri);
+ if ( matches === null ) {
+ return '';
+ }
+ return matches[0].slice(0, -1).toLowerCase();
+};
+
+/******************************************************************************/
+
+URI.isNetworkScheme = function(scheme) {
+ return this.reNetworkScheme.test(scheme);
+};
+
+URI.reNetworkScheme = /^(?:https?|wss?|ftps?)\b/;
+
+/******************************************************************************/
+
+URI.isSecureScheme = function(scheme) {
+ return this.reSecureScheme.test(scheme);
+};
+
+URI.reSecureScheme = /^(?:https|wss|ftps)\b/;
+
+/******************************************************************************/
+
+URI.authorityFromURI = function(uri) {
+ var matches = reAuthorityFromURI.exec(uri);
+ if ( !matches ) {
+ return '';
+ }
+ return matches[1].slice(2).toLowerCase();
+};
+
+/******************************************************************************/
+
+// The most used function, so it better be fast.
+
+// https://github.com/gorhill/uBlock/issues/1559
+// See http://en.wikipedia.org/wiki/FQDN
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1360285
+// Revisit punycode dependency when above issue is fixed in Firefox.
+
+URI.hostnameFromURI = function(uri) {
+ var matches = reCommonHostnameFromURL.exec(uri);
+ if ( matches !== null ) { return matches[1]; }
+ matches = reAuthorityFromURI.exec(uri);
+ if ( matches === null ) { return ''; }
+ var authority = matches[1].slice(2);
+ // Assume very simple authority (most common case for µBlock)
+ if ( reHostFromNakedAuthority.test(authority) ) {
+ return authority.toLowerCase();
+ }
+ matches = reHostFromAuthority.exec(authority);
+ if ( matches === null ) {
+ matches = reIPv6FromAuthority.exec(authority);
+ if ( matches === null ) { return ''; }
+ }
+ var hostname = matches[1];
+ while ( hostname.endsWith('.') ) {
+ hostname = hostname.slice(0, -1);
+ }
+ if ( reMustNormalizeHostname.test(hostname) ) {
+ hostname = punycode.toASCII(hostname.toLowerCase());
+ }
+ return hostname;
+};
+
+/******************************************************************************/
+
+URI.domainFromHostname = function(hostname) {
+ // Try to skip looking up the PSL database
+ var entry = domainCache.get(hostname);
+ if ( entry !== undefined ) {
+ entry.tstamp = Date.now();
+ return entry.domain;
+ }
+ // Meh.. will have to search it
+ if ( reIPAddressNaive.test(hostname) === false ) {
+ return domainCacheAdd(hostname, psl.getDomain(hostname));
+ }
+ return domainCacheAdd(hostname, hostname);
+};
+
+URI.domain = function() {
+ return this.domainFromHostname(this.hostname);
+};
+
+// It is expected that there is higher-scoped `publicSuffixList` lingering
+// somewhere. Cache it. See <https://github.com/gorhill/publicsuffixlist.js>.
+var psl = publicSuffixList;
+
+/******************************************************************************/
+
+URI.pathFromURI = function(uri) {
+ var matches = rePathFromURI.exec(uri);
+ return matches !== null ? matches[1] : '';
+};
+
+/******************************************************************************/
+
+ // Trying to alleviate the worries of looking up too often the domain name from
+// a hostname. With a cache, uBlock benefits given that it deals with a
+// specific set of hostnames within a narrow time span -- in other words, I
+// believe probability of cache hit are high in uBlock.
+
+var domainCache = new Map();
+var domainCacheCountLowWaterMark = 75;
+var domainCacheCountHighWaterMark = 100;
+var domainCacheEntryJunkyard = [];
+var domainCacheEntryJunkyardMax = domainCacheCountHighWaterMark - domainCacheCountLowWaterMark;
+
+var DomainCacheEntry = function(domain) {
+ this.init(domain);
+};
+
+DomainCacheEntry.prototype.init = function(domain) {
+ this.domain = domain;
+ this.tstamp = Date.now();
+ return this;
+};
+
+DomainCacheEntry.prototype.dispose = function() {
+ this.domain = '';
+ if ( domainCacheEntryJunkyard.length < domainCacheEntryJunkyardMax ) {
+ domainCacheEntryJunkyard.push(this);
+ }
+};
+
+var domainCacheEntryFactory = function(domain) {
+ var entry = domainCacheEntryJunkyard.pop();
+ if ( entry ) {
+ return entry.init(domain);
+ }
+ return new DomainCacheEntry(domain);
+};
+
+var domainCacheAdd = function(hostname, domain) {
+ var entry = domainCache.get(hostname);
+ if ( entry !== undefined ) {
+ entry.tstamp = Date.now();
+ } else {
+ domainCache.set(hostname, domainCacheEntryFactory(domain));
+ if ( domainCache.size === domainCacheCountHighWaterMark ) {
+ domainCachePrune();
+ }
+ }
+ return domain;
+};
+
+var domainCacheEntrySort = function(a, b) {
+ return domainCache.get(b).tstamp - domainCache.get(a).tstamp;
+};
+
+var domainCachePrune = function() {
+ var hostnames = Array.from(domainCache.keys())
+ .sort(domainCacheEntrySort)
+ .slice(domainCacheCountLowWaterMark);
+ var i = hostnames.length;
+ var hostname;
+ while ( i-- ) {
+ hostname = hostnames[i];
+ domainCache.get(hostname).dispose();
+ domainCache.delete(hostname);
+ }
+};
+
+var domainCacheReset = function() {
+ domainCache.clear();
+};
+
+psl.onChanged.addListener(domainCacheReset);
+
+/******************************************************************************/
+
+URI.domainFromURI = function(uri) {
+ if ( !uri ) {
+ return '';
+ }
+ return this.domainFromHostname(this.hostnameFromURI(uri));
+};
+
+/******************************************************************************/
+
+// Normalize the way µMatrix expects it
+
+URI.normalizedURI = function() {
+ // Will be removed:
+ // - port
+ // - user id/password
+ // - fragment
+ return this.assemble(this.normalizeBits);
+};
+
+/******************************************************************************/
+
+URI.rootURL = function() {
+ if ( !this.hostname ) {
+ return '';
+ }
+ return this.assemble(this.schemeBit | this.hostnameBit);
+};
+
+/******************************************************************************/
+
+URI.isValidHostname = function(hostname) {
+ var r;
+ try {
+ r = reValidHostname.test(hostname);
+ }
+ catch (e) {
+ return false;
+ }
+ return r;
+};
+
+/******************************************************************************/
+
+// Return the parent domain. For IP address, there is no parent domain.
+
+URI.parentHostnameFromHostname = function(hostname) {
+ // `locahost` => ``
+ // `example.org` => `example.org`
+ // `www.example.org` => `example.org`
+ // `tomato.www.example.org` => `example.org`
+ var domain = this.domainFromHostname(hostname);
+
+ // `locahost` === `` => bye
+ // `example.org` === `example.org` => bye
+ // `www.example.org` !== `example.org` => stay
+ // `tomato.www.example.org` !== `example.org` => stay
+ if ( domain === '' || domain === hostname ) {
+ return undefined;
+ }
+
+ // Parent is hostname minus first label
+ return hostname.slice(hostname.indexOf('.') + 1);
+};
+
+/******************************************************************************/
+
+// Return all possible parent hostnames which can be derived from `hostname`,
+// ordered from direct parent up to domain inclusively.
+
+URI.parentHostnamesFromHostname = function(hostname) {
+ // TODO: I should create an object which is optimized to receive
+ // the list of hostnames by making it reusable (junkyard etc.) and which
+ // has its own element counter property in order to avoid memory
+ // alloc/dealloc.
+ var domain = this.domainFromHostname(hostname);
+ if ( domain === '' || domain === hostname ) {
+ return [];
+ }
+ var nodes = [];
+ var pos;
+ for (;;) {
+ pos = hostname.indexOf('.');
+ if ( pos < 0 ) {
+ break;
+ }
+ hostname = hostname.slice(pos + 1);
+ nodes.push(hostname);
+ if ( hostname === domain ) {
+ break;
+ }
+ }
+ return nodes;
+};
+
+/******************************************************************************/
+
+// Return all possible hostnames which can be derived from `hostname`,
+// ordered from self up to domain inclusively.
+
+URI.allHostnamesFromHostname = function(hostname) {
+ var nodes = this.parentHostnamesFromHostname(hostname);
+ nodes.unshift(hostname);
+ return nodes;
+};
+
+/******************************************************************************/
+
+URI.toString = function() {
+ return this.assemble();
+};
+
+/******************************************************************************/
+
+// Export
+
+return URI;
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
diff --git a/js/user-rules.js b/js/user-rules.js
new file mode 100644
index 0000000..c9c48be
--- /dev/null
+++ b/js/user-rules.js
@@ -0,0 +1,341 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global uDom */
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+// Switches before, rules after
+
+var directiveSort = function(a, b) {
+ var aIsSwitch = a.indexOf(':') !== -1;
+ var bIsSwitch = b.indexOf(':') !== -1;
+ if ( aIsSwitch === bIsSwitch ) {
+ return a.localeCompare(b);
+ }
+ return aIsSwitch ? -1 : 1;
+};
+
+/******************************************************************************/
+
+var processUserRules = function(response) {
+ var rules, rule, i;
+ var allRules = {};
+ var permanentRules = {};
+ var temporaryRules = {};
+ var onLeft, onRight;
+
+ rules = response.permanentRules.split(/\n+/);
+ i = rules.length;
+ while ( i-- ) {
+ rule = rules[i].trim();
+ if ( rule.length !== 0 ) {
+ permanentRules[rule] = allRules[rule] = true;
+ }
+ }
+ rules = response.temporaryRules.split(/\n+/);
+ i = rules.length;
+ while ( i-- ) {
+ rule = rules[i].trim();
+ if ( rule.length !== 0 ) {
+ temporaryRules[rule] = allRules[rule] = true;
+ }
+ }
+
+ var permanentList = document.createDocumentFragment(),
+ temporaryList = document.createDocumentFragment(),
+ li;
+
+ rules = Object.keys(allRules).sort(directiveSort);
+ for ( i = 0; i < rules.length; i++ ) {
+ rule = rules[i];
+ onLeft = permanentRules.hasOwnProperty(rule);
+ onRight = temporaryRules.hasOwnProperty(rule);
+ if ( onLeft && onRight ) {
+ li = document.createElement('li');
+ li.textContent = rule;
+ permanentList.appendChild(li);
+ li = document.createElement('li');
+ li.textContent = rule;
+ temporaryList.appendChild(li);
+ } else if ( onLeft ) {
+ li = document.createElement('li');
+ li.textContent = rule;
+ permanentList.appendChild(li);
+ li = document.createElement('li');
+ li.textContent = rule;
+ li.className = 'notRight toRemove';
+ temporaryList.appendChild(li);
+ } else if ( onRight ) {
+ li = document.createElement('li');
+ li.textContent = '\xA0';
+ permanentList.appendChild(li);
+ li = document.createElement('li');
+ li.textContent = rule;
+ li.className = 'notLeft';
+ temporaryList.appendChild(li);
+ }
+ }
+
+ // TODO: build incrementally.
+
+ uDom('#diff > .left > ul > li').remove();
+ document.querySelector('#diff > .left > ul').appendChild(permanentList);
+ uDom('#diff > .right > ul > li').remove();
+ document.querySelector('#diff > .right > ul').appendChild(temporaryList);
+ uDom('#diff').toggleClass('dirty', response.temporaryRules !== response.permanentRules);
+};
+
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/757
+// Support RequestPolicy rule syntax
+
+var fromRequestPolicy = function(content) {
+ var matches = /\[origins-to-destinations\]([^\[]+)/.exec(content);
+ if ( matches === null || matches.length !== 2 ) {
+ return;
+ }
+ return matches[1].trim()
+ .replace(/\|/g, ' ')
+ .replace(/\n/g, ' * allow\n');
+};
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uMatrix/issues/270
+
+var fromNoScript = function(content) {
+ var noscript = null;
+ try {
+ noscript = JSON.parse(content);
+ } catch (e) {
+ }
+ if (
+ noscript === null ||
+ typeof noscript !== 'object' ||
+ typeof noscript.prefs !== 'object' ||
+ typeof noscript.prefs.clearClick === 'undefined' ||
+ typeof noscript.whitelist !== 'string' ||
+ typeof noscript.V !== 'string'
+ ) {
+ return;
+ }
+ var out = new Set();
+ var reBad = /[a-z]+:\w*$/;
+ var reURL = /[a-z]+:\/\/([0-9a-z.-]+)/;
+ var directives = noscript.whitelist.split(/\s+/);
+ var i = directives.length;
+ var directive, matches;
+ while ( i-- ) {
+ directive = directives[i].trim();
+ if ( directive === '' ) {
+ continue;
+ }
+ if ( reBad.test(directive) ) {
+ continue;
+ }
+ matches = reURL.exec(directive);
+ if ( matches !== null ) {
+ directive = matches[1];
+ }
+ out.add('* ' + directive + ' * allow');
+ out.add('* ' + directive + ' script allow');
+ out.add('* ' + directive + ' frame allow');
+ }
+ return Array.from(out).join('\n');
+};
+
+/******************************************************************************/
+
+var handleImportFilePicker = function() {
+ var fileReaderOnLoadHandler = function() {
+ if ( typeof this.result !== 'string' || this.result === '' ) {
+ return;
+ }
+ var result = fromRequestPolicy(this.result);
+ if ( result === undefined ) {
+ result = fromNoScript(this.result);
+ if ( result === undefined ) {
+ result = this.result;
+ }
+ }
+ if ( this.result === '' ) { return; }
+ var request = {
+ 'what': 'setUserRules',
+ 'temporaryRules': rulesFromHTML('#diff .right li') + '\n' + result
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+ };
+ var file = this.files[0];
+ if ( file === undefined || file.name === '' ) {
+ return;
+ }
+ if ( file.type.indexOf('text') !== 0 && file.type !== 'application/json') {
+ return;
+ }
+ var fr = new FileReader();
+ fr.onload = fileReaderOnLoadHandler;
+ fr.readAsText(file);
+};
+
+/******************************************************************************/
+
+var startImportFilePicker = function() {
+ var input = document.getElementById('importFilePicker');
+ // Reset to empty string, this will ensure an change event is properly
+ // triggered if the user pick a file, even if it is the same as the last
+ // one picked.
+ input.value = '';
+ input.click();
+};
+
+/******************************************************************************/
+
+function exportUserRulesToFile() {
+ vAPI.download({
+ 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li') + '\n'),
+ 'filename': uDom('[data-i18n="userRulesDefaultFileName"]').text()
+ });
+}
+
+/******************************************************************************/
+
+var rulesFromHTML = function(selector) {
+ var rules = [];
+ var lis = uDom(selector);
+ var li;
+ for ( var i = 0; i < lis.length; i++ ) {
+ li = lis.at(i);
+ if ( li.hasClassName('toRemove') ) {
+ rules.push('');
+ } else {
+ rules.push(li.text());
+ }
+ }
+ return rules.join('\n');
+};
+
+/******************************************************************************/
+
+var revertHandler = function() {
+ var request = {
+ 'what': 'setUserRules',
+ 'temporaryRules': rulesFromHTML('#diff .left li')
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+};
+
+/******************************************************************************/
+
+var commitHandler = function() {
+ var request = {
+ 'what': 'setUserRules',
+ 'permanentRules': rulesFromHTML('#diff .right li')
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+};
+
+/******************************************************************************/
+
+var editStartHandler = function() {
+ uDom('#diff .right textarea').val(rulesFromHTML('#diff .right li'));
+ var parent = uDom(this).ancestors('#diff');
+ parent.toggleClass('edit', true);
+};
+
+/******************************************************************************/
+
+var editStopHandler = function() {
+ var parent = uDom(this).ancestors('#diff');
+ parent.toggleClass('edit', false);
+ var request = {
+ 'what': 'setUserRules',
+ 'temporaryRules': uDom('#diff .right textarea').val()
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+};
+
+/******************************************************************************/
+
+var editCancelHandler = function() {
+ var parent = uDom(this).ancestors('#diff');
+ parent.toggleClass('edit', false);
+};
+
+/******************************************************************************/
+
+var temporaryRulesToggler = function() {
+ var li = uDom(this);
+ li.toggleClass('toRemove');
+ var request = {
+ 'what': 'setUserRules',
+ 'temporaryRules': rulesFromHTML('#diff .right li')
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+};
+
+/******************************************************************************/
+
+self.cloud.onPush = function() {
+ return rulesFromHTML('#diff .left li');
+};
+
+self.cloud.onPull = function(data, append) {
+ if ( typeof data !== 'string' ) { return; }
+ if ( append ) {
+ data = rulesFromHTML('#diff .right li') + '\n' + data;
+ }
+ var request = {
+ 'what': 'setUserRules',
+ 'temporaryRules': data
+ };
+ vAPI.messaging.send('user-rules.js', request, processUserRules);
+};
+
+/******************************************************************************/
+
+uDom.onLoad(function() {
+ // Handle user interaction
+ uDom('#importButton').on('click', startImportFilePicker);
+ uDom('#importFilePicker').on('change', handleImportFilePicker);
+ uDom('#exportButton').on('click', exportUserRulesToFile);
+ uDom('#revertButton').on('click', revertHandler);
+ uDom('#commitButton').on('click', commitHandler);
+ uDom('#editEnterButton').on('click', editStartHandler);
+ uDom('#editStopButton').on('click', editStopHandler);
+ uDom('#editCancelButton').on('click', editCancelHandler);
+ uDom('#diff > .right > ul').on('click', 'li', temporaryRulesToggler);
+
+ vAPI.messaging.send('user-rules.js', { what: 'getUserRules' }, processUserRules);
+});
+
+/******************************************************************************/
+
+})();
+
diff --git a/js/usersettings.js b/js/usersettings.js
new file mode 100644
index 0000000..b5c5b51
--- /dev/null
+++ b/js/usersettings.js
@@ -0,0 +1,59 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.changeUserSettings = function(name, value) {
+ if ( typeof name !== 'string' || name === '' ) {
+ return;
+ }
+
+ // Do not allow an unknown user setting to be created
+ if ( this.userSettings[name] === undefined ) {
+ return;
+ }
+
+ if ( value === undefined ) {
+ return this.userSettings[name];
+ }
+
+ // Pre-change
+ switch ( name ) {
+
+ default:
+ break;
+ }
+
+ // Change
+ this.userSettings[name] = value;
+
+ // Post-change
+ switch ( name ) {
+
+ default:
+ break;
+ }
+
+ this.saveUserSettings();
+};
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 0000000..3b1d170
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,106 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+µMatrix.gotoURL = function(details) {
+ vAPI.tabs.open(details);
+};
+
+/******************************************************************************/
+
+µMatrix.gotoExtensionURL = function(details) {
+ if ( details.url.startsWith('logger-ui.html') ) {
+ if ( details.shiftKey ) {
+ this.changeUserSettings(
+ 'alwaysDetachLogger',
+ !this.userSettings.alwaysDetachLogger
+ );
+ }
+ details.popup = this.userSettings.alwaysDetachLogger;
+ }
+ details.select = true;
+ vAPI.tabs.open(details);
+};
+
+/******************************************************************************/
+
+µMatrix.LineIterator = function(text, offset) {
+ this.text = text;
+ this.textLen = this.text.length;
+ this.offset = offset || 0;
+};
+
+µMatrix.LineIterator.prototype = {
+ next: function() {
+ var lineEnd = this.text.indexOf('\n', this.offset);
+ if ( lineEnd === -1 ) {
+ lineEnd = this.text.indexOf('\r', this.offset);
+ if ( lineEnd === -1 ) {
+ lineEnd = this.textLen;
+ }
+ }
+ var line = this.text.slice(this.offset, lineEnd);
+ this.offset = lineEnd + 1;
+ return line;
+ },
+ rewind: function() {
+ if ( this.offset <= 1 ) {
+ this.offset = 0;
+ return;
+ }
+ var lineEnd = this.text.lastIndexOf('\n', this.offset - 2);
+ if ( lineEnd !== -1 ) {
+ this.offset = lineEnd + 1;
+ } else {
+ lineEnd = this.text.lastIndexOf('\r', this.offset - 2);
+ this.offset = lineEnd !== -1 ? lineEnd + 1 : 0;
+ }
+ },
+ eot: function() {
+ return this.offset >= this.textLen;
+ }
+};
+
+/******************************************************************************/
+
+µMatrix.setToArray = typeof Array.from === 'function' ?
+ Array.from :
+ function(dict) {
+ var out = [],
+ entries = dict.values(),
+ entry;
+ for (;;) {
+ entry = entries.next();
+ if ( entry.done ) { break; }
+ out.push(entry.value);
+ }
+ return out;
+ };
+
+µMatrix.setFromArray = function(arr) {
+ return new Set(arr);
+};
+
+/******************************************************************************/
diff --git a/js/vapi-background.js b/js/vapi-background.js
new file mode 100644
index 0000000..b4f1eac
--- /dev/null
+++ b/js/vapi-background.js
@@ -0,0 +1,3466 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* jshint bitwise: false, esnext: true */
+/* global self, Components, punycode */
+
+// For background page
+
+'use strict';
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+// Useful links
+//
+// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface
+// https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Services.jsm
+
+/******************************************************************************/
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const {Services} = Cu.import('resource://gre/modules/Services.jsm', null);
+
+/******************************************************************************/
+
+var vAPI = self.vAPI = self.vAPI || {};
+vAPI.firefox = true;
+vAPI.modernFirefox = Services.appinfo.ID === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}' &&
+ Services.vc.compare(Services.appinfo.platformVersion, '44') > 0;
+
+/******************************************************************************/
+
+var deferUntil = function(testFn, mainFn, details) {
+ if ( typeof details !== 'object' ) {
+ details = {};
+ }
+
+ var now = 0;
+ var next = details.next || 200;
+ var until = details.until || 2000;
+
+ var check = function() {
+ if ( testFn() === true || now >= until ) {
+ mainFn();
+ return;
+ }
+ now += next;
+ vAPI.setTimeout(check, next);
+ };
+
+ if ( 'sync' in details && details.sync === true ) {
+ check();
+ } else {
+ vAPI.setTimeout(check, 1);
+ }
+};
+
+/******************************************************************************/
+
+vAPI.app = {
+ name: 'uMatrix',
+ version: location.hash.slice(1)
+};
+
+/******************************************************************************/
+
+vAPI.app.start = function() {
+};
+
+/******************************************************************************/
+
+vAPI.app.stop = function() {
+};
+
+/******************************************************************************/
+
+vAPI.app.restart = function() {
+ // Listening in bootstrap.js
+ Cc['@mozilla.org/childprocessmessagemanager;1']
+ .getService(Ci.nsIMessageSender)
+ .sendAsyncMessage(location.host + '-restart');
+};
+
+/******************************************************************************/
+
+// List of things that needs to be destroyed when disabling the extension
+// Only functions should be added to it
+
+var cleanupTasks = [];
+
+// This must be updated manually, every time a new task is added/removed
+
+// Fixed by github.com/AlexVallat:
+// https://github.com/AlexVallat/uBlock/commit/7b781248f00cbe3d61b1cc367c440db80fa06049
+// 7 instances of cleanupTasks.push, but one is unique to fennec, and one to desktop.
+var expectedNumberOfCleanups = 7;
+
+window.addEventListener('unload', function() {
+ if ( typeof vAPI.app.onShutdown === 'function' ) {
+ vAPI.app.onShutdown();
+ }
+
+ // IMPORTANT: cleanup tasks must be executed using LIFO order.
+ var i = cleanupTasks.length;
+ while ( i-- ) {
+ cleanupTasks[i]();
+ }
+
+ if ( cleanupTasks.length < expectedNumberOfCleanups ) {
+ console.error(
+ 'uMatrix> Cleanup tasks performed: %s (out of %s)',
+ cleanupTasks.length,
+ expectedNumberOfCleanups
+ );
+ }
+
+ // frameModule needs to be cleared too
+ var frameModuleURL = vAPI.getURL('frameModule.js');
+ var frameModule = {};
+ Cu.import(frameModuleURL, frameModule);
+ frameModule.contentObserver.unregister();
+ Cu.unload(frameModuleURL);
+});
+
+/******************************************************************************/
+
+// For now, only booleans.
+
+vAPI.browserSettings = {
+ originalValues: {},
+
+ rememberOriginalValue: function(path, setting) {
+ var key = path + '.' + setting;
+ if ( this.originalValues.hasOwnProperty(key) ) {
+ return;
+ }
+ var hasUserValue;
+ var branch = Services.prefs.getBranch(path + '.');
+ try {
+ hasUserValue = branch.prefHasUserValue(setting);
+ } catch (ex) {
+ }
+ if ( hasUserValue !== undefined ) {
+ this.originalValues[key] = hasUserValue ? this.getValue(path, setting) : undefined;
+ }
+ },
+
+ clear: function(path, setting) {
+ var key = path + '.' + setting;
+
+ // Value was not overriden -- nothing to restore
+ if ( this.originalValues.hasOwnProperty(key) === false ) {
+ return;
+ }
+
+ var value = this.originalValues[key];
+ // https://github.com/gorhill/uBlock/issues/292#issuecomment-109621979
+ // Forget the value immediately, it may change outside of
+ // uBlock control.
+ delete this.originalValues[key];
+
+ // Original value was a default one
+ if ( value === undefined ) {
+ try {
+ Services.prefs.getBranch(path + '.').clearUserPref(setting);
+ } catch (ex) {
+ }
+ return;
+ }
+
+ // Reset to original value
+ this.setValue(path, setting, value);
+ },
+
+ getValue: function(path, setting) {
+ var branch = Services.prefs.getBranch(path + '.');
+ var getMethod;
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch#getPrefType%28%29
+ switch ( branch.getPrefType(setting) ) {
+ case 64: // PREF_INT
+ getMethod = 'getIntPref';
+ break;
+ case 128: // PREF_BOOL
+ getMethod = 'getBoolPref';
+ break;
+ default: // not supported
+ return;
+ }
+
+ try {
+ return branch[getMethod](setting);
+ } catch (ex) {
+ }
+ },
+
+ setValue: function(path, setting, value) {
+ var setMethod;
+ switch ( typeof value ) {
+ case 'number':
+ setMethod = 'setIntPref';
+ break;
+ case 'boolean':
+ setMethod = 'setBoolPref';
+ break;
+ default: // not supported
+ return;
+ }
+
+ try {
+ Services.prefs.getBranch(path + '.')[setMethod](setting, value);
+ } catch (ex) {
+ }
+ },
+
+ setSetting: function(setting, value) {
+ var prefName, prefVal;
+ switch ( setting ) {
+ case 'prefetching':
+ this.rememberOriginalValue('network', 'prefetch-next');
+ // http://betanews.com/2015/08/15/firefox-stealthily-loads-webpages-when-you-hover-over-links-heres-how-to-stop-it/
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=814169
+ // Sigh.
+ this.rememberOriginalValue('network.http', 'speculative-parallel-limit');
+ // https://github.com/gorhill/uBlock/issues/292
+ // "true" means "do not disable", i.e. leave entry alone
+ if ( value ) {
+ this.clear('network', 'prefetch-next');
+ this.clear('network.http', 'speculative-parallel-limit');
+ } else {
+ this.setValue('network', 'prefetch-next', false);
+ this.setValue('network.http', 'speculative-parallel-limit', 0);
+ }
+ break;
+
+ case 'hyperlinkAuditing':
+ this.rememberOriginalValue('browser', 'send_pings');
+ this.rememberOriginalValue('beacon', 'enabled');
+ // https://github.com/gorhill/uBlock/issues/292
+ // "true" means "do not disable", i.e. leave entry alone
+ if ( value ) {
+ this.clear('browser', 'send_pings');
+ this.clear('beacon', 'enabled');
+ } else {
+ this.setValue('browser', 'send_pings', false);
+ this.setValue('beacon', 'enabled', false);
+ }
+ break;
+
+ // https://github.com/gorhill/uBlock/issues/894
+ // Do not disable completely WebRTC if it can be avoided. FF42+
+ // has a `media.peerconnection.ice.default_address_only` pref which
+ // purpose is to prevent local IP address leakage.
+ case 'webrtcIPAddress':
+ if ( this.getValue('media.peerconnection', 'ice.default_address_only') !== undefined ) {
+ prefName = 'ice.default_address_only';
+ prefVal = true;
+ } else {
+ prefName = 'enabled';
+ prefVal = false;
+ }
+
+ this.rememberOriginalValue('media.peerconnection', prefName);
+ if ( value ) {
+ this.clear('media.peerconnection', prefName);
+ } else {
+ this.setValue('media.peerconnection', prefName, prefVal);
+ }
+ break;
+
+ default:
+ break;
+ }
+ },
+
+ set: function(details) {
+ for ( var setting in details ) {
+ if ( details.hasOwnProperty(setting) === false ) {
+ continue;
+ }
+ this.setSetting(setting, !!details[setting]);
+ }
+ },
+
+ restoreAll: function() {
+ var pos;
+ for ( var key in this.originalValues ) {
+ if ( this.originalValues.hasOwnProperty(key) === false ) {
+ continue;
+ }
+ pos = key.lastIndexOf('.');
+ this.clear(key.slice(0, pos), key.slice(pos + 1));
+ }
+ }
+};
+
+cleanupTasks.push(vAPI.browserSettings.restoreAll.bind(vAPI.browserSettings));
+
+/******************************************************************************/
+
+// API matches that of chrome.storage.local:
+// https://developer.chrome.com/extensions/storage
+
+vAPI.storage = (function() {
+ var db = null;
+ var vacuumTimer = null;
+
+ var close = function() {
+ if ( vacuumTimer !== null ) {
+ clearTimeout(vacuumTimer);
+ vacuumTimer = null;
+ }
+ if ( db === null ) {
+ return;
+ }
+ db.asyncClose();
+ db = null;
+ };
+
+ var open = function() {
+ if ( db !== null ) {
+ return db;
+ }
+
+ // Create path
+ var path = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ path.append('extension-data');
+ if ( !path.exists() ) {
+ path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0774', 8));
+ }
+ if ( !path.isDirectory() ) {
+ throw Error('Should be a directory...');
+ }
+ path.append(location.host + '.sqlite');
+
+ // Open database
+ try {
+ db = Services.storage.openDatabase(path);
+ if ( db.connectionReady === false ) {
+ db.asyncClose();
+ db = null;
+ }
+ } catch (ex) {
+ }
+
+ if ( db === null ) {
+ return null;
+ }
+
+ // Database was opened, register cleanup task
+ cleanupTasks.push(close);
+
+ // Setup database
+ db.createAsyncStatement('CREATE TABLE IF NOT EXISTS "settings" ("name" TEXT PRIMARY KEY NOT NULL, "value" TEXT);')
+ .executeAsync();
+
+ if ( vacuum !== null ) {
+ vacuumTimer = vAPI.setTimeout(vacuum, 60000);
+ }
+
+ return db;
+ };
+
+ // https://developer.mozilla.org/en-US/docs/Storage/Performance#Vacuuming_and_zero-fill
+ // Vacuum only once, and only while idle
+ var vacuum = function() {
+ vacuumTimer = null;
+ if ( db === null ) {
+ return;
+ }
+ var idleSvc = Cc['@mozilla.org/widget/idleservice;1']
+ .getService(Ci.nsIIdleService);
+ if ( idleSvc.idleTime < 60000 ) {
+ vacuumTimer = vAPI.setTimeout(vacuum, 60000);
+ return;
+ }
+ db.createAsyncStatement('VACUUM').executeAsync();
+ vacuum = null;
+ };
+
+ // Execute a query
+ var runStatement = function(stmt, callback) {
+ var result = {};
+
+ stmt.executeAsync({
+ handleResult: function(rows) {
+ if ( !rows || typeof callback !== 'function' ) {
+ return;
+ }
+
+ var row;
+
+ while ( (row = rows.getNextRow()) ) {
+ // we assume that there will be two columns, since we're
+ // using it only for preferences
+ result[row.getResultByIndex(0)] = row.getResultByIndex(1);
+ }
+ },
+ handleCompletion: function(reason) {
+ if ( typeof callback === 'function' && reason === 0 ) {
+ callback(result);
+ }
+ },
+ handleError: function(error) {
+ console.error('SQLite error ', error.result, error.message);
+ // Caller expects an answer regardless of failure.
+ if ( typeof callback === 'function' ) {
+ callback(null);
+ }
+ }
+ });
+ };
+
+ var bindNames = function(stmt, names) {
+ if ( Array.isArray(names) === false || names.length === 0 ) {
+ return;
+ }
+ var params = stmt.newBindingParamsArray();
+ var i = names.length, bp;
+ while ( i-- ) {
+ bp = params.newBindingParams();
+ bp.bindByName('name', names[i]);
+ params.addParams(bp);
+ }
+ stmt.bindParameters(params);
+ };
+
+ var clear = function(callback) {
+ if ( open() === null ) {
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ return;
+ }
+ runStatement(db.createAsyncStatement('DELETE FROM "settings";'), callback);
+ };
+
+ var getBytesInUse = function(keys, callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+
+ if ( open() === null ) {
+ callback(0);
+ return;
+ }
+
+ var stmt;
+ if ( Array.isArray(keys) ) {
+ stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings" WHERE "name" = :name');
+ bindNames(keys);
+ } else {
+ stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings"');
+ }
+
+ runStatement(stmt, function(result) {
+ callback(result.size);
+ });
+ };
+
+ var read = function(details, callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+
+ var prepareResult = function(result) {
+ var key;
+ for ( key in result ) {
+ if ( result.hasOwnProperty(key) === false ) {
+ continue;
+ }
+ result[key] = JSON.parse(result[key]);
+ }
+ if ( typeof details === 'object' && details !== null ) {
+ for ( key in details ) {
+ if ( result.hasOwnProperty(key) === false ) {
+ result[key] = details[key];
+ }
+ }
+ }
+ callback(result);
+ };
+
+ if ( open() === null ) {
+ prepareResult({});
+ return;
+ }
+
+ var names = [];
+ if ( details !== null ) {
+ if ( Array.isArray(details) ) {
+ names = details;
+ } else if ( typeof details === 'object' ) {
+ names = Object.keys(details);
+ } else {
+ names = [details.toString()];
+ }
+ }
+
+ var stmt;
+ if ( names.length === 0 ) {
+ stmt = db.createAsyncStatement('SELECT * FROM "settings"');
+ } else {
+ stmt = db.createAsyncStatement('SELECT * FROM "settings" WHERE "name" = :name');
+ bindNames(stmt, names);
+ }
+
+ runStatement(stmt, prepareResult);
+ };
+
+ var remove = function(keys, callback) {
+ if ( open() === null ) {
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ return;
+ }
+ var stmt = db.createAsyncStatement('DELETE FROM "settings" WHERE "name" = :name');
+ bindNames(stmt, typeof keys === 'string' ? [keys] : keys);
+ runStatement(stmt, callback);
+ };
+
+ var write = function(details, callback) {
+ if ( open() === null ) {
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ return;
+ }
+
+ var stmt = db.createAsyncStatement('INSERT OR REPLACE INTO "settings" ("name", "value") VALUES(:name, :value)');
+ var params = stmt.newBindingParamsArray(), bp;
+ for ( var key in details ) {
+ if ( details.hasOwnProperty(key) === false ) {
+ continue;
+ }
+ bp = params.newBindingParams();
+ bp.bindByName('name', key);
+ bp.bindByName('value', JSON.stringify(details[key]));
+ params.addParams(bp);
+ }
+ if ( params.length === 0 ) {
+ return;
+ }
+
+ stmt.bindParameters(params);
+ runStatement(stmt, callback);
+ };
+
+ // Export API
+ var api = {
+ QUOTA_BYTES: 100 * 1024 * 1024,
+ clear: clear,
+ get: read,
+ getBytesInUse: getBytesInUse,
+ remove: remove,
+ set: write
+ };
+ return api;
+})();
+
+vAPI.cacheStorage = vAPI.storage;
+
+/******************************************************************************/
+
+// This must be executed/setup early.
+
+var winWatcher = (function() {
+ var windowToIdMap = new Map();
+ var windowIdGenerator = 1;
+ var api = {
+ onOpenWindow: null,
+ onCloseWindow: null
+ };
+
+ // https://github.com/gorhill/uMatrix/issues/586
+ // This is necessary hack because on SeaMonkey 2.40, for unknown reasons
+ // private windows do not have the attribute `windowtype` set to
+ // `navigator:browser`. As a fallback, the code here will also test whether
+ // the id attribute is `main-window`.
+ api.toBrowserWindow = function(win) {
+ var docElement = win && win.document && win.document.documentElement;
+ if ( !docElement ) {
+ return null;
+ }
+ if ( vAPI.thunderbird ) {
+ return docElement.getAttribute('windowtype') === 'mail:3pane' ? win : null;
+ }
+ return docElement.getAttribute('windowtype') === 'navigator:browser' ||
+ docElement.getAttribute('id') === 'main-window' ?
+ win : null;
+ };
+
+ api.getWindows = function() {
+ return windowToIdMap.keys();
+ };
+
+ api.idFromWindow = function(win) {
+ return windowToIdMap.get(win) || 0;
+ };
+
+ api.getCurrentWindow = function() {
+ return this.toBrowserWindow(Services.wm.getMostRecentWindow(null));
+ };
+
+ var addWindow = function(win) {
+ if ( !win || windowToIdMap.has(win) ) {
+ return;
+ }
+ windowToIdMap.set(win, windowIdGenerator++);
+ if ( typeof api.onOpenWindow === 'function' ) {
+ api.onOpenWindow(win);
+ }
+ };
+
+ var removeWindow = function(win) {
+ if ( !win || windowToIdMap.delete(win) !== true ) {
+ return;
+ }
+ if ( typeof api.onCloseWindow === 'function' ) {
+ api.onCloseWindow(win);
+ }
+ };
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher
+ // https://github.com/gorhill/uMatrix/issues/357
+ // Use nsIWindowMediator for being notified of opened/closed windows.
+ var listeners = {
+ onOpenWindow: function(aWindow) {
+ var win;
+ try {
+ win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ } catch (ex) {
+ }
+ addWindow(win);
+ },
+
+ onCloseWindow: function(aWindow) {
+ var win;
+ try {
+ win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ } catch (ex) {
+ }
+ removeWindow(win);
+ },
+
+ observe: function(aSubject, topic) {
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#registerNotification%28%29
+ // "aSubject - the window being opened or closed, sent as an
+ // "nsISupports which can be ... QueryInterfaced to an
+ // "nsIDOMWindow."
+ var win;
+ try {
+ win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ } catch (ex) {
+ }
+ if ( !win ) { return; }
+ if ( topic === 'domwindowopened' ) {
+ addWindow(win);
+ return;
+ }
+ if ( topic === 'domwindowclosed' ) {
+ removeWindow(win);
+ return;
+ }
+ }
+ };
+
+ (function() {
+ var winumerator, win;
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator#getEnumerator%28%29
+ winumerator = Services.wm.getEnumerator(null);
+ while ( winumerator.hasMoreElements() ) {
+ win = winumerator.getNext();
+ if ( !win.closed ) {
+ windowToIdMap.set(win, windowIdGenerator++);
+ }
+ }
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#getWindowEnumerator%28%29
+ winumerator = Services.ww.getWindowEnumerator();
+ while ( winumerator.hasMoreElements() ) {
+ win = winumerator.getNext()
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if ( !win.closed ) {
+ windowToIdMap.set(win, windowIdGenerator++);
+ }
+ }
+
+ Services.wm.addListener(listeners);
+ Services.ww.registerNotification(listeners);
+ })();
+
+ cleanupTasks.push(function() {
+ Services.wm.removeListener(listeners);
+ Services.ww.unregisterNotification(listeners);
+ windowToIdMap.clear();
+ });
+
+ return api;
+})();
+
+/******************************************************************************/
+
+var getTabBrowser = function(win) {
+ return win && win.gBrowser || null;
+};
+
+/******************************************************************************/
+
+var getOwnerWindow = function(target) {
+ if ( target.ownerDocument ) {
+ return target.ownerDocument.defaultView;
+ }
+ return null;
+};
+
+/******************************************************************************/
+
+vAPI.isBehindTheSceneTabId = function(tabId) {
+ return tabId.toString() === '-1';
+};
+
+vAPI.noTabId = '-1';
+
+/******************************************************************************/
+
+vAPI.tabs = {};
+
+/******************************************************************************/
+
+vAPI.tabs.registerListeners = function() {
+ tabWatcher.start();
+};
+
+/******************************************************************************/
+
+// Firefox:
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser
+//
+// browser --> ownerDocument --> defaultView --> gBrowser --> browsers --+
+// ^ |
+// | |
+// +-------------------------------------------------------------------
+//
+// browser (browser)
+// contentTitle
+// currentURI
+// ownerDocument (XULDocument)
+// defaultView (ChromeWindow)
+// gBrowser (tabbrowser OR browser)
+// browsers (browser)
+// selectedBrowser
+// selectedTab
+// tabs (tab.tabbrowser-tab)
+//
+// Fennec: (what I figured so far)
+//
+// tab --> browser windows --> window --> BrowserApp --> tabs --+
+// ^ window |
+// | |
+// +---------------------------------------------------------------+
+//
+// tab
+// browser
+// [manual search to go back to tab from list of windows]
+
+vAPI.tabs.get = function(tabId, callback) {
+ var browser;
+
+ if ( tabId === null ) {
+ browser = tabWatcher.currentBrowser();
+ tabId = tabWatcher.tabIdFromTarget(browser);
+ } else {
+ browser = tabWatcher.browserFromTabId(tabId);
+ }
+
+ // For internal use
+ if ( typeof callback !== 'function' ) {
+ return browser;
+ }
+
+ if ( !browser || !browser.currentURI ) {
+ callback();
+ return;
+ }
+
+ var win = getOwnerWindow(browser);
+ var tabBrowser = getTabBrowser(win);
+
+ // https://github.com/gorhill/uMatrix/issues/540
+ // The `index` property is nowhere used by uMatrix at this point, so we
+ // will refrain from returning this information for the time being.
+
+ callback({
+ id: tabId,
+ index: undefined,
+ windowId: winWatcher.idFromWindow(win),
+ active: tabBrowser !== null && browser === tabBrowser.selectedBrowser,
+ url: browser.currentURI.asciiSpec,
+ title: browser.contentTitle
+ });
+};
+
+/******************************************************************************/
+
+vAPI.tabs.getAllSync = function(window) {
+ var win, tab;
+ var tabs = [];
+
+ for ( win of winWatcher.getWindows() ) {
+ if ( window && window !== win ) {
+ continue;
+ }
+
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ continue;
+ }
+
+ // This can happens if a tab-less window is currently opened.
+ // Example of a tab-less window: one opened from clicking
+ // "View Page Source".
+ if ( !tabBrowser.tabs ) {
+ continue;
+ }
+
+ for ( tab of tabBrowser.tabs ) {
+ tabs.push(tab);
+ }
+ }
+
+ return tabs;
+};
+
+/******************************************************************************/
+
+vAPI.tabs.getAll = function(callback) {
+ var tabs = [], tab;
+
+ for ( var browser of tabWatcher.browsers() ) {
+ tab = tabWatcher.tabFromBrowser(browser);
+ if ( tab === null ) {
+ continue;
+ }
+ if ( tab.hasAttribute('pending') ) {
+ continue;
+ }
+ tabs.push({
+ id: tabWatcher.tabIdFromTarget(browser),
+ url: browser.currentURI.asciiSpec
+ });
+ }
+
+ callback(tabs);
+};
+
+/******************************************************************************/
+
+// properties of the details object:
+// url: 'URL', // the address that will be opened
+// tabId: 1, // the tab is used if set, instead of creating a new one
+// index: -1, // undefined: end of the list, -1: following tab, or after index
+// active: false, // opens the tab in background - true and undefined: foreground
+// select: true // if a tab is already opened with that url, then select it instead of opening a new one
+
+vAPI.tabs.open = function(details) {
+ if ( !details.url ) {
+ return null;
+ }
+ // extension pages
+ if ( /^[\w-]{2,}:/.test(details.url) === false ) {
+ details.url = vAPI.getURL(details.url);
+ }
+
+ var tab;
+
+ if ( details.select ) {
+ var URI = Services.io.newURI(details.url, null, null);
+
+ for ( tab of this.getAllSync() ) {
+ var browser = tabWatcher.browserFromTarget(tab);
+ // https://github.com/gorhill/uBlock/issues/2558
+ if ( browser === null ) { continue; }
+
+ // Or simply .equals if we care about the fragment
+ if ( URI.equalsExceptRef(browser.currentURI) === false ) {
+ continue;
+ }
+
+ this.select(tab);
+
+ // Update URL if fragment is different
+ if ( URI.equals(browser.currentURI) === false ) {
+ browser.loadURI(URI.asciiSpec);
+ }
+ return;
+ }
+ }
+
+ if ( details.active === undefined ) {
+ details.active = true;
+ }
+
+ if ( details.tabId ) {
+ tab = tabWatcher.browserFromTabId(details.tabId);
+ if ( tab ) {
+ tabWatcher.browserFromTarget(tab).loadURI(details.url);
+ return;
+ }
+ }
+
+ // Open in a standalone window
+ if ( details.popup === true ) {
+ Services.ww.openWindow(
+ self,
+ details.url,
+ null,
+ 'location=1,menubar=1,personalbar=1,resizable=1,toolbar=1',
+ null
+ );
+ return;
+ }
+
+ var win = winWatcher.getCurrentWindow();
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return;
+ }
+
+ if ( details.index === -1 ) {
+ details.index = tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1;
+ }
+
+ tab = tabBrowser.loadOneTab(details.url, { inBackground: !details.active });
+
+ if ( details.index !== undefined ) {
+ tabBrowser.moveTabTo(tab, details.index);
+ }
+};
+
+/******************************************************************************/
+
+// Replace the URL of a tab. Noop if the tab does not exist.
+
+vAPI.tabs.replace = function(tabId, url) {
+ var targetURL = url;
+
+ // extension pages
+ if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
+ targetURL = vAPI.getURL(targetURL);
+ }
+
+ var browser = tabWatcher.browserFromTabId(tabId);
+ if ( browser ) {
+ browser.loadURI(targetURL);
+ }
+};
+
+/******************************************************************************/
+
+vAPI.tabs._remove = function(tab, tabBrowser) {
+ if ( tabBrowser ) {
+ tabBrowser.removeTab(tab);
+ }
+};
+
+/******************************************************************************/
+
+vAPI.tabs.remove = function(tabId) {
+ var browser = tabWatcher.browserFromTabId(tabId);
+ if ( !browser ) {
+ return;
+ }
+ var tab = tabWatcher.tabFromBrowser(browser);
+ if ( !tab ) {
+ return;
+ }
+ this._remove(tab, getTabBrowser(getOwnerWindow(browser)));
+};
+
+/******************************************************************************/
+
+vAPI.tabs.reload = function(tabId) {
+ var browser = tabWatcher.browserFromTabId(tabId);
+ if ( !browser ) {
+ return;
+ }
+
+ browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+};
+
+/******************************************************************************/
+
+vAPI.tabs.select = function(tab) {
+ if ( typeof tab !== 'object' ) {
+ tab = tabWatcher.tabFromBrowser(tabWatcher.browserFromTabId(tab));
+ }
+ if ( !tab ) {
+ return;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/470
+ var win = getOwnerWindow(tab);
+ win.focus();
+
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser ) {
+ tabBrowser.selectedTab = tab;
+ }
+};
+
+/******************************************************************************/
+
+vAPI.tabs.injectScript = function(tabId, details, callback) {
+ var browser = tabWatcher.browserFromTabId(tabId);
+ if ( !browser ) {
+ return;
+ }
+
+ if ( typeof details.file !== 'string' ) {
+ return;
+ }
+
+ details.file = vAPI.getURL(details.file);
+ browser.messageManager.sendAsyncMessage(
+ location.host + ':broadcast',
+ JSON.stringify({
+ broadcast: true,
+ channelName: 'vAPI',
+ msg: {
+ cmd: 'injectScript',
+ details: details
+ }
+ })
+ );
+
+ if ( typeof callback === 'function' ) {
+ vAPI.setTimeout(callback, 13);
+ }
+};
+
+/******************************************************************************/
+
+var tabWatcher = (function() {
+ // TODO: find out whether we need a janitor to take care of stale entries.
+
+ // https://github.com/gorhill/uMatrix/issues/540
+ // Use only weak references to hold onto browser references.
+ var browserToTabIdMap = new WeakMap();
+ var tabIdToBrowserMap = new Map();
+ var tabIdGenerator = 1;
+
+ var indexFromBrowser = function(browser) {
+ if ( !browser ) {
+ return -1;
+ }
+ var win = getOwnerWindow(browser);
+ if ( !win ) {
+ return -1;
+ }
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return -1;
+ }
+ // This can happen, for example, the `view-source:` window, there is
+ // no tabbrowser object, the browser object sits directly in the
+ // window.
+ if ( tabBrowser === browser ) {
+ return 0;
+ }
+ return tabBrowser.browsers.indexOf(browser);
+ };
+
+ var indexFromTarget = function(target) {
+ return indexFromBrowser(browserFromTarget(target));
+ };
+
+ var tabFromBrowser = function(browser) {
+ var i = indexFromBrowser(browser);
+ if ( i === -1 ) {
+ return null;
+ }
+ var win = getOwnerWindow(browser);
+ if ( !win ) {
+ return null;
+ }
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return null;
+ }
+ if ( !tabBrowser.tabs || i >= tabBrowser.tabs.length ) {
+ return null;
+ }
+ return tabBrowser.tabs[i];
+ };
+
+ var browserFromTarget = function(target) {
+ if ( !target ) {
+ return null;
+ }
+ if ( target.linkedPanel ) { // target is a tab
+ target = target.linkedBrowser;
+ }
+ if ( target.localName !== 'browser' ) {
+ return null;
+ }
+ return target;
+ };
+
+ var tabIdFromTarget = function(target) {
+ var browser = browserFromTarget(target);
+ if ( browser === null ) {
+ return vAPI.noTabId;
+ }
+ var tabId = browserToTabIdMap.get(browser);
+ if ( tabId === undefined ) {
+ tabId = '' + tabIdGenerator++;
+ browserToTabIdMap.set(browser, tabId);
+ tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser));
+ }
+ return tabId;
+ };
+
+ var browserFromTabId = function(tabId) {
+ var weakref = tabIdToBrowserMap.get(tabId);
+ var browser = weakref && weakref.get();
+ return browser || null;
+ };
+
+ var currentBrowser = function() {
+ var win = winWatcher.getCurrentWindow();
+ // https://github.com/gorhill/uBlock/issues/399
+ // getTabBrowser() can return null at browser launch time.
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return null;
+ }
+ return browserFromTarget(tabBrowser.selectedTab);
+ };
+
+ var removeBrowserEntry = function(tabId, browser) {
+ if ( tabId && tabId !== vAPI.noTabId ) {
+ vAPI.tabs.onClosed(tabId);
+ delete vAPI.toolbarButton.tabs[tabId];
+ tabIdToBrowserMap.delete(tabId);
+ }
+ if ( browser ) {
+ browserToTabIdMap.delete(browser);
+ }
+ };
+
+ var removeTarget = function(target) {
+ onClose({ target: target });
+ };
+
+ var getAllBrowsers = function() {
+ var browsers = [], browser;
+ for ( var [tabId, weakref] of tabIdToBrowserMap ) {
+ browser = weakref.get();
+ // TODO:
+ // Maybe call removeBrowserEntry() if the browser no longer exists?
+ if ( browser ) {
+ browsers.push(browser);
+ }
+ }
+ return browsers;
+ };
+
+ // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen
+ //var onOpen = function({target}) {
+ // var tabId = tabIdFromTarget(target);
+ // var browser = browserFromTabId(tabId);
+ // vAPI.tabs.onNavigation({
+ // frameId: 0,
+ // tabId: tabId,
+ // url: browser.currentURI.asciiSpec,
+ // });
+ //};
+
+ // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow
+ var onShow = function({target}) {
+ tabIdFromTarget(target);
+ };
+
+ // https://developer.mozilla.org/en-US/docs/Web/Events/TabClose
+ var onClose = function({target}) {
+ // target is tab in Firefox, browser in Fennec
+ var browser = browserFromTarget(target);
+ var tabId = browserToTabIdMap.get(browser);
+ removeBrowserEntry(tabId, browser);
+ };
+
+ // https://developer.mozilla.org/en-US/docs/Web/Events/TabSelect
+ // This is an entry point: when creating a new tab, it is not always
+ // reported through onLocationChanged... Sigh. It is "reported" here
+ // however.
+ var onSelect = function({target}) {
+ var browser = browserFromTarget(target);
+ var tabId = browserToTabIdMap.get(browser);
+ if ( tabId === undefined ) {
+ tabId = tabIdFromTarget(target);
+ vAPI.tabs.onNavigation({
+ frameId: 0,
+ tabId: tabId,
+ url: browser.currentURI.asciiSpec
+ });
+ }
+ vAPI.setIcon(tabId, getOwnerWindow(target));
+ };
+
+ var locationChangedMessageName = location.host + ':locationChanged';
+
+ var onLocationChanged = function(e) {
+ var vapi = vAPI;
+ var details = e.data;
+
+ // Ignore notifications related to our popup
+ if ( details.url.lastIndexOf(vapi.getURL('popup.html'), 0) === 0 ) {
+ return;
+ }
+
+ var browser = e.target;
+ var tabId = tabIdFromTarget(browser);
+ if ( tabId === vapi.noTabId ) {
+ return;
+ }
+
+ // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document"
+ if ( details.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT ) {
+ vapi.tabs.onUpdated(tabId, {url: details.url}, {
+ frameId: 0,
+ tabId: tabId,
+ url: browser.currentURI.asciiSpec
+ });
+ return;
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/105
+ // Allow any kind of pages
+ vapi.tabs.onNavigation({
+ frameId: 0,
+ tabId: tabId,
+ url: details.url
+ });
+ };
+
+ var attachToTabBrowser = function(window) {
+ if ( typeof vAPI.toolbarButton.attachToNewWindow === 'function' ) {
+ vAPI.toolbarButton.attachToNewWindow(window);
+ }
+
+ var tabBrowser = getTabBrowser(window);
+ if ( tabBrowser === null ) {
+ return;
+ }
+
+ var tabContainer;
+ if ( tabBrowser.deck ) { // Fennec
+ tabContainer = tabBrowser.deck;
+ } else if ( tabBrowser.tabContainer ) { // Firefox
+ tabContainer = tabBrowser.tabContainer;
+ vAPI.contextMenu.register(document);
+ }
+
+ // https://github.com/gorhill/uBlock/issues/697
+ // Ignore `TabShow` events: unfortunately the `pending` attribute is
+ // not set when a tab is opened as a result of session restore -- it is
+ // set *after* the event is fired in such case.
+ if ( tabContainer ) {
+ tabContainer.addEventListener('TabShow', onShow);
+ tabContainer.addEventListener('TabClose', onClose);
+ // when new window is opened TabSelect doesn't run on the selected tab?
+ tabContainer.addEventListener('TabSelect', onSelect);
+ }
+ };
+
+ // https://github.com/gorhill/uBlock/issues/906
+ // Ensure the environment is ready before trying to attaching.
+ var canAttachToTabBrowser = function(window) {
+ var document = window && window.document;
+ if ( !document || document.readyState !== 'complete' ) {
+ return false;
+ }
+
+ // On some platforms, the tab browser isn't immediately available,
+ // try waiting a bit if this happens.
+ // https://github.com/gorhill/uBlock/issues/763
+ // Not getting a tab browser should not prevent from attaching ourself
+ // to the window.
+ var tabBrowser = getTabBrowser(window);
+ if ( tabBrowser === null ) {
+ return false;
+ }
+
+ return winWatcher.toBrowserWindow(window) !== null;
+ };
+
+ var onWindowLoad = function(win) {
+ deferUntil(
+ canAttachToTabBrowser.bind(null, win),
+ attachToTabBrowser.bind(null, win)
+ );
+ };
+
+ var onWindowUnload = function(win) {
+ vAPI.contextMenu.unregister(win.document);
+
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return;
+ }
+
+ var tabContainer = tabBrowser.tabContainer;
+ if ( tabContainer ) {
+ tabContainer.removeEventListener('TabShow', onShow);
+ tabContainer.removeEventListener('TabClose', onClose);
+ tabContainer.removeEventListener('TabSelect', onSelect);
+ }
+
+ // https://github.com/gorhill/uBlock/issues/574
+ // To keep in mind: not all windows are tab containers,
+ // sometimes the window IS the tab.
+ var tabs;
+ if ( tabBrowser.tabs ) {
+ tabs = tabBrowser.tabs;
+ } else if ( tabBrowser.localName === 'browser' ) {
+ tabs = [tabBrowser];
+ } else {
+ tabs = [];
+ }
+
+ var browser, URI, tabId;
+ var tabindex = tabs.length, tab;
+ while ( tabindex-- ) {
+ tab = tabs[tabindex];
+ browser = browserFromTarget(tab);
+ if ( browser === null ) {
+ continue;
+ }
+ URI = browser.currentURI;
+ // Close extension tabs
+ if ( URI.schemeIs('chrome') && URI.host === location.host ) {
+ vAPI.tabs._remove(tab, getTabBrowser(win));
+ }
+ tabId = browserToTabIdMap.get(browser);
+ if ( tabId !== undefined ) {
+ removeBrowserEntry(tabId, browser);
+ tabIdToBrowserMap.delete(tabId);
+ }
+ browserToTabIdMap.delete(browser);
+ }
+ };
+
+ // Initialize map with existing active tabs
+ var start = function() {
+ var tabBrowser, tabs, tab;
+ for ( var win of winWatcher.getWindows() ) {
+ onWindowLoad(win);
+ tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ continue;
+ }
+ for ( tab of tabBrowser.tabs ) {
+ if ( !tab.hasAttribute('pending') ) {
+ tabIdFromTarget(tab);
+ }
+ }
+ }
+
+ winWatcher.onOpenWindow = onWindowLoad;
+ winWatcher.onCloseWindow = onWindowUnload;
+
+ vAPI.messaging.globalMessageManager.addMessageListener(
+ locationChangedMessageName,
+ onLocationChanged
+ );
+ };
+
+ var stop = function() {
+ winWatcher.onOpenWindow = null;
+ winWatcher.onCloseWindow = null;
+
+ vAPI.messaging.globalMessageManager.removeMessageListener(
+ locationChangedMessageName,
+ onLocationChanged
+ );
+
+ for ( var win of winWatcher.getWindows() ) {
+ onWindowUnload(win);
+ }
+
+ browserToTabIdMap = new WeakMap();
+ tabIdToBrowserMap.clear();
+ };
+
+ cleanupTasks.push(stop);
+
+ return {
+ browsers: getAllBrowsers,
+ browserFromTabId: browserFromTabId,
+ browserFromTarget: browserFromTarget,
+ currentBrowser: currentBrowser,
+ indexFromTarget: indexFromTarget,
+ removeTarget: removeTarget,
+ start: start,
+ tabFromBrowser: tabFromBrowser,
+ tabIdFromTarget: tabIdFromTarget
+ };
+})();
+
+/******************************************************************************/
+
+vAPI.setIcon = function(tabId, iconId, badge) {
+ // If badge is undefined, then setIcon was called from the TabSelect event
+ var win;
+ if ( badge === undefined ) {
+ win = iconId;
+ } else {
+ win = winWatcher.getCurrentWindow();
+ }
+ var tabBrowser = getTabBrowser(win);
+ if ( tabBrowser === null ) {
+ return;
+ }
+ var curTabId = tabWatcher.tabIdFromTarget(tabBrowser.selectedTab);
+ var tb = vAPI.toolbarButton;
+
+ // from 'TabSelect' event
+ if ( tabId === undefined ) {
+ tabId = curTabId;
+ } else if ( badge !== undefined ) {
+ tb.tabs[tabId] = { badge: badge, img: iconId };
+ }
+
+ if ( tabId === curTabId ) {
+ tb.updateState(win, tabId);
+ }
+};
+
+/******************************************************************************/
+
+vAPI.messaging = {
+ get globalMessageManager() {
+ return Cc['@mozilla.org/globalmessagemanager;1']
+ .getService(Ci.nsIMessageListenerManager);
+ },
+ frameScript: vAPI.getURL('frameScript.js'),
+ listeners: {},
+ defaultHandler: null,
+ NOOPFUNC: function(){},
+ UNHANDLED: 'vAPI.messaging.notHandled'
+};
+
+/******************************************************************************/
+
+vAPI.messaging.listen = function(listenerName, callback) {
+ this.listeners[listenerName] = callback;
+};
+
+/******************************************************************************/
+
+vAPI.messaging.onMessage = function({target, data}) {
+ var messageManager = target.messageManager;
+
+ if ( !messageManager ) {
+ // Message came from a popup, and its message manager is not usable.
+ // So instead we broadcast to the parent window.
+ messageManager = getOwnerWindow(
+ target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler
+ ).messageManager;
+ }
+
+ var channelNameRaw = data.channelName;
+ var pos = channelNameRaw.indexOf('|');
+ var channelName = channelNameRaw.slice(pos + 1);
+
+ var callback = vAPI.messaging.NOOPFUNC;
+ if ( data.requestId !== undefined ) {
+ callback = CallbackWrapper.factory(
+ messageManager,
+ channelName,
+ channelNameRaw.slice(0, pos),
+ data.requestId
+ ).callback;
+ }
+
+ var sender = {
+ tab: {
+ id: tabWatcher.tabIdFromTarget(target)
+ }
+ };
+
+ // Specific handler
+ var r = vAPI.messaging.UNHANDLED;
+ var listener = vAPI.messaging.listeners[channelName];
+ if ( typeof listener === 'function' ) {
+ r = listener(data.msg, sender, callback);
+ }
+ if ( r !== vAPI.messaging.UNHANDLED ) {
+ return;
+ }
+
+ // Default handler
+ r = vAPI.messaging.defaultHandler(data.msg, sender, callback);
+ if ( r !== vAPI.messaging.UNHANDLED ) {
+ return;
+ }
+
+ console.error('uMatrix> messaging > unknown request: %o', data);
+
+ // Unhandled:
+ // Need to callback anyways in case caller expected an answer, or
+ // else there is a memory leak on caller's side
+ callback();
+};
+
+/******************************************************************************/
+
+vAPI.messaging.setup = function(defaultHandler) {
+ // Already setup?
+ if ( this.defaultHandler !== null ) {
+ return;
+ }
+
+ if ( typeof defaultHandler !== 'function' ) {
+ defaultHandler = function(){ return vAPI.messaging.UNHANDLED; };
+ }
+ this.defaultHandler = defaultHandler;
+
+ this.globalMessageManager.addMessageListener(
+ location.host + ':background',
+ this.onMessage
+ );
+
+ this.globalMessageManager.loadFrameScript(this.frameScript, true);
+
+ cleanupTasks.push(function() {
+ var gmm = vAPI.messaging.globalMessageManager;
+
+ gmm.removeDelayedFrameScript(vAPI.messaging.frameScript);
+ gmm.removeMessageListener(
+ location.host + ':background',
+ vAPI.messaging.onMessage
+ );
+ });
+};
+
+/******************************************************************************/
+
+vAPI.messaging.broadcast = function(message) {
+ this.globalMessageManager.broadcastAsyncMessage(
+ location.host + ':broadcast',
+ JSON.stringify({broadcast: true, msg: message})
+ );
+};
+
+/******************************************************************************/
+
+// This allows to avoid creating a closure for every single message which
+// expects an answer. Having a closure created each time a message is processed
+// has been always bothering me. Another benefit of the implementation here
+// is to reuse the callback proxy object, so less memory churning.
+//
+// https://developers.google.com/speed/articles/optimizing-javascript
+// "Creating a closure is significantly slower then creating an inner
+// function without a closure, and much slower than reusing a static
+// function"
+//
+// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html
+// "the dreaded 'uniformly slow code' case where every function takes 1%
+// of CPU and you have to make one hundred separate performance optimizations
+// to improve performance at all"
+//
+// http://jsperf.com/closure-no-closure/2
+
+var CallbackWrapper = function(messageManager, channelName, listenerId, requestId) {
+ this.callback = this.proxy.bind(this); // bind once
+ this.init(messageManager, channelName, listenerId, requestId);
+};
+
+CallbackWrapper.junkyard = [];
+
+CallbackWrapper.factory = function(messageManager, channelName, listenerId, requestId) {
+ var wrapper = CallbackWrapper.junkyard.pop();
+ if ( wrapper ) {
+ wrapper.init(messageManager, channelName, listenerId, requestId);
+ return wrapper;
+ }
+ return new CallbackWrapper(messageManager, channelName, listenerId, requestId);
+};
+
+CallbackWrapper.prototype.init = function(messageManager, channelName, listenerId, requestId) {
+ this.messageManager = messageManager;
+ this.channelName = channelName;
+ this.listenerId = listenerId;
+ this.requestId = requestId;
+};
+
+CallbackWrapper.prototype.proxy = function(response) {
+ var message = JSON.stringify({
+ requestId: this.requestId,
+ channelName: this.channelName,
+ msg: response !== undefined ? response : null
+ });
+
+ if ( this.messageManager.sendAsyncMessage ) {
+ this.messageManager.sendAsyncMessage(this.listenerId, message);
+ } else {
+ this.messageManager.broadcastAsyncMessage(this.listenerId, message);
+ }
+
+ // Mark for reuse
+ this.messageManager =
+ this.channelName =
+ this.requestId =
+ this.listenerId = null;
+ CallbackWrapper.junkyard.push(this);
+};
+
+/******************************************************************************/
+
+var httpRequestHeadersFactory = function(channel) {
+ var entry = httpRequestHeadersFactory.junkyard.pop();
+ if ( entry ) {
+ return entry.init(channel);
+ }
+ return new HTTPRequestHeaders(channel);
+};
+
+httpRequestHeadersFactory.junkyard = [];
+
+var HTTPRequestHeaders = function(channel) {
+ this.init(channel);
+};
+
+HTTPRequestHeaders.prototype.init = function(channel) {
+ this.channel = channel;
+ return this;
+};
+
+HTTPRequestHeaders.prototype.dispose = function() {
+ this.channel = null;
+ httpRequestHeadersFactory.junkyard.push(this);
+};
+
+HTTPRequestHeaders.prototype.getHeader = function(name) {
+ try {
+ return this.channel.getRequestHeader(name);
+ } catch (e) {
+ }
+ return '';
+};
+
+HTTPRequestHeaders.prototype.setHeader = function(name, newValue, create) {
+ var oldValue = this.getHeader(name);
+ if ( newValue === oldValue ) {
+ return false;
+ }
+ if ( oldValue === '' && create !== true ) {
+ return false;
+ }
+ this.channel.setRequestHeader(name, newValue, false);
+ return true;
+};
+
+/******************************************************************************/
+
+var httpObserver = {
+ classDescription: 'net-channel-event-sinks for ' + location.host,
+ classID: Components.ID('{5d2e2797-6d68-42e2-8aeb-81ce6ba16b95}'),
+ contractID: '@' + location.host + '/net-channel-event-sinks;1',
+ REQDATAKEY: location.host + 'reqdata',
+ ABORT: Components.results.NS_BINDING_ABORTED,
+ ACCEPT: Components.results.NS_SUCCEEDED,
+ // Request types:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy#Constants
+ frameTypeMap: {
+ 6: 'main_frame',
+ 7: 'sub_frame'
+ },
+ typeMap: {
+ 1: 'other',
+ 2: 'script',
+ 3: 'image',
+ 4: 'stylesheet',
+ 5: 'object',
+ 6: 'main_frame',
+ 7: 'sub_frame',
+ 9: 'xbl',
+ 10: 'ping',
+ 11: 'xmlhttprequest',
+ 12: 'object',
+ 13: 'xml_dtd',
+ 14: 'font',
+ 15: 'media',
+ 16: 'websocket',
+ 17: 'csp_report',
+ 18: 'xslt',
+ 19: 'beacon',
+ 20: 'xmlhttprequest',
+ 21: 'imageset',
+ 22: 'web_manifest'
+ },
+ mimeTypeMap: {
+ 'audio': 15,
+ 'video': 15
+ },
+
+ get componentRegistrar() {
+ return Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ },
+
+ get categoryManager() {
+ return Cc['@mozilla.org/categorymanager;1']
+ .getService(Ci.nsICategoryManager);
+ },
+
+ QueryInterface: (function() {
+ var {XPCOMUtils} = Cu.import('resource://gre/modules/XPCOMUtils.jsm', null);
+
+ return XPCOMUtils.generateQI([
+ Ci.nsIFactory,
+ Ci.nsIObserver,
+ Ci.nsIChannelEventSink,
+ Ci.nsISupportsWeakReference
+ ]);
+ })(),
+
+ createInstance: function(outer, iid) {
+ if ( outer ) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return this.QueryInterface(iid);
+ },
+
+ register: function() {
+ this.pendingRingBufferInit();
+
+ // https://developer.mozilla.org/en/docs/Observer_Notifications#HTTP_requests
+ Services.obs.addObserver(this, 'http-on-modify-request', true);
+ Services.obs.addObserver(this, 'http-on-examine-response', true);
+ Services.obs.addObserver(this, 'http-on-examine-cached-response', true);
+
+ // Guard against stale instances not having been unregistered
+ if ( this.componentRegistrar.isCIDRegistered(this.classID) ) {
+ try {
+ this.componentRegistrar.unregisterFactory(this.classID, Components.manager.getClassObject(this.classID, Ci.nsIFactory));
+ } catch (ex) {
+ console.error('uMatrix> httpObserver > unable to unregister stale instance: ', ex);
+ }
+ }
+
+ this.componentRegistrar.registerFactory(
+ this.classID,
+ this.classDescription,
+ this.contractID,
+ this
+ );
+ this.categoryManager.addCategoryEntry(
+ 'net-channel-event-sinks',
+ this.contractID,
+ this.contractID,
+ false,
+ true
+ );
+ },
+
+ unregister: function() {
+ Services.obs.removeObserver(this, 'http-on-modify-request');
+ Services.obs.removeObserver(this, 'http-on-examine-response');
+ Services.obs.removeObserver(this, 'http-on-examine-cached-response');
+
+ this.componentRegistrar.unregisterFactory(this.classID, this);
+ this.categoryManager.deleteCategoryEntry(
+ 'net-channel-event-sinks',
+ this.contractID,
+ false
+ );
+ },
+
+ PendingRequest: function() {
+ this.rawType = 0;
+ this.tabId = 0;
+ this._key = ''; // key is url, from URI.spec
+ },
+
+ // If all work fine, this map should not grow indefinitely. It can have
+ // stale items in it, but these will be taken care of when entries in
+ // the ring buffer are overwritten.
+ pendingURLToIndex: new Map(),
+ pendingWritePointer: 0,
+ pendingRingBuffer: new Array(256),
+ pendingRingBufferInit: function() {
+ // Use and reuse pre-allocated PendingRequest objects = less memory
+ // churning.
+ var i = this.pendingRingBuffer.length;
+ while ( i-- ) {
+ this.pendingRingBuffer[i] = new this.PendingRequest();
+ }
+ },
+
+ // Pending request ring buffer:
+ // +-------+-------+-------+-------+-------+-------+-------
+ // |0 |1 |2 |3 |4 |5 |...
+ // +-------+-------+-------+-------+-------+-------+-------
+ //
+ // URL to ring buffer index map:
+ // { k = URL, s = ring buffer indices }
+ //
+ // s is a string which character codes map to ring buffer indices -- for
+ // when the same URL is received multiple times by shouldLoadListener()
+ // before the existing one is serviced by the network request observer.
+ // I believe the use of a string in lieu of an array reduces memory
+ // churning.
+
+ createPendingRequest: function(url) {
+ var bucket;
+ var i = this.pendingWritePointer;
+ this.pendingWritePointer = i + 1 & 255;
+ var preq = this.pendingRingBuffer[i];
+ var si = String.fromCharCode(i);
+ // Cleanup unserviced pending request
+ if ( preq._key !== '' ) {
+ bucket = this.pendingURLToIndex.get(preq._key);
+ if ( bucket.length === 1 ) {
+ this.pendingURLToIndex.delete(preq._key);
+ } else {
+ var pos = bucket.indexOf(si);
+ this.pendingURLToIndex.set(preq._key, bucket.slice(0, pos) + bucket.slice(pos + 1));
+ }
+ }
+ bucket = this.pendingURLToIndex.get(url);
+ this.pendingURLToIndex.set(url, bucket === undefined ? si : bucket + si);
+ preq._key = url;
+ return preq;
+ },
+
+ lookupPendingRequest: function(url) {
+ var bucket = this.pendingURLToIndex.get(url);
+ if ( bucket === undefined ) {
+ return null;
+ }
+ var i = bucket.charCodeAt(0);
+ if ( bucket.length === 1 ) {
+ this.pendingURLToIndex.delete(url);
+ } else {
+ this.pendingURLToIndex.set(url, bucket.slice(1));
+ }
+ var preq = this.pendingRingBuffer[i];
+ preq._key = ''; // mark as "serviced"
+ return preq;
+ },
+
+ handleRequest: function(channel, URI, tabId, rawType) {
+ var type = this.typeMap[rawType] || 'other';
+
+ var onBeforeRequest = vAPI.net.onBeforeRequest;
+ if ( onBeforeRequest.types === null || onBeforeRequest.types.has(type) ) {
+ var result = onBeforeRequest.callback({
+ parentFrameId: type === 'main_frame' ? -1 : 0,
+ tabId: tabId,
+ type: type,
+ url: URI.asciiSpec
+ });
+ if ( typeof result === 'object' ) {
+ channel.cancel(this.ABORT);
+ return true;
+ }
+ }
+
+ var onBeforeSendHeaders = vAPI.net.onBeforeSendHeaders;
+ if ( onBeforeSendHeaders.types === null || onBeforeSendHeaders.types.has(type) ) {
+ var requestHeaders = httpRequestHeadersFactory(channel);
+ onBeforeSendHeaders.callback({
+ parentFrameId: type === 'main_frame' ? -1 : 0,
+ requestHeaders: requestHeaders,
+ tabId: tabId,
+ type: type,
+ url: URI.asciiSpec
+ });
+ requestHeaders.dispose();
+ }
+
+ return false;
+ },
+
+ channelDataFromChannel: function(channel) {
+ if ( channel instanceof Ci.nsIWritablePropertyBag ) {
+ try {
+ return channel.getProperty(this.REQDATAKEY) || null;
+ } catch (ex) {
+ }
+ }
+ return null;
+ },
+
+ // https://github.com/gorhill/uMatrix/issues/165
+ // https://developer.mozilla.org/en-US/Firefox/Releases/3.5/Updating_extensions#Getting_a_load_context_from_a_request
+ // Not sure `umatrix:shouldLoad` is still needed, uMatrix does not
+ // care about embedded frames topography.
+ // Also:
+ // https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts
+ tabIdFromChannel: function(channel) {
+ var lc;
+ try {
+ lc = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch(ex) {
+ }
+ if ( !lc ) {
+ try {
+ lc = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch(ex) {
+ }
+ if ( !lc ) {
+ return vAPI.noTabId;
+ }
+ }
+ if ( lc.topFrameElement ) {
+ return tabWatcher.tabIdFromTarget(lc.topFrameElement);
+ }
+ var win;
+ try {
+ win = lc.associatedWindow;
+ } catch (ex) { }
+ if ( !win ) {
+ return vAPI.noTabId;
+ }
+ if ( win.top ) {
+ win = win.top;
+ }
+ var tabBrowser;
+ try {
+ tabBrowser = getTabBrowser(
+ win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow)
+ );
+ } catch (ex) { }
+ if ( !tabBrowser ) {
+ return vAPI.noTabId;
+ }
+ if ( tabBrowser.getBrowserForContentWindow ) {
+ return tabWatcher.tabIdFromTarget(tabBrowser.getBrowserForContentWindow(win));
+ }
+ // Falling back onto _getTabForContentWindow to ensure older versions
+ // of Firefox work well.
+ return tabBrowser._getTabForContentWindow ?
+ tabWatcher.tabIdFromTarget(tabBrowser._getTabForContentWindow(win)) :
+ vAPI.noTabId;
+ },
+
+ rawtypeFromContentType: function(channel) {
+ var mime = channel.contentType;
+ if ( !mime ) {
+ return 0;
+ }
+ var pos = mime.indexOf('/');
+ if ( pos === -1 ) {
+ pos = mime.length;
+ }
+ return this.mimeTypeMap[mime.slice(0, pos)] || 0;
+ },
+
+ observe: function(channel, topic) {
+ if ( channel instanceof Ci.nsIHttpChannel === false ) {
+ return;
+ }
+
+ var URI = channel.URI;
+ var channelData = this.channelDataFromChannel(channel);
+
+ if ( topic.lastIndexOf('http-on-examine-', 0) === 0 ) {
+ if ( channelData === null ) {
+ return;
+ }
+
+ var type = this.frameTypeMap[channelData[1]];
+ if ( !type ) {
+ return;
+ }
+
+ topic = 'Content-Security-Policy';
+
+ var result;
+ try {
+ result = channel.getResponseHeader(topic);
+ } catch (ex) {
+ result = null;
+ }
+
+ result = vAPI.net.onHeadersReceived.callback({
+ parentFrameId: type === 'main_frame' ? -1 : 0,
+ responseHeaders: result ? [{name: topic, value: result}] : [],
+ tabId: channelData[0],
+ type: type,
+ url: URI.asciiSpec
+ });
+
+ if ( result ) {
+ channel.setResponseHeader(
+ topic,
+ result.responseHeaders.pop().value,
+ true
+ );
+ }
+
+ return;
+ }
+
+ // http-on-modify-request
+
+ // The channel was previously serviced.
+ if ( channelData !== null ) {
+ this.handleRequest(channel, URI, channelData[0], channelData[1]);
+ return;
+ }
+
+ // The channel was never serviced.
+ var tabId;
+ var pendingRequest = this.lookupPendingRequest(URI.asciiSpec);
+ var rawType = 1;
+ var loadInfo = channel.loadInfo;
+
+ // https://github.com/gorhill/uMatrix/issues/390#issuecomment-155717004
+ if ( loadInfo ) {
+ rawType = loadInfo.externalContentPolicyType !== undefined ?
+ loadInfo.externalContentPolicyType :
+ loadInfo.contentPolicyType;
+ if ( !rawType ) {
+ rawType = 1;
+ }
+ }
+
+ if ( pendingRequest !== null ) {
+ tabId = pendingRequest.tabId;
+ // https://github.com/gorhill/uBlock/issues/654
+ // Use the request type from the HTTP observer point of view.
+ if ( rawType !== 1 ) {
+ pendingRequest.rawType = rawType;
+ } else {
+ rawType = pendingRequest.rawType;
+ }
+ } else {
+ tabId = this.tabIdFromChannel(channel);
+ }
+
+ if ( this.handleRequest(channel, URI, tabId, rawType) ) {
+ return;
+ }
+
+ if ( channel instanceof Ci.nsIWritablePropertyBag === false ) {
+ return;
+ }
+
+ // Carry data for behind-the-scene redirects
+ channel.setProperty(this.REQDATAKEY, [tabId, rawType]);
+ },
+
+ // contentPolicy.shouldLoad doesn't detect redirects, this needs to be used
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
+ // If error thrown, the redirect will fail
+ try {
+ var URI = newChannel.URI;
+ if ( !URI.schemeIs('http') && !URI.schemeIs('https') ) {
+ return;
+ }
+
+ if ( newChannel instanceof Ci.nsIWritablePropertyBag === false ) {
+ return;
+ }
+
+ var channelData = this.channelDataFromChannel(oldChannel);
+ if ( channelData === null ) {
+ return;
+ }
+
+ // Carry the data on in case of multiple redirects
+ newChannel.setProperty(this.REQDATAKEY, channelData);
+ } catch (ex) {
+ // console.error(ex);
+ } finally {
+ callback.onRedirectVerifyCallback(this.ACCEPT);
+ }
+ }
+};
+
+/******************************************************************************/
+
+vAPI.net = {};
+
+/******************************************************************************/
+
+vAPI.net.registerListeners = function() {
+ this.onBeforeRequest.types = this.onBeforeRequest.types ?
+ new Set(this.onBeforeRequest.types) :
+ null;
+ this.onBeforeSendHeaders.types = this.onBeforeSendHeaders.types ?
+ new Set(this.onBeforeSendHeaders.types) :
+ null;
+
+ var shouldLoadListenerMessageName = location.host + ':shouldLoad';
+ var shouldLoadListener = function(e) {
+ var details = e.data;
+ var pendingReq = httpObserver.createPendingRequest(details.url);
+ pendingReq.rawType = details.rawType;
+ pendingReq.tabId = tabWatcher.tabIdFromTarget(e.target);
+ };
+
+ // https://github.com/gorhill/uMatrix/issues/200
+ // We need this only for Firefox 34 and less: the tab id is derived from
+ // the origin of the message.
+ if ( !vAPI.modernFirefox ) {
+ vAPI.messaging.globalMessageManager.addMessageListener(
+ shouldLoadListenerMessageName,
+ shouldLoadListener
+ );
+ }
+
+ httpObserver.register();
+
+ cleanupTasks.push(function() {
+ if ( !vAPI.modernFirefox ) {
+ vAPI.messaging.globalMessageManager.removeMessageListener(
+ shouldLoadListenerMessageName,
+ shouldLoadListener
+ );
+ }
+
+ httpObserver.unregister();
+ });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.toolbarButton = {
+ id: location.host + '-button',
+ type: 'view',
+ viewId: location.host + '-panel',
+ label: vAPI.app.name,
+ tooltiptext: vAPI.app.name,
+ tabs: {/*tabId: {badge: 0, img: boolean}*/},
+ init: null,
+ codePath: ''
+};
+
+/******************************************************************************/
+
+// Non-Fennec: common code paths.
+
+(function() {
+ if ( vAPI.fennec ) {
+ return;
+ }
+
+ var tbb = vAPI.toolbarButton;
+ var popupCommittedWidth = 0;
+ var popupCommittedHeight = 0;
+
+ tbb.onViewShowing = function({target}) {
+ popupCommittedWidth = popupCommittedHeight = 0;
+ target.firstChild.setAttribute('src', vAPI.getURL('popup.html'));
+ };
+
+ tbb.onViewHiding = function({target}) {
+ target.parentNode.style.maxWidth = '';
+ target.firstChild.setAttribute('src', 'about:blank');
+ };
+
+ tbb.updateState = function(win, tabId) {
+ var button = win.document.getElementById(this.id);
+
+ if ( !button ) {
+ return;
+ }
+
+ var icon = this.tabs[tabId];
+ button.setAttribute('badge', icon && icon.badge || '');
+ button.classList.toggle('off', !icon || !icon.img);
+
+ var iconId = icon && icon.img ? icon.img : 'off';
+ icon = 'url(' + vAPI.getURL('img/browsericons/icon19-' + iconId + '.png') + ')';
+ button.style.listStyleImage = icon;
+ };
+
+ tbb.populatePanel = function(doc, panel) {
+ panel.setAttribute('id', this.viewId);
+
+ var iframe = doc.createElement('iframe');
+ iframe.setAttribute('type', 'content');
+
+ panel.appendChild(iframe);
+
+ var toPx = function(pixels) {
+ return pixels.toString() + 'px';
+ };
+
+ var scrollBarWidth = 0;
+ var resizeTimer = null;
+
+ var resizePopupDelayed = function(attempts) {
+ if ( resizeTimer !== null ) {
+ return false;
+ }
+
+ // Sanity check
+ attempts = (attempts || 0) + 1;
+ if ( attempts > 1/*000*/ ) {
+ //console.error('uMatrix> resizePopupDelayed: giving up after too many attempts');
+ return false;
+ }
+
+ resizeTimer = vAPI.setTimeout(resizePopup, 10, attempts);
+ return true;
+ };
+
+ var resizePopup = function(attempts) {
+ resizeTimer = null;
+
+ panel.parentNode.style.maxWidth = 'none';
+ var body = iframe.contentDocument.body;
+
+ // https://github.com/gorhill/uMatrix/issues/301
+ // Don't resize if committed size did not change.
+ if (
+ popupCommittedWidth === body.clientWidth &&
+ popupCommittedHeight === body.clientHeight
+ ) {
+ return;
+ }
+
+ // We set a limit for height
+ var height = Math.min(body.clientHeight, 600);
+
+ // https://github.com/chrisaljoudi/uBlock/issues/730
+ // Voodoo programming: this recipe works
+ panel.style.setProperty('height', toPx(height));
+ iframe.style.setProperty('height', toPx(height));
+
+ // Adjust width for presence/absence of vertical scroll bar which may
+ // have appeared as a result of last operation.
+ var contentWindow = iframe.contentWindow;
+ var width = body.clientWidth;
+ if ( contentWindow.scrollMaxY !== 0 ) {
+ width += scrollBarWidth;
+ }
+ panel.style.setProperty('width', toPx(width));
+
+ // scrollMaxX should always be zero once we know the scrollbar width
+ if ( contentWindow.scrollMaxX !== 0 ) {
+ scrollBarWidth = contentWindow.scrollMaxX;
+ width += scrollBarWidth;
+ panel.style.setProperty('width', toPx(width));
+ }
+
+ if ( iframe.clientHeight !== height || panel.clientWidth !== width ) {
+ if ( resizePopupDelayed(attempts) ) {
+ return;
+ }
+ // resizePopupDelayed won't be called again, so commit
+ // dimentsions.
+ }
+
+ popupCommittedWidth = body.clientWidth;
+ popupCommittedHeight = body.clientHeight;
+ };
+
+ var onResizeRequested = function() {
+ var body = iframe.contentDocument.body;
+ if ( body.getAttribute('data-resize-popup') !== 'true' ) {
+ return;
+ }
+ body.removeAttribute('data-resize-popup');
+ resizePopupDelayed();
+ };
+
+ var onPopupReady = function() {
+ var win = this.contentWindow;
+
+ if ( !win || win.location.host !== location.host ) {
+ return;
+ }
+
+ if ( typeof tbb.onBeforePopupReady === 'function' ) {
+ tbb.onBeforePopupReady.call(this);
+ }
+
+ resizePopupDelayed();
+
+ var body = win.document.body;
+ body.removeAttribute('data-resize-popup');
+ var mutationObserver = new win.MutationObserver(onResizeRequested);
+ mutationObserver.observe(body, {
+ attributes: true,
+ attributeFilter: [ 'data-resize-popup' ]
+ });
+ };
+
+ iframe.addEventListener('load', onPopupReady, true);
+ };
+})();
+
+/******************************************************************************/
+
+// Firefox 28 and less
+
+(function() {
+ var tbb = vAPI.toolbarButton;
+ if ( tbb.init !== null ) {
+ return;
+ }
+ var CustomizableUI = null;
+ var forceLegacyToolbarButton = vAPI.localStorage.getBool('forceLegacyToolbarButton');
+ if ( !forceLegacyToolbarButton ) {
+ try {
+ CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
+ } catch (ex) {
+ }
+ }
+ if ( CustomizableUI !== null ) {
+ return;
+ }
+
+ tbb.codePath = 'legacy';
+ tbb.id = 'umatrix-legacy-button'; // NOTE: must match legacy-toolbar-button.css
+ tbb.viewId = tbb.id + '-panel';
+
+ var styleSheetUri = null;
+
+ var createToolbarButton = function(window) {
+ var document = window.document;
+
+ var toolbarButton = document.createElement('toolbarbutton');
+ toolbarButton.setAttribute('id', tbb.id);
+ // type = panel would be more accurate, but doesn't look as good
+ toolbarButton.setAttribute('type', 'menu');
+ toolbarButton.setAttribute('removable', 'true');
+ toolbarButton.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional');
+ toolbarButton.setAttribute('label', tbb.label);
+ toolbarButton.setAttribute('tooltiptext', tbb.label);
+
+ var toolbarButtonPanel = document.createElement('panel');
+ // NOTE: Setting level to parent breaks the popup for PaleMoon under
+ // linux (mouse pointer misaligned with content). For some reason.
+ // toolbarButtonPanel.setAttribute('level', 'parent');
+ tbb.populatePanel(document, toolbarButtonPanel);
+ toolbarButtonPanel.addEventListener('popupshowing', tbb.onViewShowing);
+ toolbarButtonPanel.addEventListener('popuphiding', tbb.onViewHiding);
+ toolbarButton.appendChild(toolbarButtonPanel);
+
+ return toolbarButton;
+ };
+
+ var addLegacyToolbarButton = function(window) {
+ // uMatrix's stylesheet lazily added.
+ if ( styleSheetUri === null ) {
+ var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
+ .getService(Ci.nsIStyleSheetService);
+ styleSheetUri = Services.io.newURI(vAPI.getURL("css/legacy-toolbar-button.css"), null, null);
+
+ // Register global so it works in all windows, including palette
+ if ( !sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) {
+ sss.loadAndRegisterSheet(styleSheetUri, sss.AUTHOR_SHEET);
+ }
+ }
+
+ var document = window.document;
+
+ // https://github.com/gorhill/uMatrix/issues/357
+ // Already installed?
+ if ( document.getElementById(tbb.id) !== null ) {
+ return;
+ }
+
+ var toolbox = document.getElementById('navigator-toolbox') ||
+ document.getElementById('mail-toolbox');
+ if ( toolbox === null ) {
+ return;
+ }
+
+ var toolbarButton = createToolbarButton(window);
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/toolbarpalette
+ var palette = toolbox.palette;
+ if ( palette && palette.querySelector('#' + tbb.id) === null ) {
+ palette.appendChild(toolbarButton);
+ }
+
+ // Find the place to put the button.
+ // Pale Moon: `toolbox.externalToolbars` can be undefined. Seen while
+ // testing popup test number 3:
+ // http://raymondhill.net/ublock/popup.html
+ var toolbars = toolbox.externalToolbars ? toolbox.externalToolbars.slice() : [];
+ for ( var child of toolbox.children ) {
+ if ( child.localName === 'toolbar' ) {
+ toolbars.push(child);
+ }
+ }
+
+ for ( var toolbar of toolbars ) {
+ var currentsetString = toolbar.getAttribute('currentset');
+ if ( !currentsetString ) {
+ continue;
+ }
+ var currentset = currentsetString.split(/\s*,\s*/);
+ var index = currentset.indexOf(tbb.id);
+ if ( index === -1 ) {
+ continue;
+ }
+ // This can occur with Pale Moon:
+ // "TypeError: toolbar.insertItem is not a function"
+ if ( typeof toolbar.insertItem !== 'function' ) {
+ continue;
+ }
+ // Found our button on this toolbar - but where on it?
+ var before = null;
+ for ( var i = index + 1; i < currentset.length; i++ ) {
+ before = toolbar.querySelector('[id="' + currentset[i] + '"]');
+ if ( before !== null ) {
+ break;
+ }
+ }
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/insertItem
+ toolbar.insertItem(tbb.id, before);
+ break;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/763
+ // We are done if our toolbar button is already installed in one of the
+ // toolbar.
+ if ( palette !== null && toolbarButton.parentElement !== palette ) {
+ return;
+ }
+
+ // No button yet so give it a default location. If forcing the button,
+ // just put in in the palette rather than on any specific toolbar (who
+ // knows what toolbars will be available or visible!)
+ var navbar = document.getElementById('nav-bar');
+ if ( navbar !== null && !vAPI.localStorage.getBool('legacyToolbarButtonAdded') ) {
+ // https://github.com/gorhill/uBlock/issues/264
+ // Find a child customizable palette, if any.
+ navbar = navbar.querySelector('.customization-target') || navbar;
+ navbar.appendChild(toolbarButton);
+ navbar.setAttribute('currentset', navbar.currentSet);
+ document.persist(navbar.id, 'currentset');
+ vAPI.localStorage.setBool('legacyToolbarButtonAdded', 'true');
+ }
+ };
+
+ var canAddLegacyToolbarButton = function(window) {
+ var document = window.document;
+ if (
+ !document ||
+ document.readyState !== 'complete' ||
+ document.getElementById('nav-bar') === null
+ ) {
+ return false;
+ }
+ var toolbox = document.getElementById('navigator-toolbox') ||
+ document.getElementById('mail-toolbox');
+ return toolbox !== null && !!toolbox.palette;
+ };
+
+ var onPopupCloseRequested = function({target}) {
+ var document = target.ownerDocument;
+ if ( !document ) {
+ return;
+ }
+ var toolbarButtonPanel = document.getElementById(tbb.viewId);
+ if ( toolbarButtonPanel === null ) {
+ return;
+ }
+ // `hidePopup` reported as not existing while testing legacy button
+ // on FF 41.0.2.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1151796
+ if ( typeof toolbarButtonPanel.hidePopup === 'function' ) {
+ toolbarButtonPanel.hidePopup();
+ }
+ };
+
+ var shutdown = function() {
+ for ( var win of winWatcher.getWindows() ) {
+ var toolbarButton = win.document.getElementById(tbb.id);
+ if ( toolbarButton ) {
+ toolbarButton.parentNode.removeChild(toolbarButton);
+ }
+ }
+
+ if ( styleSheetUri !== null ) {
+ var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
+ .getService(Ci.nsIStyleSheetService);
+ if ( sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) {
+ sss.unregisterSheet(styleSheetUri, sss.AUTHOR_SHEET);
+ }
+ styleSheetUri = null;
+ }
+
+ vAPI.messaging.globalMessageManager.removeMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+ };
+
+ tbb.attachToNewWindow = function(win) {
+ deferUntil(
+ canAddLegacyToolbarButton.bind(null, win),
+ addLegacyToolbarButton.bind(null, win)
+ );
+ };
+
+ tbb.init = function() {
+ vAPI.messaging.globalMessageManager.addMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+
+ cleanupTasks.push(shutdown);
+ };
+})();
+
+/******************************************************************************/
+
+// Firefox Australis < 36.
+
+(function() {
+ var tbb = vAPI.toolbarButton;
+ if ( tbb.init !== null ) {
+ return;
+ }
+ if ( Services.vc.compare(Services.appinfo.platformVersion, '36.0') >= 0 ) {
+ return null;
+ }
+ if ( vAPI.localStorage.getBool('forceLegacyToolbarButton') ) {
+ return null;
+ }
+ var CustomizableUI = null;
+ try {
+ CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
+ } catch (ex) {
+ }
+ if ( CustomizableUI === null ) {
+ return;
+ }
+ tbb.codePath = 'australis';
+ tbb.CustomizableUI = CustomizableUI;
+ tbb.defaultArea = CustomizableUI.AREA_NAVBAR;
+
+ var styleURI = null;
+
+ var onPopupCloseRequested = function({target}) {
+ if ( typeof tbb.closePopup === 'function' ) {
+ tbb.closePopup(target);
+ }
+ };
+
+ var shutdown = function() {
+ CustomizableUI.destroyWidget(tbb.id);
+
+ for ( var win of winWatcher.getWindows() ) {
+ var panel = win.document.getElementById(tbb.viewId);
+ panel.parentNode.removeChild(panel);
+ win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .removeSheet(styleURI, 1);
+ }
+
+ vAPI.messaging.globalMessageManager.removeMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+ };
+
+ tbb.onBeforeCreated = function(doc) {
+ var panel = doc.createElement('panelview');
+
+ this.populatePanel(doc, panel);
+
+ doc.getElementById('PanelUI-multiView').appendChild(panel);
+
+ doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .loadSheet(styleURI, 1);
+ };
+
+ tbb.onBeforePopupReady = function() {
+ // https://github.com/gorhill/uBlock/issues/83
+ // Add `portrait` class if width is constrained.
+ try {
+ this.contentDocument.body.classList.toggle(
+ 'portrait',
+ CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL
+ );
+ } catch (ex) {
+ /* noop */
+ }
+ };
+
+ tbb.init = function() {
+ vAPI.messaging.globalMessageManager.addMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+
+ var style = [
+ '#' + this.id + '.off {',
+ 'list-style-image: url(',
+ vAPI.getURL('img/browsericons/icon19-off.png'),
+ ');',
+ '}',
+ '#' + this.id + ' {',
+ 'list-style-image: url(',
+ vAPI.getURL('img/browsericons/icon19.png'),
+ ');',
+ '}',
+ '#' + this.viewId + ', #' + this.viewId + ' > iframe {',
+ 'width: 160px;',
+ 'height: 290px;',
+ 'overflow: hidden !important;',
+ '}',
+ '#' + this.id + '[badge]:not([badge=""])::after {',
+ 'position: absolute;',
+ 'margin-left: -16px;',
+ 'margin-top: 3px;',
+ 'padding: 1px 2px;',
+ 'font-size: 9px;',
+ 'font-weight: bold;',
+ 'color: #fff;',
+ 'background: #000;',
+ 'content: attr(badge);',
+ '}'
+ ];
+
+ styleURI = Services.io.newURI(
+ 'data:text/css,' + encodeURIComponent(style.join('')),
+ null,
+ null
+ );
+
+ this.closePopup = function(tabBrowser) {
+ CustomizableUI.hidePanelForNode(
+ tabBrowser.ownerDocument.getElementById(this.viewId)
+ );
+ };
+
+ CustomizableUI.createWidget(this);
+
+ cleanupTasks.push(shutdown);
+ };
+})();
+
+/******************************************************************************/
+
+// Firefox Australis >= 36.
+
+(function() {
+ var tbb = vAPI.toolbarButton;
+ if ( tbb.init !== null ) {
+ return;
+ }
+ if ( Services.vc.compare(Services.appinfo.platformVersion, '36.0') < 0 ) {
+ return null;
+ }
+ if ( vAPI.localStorage.getBool('forceLegacyToolbarButton') ) {
+ return null;
+ }
+ var CustomizableUI = null;
+ try {
+ CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
+ } catch (ex) {
+ }
+ if ( CustomizableUI === null ) {
+ return null;
+ }
+ tbb.codePath = 'australis';
+ tbb.CustomizableUI = CustomizableUI;
+ tbb.defaultArea = CustomizableUI.AREA_NAVBAR;
+
+ var CUIEvents = {};
+
+ var badgeCSSRules = [
+ 'background: #000',
+ 'color: #fff'
+ ].join(';');
+
+ var updateBadgeStyle = function() {
+ for ( var win of winWatcher.getWindows() ) {
+ var button = win.document.getElementById(tbb.id);
+ if ( button === null ) {
+ continue;
+ }
+ var badge = button.ownerDocument.getAnonymousElementByAttribute(
+ button,
+ 'class',
+ 'toolbarbutton-badge'
+ );
+ if ( !badge ) {
+ continue;
+ }
+
+ badge.style.cssText = badgeCSSRules;
+ }
+ };
+
+ var updateBadge = function() {
+ var wId = tbb.id;
+ var buttonInPanel = CustomizableUI.getWidget(wId).areaType === CustomizableUI.TYPE_MENU_PANEL;
+
+ for ( var win of winWatcher.getWindows() ) {
+ var button = win.document.getElementById(wId);
+ if ( button === null ) {
+ continue;
+ }
+ if ( buttonInPanel ) {
+ button.classList.remove('badged-button');
+ continue;
+ }
+ button.classList.add('badged-button');
+ }
+
+ if ( buttonInPanel ) {
+ return;
+ }
+
+ // Anonymous elements need some time to be reachable
+ vAPI.setTimeout(updateBadgeStyle, 250);
+ }.bind(CUIEvents);
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/CustomizableUI.jsm#Listeners
+ CUIEvents.onCustomizeEnd = updateBadge;
+ CUIEvents.onWidgetAdded = updateBadge;
+ CUIEvents.onWidgetUnderflow = updateBadge;
+
+ var onPopupCloseRequested = function({target}) {
+ if ( typeof tbb.closePopup === 'function' ) {
+ tbb.closePopup(target);
+ }
+ };
+
+ var shutdown = function() {
+ for ( var win of winWatcher.getWindows() ) {
+ var panel = win.document.getElementById(tbb.viewId);
+ if ( panel !== null && panel.parentNode !== null ) {
+ panel.parentNode.removeChild(panel);
+ }
+ win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .removeSheet(styleURI, 1);
+ }
+
+ CustomizableUI.removeListener(CUIEvents);
+ CustomizableUI.destroyWidget(tbb.id);
+
+ vAPI.messaging.globalMessageManager.removeMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+ };
+
+ var styleURI = null;
+
+ tbb.onBeforeCreated = function(doc) {
+ var panel = doc.createElement('panelview');
+
+ this.populatePanel(doc, panel);
+
+ doc.getElementById('PanelUI-multiView').appendChild(panel);
+
+ doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .loadSheet(styleURI, 1);
+ };
+
+ tbb.onCreated = function(button) {
+ button.setAttribute('badge', '');
+ vAPI.setTimeout(updateBadge, 250);
+ };
+
+ tbb.onBeforePopupReady = function() {
+ // https://github.com/gorhill/uBlock/issues/83
+ // Add `portrait` class if width is constrained.
+ try {
+ this.contentDocument.body.classList.toggle(
+ 'portrait',
+ CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL
+ );
+ } catch (ex) {
+ /* noop */
+ }
+ };
+
+ tbb.closePopup = function(tabBrowser) {
+ CustomizableUI.hidePanelForNode(
+ tabBrowser.ownerDocument.getElementById(tbb.viewId)
+ );
+ };
+
+ tbb.init = function() {
+ vAPI.messaging.globalMessageManager.addMessageListener(
+ location.host + ':closePopup',
+ onPopupCloseRequested
+ );
+
+ CustomizableUI.addListener(CUIEvents);
+
+ var style = [
+ '#' + this.id + '.off {',
+ 'list-style-image: url(',
+ vAPI.getURL('img/browsericons/icon19-off.png'),
+ ');',
+ '}',
+ '#' + this.id + ' {',
+ 'list-style-image: url(',
+ vAPI.getURL('img/browsericons/icon19-19.png'),
+ ');',
+ '}',
+ '#' + this.viewId + ', #' + this.viewId + ' > iframe {',
+ 'height: 290px;',
+ 'max-width: none !important;',
+ 'min-width: 0 !important;',
+ 'overflow: hidden !important;',
+ 'padding: 0 !important;',
+ 'width: 160px;',
+ '}'
+ ];
+
+ styleURI = Services.io.newURI(
+ 'data:text/css,' + encodeURIComponent(style.join('')),
+ null,
+ null
+ );
+
+ CustomizableUI.createWidget(this);
+
+ cleanupTasks.push(shutdown);
+ };
+})();
+
+/******************************************************************************/
+
+// No toolbar button.
+
+(function() {
+ // Just to ensure the number of cleanup tasks is as expected: toolbar
+ // button code is one single cleanup task regardless of platform.
+ if ( vAPI.toolbarButton.init === null ) {
+ cleanupTasks.push(function(){});
+ }
+})();
+
+/******************************************************************************/
+
+if ( vAPI.toolbarButton.init !== null ) {
+ vAPI.toolbarButton.init();
+}
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.contextMenu = {
+ contextMap: {
+ frame: 'inFrame',
+ link: 'onLink',
+ image: 'onImage',
+ audio: 'onAudio',
+ video: 'onVideo',
+ editable: 'onEditableArea'
+ }
+};
+
+/******************************************************************************/
+
+vAPI.contextMenu.displayMenuItem = function({target}) {
+ var doc = target.ownerDocument;
+ var gContextMenu = doc.defaultView.gContextMenu;
+ if ( !gContextMenu.browser ) {
+ return;
+ }
+
+ var menuitem = doc.getElementById(vAPI.contextMenu.menuItemId);
+ var currentURI = gContextMenu.browser.currentURI;
+
+ // https://github.com/chrisaljoudi/uBlock/issues/105
+ // TODO: Should the element picker works on any kind of pages?
+ if ( !currentURI.schemeIs('http') && !currentURI.schemeIs('https') ) {
+ menuitem.setAttribute('hidden', true);
+ return;
+ }
+
+ var ctx = vAPI.contextMenu.contexts;
+
+ if ( !ctx ) {
+ menuitem.setAttribute('hidden', false);
+ return;
+ }
+
+ var ctxMap = vAPI.contextMenu.contextMap;
+
+ for ( var context of ctx ) {
+ if (
+ context === 'page' &&
+ !gContextMenu.onLink &&
+ !gContextMenu.onImage &&
+ !gContextMenu.onEditableArea &&
+ !gContextMenu.inFrame &&
+ !gContextMenu.onVideo &&
+ !gContextMenu.onAudio
+ ) {
+ menuitem.setAttribute('hidden', false);
+ return;
+ }
+
+ if (
+ ctxMap.hasOwnProperty(context) &&
+ gContextMenu[ctxMap[context]]
+ ) {
+ menuitem.setAttribute('hidden', false);
+ return;
+ }
+ }
+
+ menuitem.setAttribute('hidden', true);
+};
+
+/******************************************************************************/
+
+vAPI.contextMenu.register = (function() {
+ var register = function(doc) {
+ if ( !this.menuItemId ) {
+ return;
+ }
+
+ // Already installed?
+ if ( doc.getElementById(this.menuItemId) !== null ) {
+ return;
+ }
+
+ var contextMenu = doc.getElementById('contentAreaContextMenu');
+ var menuitem = doc.createElement('menuitem');
+ menuitem.setAttribute('id', this.menuItemId);
+ menuitem.setAttribute('label', this.menuLabel);
+ menuitem.setAttribute('image', vAPI.getURL('img/browsericons/icon19-19.png'));
+ menuitem.setAttribute('class', 'menuitem-iconic');
+ menuitem.addEventListener('command', this.onCommand);
+ contextMenu.addEventListener('popupshowing', this.displayMenuItem);
+ contextMenu.insertBefore(menuitem, doc.getElementById('inspect-separator'));
+ };
+
+ // https://github.com/gorhill/uBlock/issues/906
+ // Be sure document.readyState is 'complete': it could happen at launch
+ // time that we are called by vAPI.contextMenu.create() directly before
+ // the environment is properly initialized.
+ var registerSafely = function(doc, tryCount) {
+ if ( doc.readyState === 'complete' ) {
+ register.call(this, doc);
+ return;
+ }
+ if ( typeof tryCount !== 'number' ) {
+ tryCount = 0;
+ }
+ tryCount += 1;
+ if ( tryCount < 8 ) {
+ vAPI.setTimeout(registerSafely.bind(this, doc, tryCount), 200);
+ }
+ };
+
+ return registerSafely;
+})();
+
+/******************************************************************************/
+
+vAPI.contextMenu.unregister = function(doc) {
+ if ( !this.menuItemId ) {
+ return;
+ }
+
+ var menuitem = doc.getElementById(this.menuItemId);
+ if ( menuitem === null ) {
+ return;
+ }
+ var contextMenu = menuitem.parentNode;
+ menuitem.removeEventListener('command', this.onCommand);
+ contextMenu.removeEventListener('popupshowing', this.displayMenuItem);
+ contextMenu.removeChild(menuitem);
+};
+
+/******************************************************************************/
+
+vAPI.contextMenu.create = function(details, callback) {
+ this.menuItemId = details.id;
+ this.menuLabel = details.title;
+ this.contexts = details.contexts;
+
+ if ( Array.isArray(this.contexts) && this.contexts.length ) {
+ this.contexts = this.contexts.indexOf('all') === -1 ? this.contexts : null;
+ } else {
+ // default in Chrome
+ this.contexts = ['page'];
+ }
+
+ this.onCommand = function() {
+ var gContextMenu = getOwnerWindow(this).gContextMenu;
+ var details = {
+ menuItemId: this.id
+ };
+
+ if ( gContextMenu.inFrame ) {
+ details.tagName = 'iframe';
+ // Probably won't work with e10s
+ details.frameUrl = gContextMenu.focusedWindow.location.href;
+ } else if ( gContextMenu.onImage ) {
+ details.tagName = 'img';
+ details.srcUrl = gContextMenu.mediaURL;
+ } else if ( gContextMenu.onAudio ) {
+ details.tagName = 'audio';
+ details.srcUrl = gContextMenu.mediaURL;
+ } else if ( gContextMenu.onVideo ) {
+ details.tagName = 'video';
+ details.srcUrl = gContextMenu.mediaURL;
+ } else if ( gContextMenu.onLink ) {
+ details.tagName = 'a';
+ details.linkUrl = gContextMenu.linkURL;
+ }
+
+ callback(details, {
+ id: tabWatcher.tabIdFromTarget(gContextMenu.browser),
+ url: gContextMenu.browser.currentURI.asciiSpec
+ });
+ };
+
+ for ( var win of winWatcher.getWindows() ) {
+ this.register(win.document);
+ }
+};
+
+/******************************************************************************/
+
+vAPI.contextMenu.remove = function() {
+ for ( var win of winWatcher.getWindows() ) {
+ this.unregister(win.document);
+ }
+
+ this.menuItemId = null;
+ this.menuLabel = null;
+ this.contexts = null;
+ this.onCommand = null;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+var optionsObserver = (function() {
+ var addonId = 'uMatrix@raymondhill.net';
+
+ var commandHandler = function() {
+ switch ( this.id ) {
+ case 'showDashboardButton':
+ vAPI.tabs.open({ url: 'dashboard.html', index: -1 });
+ break;
+ case 'showLoggerButton':
+ vAPI.tabs.open({ url: 'logger-ui.html', index: -1 });
+ break;
+ default:
+ break;
+ }
+ };
+
+ var setupOptionsButton = function(doc, id) {
+ var button = doc.getElementById(id);
+ if ( button === null ) {
+ return;
+ }
+ button.addEventListener('command', commandHandler);
+ button.label = vAPI.i18n(id);
+ };
+
+ var setupOptionsButtons = function(doc) {
+ setupOptionsButton(doc, 'showDashboardButton');
+ setupOptionsButton(doc, 'showLoggerButton');
+ };
+
+ var observer = {
+ observe: function(doc, topic, id) {
+ if ( id !== addonId ) {
+ return;
+ }
+
+ setupOptionsButtons(doc);
+ }
+ };
+
+ // https://github.com/gorhill/uBlock/issues/948
+ // Older versions of Firefox can throw here when looking up `currentURI`.
+
+ var canInit = function() {
+ try {
+ var tabBrowser = tabWatcher.currentBrowser();
+ return tabBrowser &&
+ tabBrowser.currentURI &&
+ tabBrowser.currentURI.spec === 'about:addons' &&
+ tabBrowser.contentDocument &&
+ tabBrowser.contentDocument.readyState === 'complete';
+ } catch (ex) {
+ }
+ };
+
+ // Manually add the buttons if the `about:addons` page is already opened.
+
+ var init = function() {
+ if ( canInit() ) {
+ setupOptionsButtons(tabWatcher.currentBrowser().contentDocument);
+ }
+ };
+
+ var unregister = function() {
+ Services.obs.removeObserver(observer, 'addon-options-displayed');
+ };
+
+ var register = function() {
+ Services.obs.addObserver(observer, 'addon-options-displayed', false);
+ cleanupTasks.push(unregister);
+ deferUntil(canInit, init, { next: 463 });
+ };
+
+ return {
+ register: register,
+ unregister: unregister
+ };
+})();
+
+optionsObserver.register();
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.lastError = function() {
+ return null;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// This is called only once, when everything has been loaded in memory after
+// the extension was launched. It can be used to inject content scripts
+// in already opened web pages, to remove whatever nuisance could make it to
+// the web pages before uBlock was ready.
+
+vAPI.onLoadAllCompleted = function() {
+ for ( var browser of tabWatcher.browsers() ) {
+ browser.messageManager.sendAsyncMessage(
+ location.host + '-load-completed'
+ );
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Likelihood is that we do not have to punycode: given punycode overhead,
+// it's faster to check and skip than do it unconditionally all the time.
+
+var punycodeHostname = punycode.toASCII;
+var isNotASCII = /[^\x21-\x7F]/;
+
+vAPI.punycodeHostname = function(hostname) {
+ return isNotASCII.test(hostname) ? punycodeHostname(hostname) : hostname;
+};
+
+vAPI.punycodeURL = function(url) {
+ if ( isNotASCII.test(url) ) {
+ return Services.io.newURI(url, null, null).asciiSpec;
+ }
+ return url;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.cloud = (function() {
+ var extensionBranchPath = 'extensions.' + location.host;
+ var cloudBranchPath = extensionBranchPath + '.cloudStorage';
+
+ // https://github.com/gorhill/uBlock/issues/80#issuecomment-132081658
+ // We must use get/setComplexValue in order to properly handle strings
+ // with unicode characters.
+ var iss = Ci.nsISupportsString;
+ var argstr = Components.classes['@mozilla.org/supports-string;1']
+ .createInstance(iss);
+
+ var options = {
+ defaultDeviceName: '',
+ deviceName: ''
+ };
+
+ // User-supplied device name.
+ try {
+ options.deviceName = Services.prefs
+ .getBranch(extensionBranchPath + '.')
+ .getComplexValue('deviceName', iss)
+ .data;
+ } catch(ex) {
+ }
+
+ var getDefaultDeviceName = function() {
+ var name = '';
+ try {
+ name = Services.prefs
+ .getBranch('services.sync.client.')
+ .getComplexValue('name', iss)
+ .data;
+ } catch(ex) {
+ }
+
+ return name || window.navigator.platform || window.navigator.oscpu;
+ };
+
+ var start = function(dataKeys) {
+ var extensionBranch = Services.prefs.getBranch(extensionBranchPath + '.');
+ var syncBranch = Services.prefs.getBranch('services.sync.prefs.sync.');
+
+ // Mark config entries as syncable
+ argstr.data = '';
+ var dataKey;
+ for ( var i = 0; i < dataKeys.length; i++ ) {
+ dataKey = dataKeys[i];
+ if ( extensionBranch.prefHasUserValue('cloudStorage.' + dataKey) === false ) {
+ extensionBranch.setComplexValue('cloudStorage.' + dataKey, iss, argstr);
+ }
+ syncBranch.setBoolPref(cloudBranchPath + '.' + dataKey, true);
+ }
+ };
+
+ var push = function(datakey, data, callback) {
+ var branch = Services.prefs.getBranch(cloudBranchPath + '.');
+ var bin = {
+ 'source': options.deviceName || getDefaultDeviceName(),
+ 'tstamp': Date.now(),
+ 'data': data,
+ 'size': 0
+ };
+ bin.size = JSON.stringify(bin).length;
+ argstr.data = JSON.stringify(bin);
+ branch.setComplexValue(datakey, iss, argstr);
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ };
+
+ var pull = function(datakey, callback) {
+ var result = null;
+ var branch = Services.prefs.getBranch(cloudBranchPath + '.');
+ try {
+ var json = branch.getComplexValue(datakey, iss).data;
+ if ( typeof json === 'string' ) {
+ result = JSON.parse(json);
+ }
+ } catch(ex) {
+ }
+ callback(result);
+ };
+
+ var getOptions = function(callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+ options.defaultDeviceName = getDefaultDeviceName();
+ callback(options);
+ };
+
+ var setOptions = function(details, callback) {
+ if ( typeof details !== 'object' || details === null ) {
+ return;
+ }
+
+ var branch = Services.prefs.getBranch(extensionBranchPath + '.');
+
+ if ( typeof details.deviceName === 'string' ) {
+ argstr.data = details.deviceName;
+ branch.setComplexValue('deviceName', iss, argstr);
+ options.deviceName = details.deviceName;
+ }
+
+ getOptions(callback);
+ };
+
+ return {
+ start: start,
+ push: push,
+ pull: pull,
+ getOptions: getOptions,
+ setOptions: setOptions
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.browserData = {};
+
+/******************************************************************************/
+
+// https://developer.mozilla.org/en-US/docs/HTTP_Cache
+
+vAPI.browserData.clearCache = function(callback) {
+ // PURGE_DISK_DATA_ONLY:1
+ // PURGE_DISK_ALL:2
+ // PURGE_EVERYTHING:3
+ // However I verified that no argument does clear the cache data.
+ // There is no cache2 for older versions of Firefox.
+ if ( Services.cache2 ) {
+ Services.cache2.clear();
+ } else if ( Services.cache ) {
+ Services.cache.evictEntries(Services.cache.STORE_ON_DISK);
+ }
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+};
+
+/******************************************************************************/
+
+vAPI.browserData.clearOrigin = function(/* domain */) {
+ // TODO
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookieManager2
+// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookie2
+// https://developer.mozilla.org/en-US/docs/Observer_Notifications#Cookies
+
+vAPI.cookies = {};
+
+/******************************************************************************/
+
+vAPI.cookies.CookieEntry = function(ffCookie) {
+ this.domain = ffCookie.host;
+ this.name = ffCookie.name;
+ this.path = ffCookie.path;
+ this.secure = ffCookie.isSecure === true;
+ this.session = ffCookie.expires === 0;
+ this.value = ffCookie.value;
+};
+
+/******************************************************************************/
+
+vAPI.cookies.start = function() {
+ Services.obs.addObserver(this, 'cookie-changed', false);
+ Services.obs.addObserver(this, 'private-cookie-changed', false);
+ cleanupTasks.push(this.stop.bind(this));
+};
+
+/******************************************************************************/
+
+vAPI.cookies.stop = function() {
+ Services.obs.removeObserver(this, 'cookie-changed');
+ Services.obs.removeObserver(this, 'private-cookie-changed');
+};
+
+/******************************************************************************/
+
+vAPI.cookies.observe = function(subject, topic, reason) {
+ //if ( topic !== 'cookie-changed' && topic !== 'private-cookie-changed' ) {
+ // return;
+ //}
+ //
+ if ( reason === 'cleared' && typeof this.onAllRemoved === 'function' ) {
+ this.onAllRemoved();
+ return;
+ }
+ if ( subject === null ) {
+ return;
+ }
+ if ( subject instanceof Ci.nsICookie2 === false ) {
+ try {
+ subject = subject.QueryInterface(Ci.nsICookie2);
+ } catch (ex) {
+ return;
+ }
+ }
+ if ( reason === 'deleted' ) {
+ if ( typeof this.onRemoved === 'function' ) {
+ this.onRemoved(new this.CookieEntry(subject));
+ }
+ return;
+ }
+ if ( typeof this.onChanged === 'function' ) {
+ this.onChanged(new this.CookieEntry(subject));
+ }
+};
+
+/******************************************************************************/
+
+// Meant and expected to be asynchronous.
+
+vAPI.cookies.getAll = function(callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+ var onAsync = function() {
+ var out = [];
+ var enumerator = Services.cookies.enumerator;
+ var ffcookie;
+ while ( enumerator.hasMoreElements() ) {
+ ffcookie = enumerator.getNext();
+ if ( ffcookie instanceof Ci.nsICookie ) {
+ out.push(new this.CookieEntry(ffcookie));
+ }
+ }
+ callback(out);
+ };
+ vAPI.setTimeout(onAsync.bind(this), 0);
+};
+
+/******************************************************************************/
+
+vAPI.cookies.remove = function(details, callback) {
+ var uri = Services.io.newURI(details.url, null, null);
+ var cookies = Services.cookies;
+ cookies.remove(uri.asciiHost, details.name, uri.path, false, {});
+ cookies.remove( '.' + uri.asciiHost, details.name, uri.path, false, {});
+ if ( typeof callback === 'function' ) {
+ callback({
+ domain: uri.asciiHost,
+ name: details.name,
+ path: uri.path
+ });
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/js/vapi-client.js b/js/vapi-client.js
new file mode 100644
index 0000000..7f9521d
--- /dev/null
+++ b/js/vapi-client.js
@@ -0,0 +1,226 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* jshint esnext: true */
+/* global addMessageListener, removeMessageListener, sendAsyncMessage */
+
+// For non background pages
+
+'use strict';
+
+/******************************************************************************/
+
+(function(self) {
+
+/******************************************************************************/
+
+// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10
+if ( self.vAPI === undefined || self.vAPI.uMatrix !== true ) {
+ self.vAPI = { uMatrix: true };
+}
+
+var vAPI = self.vAPI;
+vAPI.firefox = true;
+vAPI.sessionId = String.fromCharCode(Date.now() % 25 + 97) +
+ Math.random().toString(36).slice(2);
+
+/******************************************************************************/
+
+vAPI.setTimeout = vAPI.setTimeout || function(callback, delay) {
+ return setTimeout(function() { callback(); }, delay);
+};
+
+/******************************************************************************/
+
+vAPI.shutdown = (function() {
+ var jobs = [];
+
+ var add = function(job) {
+ jobs.push(job);
+ };
+
+ var exec = function() {
+ //console.debug('Shutting down...');
+ var job;
+ while ( (job = jobs.pop()) ) {
+ job();
+ }
+ };
+
+ return {
+ add: add,
+ exec: exec
+ };
+})();
+
+/******************************************************************************/
+
+vAPI.messaging = {
+ listeners: new Set(),
+ pending: new Map(),
+ requestId: 1,
+ connected: false,
+
+ setup: function() {
+ this.addListener(this.builtinListener);
+ if ( this.toggleListenerCallback === null ) {
+ this.toggleListenerCallback = this.toggleListener.bind(this);
+ }
+ window.addEventListener('pagehide', this.toggleListenerCallback, true);
+ window.addEventListener('pageshow', this.toggleListenerCallback, true);
+ },
+
+ shutdown: function() {
+ if ( this.toggleListenerCallback !== null ) {
+ window.removeEventListener('pagehide', this.toggleListenerCallback, true);
+ window.removeEventListener('pageshow', this.toggleListenerCallback, true);
+ }
+ this.removeAllListeners();
+ //service pending callbacks
+ var pending = this.pending;
+ this.pending.clear();
+ for ( var callback of pending.values() ) {
+ if ( typeof callback === 'function' ) {
+ callback(null);
+ }
+ }
+ },
+
+ connect: function() {
+ if ( !this.connected ) {
+ if ( this.messageListenerCallback === null ) {
+ this.messageListenerCallback = this.messageListener.bind(this);
+ }
+ addMessageListener(this.messageListenerCallback);
+ this.connected = true;
+ }
+ },
+
+ disconnect: function() {
+ if ( this.connected ) {
+ removeMessageListener();
+ this.connected = false;
+ }
+ },
+
+ messageListener: function(msg) {
+ var details = JSON.parse(msg);
+ if ( !details ) {
+ return;
+ }
+
+ if ( details.broadcast ) {
+ this.sendToListeners(details.msg);
+ return;
+ }
+
+ if ( details.requestId ) {
+ var listener = this.pending.get(details.requestId);
+ if ( listener !== undefined ) {
+ this.pending.delete(details.requestId);
+ listener(details.msg);
+ return;
+ }
+ }
+ },
+ messageListenerCallback: null,
+
+ builtinListener: function(msg) {
+ if ( typeof msg.cmd === 'string' && msg.cmd === 'injectScript' ) {
+ var details = msg.details;
+ if ( !details.allFrames && window !== window.top ) {
+ return;
+ }
+ self.injectScript(details.file);
+ }
+ },
+
+ send: function(channelName, message, callback) {
+ this.connect()
+
+ message = {
+ channelName: self._sandboxId_ + '|' + channelName,
+ msg: message
+ };
+
+ if ( callback ) {
+ message.requestId = this.requestId++;
+ this.pending.set(message.requestId, callback);
+ }
+
+ sendAsyncMessage('umatrix:background', message);
+ },
+
+ toggleListener: function({type, persisted}) {
+ if ( type === 'pagehide' && !persisted ) {
+ vAPI.shutdown.exec();
+ this.shutdown();
+ return;
+ }
+
+ if ( type === 'pagehide' ) {
+ this.disconnect();
+ } else /* if ( type === 'pageshow' ) */ {
+ this.connect();
+ }
+ },
+ toggleListenerCallback: null,
+
+ sendToListeners: function(msg) {
+ for ( var listener of this.listeners ) {
+ listener(msg);
+ }
+ },
+
+ addListener: function(listener) {
+ this.listeners.add(listener);
+ this.connect()
+ },
+
+ removeListener: function(listener) {
+ this.listeners.delete(listener);
+ },
+
+ removeAllListeners: function() {
+ this.disconnect();
+ this.listeners.clear();;
+ }
+};
+
+vAPI.messaging.setup()
+
+/******************************************************************************/
+
+// No need to have vAPI client linger around after shutdown if
+// we are not a top window (because element picker can still
+// be injected in top window).
+if ( window !== window.top ) {
+ vAPI.shutdown.add(function() {
+ vAPI = null;
+ });
+}
+
+/******************************************************************************/
+
+})(this);
+
+/******************************************************************************/
diff --git a/js/vapi-common.js b/js/vapi-common.js
new file mode 100644
index 0000000..3b51d17
--- /dev/null
+++ b/js/vapi-common.js
@@ -0,0 +1,192 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global sendAsyncMessage */
+
+// For background page or non-background pages
+
+'use strict';
+
+/******************************************************************************/
+
+(function(self) {
+
+/******************************************************************************/
+
+const {Services} = Components.utils.import(
+ 'resource://gre/modules/Services.jsm',
+ null
+);
+
+// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10
+if ( self.vAPI === undefined || self.vAPI.uMatrix !== true ) {
+ self.vAPI = { uMatrix: true };
+}
+
+var vAPI = self.vAPI;
+
+/******************************************************************************/
+
+vAPI.setTimeout = vAPI.setTimeout || function(callback, delay, extra) {
+ return setTimeout(function(a) { callback(a); }, delay, extra);
+};
+
+/******************************************************************************/
+
+// http://www.w3.org/International/questions/qa-scripts#directions
+
+var setScriptDirection = function(language) {
+ document.body.setAttribute(
+ 'dir',
+ ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(language) !== -1 ? 'rtl' : 'ltr'
+ );
+};
+
+/******************************************************************************/
+
+vAPI.download = function(details) {
+ if ( !details.url ) {
+ return;
+ }
+
+ var a = document.createElement('a');
+ a.href = details.url;
+ a.setAttribute('download', details.filename || '');
+ a.dispatchEvent(new MouseEvent('click'));
+};
+
+/******************************************************************************/
+
+vAPI.insertHTML = (function() {
+ const parser = Components.classes['@mozilla.org/parserutils;1']
+ .getService(Components.interfaces.nsIParserUtils);
+
+ // https://github.com/gorhill/uBlock/issues/845
+ // Apparently dashboard pages execute with `about:blank` principal.
+
+ return function(node, html) {
+ while ( node.firstChild ) {
+ node.removeChild(node.firstChild);
+ }
+
+ node.appendChild(parser.parseFragment(
+ html,
+ parser.SanitizerAllowStyle,
+ false,
+ Services.io.newURI('about:blank', null, null),
+ document.documentElement
+ ));
+ };
+})();
+
+/******************************************************************************/
+
+vAPI.getURL = function(path) {
+ return 'chrome://' + location.host + '/content/' + path.replace(/^\/+/, '');
+};
+
+/******************************************************************************/
+
+vAPI.i18n = (function() {
+ var stringBundle = Services.strings.createBundle(
+ 'chrome://' + location.host + '/locale/messages.properties'
+ );
+
+ return function(s) {
+ try {
+ return stringBundle.GetStringFromName(s);
+ } catch (ex) {
+ return '';
+ }
+ };
+})();
+
+setScriptDirection(navigator.language);
+
+/******************************************************************************/
+
+vAPI.closePopup = function() {
+ sendAsyncMessage(location.host + ':closePopup');
+};
+
+/******************************************************************************/
+
+// A localStorage-like object which should be accessible from the
+// background page or auxiliary pages.
+// This storage is optional, but it is nice to have, for a more polished user
+// experience.
+
+vAPI.localStorage = {
+ pbName: '',
+ pb: null,
+ str: Components.classes['@mozilla.org/supports-string;1']
+ .createInstance(Components.interfaces.nsISupportsString),
+ init: function(pbName) {
+ this.pbName = pbName;
+ this.pb = Services.prefs.getBranch(pbName);
+ },
+ getItem: function(key) {
+ try {
+ return this.pb.getComplexValue(
+ key,
+ Components.interfaces.nsISupportsString
+ ).data;
+ } catch (ex) {
+ return null;
+ }
+ },
+ setItem: function(key, value) {
+ this.str.data = value;
+ this.pb.setComplexValue(
+ key,
+ Components.interfaces.nsISupportsString,
+ this.str
+ );
+ },
+ getBool: function(key) {
+ try {
+ return this.pb.getBoolPref(key);
+ } catch (ex) {
+ return null;
+ }
+ },
+ setBool: function(key, value) {
+ this.pb.setBoolPref(key, value);
+ },
+ setDefaultBool: function(key, defaultValue) {
+ Services.prefs.getDefaultBranch(this.pbName).setBoolPref(key, defaultValue);
+ },
+ removeItem: function(key) {
+ this.pb.clearUserPref(key);
+ },
+ clear: function() {
+ this.pb.deleteBranch('');
+ }
+};
+
+vAPI.localStorage.init('extensions.' + location.host + '.');
+
+/******************************************************************************/
+
+})(this);
+
+/******************************************************************************/
diff --git a/js/vapi-popup.js b/js/vapi-popup.js
new file mode 100644
index 0000000..c1fbdbb
--- /dev/null
+++ b/js/vapi-popup.js
@@ -0,0 +1,23 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* Firefox: no platform-specific code */
diff --git a/js/xal.js b/js/xal.js
new file mode 100644
index 0000000..9dba5b9
--- /dev/null
+++ b/js/xal.js
@@ -0,0 +1,72 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global chrome, µMatrix */
+
+/******************************************************************************/
+
+µMatrix.XAL = (function(){
+
+/******************************************************************************/
+
+var exports = {};
+var noopFunc = function(){};
+
+/******************************************************************************/
+
+exports.keyvalSetOne = function(key, val, callback) {
+ var bin = {};
+ bin[key] = val;
+ vAPI.storage.set(bin, callback || noopFunc);
+};
+
+/******************************************************************************/
+
+exports.keyvalGetOne = function(key, callback) {
+ vAPI.storage.get(key, callback);
+};
+
+/******************************************************************************/
+
+exports.keyvalSetMany = function(dict, callback) {
+ vAPI.storage.set(dict, callback || noopFunc);
+};
+
+/******************************************************************************/
+
+exports.keyvalRemoveOne = function(key, callback) {
+ vAPI.storage.remove(key, callback || noopFunc);
+};
+
+/******************************************************************************/
+
+exports.keyvalRemoveAll = function(callback) {
+ vAPI.storage.clear(callback || noopFunc);
+};
+
+/******************************************************************************/
+
+return exports;
+
+/******************************************************************************/
+
+})();