CanvasBlocker/lib/settings.js

671 lines
18 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(){
"use strict";
var scope;
if ((typeof exports) !== "undefined"){
scope = exports;
}
else {
scope = {};
window.scope.settings = scope;
}
var logging = {};
(function(){
var loggingQueue = [];
require.on("./logging", function(realLogging){
logging = realLogging;
loggingQueue.forEach(function(logEntry){
logging[logEntry.name](...logEntry.args, logEntry.date);
});
loggingQueue = [];
});
["error", "warning", "message", "notice", "verbose"].forEach(function(name){
logging[name] = function(...args){
loggingQueue.push({name, args, date: new Date()});
};
});
}());
const settingDefinitions = require("./settingDefinitions.js");
const definitionsByName = {};
const defaultSymbol = "";
const eventHandler = {any: {}};
eventHandler.any[defaultSymbol] = [];
eventHandler.all = eventHandler.any;
const settings = {};
let urlContainer;
let hideContainer;
function isDefinitionInvalid(settingDefinition, newValue){
if (newValue === undefined && settingDefinition.optional){
return false;
}
else if (settingDefinition.fixed){
return "fixed";
}
else if ((typeof newValue) !== (typeof settingDefinition.defaultValue)){
return "wrongType";
}
else if (Array.isArray(settingDefinition.defaultValue)){
if (!Array.isArray(newValue)){
return "wrongType";
}
var entriesInvalid = newValue.reduce(function(v, entry){
v = v || settingDefinition.entries.reduce(function(v, entryDefinition){
return v || isDefinitionInvalid(entryDefinition, entry[entryDefinition.name]);
}, false);
if (!v){
if (Object.keys(entry).some(function(key){
return !settingDefinition.entries.some(function(entryDefinition){
return key === entryDefinition.name;
});
})){
return "noOption";
}
}
return v;
}, false);
if (entriesInvalid){
return entriesInvalid;
}
}
else if (
settingDefinition.options &&
!settingDefinition.options.includes(newValue)
){
return "noOption";
}
return false;
}
function createGetter(settingDefinition){
if (settingDefinition.dynamic){
return function getValue(){
return settingDefinition.getter(scope);
};
}
else if (settingDefinition.urlSpecific){
return function getValue(url){
if (url){
var matching = urlContainer.get().filter(function(urlSetting){
return urlSetting.hasOwnProperty(settingDefinition.name);
}).filter(function(urlSetting){
return urlSetting.match(url);
});
if (matching.length){
return matching[0][settingDefinition.name];
}
}
return settings[settingDefinition.name];
};
}
else {
return function getValue(){
return settings[settingDefinition.name];
};
}
}
function createSetter(settingDefinition){
if (settingDefinition.dynamic){
return function setValue(newValue){
settingDefinition.setter(scope);
};
}
else {
const name = settingDefinition.name;
const isValid = function isValid(newValue){
var invalid = settingDefinition.invalid(newValue);
if (invalid){
if (invalid === "fixed"){
logging.warning("Trying to set the fixed setting", name, ":", newValue);
}
else if (invalid === "wrongType"){
logging.warning("Wrong type provided for setting", name, ":", newValue);
}
else if (invalid === "noOption"){
logging.warning("Provided value outside specified options for ", name, ":", newValue);
}
else {
logging.warning("Unknown invalid state:", invalid);
}
return false;
}
return true;
};
const storeValue = function storeValue(newValue){
settings[name] = newValue;
if (!settingDefinition.transient){
var storeObject = {};
storeObject[name] = newValue;
browser.storage.local.set(storeObject);
}
};
if (settingDefinition.urlSpecific){
return function setValue(newValue, url){
logging.verbose("New value for %s:", name, newValue);
if (isValid(newValue)){
if (url){
var urlContainerValue = urlContainer.get();
var matching = urlContainerValue.filter(function(urlSetting){
return urlSetting.match(url);
});
if (!matching.length){
let newEntry = {url};
newEntry[settingDefinition.name] = newValue;
urlContainerValue.push(newEntry);
matching = [newEntry];
}
matching[0][settingDefinition.name] = newValue;
urlContainer.set(urlContainerValue);
}
else {
storeValue(newValue);
}
}
};
}
else {
return function setValue(newValue){
logging.verbose("New value for %s:", name, newValue);
if (isValid(newValue)){
storeValue(newValue);
}
};
}
}
}
function createResetter(settingDefinition){
if (settingDefinition.dynamic){
return function(){};
}
else {
const name = settingDefinition.name;
let reset = function(){
settings[name] = settingDefinition.defaultValue;
browser.storage.local.remove(name);
};
if (settingDefinition.urlSpecific){
return function(url){
if (url){
var urlContainerValue = urlContainer.get();
var matching = urlContainerValue.filter(function(urlSetting){
return urlSetting.match(url);
});
if (matching.length){
delete matching[0][name];
if (Object.keys(matching[0]).every(function(key){return key === "url";})){
urlContainerValue = urlContainerValue.filter(function(urlSetting){
return urlSetting !== matching[0];
});
}
urlContainer.set(urlContainerValue);
}
}
else {
reset();
}
};
}
else {
return reset;
}
}
}
scope.on = function onSettingsChange(name, callback, url){
if (Array.isArray(name)){
name.forEach(function(name){
onSettingsChange(name, callback, url);
});
}
else {
if (eventHandler.hasOwnProperty(name)){
if (!url){
url = defaultSymbol;
}
if (!eventHandler[name].hasOwnProperty(url)){
eventHandler[name][url] = [];
}
eventHandler[name][url].push(callback);
}
else {
logging.warning("Unable to register event handler for unknown setting", name);
}
}
};
settingDefinitions.forEach(function(settingDefinition){
if (settingDefinition.urlContainer){
urlContainer = settingDefinition;
settingDefinition.refresh = function(){
settingDefinition.set(settingDefinition.get());
};
}
var name = settingDefinition.name;
definitionsByName[name] = settingDefinition;
if (typeof settingDefinition.defaultValue === "function"){
settingDefinition.defaultValue = settingDefinition.defaultValue();
}
settings[name] = settingDefinition.defaultValue;
eventHandler[name] = {};
settingDefinition.on = function on(callback, url){
if (!settingDefinition.dynamic){
scope.on(name, callback, url);
}
if (settingDefinition.dependencies){
settingDefinition.dependencies.forEach(function(dependency){
scope.on(dependency, function(){
callback({name, newValue: settingDefinition.get()});
}, url);
});
}
};
settingDefinition.invalid = function invalid(newValue){
return isDefinitionInvalid(settingDefinition, newValue);
};
settingDefinition.get = createGetter(settingDefinition);
settingDefinition.set = createSetter(settingDefinition);
settingDefinition.reset = createResetter(settingDefinition);
if (settingDefinition.urlSpecific){
if (!urlContainer){
logging.error("Unable to use url specific settings without url-container");
}
else {
settingDefinition.urlContainer = urlContainer;
let entry = Object.create(settingDefinition);
entry.optional = true;
urlContainer.entries.push(entry);
}
}
Object.defineProperty(
scope,
name,
{
get: settingDefinition.get,
set: settingDefinition.set,
enumerable: true
}
);
if (settingDefinition.hideContainer){
hideContainer = settingDefinition;
let changeListeners = {};
settingDefinition.setHideByName = function(name, value){
logging.verbose("set hide of", name, "to", value);
const hideStore = settingDefinition.get();
hideStore[name] = value;
settingDefinition.set(hideStore);
(changeListeners[name] || []).forEach(function(listener){
listener(value);
});
};
settingDefinition.getHideByName = function(name){
const hideStore = settingDefinition.get();
return hideStore[name] || false;
};
settingDefinition.onHideChange = function(name, listener){
if (!changeListeners[name]){
changeListeners[name] = [];
}
changeListeners[name].push(listener);
};
settingDefinition.on(function(event){
const value = event.newValue;
Object.keys(value).forEach(function(name){
if (value[name]){
(changeListeners[name] || []).forEach(function(listener){
listener(true);
});
}
});
});
settingDefinition.hideAble = false;
}
});
scope.getDefinition = function(name){
var foundDefinition = definitionsByName[name];
if (foundDefinition){
return Object.create(foundDefinition);
}
else {
return undefined;
}
};
scope.getContainers = function(){
return {
url: Object.create(urlContainer),
hide: Object.create(hideContainer)
};
};
scope.set = function(name, ...args){
var foundDefinition = definitionsByName[name];
if (foundDefinition){
return foundDefinition.set(...args);
}
else {
logging.error("Try to set unkown setting:", name);
}
};
scope.get = function(name, ...args){
var foundDefinition = definitionsByName[name];
if (foundDefinition){
return foundDefinition.get(...args);
}
else {
logging.error("Try to get unkown setting:", name);
}
};
scope.forEach = function forEachSetting(...args){
settingDefinitions.filter(function(settingDefinition){
return !settingDefinition.dynamic;
}).map(function(settingDefinition){
return Object.create(settingDefinition);
}).forEach(...args);
};
const resetSymbol = Symbol("reset");
function changeValue(name, newValue){
var settingDefinition = scope.getDefinition(name);
if (settingDefinition){
var oldValue = settings[name];
if (newValue === resetSymbol){
newValue = settingDefinition.defaultValue;
}
settings[name] = newValue;
((eventHandler[name] || {})[defaultSymbol] || []).forEach(function(callback){
callback({name, newValue, oldValue});
});
if (settingDefinition.urlSpecific){
urlContainer.get().forEach(function(entry){
if (!entry.hasOwnProperty(name)){
((eventHandler[name] || {})[entry.url] || []).forEach(function(callback){
callback({name, newValue, oldValue, url: entry.url});
});
}
});
}
}
}
logging.verbose("registering storage onchange listener");
browser.storage.onChanged.addListener(function(changes, area){
if (area === "local"){
logging.notice("settings changed", changes);
var delayedChange = [];
Object.entries(changes).forEach(function(entry){
const [name, change] = entry;
if (urlContainer && name === urlContainer.name){
// changes in the url container have to trigger after the other changes
delayedChange.push(entry);
}
else {
if (change.hasOwnProperty("newValue")){
changeValue(name, change.newValue);
}
else {
changeValue(name, resetSymbol);
}
}
});
delayedChange.forEach(function(entry){
const [name, change] = entry;
if (change.hasOwnProperty("newValue")){
changeValue(name, change.newValue);
}
else {
changeValue(name, resetSymbol);
}
});
eventHandler.any[""].forEach(function(callback){
callback();
});
}
});
if (urlContainer){
urlContainer.on(function({newValue, oldValue}){
newValue.forEach(function(urlSetting){
var regExp;
var domain = !!urlSetting.url.match(/^[\w.]+$/);
if (domain){
regExp = new RegExp(
"(?:^|\\.)" + urlSetting.url.replace(/([\\+*?[^\]$(){}=!|.])/g, "\\$1") + "\\.?$",
"i"
);
}
else {
regExp = new RegExp(urlSetting.url, "i");
}
const match = function(url){
if (!url){
return false;
}
else if (
url instanceof String ||
(typeof url) === "string"
){
return url === urlSetting.url;
}
else if (domain){
return (url.hostname || "").match(regExp);
}
else {
return url.href.match(regExp);
}
};
Object.defineProperty(
urlSetting,
"match",
{
enumerable: false,
writable: true,
configurable: true,
value: match
}
);
});
var newUrls = newValue.map(function(entry){return entry.url;});
var oldUrls = oldValue.map(function(entry){return entry.url;});
var matching = {};
newUrls.forEach(function(url, i){
matching[url] = {new: i, old: oldUrls.indexOf(url)};
});
oldUrls.forEach(function(url, i){
if (!matching[url]){
matching[url] = {new: -1, old: i};
}
});
Object.keys(matching).forEach(function(url){
var oldEntry = oldValue[matching[url].old] || {};
var newEntry = newValue[matching[url].new] || {};
urlContainer.entries.forEach(function(settingDefinition){
var name = settingDefinition.name;
var oldValue = oldEntry[name];
var newValue = newEntry[name];
if (oldValue !== newValue){
((eventHandler[name] || {})[url] || []).forEach(function(callback){
callback({name, newValue, oldValue, url});
});
}
});
});
});
}
const settingsMigration = {
validVersions: [undefined, 0.1, 0.2],
transitions: {
"": function(oldStorage){
return {
storageVersion: 0.2
};
},
0.1: function(oldStorage){
var newStorage = {
storageVersion: 0.2
};
if (oldStorage.hasOwnProperty("askOnlyOnce")){
newStorage.askOnlyOnce = oldStorage.askOnlyOnce? "individual": "no";
}
return newStorage;
},
0.2: function(oldStorage){
var newStorage = {
storageVersion: 0.3,
urlSettings: (
oldStorage.urlSettings &&
Array.isArray(oldStorage.urlSettings)
)? oldStorage.urlSettings: []
};
var urlSettings = {};
(oldStorage.blackList || "").split(",")
.map(function(url){return url.trim();})
.filter(function(url){return !!url;})
.forEach(function(url){
var entry = urlSettings[url];
if (!entry){
entry = {url, blockMode: "block"};
urlSettings[url] = entry;
newStorage.urlSettings.push(entry);
}
});
(oldStorage.whiteList || "").split(",")
.map(function(url){return url.trim();})
.filter(function(url){return !!url;})
.forEach(function(url){
var entry = urlSettings[url];
if (!entry){
entry = {url, blockMode: "allow"};
urlSettings[url] = entry;
newStorage.urlSettings.push(entry);
}
});
(oldStorage.ignoreList || "").split(",")
.map(function(url){return url.trim();})
.filter(function(url){return !!url;})
.forEach(function(url){
var entry = urlSettings[url];
if (!entry){
entry = {url, showNotifications: false};
urlSettings[url] = entry;
newStorage.urlSettings.push(entry);
}
else {
entry.showNotifications = false;
}
});
["whiteList", "blackList", "ignoreList"].forEach(function(list){
if (oldStorage.hasOwnProperty(list)){
newStorage[list] = "";
}
});
return newStorage;
}
}
};
logging.verbose("loading settings");
let initialized = false;
const initEvents = [];
scope.init = function(storage){
if (initialized){
return false;
}
initialized = true;
logging.message("settings loaded");
if (!storage.storageVersion){
logging.message("No storage version found. Initializing storage.");
browser.storage.local.remove(Object.keys(storage));
storage = settingsMigration.transitions[""]({});
browser.storage.local.set(storage);
}
else if (storage.storageVersion !== settings.storageVersion){
var toChange = {};
while (storage.storageVersion !== settings.storageVersion){
logging.message("Old storage found. Storage version", storage.storageVersion);
if (settingsMigration.transitions[storage.storageVersion]){
var changes = settingsMigration.transitions[storage.storageVersion](storage);
Object.entries(changes).forEach(function(entry){
const [name, value] = entry;
toChange[name] = value;
storage[name] = value;
});
}
else {
logging.warning("Unable to migrate storage.");
break;
}
}
logging.notice("Changed settings:", toChange);
browser.storage.local.set(toChange);
}
var delayedChange = [];
Object.entries(storage).forEach(function(entry){
const [name, value] = entry;
if (urlContainer && name === urlContainer.name){
// changes in the url container have to trigger after the other changes
delayedChange.push(entry);
}
else {
changeValue(name, value);
}
});
delayedChange.forEach(function(entry){
const [name, value] = entry;
changeValue(name, value);
});
changeValue("isStillDefault", false);
initEvents.forEach(function(callback){callback();});
return true;
};
scope.loaded = browser.storage.local.get().then(scope.init);
scope.onloaded = function(callback){
if (scope.isStillDefault){
initEvents.push(callback);
}
else {
callback();
}
};
scope.forceLoad = function(){
while (settings.isStillDefault){
logging.message("Starting synchronous request to wait for settings.");
try {
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://[::]", false);
xhr.send();
xhr = null;
}
catch (e){
logging.verbose("Error in XHR:", e);
}
logging.message("settings still default?", settings.isStillDefault);
}
};
Object.seal(scope);
}());