CanvasBlocker/lib/settings.js

476 lines
13 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(require){
"use strict";
let scope;
if ((typeof exports) !== "undefined"){
scope = exports;
}
else {
scope = require.register("./settings", {});
}
const logging = require("./logging");
const settingDefinitions = require("./settingDefinitions");
const settingContainers = require("./settingContainers");
const definitionsByName = {};
const defaultSymbol = "";
const eventHandler = {any: {}};
eventHandler.any[defaultSymbol] = [];
eventHandler.all = eventHandler.any;
const settings = {};
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";
}
const 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){
const match = settingContainers.getUrlValueContainer(settingDefinition.name, url);
if (match){
return match[settingDefinition.name];
}
}
return settings[settingDefinition.name];
};
}
else {
return function getValue(){
return settings[settingDefinition.name];
};
}
}
function getDefaultValue(settingDefinition){
let defaultValue = settingDefinition.defaultValue;
if ((typeof defaultValue) === "object"){
if (Array.isArray(defaultValue)){
return defaultValue.slice();
}
else {
return Object.create(defaultValue);
}
}
return defaultValue;
}
function createSetter(settingDefinition){
if (settingDefinition.dynamic){
return function setValue(newValue){
settingDefinition.setter(scope, newValue);
};
}
else {
const name = settingDefinition.name;
const isValid = function isValid(newValue){
const 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 = async function storeValue(newValue){
logging.verbose("Trying to store new value for %s", name, newValue);
settings[name] = newValue;
if (!settingDefinition.transient){
const storeObject = {};
storeObject[name] = newValue;
try {
await browser.storage.local.set(storeObject);
logging.verbose("New value stored for %s:", name, newValue);
}
catch (error){
logging.error("Unable to store new value for %s:", name, newValue, error);
throw error;
}
}
else {
logging.warning("Transient setting %s cannot be stored.", name);
throw "Transient setting " + name + " cannot be stored.";
}
};
if (settingDefinition.urlSpecific){
return function setValue(newValue, url){
logging.verbose("New value for %s (%s):", name, url, newValue);
if (isValid(newValue)){
if (url){
return settingContainers.setUrlValue(name, newValue, url);
}
else {
return storeValue(newValue);
}
}
else{
logging.warning("Invalid value for %s (%s):", name, url, newValue);
return Promise.reject("Invalid value for " + name + " (" + url + "): " + newValue);
}
};
}
else {
return function setValue(newValue){
logging.verbose("New value for %s:", name, newValue);
if (isValid(newValue)){
return storeValue(newValue);
}
else{
logging.warning("Invalid value for %s:", name, newValue);
return Promise.reject("Invalid value for " + name + ": " + newValue);
}
};
}
}
}
function createResetter(settingDefinition){
if (settingDefinition.dynamic){
return function(){};
}
else {
const name = settingDefinition.name;
let reset = function(){
settings[name] = getDefaultValue(settingDefinition);
browser.storage.local.remove(name);
};
if (settingDefinition.urlSpecific){
return function(url){
if (url){
settingContainers.resetUrlValue(name, url);
}
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){
const name = settingDefinition.name;
definitionsByName[name] = settingDefinition;
if (typeof settingDefinition.defaultValue === "function"){
settingDefinition.defaultValue = settingDefinition.defaultValue();
}
settings[name] = getDefaultValue(settingDefinition);
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 (!settingContainers.urlContainer){
logging.error("Unable to use url specific settings without url-container");
}
else {
settingDefinition.urlContainer = settingContainers.urlContainer;
let entry = Object.create(settingDefinition);
entry.optional = true;
settingContainers.urlContainer.entries.push(entry);
}
}
Object.defineProperty(
scope,
name,
{
get: settingDefinition.get,
set: settingDefinition.set,
enumerable: true
}
);
settingContainers.check(settingDefinition);
});
scope.getDefinition = function(name){
const foundDefinition = definitionsByName[name];
if (foundDefinition){
return Object.create(foundDefinition);
}
else {
return undefined;
}
};
scope.getContainers = function(){
return {
url: Object.create(settingContainers.urlContainer),
hide: Object.create(settingContainers.hideContainer),
expand: Object.create(settingContainers.expandContainer)
};
};
scope.set = function(name, ...args){
const foundDefinition = definitionsByName[name];
if (foundDefinition){
return foundDefinition.set(...args);
}
else {
logging.error("Try to set unknown setting:", name);
return Promise.reject("Try to set unknown setting: " + name);
}
};
scope.get = function(name, ...args){
const foundDefinition = definitionsByName[name];
if (foundDefinition){
return foundDefinition.get(...args);
}
else {
logging.error("Try to get unknown setting:", name);
return undefined;
}
};
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){
const settingDefinition = scope.getDefinition(name);
if (settingDefinition){
const oldValue = settings[name];
if (newValue === resetSymbol){
newValue = getDefaultValue(settingDefinition);
}
settings[name] = newValue;
((eventHandler[name] || {})[defaultSymbol] || []).forEach(function(callback){
callback({name, newValue, oldValue});
});
if (settingDefinition.urlSpecific){
settingContainers.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);
const delayedChange = [];
Object.entries(changes).forEach(function(entry){
const [name, change] = entry;
if (settingContainers.urlContainer && name === settingContainers.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();
});
}
});
settingContainers.initializeUrlContainer(eventHandler);
logging.verbose("loading settings");
let initialized = false;
scope.isInitialized = function(){
return initialized;
};
const initEvents = [];
scope.init = function(storage){
if (initialized){
return false;
}
initialized = true;
logging.message("settings loaded");
if (require("./extension").inBackgroundScript){
const settingsMigration = require("./settingsMigration");
settingsMigration.check(
storage,
{settings, logging, changeValue, urlContainer: settingContainers.urlContainer}
);
}
const delayedChange = [];
Object.entries(storage).forEach(function(entry){
const [name, value] = entry;
if (settingContainers.urlContainer && name === settingContainers.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;
};
if (require.exists("./settingsData")){
scope.init(require("./settingsData"));
scope.loaded = Promise.resolve(false);
}
else {
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 (error){
logging.verbose("Error in XHR:", error);
}
logging.message("settings still default?", settings.isStillDefault);
}
};
scope.startupReset = function(){
scope.forEach(function(definition){
if (definition.resetOnStartup){
definition.set(getDefaultValue(definition));
}
});
};
Object.seal(scope);
logging.setSettings(scope);
}(require));