/* 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 = {};