aboutsummaryrefslogtreecommitdiffstats
path: root/js/storage.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/storage.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/storage.js')
-rw-r--r--js/storage.js615
1 files changed, 615 insertions, 0 deletions
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;
+ }
+};