|
|
|
/*******************************************************************************
|
|
|
|
|
|
|
|
uMatrix - a Chromium browser extension to black/white list requests.
|
|
|
|
Copyright (C) 2014-present Raymond Hill
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
|
|
|
|
Home: https://github.com/gorhill/uMatrix
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* global punycode, 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 onAvailableRulesetFilesReady = function(availableRulesetFiles) {
|
|
|
|
let selectedAssetKeys = new Set();
|
|
|
|
for ( let entry of availableRulesetFiles ) {
|
|
|
|
let assetKey = entry[0];
|
|
|
|
let assetLang = entry[1].lang;
|
|
|
|
if ( assetLang === undefined ) {
|
|
|
|
selectedAssetKeys.add(assetKey);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
for ( let lang of navigator.languages ) {
|
|
|
|
if ( assetLang.indexOf(lang) !== -1 ) {
|
|
|
|
selectedAssetKeys.add(assetKey);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
µm.userSettings.selectedRecipeFiles = Array.from(selectedAssetKeys);
|
|
|
|
vAPI.storage.set({
|
|
|
|
selectedRecipeFiles: µm.userSettings.selectedRecipeFiles
|
|
|
|
});
|
|
|
|
callback(µm.userSettings);
|
|
|
|
};
|
|
|
|
|
|
|
|
var initializeSelectedRulesetFiles = function() {
|
|
|
|
if (
|
|
|
|
µm.userSettings.selectedRecipeFiles.length === 1 &&
|
|
|
|
µm.userSettings.selectedRecipeFiles[0] === ''
|
|
|
|
) {
|
|
|
|
µm.getAvailableRecipeFiles(onAvailableRulesetFilesReady);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
callback(µm.userSettings);
|
|
|
|
};
|
|
|
|
|
|
|
|
var onAvailableHostsFilesReady = function(availableHostFiles) {
|
|
|
|
µm.userSettings.selectedHostsFiles =
|
|
|
|
Array.from(availableHostFiles.keys());
|
|
|
|
vAPI.storage.set({
|
|
|
|
selectedHostsFiles: µm.userSettings.selectedHostsFiles
|
|
|
|
});
|
|
|
|
initializeSelectedRulesetFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
var migrateSelectedHostsFiles = function(bin) {
|
|
|
|
if (
|
|
|
|
bin instanceof Object === false ||
|
|
|
|
bin.liveHostsFiles instanceof Object === false
|
|
|
|
) {
|
|
|
|
µm.getAvailableHostsFiles(onAvailableHostsFilesReady);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let selectedHostsFiles = new Set();
|
|
|
|
for ( let entry of µm.toMap(bin.liveHostsFiles) ) {
|
|
|
|
if ( entry[1].off !== true ) {
|
|
|
|
selectedHostsFiles.add(entry[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
µm.userSettings.selectedHostsFiles = Array.from(selectedHostsFiles);
|
|
|
|
vAPI.storage.set({
|
|
|
|
selectedHostsFiles: µm.userSettings.selectedHostsFiles
|
|
|
|
});
|
|
|
|
initializeSelectedRulesetFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
var initializeSelectedHostsFiles = function() {
|
|
|
|
// Backward-compatibility: populate the new list selection array with
|
|
|
|
// existing data.
|
|
|
|
if (
|
|
|
|
µm.userSettings.selectedHostsFiles.length === 1 &&
|
|
|
|
µm.userSettings.selectedHostsFiles[0] === ''
|
|
|
|
) {
|
|
|
|
vAPI.storage.get('liveHostsFiles', migrateSelectedHostsFiles);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
initializeSelectedRulesetFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
var settingsLoaded = function(store) {
|
|
|
|
µm.userSettings = store;
|
|
|
|
if ( typeof µm.userSettings.externalHostsFiles === 'string' ) {
|
|
|
|
µm.userSettings.externalHostsFiles =
|
|
|
|
µm.userSettings.externalHostsFiles.length !== 0 ?
|
|
|
|
µm.userSettings.externalHostsFiles.split('\n') :
|
|
|
|
[];
|
|
|
|
}
|
|
|
|
initializeSelectedHostsFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
vAPI.storage.get(this.userSettings, settingsLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadRawSettings = function(callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
const onLoaded = bin => {
|
|
|
|
if (
|
|
|
|
bin instanceof Object === false ||
|
|
|
|
bin.rawSettings instanceof Object === false
|
|
|
|
) {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
for ( const key of Object.keys(bin.rawSettings) ) {
|
|
|
|
if (
|
|
|
|
this.rawSettings.hasOwnProperty(key) === false ||
|
|
|
|
typeof bin.rawSettings[key] !== typeof this.rawSettings[key]
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this.rawSettings[key] = bin.rawSettings[key];
|
|
|
|
}
|
|
|
|
this.rawSettingsWriteTime = Date.now();
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
vAPI.storage.get('rawSettings', onLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
µMatrix.saveRawSettings = function(rawSettings, callback) {
|
|
|
|
const keys = Object.keys(rawSettings);
|
|
|
|
if ( keys.length === 0 ) {
|
|
|
|
if ( typeof callback === 'function' ) {
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for ( const 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.saveImmediateHiddenSettings();
|
|
|
|
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() {
|
|
|
|
const out = [];
|
|
|
|
for ( const key of Object.keys(this.rawSettings).sort() ) {
|
|
|
|
out.push(key + ' ' + this.rawSettings[key]);
|
|
|
|
}
|
|
|
|
return out.join('\n');
|
|
|
|
};
|
|
|
|
|
|
|
|
// These settings must be available immediately on startup, without delay
|
|
|
|
// through the vAPI.localStorage. Add/remove settings as needed.
|
|
|
|
|
|
|
|
µMatrix.saveImmediateHiddenSettings = function() {
|
|
|
|
vAPI.localStorage.setItem(
|
|
|
|
'immediateRawSettings',
|
|
|
|
JSON.stringify({
|
|
|
|
suspendTabsUntilReady: this.rawSettings.suspendTabsUntilReady,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.saveMatrix = function() {
|
|
|
|
vAPI.storage.set({ userMatrix: this.pMatrix.toArray() });
|
|
|
|
};
|
|
|
|
|
|
|
|
µMatrix.loadMatrix = function(callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
const onLoaded = bin => {
|
|
|
|
if ( bin instanceof Object === false ) {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
if ( typeof bin.userMatrix === 'string' ) {
|
|
|
|
this.pMatrix.fromString(bin.userMatrix);
|
|
|
|
} else if ( Array.isArray(bin.userMatrix) ) {
|
|
|
|
this.pMatrix.fromArray(bin.userMatrix);
|
|
|
|
}
|
|
|
|
this.tMatrix.assign(this.pMatrix);
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
vAPI.storage.get('userMatrix', onLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadRecipes = function(reset, callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
let µm = this,
|
|
|
|
countdownCount = 0;
|
|
|
|
|
|
|
|
if ( reset ) {
|
|
|
|
µm.recipeManager.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
let recipeMetadata;
|
|
|
|
|
|
|
|
let onDone = function() {
|
|
|
|
vAPI.messaging.broadcast({ what: 'loadRecipeFilesCompleted' });
|
|
|
|
µm.getBytesInUse();
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
let onLoaded = function(details) {
|
|
|
|
if ( details.content ) {
|
|
|
|
let entry = recipeMetadata.get(details.assetKey);
|
|
|
|
if ( entry.submitter === 'user' ) {
|
|
|
|
let match = /^! +Title: *(.+)$/im.exec(
|
|
|
|
details.content.slice(2048)
|
|
|
|
);
|
|
|
|
if ( match !== null && match[1] !== entry.title ) {
|
|
|
|
µm.assets.registerAssetSource(
|
|
|
|
details.assetKey,
|
|
|
|
{ title: match[1] }
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
µm.recipeManager.fromString(details.content);
|
|
|
|
}
|
|
|
|
countdownCount -= 1;
|
|
|
|
if ( countdownCount === 0 ) {
|
|
|
|
onDone();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let onMetadataReady = function(metadata) {
|
|
|
|
recipeMetadata = metadata;
|
|
|
|
for ( let entry of metadata ) {
|
|
|
|
let assetKey = entry[0];
|
|
|
|
let recipeFile = entry[1];
|
|
|
|
if ( recipeFile.selected !== true ) { continue; }
|
|
|
|
µm.assets.get(assetKey, onLoaded);
|
|
|
|
countdownCount += 1;
|
|
|
|
}
|
|
|
|
let userRecipes = µm.userSettings.userRecipes;
|
|
|
|
if ( userRecipes.enabled ) {
|
|
|
|
µm.recipeManager.fromString(
|
|
|
|
'! uMatrix: Ruleset recipes 1.0\n' + userRecipes.content
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if ( countdownCount === 0 ) {
|
|
|
|
onDone();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.getAvailableRecipeFiles(onMetadataReady);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.assetKeysFromImportedAssets = function(raw) {
|
|
|
|
var out = new Set(),
|
|
|
|
reIgnore = /^[!#]/,
|
|
|
|
reValid = /^[a-z-]+:\/\/\S+\/./,
|
|
|
|
lineIter = new this.LineIterator(raw);
|
|
|
|
while ( lineIter.eot() === false ) {
|
|
|
|
let location = lineIter.next().trim();
|
|
|
|
if ( reIgnore.test(location) || !reValid.test(location) ) { continue; }
|
|
|
|
out.add(location);
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.getAvailableHostsFiles = function(callback) {
|
|
|
|
var µm = this,
|
|
|
|
availableHostsFiles = new Map();
|
|
|
|
|
|
|
|
// Custom filter lists.
|
|
|
|
var importedListKeys = new Set(µm.userSettings.externalHostsFiles);
|
|
|
|
|
|
|
|
for ( let assetKey of importedListKeys ) {
|
|
|
|
let entry = {
|
|
|
|
type: 'filters',
|
|
|
|
contentURL: assetKey,
|
|
|
|
external: true,
|
|
|
|
submitter: 'user',
|
|
|
|
title: assetKey
|
|
|
|
};
|
|
|
|
this.assets.registerAssetSource(assetKey, entry);
|
|
|
|
availableHostsFiles.set(assetKey, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate available lists with useful data.
|
|
|
|
var onHostsFilesDataReady = function(bin) {
|
|
|
|
if ( bin && bin.liveHostsFiles ) {
|
|
|
|
for ( let entry of µm.toMap(bin.liveHostsFiles) ) {
|
|
|
|
let assetKey = entry[0];
|
|
|
|
let availableAsset = availableHostsFiles.get(assetKey);
|
|
|
|
if ( availableAsset === undefined ) { continue; }
|
|
|
|
let liveAsset = entry[1];
|
|
|
|
if ( liveAsset.entryCount !== undefined ) {
|
|
|
|
availableAsset.entryCount = liveAsset.entryCount;
|
|
|
|
}
|
|
|
|
if ( liveAsset.entryUsedCount !== undefined ) {
|
|
|
|
availableAsset.entryUsedCount = liveAsset.entryUsedCount;
|
|
|
|
}
|
|
|
|
// This may happen if the list name was pulled from the list content
|
|
|
|
if ( availableAsset.title === '' && liveAsset.title !== undefined ) {
|
|
|
|
availableAsset.title = liveAsset.title;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for ( let asseyKey of µm.userSettings.selectedHostsFiles ) {
|
|
|
|
let asset = availableHostsFiles.get(asseyKey);
|
|
|
|
if ( asset !== undefined ) {
|
|
|
|
asset.selected = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(availableHostsFiles);
|
|
|
|
};
|
|
|
|
|
|
|
|
// built-in lists
|
|
|
|
var onBuiltinHostsFilesLoaded = function(entries) {
|
|
|
|
for ( let assetKey in entries ) {
|
|
|
|
if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
|
|
|
|
let entry = entries[assetKey];
|
|
|
|
if ( entry.type !== 'filters' ) { continue; }
|
|
|
|
if (
|
|
|
|
entry.submitter === 'user' &&
|
|
|
|
importedListKeys.has(assetKey) === false
|
|
|
|
) {
|
|
|
|
µm.assets.unregisterAssetSource(assetKey);
|
|
|
|
µm.assets.remove(assetKey);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
availableHostsFiles.set(assetKey, Object.assign({}, entry));
|
|
|
|
}
|
|
|
|
|
|
|
|
vAPI.storage.get('liveHostsFiles', onHostsFilesDataReady);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.assets.metadata(onBuiltinHostsFilesLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.getAvailableRecipeFiles = function(callback) {
|
|
|
|
var µm = this,
|
|
|
|
availableRecipeFiles = new Map();
|
|
|
|
|
|
|
|
// Imported recipe resources.
|
|
|
|
var importedResourceKeys = new Set(µm.userSettings.externalRecipeFiles);
|
|
|
|
|
|
|
|
for ( let assetKey of importedResourceKeys ) {
|
|
|
|
let entry = {
|
|
|
|
type: 'recipes',
|
|
|
|
contentURL: assetKey,
|
|
|
|
external: true,
|
|
|
|
submitter: 'user'
|
|
|
|
};
|
|
|
|
this.assets.registerAssetSource(assetKey, entry);
|
|
|
|
availableRecipeFiles.set(assetKey, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
var onBuiltinRecipeFilesLoaded = function(entries) {
|
|
|
|
for ( let assetKey in entries ) {
|
|
|
|
if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
|
|
|
|
let entry = entries[assetKey];
|
|
|
|
if ( entry.type !== 'recipes' ) { continue; }
|
|
|
|
if (
|
|
|
|
entry.submitter === 'user' &&
|
|
|
|
importedResourceKeys.has(assetKey) === false
|
|
|
|
) {
|
|
|
|
µm.assets.unregisterAssetSource(assetKey);
|
|
|
|
µm.assets.remove(assetKey);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
availableRecipeFiles.set(assetKey, Object.assign({}, entry));
|
|
|
|
}
|
|
|
|
|
|
|
|
for ( let asseyKey of µm.userSettings.selectedRecipeFiles ) {
|
|
|
|
let asset = availableRecipeFiles.get(asseyKey);
|
|
|
|
if ( asset !== undefined ) {
|
|
|
|
asset.selected = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(availableRecipeFiles);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.assets.metadata(onBuiltinRecipeFilesLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadHostsFiles = function(callback) {
|
|
|
|
let hostsFileLoadCount;
|
|
|
|
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
const loadHostsFilesEnd = fromSelfie => {
|
|
|
|
if ( fromSelfie !== true ) {
|
|
|
|
this.ubiquitousBlacklist.freeze();
|
|
|
|
vAPI.storage.set({ liveHostsFiles: Array.from(this.liveHostsFiles) });
|
|
|
|
this.hostsFilesSelfie.create();
|
|
|
|
}
|
|
|
|
vAPI.messaging.broadcast({ what: 'loadHostsFilesCompleted' });
|
|
|
|
this.getBytesInUse();
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
const mergeHostsFile = details => {
|
|
|
|
this.mergeHostsFile(details);
|
|
|
|
hostsFileLoadCount -= 1;
|
|
|
|
if ( hostsFileLoadCount === 0 ) {
|
|
|
|
loadHostsFilesEnd();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const loadHostsFilesStart = hostsFiles => {
|
|
|
|
this.liveHostsFiles = hostsFiles;
|
|
|
|
this.ubiquitousBlacklist.reset();
|
|
|
|
hostsFileLoadCount = this.userSettings.selectedHostsFiles.length;
|
|
|
|
|
|
|
|
// Load all hosts file which are not disabled.
|
|
|
|
for ( const assetKey of this.userSettings.selectedHostsFiles ) {
|
|
|
|
this.assets.get(assetKey, mergeHostsFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/2
|
|
|
|
if ( hostsFileLoadCount === 0 ) {
|
|
|
|
loadHostsFilesEnd();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onSelfieReady = status => {
|
|
|
|
if ( status === true ) {
|
|
|
|
return loadHostsFilesEnd(true);
|
|
|
|
}
|
|
|
|
this.getAvailableHostsFiles(loadHostsFilesStart);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.hostsFilesSelfie.load(onSelfieReady);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µ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;
|
|
|
|
|
|
|
|
let hostsFileMeta = this.liveHostsFiles.get(details.assetKey);
|
|
|
|
hostsFileMeta.entryCount = usedCount + duplicateCount;
|
|
|
|
hostsFileMeta.entryUsedCount = usedCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.mergeHostsFileContent = function(rawText) {
|
|
|
|
var rawEnd = rawText.length;
|
|
|
|
var ubiquitousBlacklist = this.ubiquitousBlacklist;
|
|
|
|
var reLocalhost = new RegExp(
|
|
|
|
[
|
|
|
|
'(?:^|\\s+)(?:',
|
|
|
|
[
|
|
|
|
'localhost\\.localdomain',
|
|
|
|
'ip6-allrouters',
|
|
|
|
'broadcasthost',
|
|
|
|
'ip6-localhost',
|
|
|
|
'ip6-allnodes',
|
|
|
|
'ip6-loopback',
|
|
|
|
'localhost',
|
|
|
|
'local',
|
|
|
|
'255\\.255\\.255\\.255',
|
|
|
|
'127\\.0\\.0\\.1',
|
|
|
|
'0\\.0\\.0\\.0',
|
|
|
|
'fe80::1%lo0',
|
|
|
|
'ff02::1',
|
|
|
|
'ff02::2',
|
|
|
|
'::1'
|
|
|
|
].join('|'),
|
|
|
|
')(?=\\s+|$)'
|
|
|
|
].join(''),
|
|
|
|
'g'
|
|
|
|
);
|
|
|
|
var reAsciiSegment = /^[\x21-\x7e]+$/;
|
|
|
|
var lineBeg = 0, lineEnd;
|
|
|
|
|
|
|
|
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.
|
|
|
|
let 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
|
|
|
|
let matches = reAsciiSegment.exec(line);
|
|
|
|
if ( matches === null ) { 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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.selectAssets = function(details, callback) {
|
|
|
|
var µm = this;
|
|
|
|
|
|
|
|
var applyAssetSelection = function(
|
|
|
|
metadata,
|
|
|
|
details,
|
|
|
|
propSelectedAssetKeys,
|
|
|
|
propImportedAssetKeys,
|
|
|
|
propInlineAsset
|
|
|
|
) {
|
|
|
|
let µmus = µm.userSettings;
|
|
|
|
let selectedAssetKeys = new Set();
|
|
|
|
let importedAssetKeys = new Set(µmus[propImportedAssetKeys]);
|
|
|
|
|
|
|
|
if ( Array.isArray(details.toSelect) ) {
|
|
|
|
for ( let assetKey of details.toSelect ) {
|
|
|
|
if ( metadata.has(assetKey) ) {
|
|
|
|
selectedAssetKeys.add(assetKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( Array.isArray(details.toRemove) ) {
|
|
|
|
for ( let assetKey of details.toRemove ) {
|
|
|
|
importedAssetKeys.delete(assetKey);
|
|
|
|
µm.assets.remove(assetKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hosts file to import
|
|
|
|
// https://github.com/gorhill/uBlock/issues/1181
|
|
|
|
// Try mapping the URL of an imported filter list to the assetKey of
|
|
|
|
// an existing stock list.
|
|
|
|
if ( typeof details.toImport === 'string' ) {
|
|
|
|
var assetKeyFromURL = function(url) {
|
|
|
|
var needle = url.replace(/^https?:/, '');
|
|
|
|
for ( let entry of metadata ) {
|
|
|
|
let asset = entry[1];
|
|
|
|
if ( asset.type === 'internal' ) { continue; }
|
|
|
|
let assetKey = entry[0];
|
|
|
|
if ( typeof asset.contentURL === 'string' ) {
|
|
|
|
if ( asset.contentURL.endsWith(needle) ) { return assetKey; }
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( Array.isArray(asset.contentURL) === false ) { continue; }
|
|
|
|
for ( let i = 0, n = asset.contentURL.length; i < n; i++ ) {
|
|
|
|
if ( asset.contentURL[i].endsWith(needle) ) {
|
|
|
|
return assetKey;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return url;
|
|
|
|
};
|
|
|
|
var toImport = µm.assetKeysFromImportedAssets(details.toImport);
|
|
|
|
for ( let url of toImport ) {
|
|
|
|
if ( importedAssetKeys.has(url) ) { continue; }
|
|
|
|
let assetKey = assetKeyFromURL(url);
|
|
|
|
if ( assetKey === url ) {
|
|
|
|
importedAssetKeys.add(assetKey);
|
|
|
|
}
|
|
|
|
selectedAssetKeys.add(assetKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let bin = {},
|
|
|
|
needReload = false;
|
|
|
|
|
|
|
|
if ( details.toInline instanceof Object ) {
|
|
|
|
let newInline = details.toInline;
|
|
|
|
let oldInline = µmus[propInlineAsset];
|
|
|
|
newInline.content = newInline.content.trim();
|
|
|
|
if ( newInline.content.length !== 0 ) {
|
|
|
|
newInline.content += '\n';
|
|
|
|
}
|
|
|
|
let newContent = newInline.enabled ? newInline.content : '';
|
|
|
|
let oldContent = oldInline.enabled ? oldInline.content : '';
|
|
|
|
if ( newContent !== oldContent ) {
|
|
|
|
needReload = true;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
newInline.enabled !== oldInline.enabled ||
|
|
|
|
newInline.content !== oldInline.content
|
|
|
|
) {
|
|
|
|
µmus[propInlineAsset] = newInline;
|
|
|
|
bin[propInlineAsset] = newInline;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
selectedAssetKeys = Array.from(selectedAssetKeys).sort();
|
|
|
|
µmus[propSelectedAssetKeys].sort();
|
|
|
|
if ( selectedAssetKeys.join() !== µmus[propSelectedAssetKeys].join() ) {
|
|
|
|
µmus[propSelectedAssetKeys] = selectedAssetKeys;
|
|
|
|
bin[propSelectedAssetKeys] = selectedAssetKeys;
|
|
|
|
needReload = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
importedAssetKeys = Array.from(importedAssetKeys).sort();
|
|
|
|
µmus[propImportedAssetKeys].sort();
|
|
|
|
if ( importedAssetKeys.join() !== µmus[propImportedAssetKeys].join() ) {
|
|
|
|
µmus[propImportedAssetKeys] = importedAssetKeys;
|
|
|
|
bin[propImportedAssetKeys] = importedAssetKeys;
|
|
|
|
needReload = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( Object.keys(bin).length !== 0 ) {
|
|
|
|
vAPI.storage.set(bin);
|
|
|
|
}
|
|
|
|
|
|
|
|
return needReload;
|
|
|
|
};
|
|
|
|
|
|
|
|
var onMetadataReady = function(response) {
|
|
|
|
let metadata = µm.toMap(response);
|
|
|
|
let hostsChanged = applyAssetSelection(
|
|
|
|
metadata,
|
|
|
|
details.hosts,
|
|
|
|
'selectedHostsFiles',
|
|
|
|
'externalHostsFiles',
|
|
|
|
'userHosts'
|
|
|
|
);
|
|
|
|
if ( hostsChanged ) {
|
|
|
|
µm.hostsFilesSelfie.destroy();
|
|
|
|
}
|
|
|
|
let recipesChanged = applyAssetSelection(
|
|
|
|
metadata,
|
|
|
|
details.recipes,
|
|
|
|
'selectedRecipeFiles',
|
|
|
|
'externalRecipeFiles',
|
|
|
|
'userRecipes'
|
|
|
|
);
|
|
|
|
if ( typeof callback === 'function' ) {
|
|
|
|
callback({
|
|
|
|
hostsChanged: hostsChanged,
|
|
|
|
recipesChanged: recipesChanged
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.assets.metadata(onMetadataReady);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
// `switches` contains the preset blacklists for which the switch must be
|
|
|
|
// revisited.
|
|
|
|
|
|
|
|
µMatrix.reloadHostsFiles = function() {
|
|
|
|
this.loadHostsFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.hostsFilesSelfie = (function() {
|
|
|
|
let timer;
|
|
|
|
|
|
|
|
return {
|
|
|
|
create: function() {
|
|
|
|
this.cancel();
|
|
|
|
timer = vAPI.setTimeout(
|
|
|
|
function() {
|
|
|
|
timer = undefined;
|
|
|
|
vAPI.cacheStorage.set({
|
|
|
|
hostsFilesSelfie: µMatrix.ubiquitousBlacklist.toSelfie()
|
|
|
|
});
|
|
|
|
},
|
|
|
|
120000
|
|
|
|
);
|
|
|
|
},
|
|
|
|
destroy: function() {
|
|
|
|
this.cancel();
|
|
|
|
vAPI.cacheStorage.remove('hostsFilesSelfie');
|
|
|
|
},
|
|
|
|
load: function(callback) {
|
|
|
|
this.cancel();
|
|
|
|
vAPI.cacheStorage.get('hostsFilesSelfie', function(bin) {
|
|
|
|
callback(
|
|
|
|
bin instanceof Object &&
|
|
|
|
bin.hostsFilesSelfie instanceof Object &&
|
|
|
|
µMatrix.ubiquitousBlacklist.fromSelfie(bin.hostsFilesSelfie)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
cancel: function() {
|
|
|
|
if ( timer !== undefined ) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
}
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.publicSuffixList = (function() {
|
|
|
|
let µm = µMatrix;
|
|
|
|
|
|
|
|
var onPSLReady = function(details, callback) {
|
|
|
|
if (
|
|
|
|
!details.error &&
|
|
|
|
typeof details.content === 'string' &&
|
|
|
|
details.content.length !== 0
|
|
|
|
) {
|
|
|
|
publicSuffixList.parse(details.content, punycode.toASCII);
|
|
|
|
vAPI.cacheStorage.set({
|
|
|
|
publicSuffixListSelfie: publicSuffixList.toSelfie()
|
|
|
|
});
|
|
|
|
}
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
let onSelfieReady = function(bin, callback) {
|
|
|
|
if (
|
|
|
|
bin instanceof Object &&
|
|
|
|
bin.publicSuffixListSelfie instanceof Object &&
|
|
|
|
publicSuffixList.fromSelfie(bin.publicSuffixListSelfie)
|
|
|
|
) {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
µm.assets.get(µm.pslAssetKey, function(details) {
|
|
|
|
onPSLReady(details, callback);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
update: function(details) {
|
|
|
|
onPSLReady(details, µm.noopFunc);
|
|
|
|
},
|
|
|
|
load: function(callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = µm.noopFunc;
|
|
|
|
}
|
|
|
|
vAPI.cacheStorage.get('publicSuffixListSelfie', function(bin) {
|
|
|
|
onSelfieReady(bin, callback);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.scheduleAssetUpdater = (function() {
|
|
|
|
var timer, next = 0;
|
|
|
|
return function(updateDelay) {
|
|
|
|
if ( timer ) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
if ( updateDelay === 0 ) {
|
|
|
|
this.assets.updateStop();
|
|
|
|
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) {
|
|
|
|
let µmus = this.userSettings;
|
|
|
|
|
|
|
|
// Do not update filter list if not in use.
|
|
|
|
if ( topic === 'before-asset-updated' ) {
|
|
|
|
if (
|
|
|
|
details.type === 'internal' ||
|
|
|
|
details.type === 'filters' &&
|
|
|
|
µmus.selectedHostsFiles.indexOf(details.assetKey) !== -1 ||
|
|
|
|
details.type === 'recipes' &&
|
|
|
|
µmus.selectedRecipeFiles.indexOf(details.assetKey) !== -1
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( topic === 'after-asset-updated' ) {
|
|
|
|
if (
|
|
|
|
details.type === 'filters' &&
|
|
|
|
µmus.selectedHostsFiles.indexOf(details.assetKey) !== -1
|
|
|
|
) {
|
|
|
|
this.hostsFilesSelfie.destroy();
|
|
|
|
} else if ( details.assetKey === this.pslAssetKey ) {
|
|
|
|
this.publicSuffixList.update(details);
|
|
|
|
}
|
|
|
|
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 (
|
|
|
|
this.arraysIntersect(
|
|
|
|
details.assetKeys,
|
|
|
|
µmus.selectedRecipeFiles
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
this.loadRecipes(true);
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
this.arraysIntersect(
|
|
|
|
details.assetKeys,
|
|
|
|
µmus.selectedHostsFiles
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
this.hostsFilesSelfie.destroy();
|
|
|
|
this.loadHostsFiles();
|
|
|
|
}
|
|
|
|
if ( µmus.autoUpdate ) {
|
|
|
|
this.scheduleAssetUpdater(25200000);
|
|
|
|
} else {
|
|
|
|
this.scheduleAssetUpdater(0);
|
|
|
|
}
|
|
|
|
vAPI.messaging.broadcast({
|
|
|
|
what: 'assetsUpdated',
|
|
|
|
assetKeys: details.assetKeys
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|