566 lines
16 KiB
JavaScript
566 lines
16 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/. */
|
|
|
|
"use strict";
|
|
|
|
/* globals localStorage, window */
|
|
|
|
// XXX: This file is a copy of the Services shim from devtools-services.
|
|
// See https://github.com/firefox-devtools/devtools-core/blob/a9263b4c3f88ea42879a36cdc3ca8217b4a528ea/packages/devtools-services/index.js
|
|
// Many Jest tests in the debugger rely on preferences, but can't use Services.
|
|
// This fixture is probably doing too much and should be reduced to the minimum
|
|
// needed to pass the tests.
|
|
|
|
/* eslint-disable mozilla/valid-services */
|
|
|
|
// Some constants from nsIPrefBranch.idl.
|
|
const PREF_INVALID = 0;
|
|
const PREF_STRING = 32;
|
|
const PREF_INT = 64;
|
|
const PREF_BOOL = 128;
|
|
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
|
|
|
|
// We prefix all our local storage items with this.
|
|
const PREFIX = "Services.prefs:";
|
|
|
|
/**
|
|
* Create a new preference branch. This object conforms largely to
|
|
* nsIPrefBranch and nsIPrefService, though it only implements the
|
|
* subset needed by devtools. A preference branch can hold child
|
|
* preferences while also holding a preference value itself.
|
|
*
|
|
* @param {PrefBranch} parent the parent branch, or null for the root
|
|
* branch.
|
|
* @param {String} name the base name of this branch
|
|
* @param {String} fullName the fully-qualified name of this branch
|
|
*/
|
|
function PrefBranch(parent, name, fullName) {
|
|
this._parent = parent;
|
|
this._name = name;
|
|
this._fullName = fullName;
|
|
this._observers = {};
|
|
this._children = {};
|
|
|
|
// Properties used when this branch has a value as well.
|
|
this._defaultValue = null;
|
|
this._hasUserValue = false;
|
|
this._userValue = null;
|
|
this._type = PREF_INVALID;
|
|
}
|
|
|
|
PrefBranch.prototype = {
|
|
PREF_INVALID,
|
|
PREF_STRING,
|
|
PREF_INT,
|
|
PREF_BOOL,
|
|
|
|
/** @see nsIPrefBranch.root. */
|
|
get root() {
|
|
return this._fullName;
|
|
},
|
|
|
|
/** @see nsIPrefBranch.getPrefType. */
|
|
getPrefType(prefName) {
|
|
return this._findPref(prefName)._type;
|
|
},
|
|
|
|
/** @see nsIPrefBranch.getBoolPref. */
|
|
getBoolPref(prefName, defaultValue) {
|
|
try {
|
|
const thePref = this._findPref(prefName);
|
|
if (thePref._type !== PREF_BOOL) {
|
|
throw new Error(`${prefName} does not have bool type`);
|
|
}
|
|
return thePref._get();
|
|
} catch (e) {
|
|
if (typeof defaultValue !== "undefined") {
|
|
return defaultValue;
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
/** @see nsIPrefBranch.setBoolPref. */
|
|
setBoolPref(prefName, value) {
|
|
if (typeof value !== "boolean") {
|
|
throw new Error("non-bool passed to setBoolPref");
|
|
}
|
|
const thePref = this._findOrCreatePref(prefName, value, true, value);
|
|
if (thePref._type !== PREF_BOOL) {
|
|
throw new Error(`${prefName} does not have bool type`);
|
|
}
|
|
thePref._set(value);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.getCharPref. */
|
|
getCharPref(prefName, defaultValue) {
|
|
try {
|
|
const thePref = this._findPref(prefName);
|
|
if (thePref._type !== PREF_STRING) {
|
|
throw new Error(`${prefName} does not have string type`);
|
|
}
|
|
return thePref._get();
|
|
} catch (e) {
|
|
if (typeof defaultValue !== "undefined") {
|
|
return defaultValue;
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
/** @see nsIPrefBranch.getStringPref. */
|
|
getStringPref() {
|
|
return this.getCharPref.apply(this, arguments);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.setCharPref. */
|
|
setCharPref(prefName, value) {
|
|
if (typeof value !== "string") {
|
|
throw new Error("non-string passed to setCharPref");
|
|
}
|
|
const thePref = this._findOrCreatePref(prefName, value, true, value);
|
|
if (thePref._type !== PREF_STRING) {
|
|
throw new Error(`${prefName} does not have string type`);
|
|
}
|
|
thePref._set(value);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.setStringPref. */
|
|
setStringPref() {
|
|
return this.setCharPref.apply(this, arguments);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.getIntPref. */
|
|
getIntPref(prefName, defaultValue) {
|
|
try {
|
|
const thePref = this._findPref(prefName);
|
|
if (thePref._type !== PREF_INT) {
|
|
throw new Error(`${prefName} does not have int type`);
|
|
}
|
|
return thePref._get();
|
|
} catch (e) {
|
|
if (typeof defaultValue !== "undefined") {
|
|
return defaultValue;
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
/** @see nsIPrefBranch.setIntPref. */
|
|
setIntPref(prefName, value) {
|
|
if (typeof value !== "number") {
|
|
throw new Error("non-number passed to setIntPref");
|
|
}
|
|
const thePref = this._findOrCreatePref(prefName, value, true, value);
|
|
if (thePref._type !== PREF_INT) {
|
|
throw new Error(`${prefName} does not have int type`);
|
|
}
|
|
thePref._set(value);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.clearUserPref */
|
|
clearUserPref(prefName) {
|
|
const thePref = this._findPref(prefName);
|
|
thePref._clearUserValue();
|
|
},
|
|
|
|
/** @see nsIPrefBranch.prefHasUserValue */
|
|
prefHasUserValue(prefName) {
|
|
const thePref = this._findPref(prefName);
|
|
return thePref._hasUserValue;
|
|
},
|
|
|
|
/** @see nsIPrefBranch.addObserver */
|
|
addObserver(domain, observer, holdWeak) {
|
|
if (holdWeak) {
|
|
throw new Error("shim prefs only supports strong observers");
|
|
}
|
|
|
|
if (!(domain in this._observers)) {
|
|
this._observers[domain] = [];
|
|
}
|
|
this._observers[domain].push(observer);
|
|
},
|
|
|
|
/** @see nsIPrefBranch.removeObserver */
|
|
removeObserver(domain, observer) {
|
|
if (!(domain in this._observers)) {
|
|
return;
|
|
}
|
|
const index = this._observers[domain].indexOf(observer);
|
|
if (index >= 0) {
|
|
this._observers[domain].splice(index, 1);
|
|
}
|
|
},
|
|
|
|
/** @see nsIPrefService.savePrefFile */
|
|
savePrefFile(file) {
|
|
if (file) {
|
|
throw new Error("shim prefs only supports null file in savePrefFile");
|
|
}
|
|
// Nothing to do - this implementation always writes back.
|
|
},
|
|
|
|
/** @see nsIPrefService.getBranch */
|
|
getBranch(prefRoot) {
|
|
if (!prefRoot) {
|
|
return this;
|
|
}
|
|
if (prefRoot.endsWith(".")) {
|
|
prefRoot = prefRoot.slice(0, -1);
|
|
}
|
|
// This is a bit weird since it could erroneously return a pref,
|
|
// not a pref branch.
|
|
return this._findPref(prefRoot);
|
|
},
|
|
|
|
/**
|
|
* Return this preference's current value.
|
|
*
|
|
* @return {Any} The current value of this preference. This may
|
|
* return a string, a number, or a boolean depending on the
|
|
* preference's type.
|
|
*/
|
|
_get() {
|
|
if (this._hasUserValue) {
|
|
return this._userValue;
|
|
}
|
|
return this._defaultValue;
|
|
},
|
|
|
|
/**
|
|
* Set the preference's value. The new value is assumed to be a
|
|
* user value. After setting the value, this function emits a
|
|
* change notification.
|
|
*
|
|
* @param {Any} value the new value
|
|
*/
|
|
_set(value) {
|
|
if (!this._hasUserValue || value !== this._userValue) {
|
|
this._userValue = value;
|
|
this._hasUserValue = true;
|
|
this._saveAndNotify();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set the default value for this preference, and emit a
|
|
* notification if this results in a visible change.
|
|
*
|
|
* @param {Any} value the new default value
|
|
*/
|
|
_setDefault(value) {
|
|
if (this._defaultValue !== value) {
|
|
this._defaultValue = value;
|
|
if (!this._hasUserValue) {
|
|
this._saveAndNotify();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If this preference has a user value, clear it. If a change was
|
|
* made, emit a change notification.
|
|
*/
|
|
_clearUserValue() {
|
|
if (this._hasUserValue) {
|
|
this._userValue = null;
|
|
this._hasUserValue = false;
|
|
this._saveAndNotify();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper function to write the preference's value to local storage
|
|
* and then emit a change notification.
|
|
*/
|
|
_saveAndNotify() {
|
|
const store = {
|
|
type: this._type,
|
|
defaultValue: this._defaultValue,
|
|
hasUserValue: this._hasUserValue,
|
|
userValue: this._userValue,
|
|
};
|
|
|
|
localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
|
|
this._parent._notify(this._name);
|
|
},
|
|
|
|
/**
|
|
* Change this preference's value without writing it back to local
|
|
* storage. This is used to handle changes to local storage that
|
|
* were made externally.
|
|
*
|
|
* @param {Number} type one of the PREF_* values
|
|
* @param {Any} userValue the user value to use if the pref does not exist
|
|
* @param {Any} defaultValue the default value to use if the pref
|
|
* does not exist
|
|
* @param {Boolean} hasUserValue if a new pref is created, whether
|
|
* the default value is also a user value
|
|
* @param {Object} store the new value of the preference. It should
|
|
* be of the form {type, defaultValue, hasUserValue, userValue};
|
|
* where |type| is one of the PREF_* type constants; |defaultValue|
|
|
* and |userValue| are the default and user values, respectively;
|
|
* and |hasUserValue| is a boolean indicating whether the user value
|
|
* is valid
|
|
*/
|
|
_storageUpdated(type, userValue, hasUserValue, defaultValue) {
|
|
this._type = type;
|
|
this._defaultValue = defaultValue;
|
|
this._hasUserValue = hasUserValue;
|
|
this._userValue = userValue;
|
|
// There's no need to write this back to local storage, since it
|
|
// came from there; and this avoids infinite event loops.
|
|
this._parent._notify(this._name);
|
|
},
|
|
|
|
/**
|
|
* Helper function to find either a Preference or PrefBranch object
|
|
* given its name. If the name is not found, throws an exception.
|
|
*
|
|
* @param {String} prefName the fully-qualified preference name
|
|
* @return {Object} Either a Preference or PrefBranch object
|
|
*/
|
|
_findPref(prefName) {
|
|
const branchNames = prefName.split(".");
|
|
let branch = this;
|
|
|
|
for (const branchName of branchNames) {
|
|
branch = branch._children[branchName];
|
|
if (!branch) {
|
|
// throw new Error(`could not find pref branch ${ prefName}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return branch;
|
|
},
|
|
|
|
/**
|
|
* Helper function to notify any observers when a preference has
|
|
* changed. This will also notify the parent branch for further
|
|
* reporting.
|
|
*
|
|
* @param {String} relativeName the name of the updated pref,
|
|
* relative to this branch
|
|
*/
|
|
_notify(relativeName) {
|
|
for (const domain in this._observers) {
|
|
if (
|
|
relativeName === domain ||
|
|
domain === "" ||
|
|
(domain.endsWith(".") && relativeName.startsWith(domain))
|
|
) {
|
|
// Allow mutation while walking.
|
|
const localList = this._observers[domain].slice();
|
|
for (const observer of localList) {
|
|
try {
|
|
if ("observe" in observer) {
|
|
observer.observe(
|
|
this,
|
|
NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
|
|
relativeName
|
|
);
|
|
} else {
|
|
// Function-style observer -- these aren't mentioned in
|
|
// the IDL, but they're accepted and devtools uses them.
|
|
observer(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, relativeName);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._parent) {
|
|
this._parent._notify(`${this._name}.${relativeName}`);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper function to create a branch given an array of branch names
|
|
* representing the path of the new branch.
|
|
*
|
|
* @param {Array} branchList an array of strings, one per component
|
|
* of the branch to be created
|
|
* @return {PrefBranch} the new branch
|
|
*/
|
|
_createBranch(branchList) {
|
|
let parent = this;
|
|
for (const branch of branchList) {
|
|
if (!parent._children[branch]) {
|
|
const isParentRoot = !parent._parent;
|
|
const branchName = (isParentRoot ? "" : `${parent.root}.`) + branch;
|
|
parent._children[branch] = new PrefBranch(parent, branch, branchName);
|
|
}
|
|
parent = parent._children[branch];
|
|
}
|
|
return parent;
|
|
},
|
|
|
|
/**
|
|
* Create a new preference. The new preference is assumed to be in
|
|
* local storage already, and the new value is taken from there.
|
|
*
|
|
* @param {String} keyName the full-qualified name of the preference.
|
|
* This is also the name of the key in local storage.
|
|
* @param {Any} userValue the user value to use if the pref does not exist
|
|
* @param {Boolean} hasUserValue if a new pref is created, whether
|
|
* the default value is also a user value
|
|
* @param {Any} defaultValue the default value to use if the pref
|
|
* does not exist
|
|
* @param {Boolean} init if true, then this call is initialization
|
|
* from local storage and should override the default prefs
|
|
*/
|
|
_findOrCreatePref(
|
|
keyName,
|
|
userValue,
|
|
hasUserValue,
|
|
defaultValue,
|
|
init = false
|
|
) {
|
|
const branch = this._createBranch(keyName.split("."));
|
|
|
|
if (hasUserValue && typeof userValue !== typeof defaultValue) {
|
|
throw new Error(`inconsistent values when creating ${keyName}`);
|
|
}
|
|
|
|
let type;
|
|
switch (typeof defaultValue) {
|
|
case "boolean":
|
|
type = PREF_BOOL;
|
|
break;
|
|
case "number":
|
|
type = PREF_INT;
|
|
break;
|
|
case "string":
|
|
type = PREF_STRING;
|
|
break;
|
|
default:
|
|
throw new Error(`unhandled argument type: ${typeof defaultValue}`);
|
|
}
|
|
|
|
if (init || branch._type === PREF_INVALID) {
|
|
branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
|
|
} else if (branch._type !== type) {
|
|
throw new Error(`attempt to change type of pref ${keyName}`);
|
|
}
|
|
|
|
return branch;
|
|
},
|
|
|
|
getKeyName(keyName) {
|
|
if (keyName.startsWith(PREFIX)) {
|
|
return keyName.slice(PREFIX.length);
|
|
}
|
|
|
|
return keyName;
|
|
},
|
|
|
|
/**
|
|
* Helper function that is called when local storage changes. This
|
|
* updates the preferences and notifies pref observers as needed.
|
|
*
|
|
* @param {StorageEvent} event the event representing the local
|
|
* storage change
|
|
*/
|
|
_onStorageChange(event) {
|
|
if (event.storageArea !== localStorage) {
|
|
return;
|
|
}
|
|
|
|
const key = this.getKeyName(event.key);
|
|
|
|
// Ignore delete events. Not clear what's correct.
|
|
if (key === null || event.newValue === null) {
|
|
return;
|
|
}
|
|
|
|
const { type, userValue, hasUserValue, defaultValue } = JSON.parse(
|
|
event.newValue
|
|
);
|
|
if (event.oldValue === null) {
|
|
this._findOrCreatePref(key, userValue, hasUserValue, defaultValue);
|
|
} else {
|
|
const thePref = this._findPref(key);
|
|
thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper function to initialize the root PrefBranch.
|
|
*/
|
|
_initializeRoot() {
|
|
if (Services._defaultPrefsEnabled) {
|
|
/* eslint-disable no-eval */
|
|
// let devtools = require("raw!prefs!devtools/client/preferences/devtools");
|
|
// eval(devtools);
|
|
// let all = require("raw!prefs!modules/libpref/init/all");
|
|
// eval(all);
|
|
/* eslint-enable no-eval */
|
|
}
|
|
|
|
// Read the prefs from local storage and create the local
|
|
// representations.
|
|
for (let i = 0; i < localStorage.length; ++i) {
|
|
const keyName = localStorage.key(i);
|
|
if (keyName.startsWith(PREFIX)) {
|
|
const { userValue, hasUserValue, defaultValue } = JSON.parse(
|
|
localStorage.getItem(keyName)
|
|
);
|
|
this._findOrCreatePref(
|
|
keyName.slice(PREFIX.length),
|
|
userValue,
|
|
hasUserValue,
|
|
defaultValue,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
this._onStorageChange = this._onStorageChange.bind(this);
|
|
window.addEventListener("storage", this._onStorageChange);
|
|
},
|
|
};
|
|
|
|
const Services = {
|
|
_prefs: null,
|
|
|
|
_defaultPrefsEnabled: true,
|
|
|
|
get prefs() {
|
|
if (!this._prefs) {
|
|
this._prefs = new PrefBranch(null, "", "");
|
|
this._prefs._initializeRoot();
|
|
}
|
|
return this._prefs;
|
|
},
|
|
|
|
appinfo: "",
|
|
obs: { addObserver: () => {} },
|
|
strings: {
|
|
createBundle() {
|
|
return {
|
|
GetStringFromName() {
|
|
return "NodeTest";
|
|
},
|
|
};
|
|
},
|
|
},
|
|
intl: {
|
|
stringHasRTLChars: () => false,
|
|
},
|
|
};
|
|
|
|
function pref(name, value) {
|
|
// eslint-disable-next-line mozilla/valid-services-property
|
|
const thePref = Services.prefs._findOrCreatePref(name, value, true, value);
|
|
thePref._setDefault(value);
|
|
}
|
|
|
|
module.exports = Services;
|
|
Services.pref = pref;
|
|
Services.uuid = { generateUUID: () => {} };
|
|
Services.dns = {};
|