summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/Preferences.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/Preferences.jsm')
-rw-r--r--toolkit/modules/Preferences.jsm428
1 files changed, 428 insertions, 0 deletions
diff --git a/toolkit/modules/Preferences.jsm b/toolkit/modules/Preferences.jsm
new file mode 100644
index 000000000..232d877fb
--- /dev/null
+++ b/toolkit/modules/Preferences.jsm
@@ -0,0 +1,428 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["Preferences"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// The minimum and maximum integers that can be set as preferences.
+// The range of valid values is narrower than the range of valid JS values
+// because the native preferences code treats integers as NSPR PRInt32s,
+// which are 32-bit signed integers on all platforms.
+const MAX_INT = 0x7FFFFFFF; // Math.pow(2, 31) - 1
+const MIN_INT = -0x80000000;
+
+this.Preferences =
+ function Preferences(args) {
+ this._cachedPrefBranch = null;
+ if (isObject(args)) {
+ if (args.branch)
+ this._branchStr = args.branch;
+ if (args.defaultBranch)
+ this._defaultBranch = args.defaultBranch;
+ if (args.privacyContext)
+ this._privacyContext = args.privacyContext;
+ }
+ else if (args)
+ this._branchStr = args;
+ };
+
+/**
+ * Get the value of a pref, if any; otherwise return the default value.
+ *
+ * @param prefName {String|Array}
+ * the pref to get, or an array of prefs to get
+ *
+ * @param defaultValue
+ * the default value, if any, for prefs that don't have one
+ *
+ * @param valueType
+ * the XPCOM interface of the pref's complex value type, if any
+ *
+ * @returns the value of the pref, if any; otherwise the default value
+ */
+Preferences.get = function(prefName, defaultValue, valueType = Ci.nsISupportsString) {
+ if (Array.isArray(prefName))
+ return prefName.map(v => this.get(v, defaultValue));
+
+ return this._get(prefName, defaultValue, valueType);
+};
+
+Preferences._get = function(prefName, defaultValue, valueType) {
+ switch (this._prefBranch.getPrefType(prefName)) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ return this._prefBranch.getComplexValue(prefName, valueType).data;
+
+ case Ci.nsIPrefBranch.PREF_INT:
+ return this._prefBranch.getIntPref(prefName);
+
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ return this._prefBranch.getBoolPref(prefName);
+
+ case Ci.nsIPrefBranch.PREF_INVALID:
+ return defaultValue;
+
+ default:
+ // This should never happen.
+ throw "Error getting pref " + prefName + "; its value's type is " +
+ this._prefBranch.getPrefType(prefName) + ", which I don't " +
+ "know how to handle.";
+ }
+};
+
+/**
+ * Set a preference to a value.
+ *
+ * You can set multiple prefs by passing an object as the only parameter.
+ * In that case, this method will treat the properties of the object
+ * as preferences to set, where each property name is the name of a pref
+ * and its corresponding property value is the value of the pref.
+ *
+ * @param prefName {String|Object}
+ * the name of the pref to set; or an object containing a set
+ * of prefs to set
+ *
+ * @param prefValue {String|Number|Boolean}
+ * the value to which to set the pref
+ *
+ * Note: Preferences cannot store non-integer numbers or numbers outside
+ * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number,
+ * store it as a string by calling toString() on the number before passing
+ * it to this method, i.e.:
+ * Preferences.set("pi", 3.14159.toString())
+ * Preferences.set("big", Math.pow(2, 31).toString()).
+ */
+Preferences.set = function(prefName, prefValue) {
+ if (isObject(prefName)) {
+ for (let [name, value] of Object.entries(prefName))
+ this.set(name, value);
+ return;
+ }
+
+ this._set(prefName, prefValue);
+};
+
+Preferences._set = function(prefName, prefValue) {
+ let prefType;
+ if (typeof prefValue != "undefined" && prefValue != null)
+ prefType = prefValue.constructor.name;
+
+ switch (prefType) {
+ case "String":
+ {
+ let str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = prefValue;
+ this._prefBranch.setComplexValue(prefName, Ci.nsISupportsString, str);
+ }
+ break;
+
+ case "Number":
+ // We throw if the number is outside the range, since the result
+ // will never be what the consumer wanted to store, but we only warn
+ // if the number is non-integer, since the consumer might not mind
+ // the loss of precision.
+ if (prefValue > MAX_INT || prefValue < MIN_INT)
+ throw ("you cannot set the " + prefName + " pref to the number " +
+ prefValue + ", as number pref values must be in the signed " +
+ "32-bit integer range -(2^31-1) to 2^31-1. To store numbers " +
+ "outside that range, store them as strings.");
+ this._prefBranch.setIntPref(prefName, prefValue);
+ if (prefValue % 1 != 0)
+ Cu.reportError("Warning: setting the " + prefName + " pref to the " +
+ "non-integer number " + prefValue + " converted it " +
+ "to the integer number " + this.get(prefName) +
+ "; to retain fractional precision, store non-integer " +
+ "numbers as strings.");
+ break;
+
+ case "Boolean":
+ this._prefBranch.setBoolPref(prefName, prefValue);
+ break;
+
+ default:
+ throw "can't set pref " + prefName + " to value '" + prefValue +
+ "'; it isn't a String, Number, or Boolean";
+ }
+};
+
+/**
+ * Whether or not the given pref has a value. This is different from isSet
+ * because it returns true whether the value of the pref is a default value
+ * or a user-set value, while isSet only returns true if the value
+ * is a user-set value.
+ *
+ * @param prefName {String|Array}
+ * the pref to check, or an array of prefs to check
+ *
+ * @returns {Boolean|Array}
+ * whether or not the pref has a value; or, if the caller provided
+ * an array of pref names, an array of booleans indicating whether
+ * or not the prefs have values
+ */
+Preferences.has = function(prefName) {
+ if (Array.isArray(prefName))
+ return prefName.map(this.has, this);
+
+ return (this._prefBranch.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID);
+};
+
+/**
+ * Whether or not the given pref has a user-set value. This is different
+ * from |has| because it returns true only if the value of the pref is a user-
+ * set value, while |has| returns true if the value of the pref is a default
+ * value or a user-set value.
+ *
+ * @param prefName {String|Array}
+ * the pref to check, or an array of prefs to check
+ *
+ * @returns {Boolean|Array}
+ * whether or not the pref has a user-set value; or, if the caller
+ * provided an array of pref names, an array of booleans indicating
+ * whether or not the prefs have user-set values
+ */
+Preferences.isSet = function(prefName) {
+ if (Array.isArray(prefName))
+ return prefName.map(this.isSet, this);
+
+ return (this.has(prefName) && this._prefBranch.prefHasUserValue(prefName));
+},
+
+/**
+ * Whether or not the given pref has a user-set value. Use isSet instead,
+ * which is equivalent.
+ * @deprecated
+ */
+Preferences.modified = function(prefName) { return this.isSet(prefName) },
+
+Preferences.reset = function(prefName) {
+ if (Array.isArray(prefName)) {
+ prefName.map(v => this.reset(v));
+ return;
+ }
+
+ this._prefBranch.clearUserPref(prefName);
+};
+
+/**
+ * Lock a pref so it can't be changed.
+ *
+ * @param prefName {String|Array}
+ * the pref to lock, or an array of prefs to lock
+ */
+Preferences.lock = function(prefName) {
+ if (Array.isArray(prefName))
+ prefName.map(this.lock, this);
+
+ this._prefBranch.lockPref(prefName);
+};
+
+/**
+ * Unlock a pref so it can be changed.
+ *
+ * @param prefName {String|Array}
+ * the pref to lock, or an array of prefs to lock
+ */
+Preferences.unlock = function(prefName) {
+ if (Array.isArray(prefName))
+ prefName.map(this.unlock, this);
+
+ this._prefBranch.unlockPref(prefName);
+};
+
+/**
+ * Whether or not the given pref is locked against changes.
+ *
+ * @param prefName {String|Array}
+ * the pref to check, or an array of prefs to check
+ *
+ * @returns {Boolean|Array}
+ * whether or not the pref has a user-set value; or, if the caller
+ * provided an array of pref names, an array of booleans indicating
+ * whether or not the prefs have user-set values
+ */
+Preferences.locked = function(prefName) {
+ if (Array.isArray(prefName))
+ return prefName.map(this.locked, this);
+
+ return this._prefBranch.prefIsLocked(prefName);
+};
+
+/**
+ * Start observing a pref.
+ *
+ * The callback can be a function or any object that implements nsIObserver.
+ * When the callback is a function and thisObject is provided, it gets called
+ * as a method of thisObject.
+ *
+ * @param prefName {String}
+ * the name of the pref to observe
+ *
+ * @param callback {Function|Object}
+ * the code to notify when the pref changes;
+ *
+ * @param thisObject {Object} [optional]
+ * the object to use as |this| when calling a Function callback;
+ *
+ * @returns the wrapped observer
+ */
+Preferences.observe = function(prefName, callback, thisObject) {
+ let fullPrefName = this._branchStr + (prefName || "");
+
+ let observer = new PrefObserver(fullPrefName, callback, thisObject);
+ Preferences._prefBranch.addObserver(fullPrefName, observer, true);
+ observers.push(observer);
+
+ return observer;
+};
+
+/**
+ * Stop observing a pref.
+ *
+ * You must call this method with the same prefName, callback, and thisObject
+ * with which you originally registered the observer. However, you don't have
+ * to call this method on the same exact instance of Preferences; you can call
+ * it on any instance. For example, the following code first starts and then
+ * stops observing the "foo.bar.baz" preference:
+ *
+ * let observer = function() {...};
+ * Preferences.observe("foo.bar.baz", observer);
+ * new Preferences("foo.bar.").ignore("baz", observer);
+ *
+ * @param prefName {String}
+ * the name of the pref being observed
+ *
+ * @param callback {Function|Object}
+ * the code being notified when the pref changes
+ *
+ * @param thisObject {Object} [optional]
+ * the object being used as |this| when calling a Function callback
+ */
+Preferences.ignore = function(prefName, callback, thisObject) {
+ let fullPrefName = this._branchStr + (prefName || "");
+
+ // This seems fairly inefficient, but I'm not sure how much better we can
+ // make it. We could index by fullBranch, but we can't index by callback
+ // or thisObject, as far as I know, since the keys to JavaScript hashes
+ // (a.k.a. objects) can apparently only be primitive values.
+ let [observer] = observers.filter(v => v.prefName == fullPrefName &&
+ v.callback == callback &&
+ v.thisObject == thisObject);
+
+ if (observer) {
+ Preferences._prefBranch.removeObserver(fullPrefName, observer);
+ observers.splice(observers.indexOf(observer), 1);
+ } else {
+ Cu.reportError(`Attempt to stop observing a preference "${prefName}" that's not being observed`);
+ }
+};
+
+Preferences.resetBranch = function(prefBranch = "") {
+ try {
+ this._prefBranch.resetBranch(prefBranch);
+ }
+ catch (ex) {
+ // The current implementation of nsIPrefBranch in Mozilla
+ // doesn't implement resetBranch, so we do it ourselves.
+ if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED)
+ this.reset(this._prefBranch.getChildList(prefBranch, []));
+ else
+ throw ex;
+ }
+},
+
+/**
+ * A string identifying the branch of the preferences tree to which this
+ * instance provides access.
+ * @private
+ */
+Preferences._branchStr = "";
+
+/**
+ * The cached preferences branch object this instance encapsulates, or null.
+ * Do not use! Use _prefBranch below instead.
+ * @private
+ */
+Preferences._cachedPrefBranch = null;
+
+/**
+ * The preferences branch object for this instance.
+ * @private
+ */
+Object.defineProperty(Preferences, "_prefBranch",
+{
+ get: function _prefBranch() {
+ if (!this._cachedPrefBranch) {
+ let prefSvc = Services.prefs;
+ this._cachedPrefBranch = this._defaultBranch ?
+ prefSvc.getDefaultBranch(this._branchStr) :
+ prefSvc.getBranch(this._branchStr);
+ }
+ return this._cachedPrefBranch;
+ },
+ enumerable: true,
+ configurable: true
+});
+
+// Constructor-based access (Preferences.get(...) and set) is preferred over
+// instance-based access (new Preferences().get(...) and set) and when using the
+// root preferences branch, as it's desirable not to allocate the extra object.
+// But both forms are acceptable.
+Preferences.prototype = Preferences;
+
+/**
+ * A cache of pref observers.
+ *
+ * We use this to remove observers when a caller calls Preferences::ignore.
+ *
+ * All Preferences instances share this object, because we want callers to be
+ * able to remove an observer using a different Preferences object than the one
+ * with which they added it. That means we have to identify the observers
+ * in this object by their complete pref name, not just their name relative to
+ * the root branch of the Preferences object with which they were created.
+ */
+var observers = [];
+
+function PrefObserver(prefName, callback, thisObject) {
+ this.prefName = prefName;
+ this.callback = callback;
+ this.thisObject = thisObject;
+}
+
+PrefObserver.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe: function(subject, topic, data) {
+ // The pref service only observes whole branches, but we only observe
+ // individual preferences, so we check here that the pref that changed
+ // is the exact one we're observing (and not some sub-pref on the branch).
+ if (data.indexOf(this.prefName) != 0)
+ return;
+
+ if (typeof this.callback == "function") {
+ let prefValue = Preferences.get(data);
+
+ if (this.thisObject)
+ this.callback.call(this.thisObject, prefValue);
+ else
+ this.callback(prefValue);
+ }
+ else // typeof this.callback == "object" (nsIObserver)
+ this.callback.observe(subject, topic, data);
+ }
+};
+
+function isObject(val) {
+ // We can't check for |val.constructor == Object| here, since the value
+ // might be from a different context whose Object constructor is not the same
+ // as ours, so instead we match based on the name of the constructor.
+ return (typeof val != "undefined" && val != null && typeof val == "object" &&
+ val.constructor.name == "Object");
+}