1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // The minimum and maximum integers that can be set as preferences.
6 // The range of valid values is narrower than the range of valid JS values
7 // because the native preferences code treats integers as NSPR PRInt32s,
8 // which are 32-bit signed integers on all platforms.
9 const MAX_INT = 0x7fffffff; // Math.pow(2, 31) - 1
10 const MIN_INT = -0x80000000;
12 export function Preferences(args) {
13 this._cachedPrefBranch = null;
16 this._branchStr = args.branch;
18 if (args.defaultBranch) {
19 this._defaultBranch = args.defaultBranch;
21 if (args.privacyContext) {
22 this._privacyContext = args.privacyContext;
25 this._branchStr = args;
30 * Get the value of a pref, if any; otherwise return the default value.
32 * @param prefName {String|Array}
33 * the pref to get, or an array of prefs to get
36 * the default value, if any, for prefs that don't have one
39 * the XPCOM interface of the pref's complex value type, if any
41 * @returns the value of the pref, if any; otherwise the default value
43 Preferences.get = function (prefName, defaultValue, valueType = null) {
44 if (Array.isArray(prefName)) {
45 return prefName.map(v => this.get(v, defaultValue));
48 return this._get(prefName, defaultValue, valueType);
51 Preferences._get = function (prefName, defaultValue, valueType) {
52 switch (this._prefBranch.getPrefType(prefName)) {
53 case Ci.nsIPrefBranch.PREF_STRING:
55 let ifaces = ["nsIFile", "nsIPrefLocalizedString"];
56 if (ifaces.includes(valueType.name)) {
57 return this._prefBranch.getComplexValue(prefName, valueType).data;
60 return this._prefBranch.getStringPref(prefName);
62 case Ci.nsIPrefBranch.PREF_INT:
63 return this._prefBranch.getIntPref(prefName);
65 case Ci.nsIPrefBranch.PREF_BOOL:
66 return this._prefBranch.getBoolPref(prefName);
68 case Ci.nsIPrefBranch.PREF_INVALID:
72 // This should never happen.
74 `Error getting pref ${prefName}; its value's type is ` +
75 `${this._prefBranch.getPrefType(prefName)}, which I don't ` +
82 * Set a preference to a value.
84 * You can set multiple prefs by passing an object as the only parameter.
85 * In that case, this method will treat the properties of the object
86 * as preferences to set, where each property name is the name of a pref
87 * and its corresponding property value is the value of the pref.
89 * @param prefName {String|Object}
90 * the name of the pref to set; or an object containing a set
93 * @param prefValue {String|Number|Boolean}
94 * the value to which to set the pref
96 * Note: Preferences cannot store non-integer numbers or numbers outside
97 * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number,
98 * store it as a string by calling toString() on the number before passing
99 * it to this method, i.e.:
100 * Preferences.set("pi", 3.14159.toString())
101 * Preferences.set("big", Math.pow(2, 31).toString()).
103 Preferences.set = function (prefName, prefValue) {
104 if (isObject(prefName)) {
105 for (let [name, value] of Object.entries(prefName)) {
106 this.set(name, value);
111 this._set(prefName, prefValue);
114 Preferences._set = function (prefName, prefValue) {
116 if (typeof prefValue != "undefined" && prefValue != null) {
117 prefType = prefValue.constructor.name;
122 this._prefBranch.setStringPref(prefName, prefValue);
126 // We throw if the number is outside the range, since the result
127 // will never be what the consumer wanted to store, but we only warn
128 // if the number is non-integer, since the consumer might not mind
129 // the loss of precision.
130 if (prefValue > MAX_INT || prefValue < MIN_INT) {
132 `you cannot set the ${prefName} pref to the number ` +
133 `${prefValue}, as number pref values must be in the signed ` +
134 `32-bit integer range -(2^31-1) to 2^31-1. To store numbers ` +
135 `outside that range, store them as strings.`
138 this._prefBranch.setIntPref(prefName, prefValue);
139 if (prefValue % 1 != 0) {
141 "Warning: setting the ",
143 " pref to the non-integer number ",
146 "to the integer number " +
148 "; to retain fractional precision, store non-integer " +
149 "numbers as strings."
155 this._prefBranch.setBoolPref(prefName, prefValue);
160 `can't set pref ${prefName} to value '${prefValue}'; ` +
161 `it isn't a String, Number, or Boolean`
167 * Whether or not the given pref has a value. This is different from isSet
168 * because it returns true whether the value of the pref is a default value
169 * or a user-set value, while isSet only returns true if the value
170 * is a user-set value.
172 * @param prefName {String|Array}
173 * the pref to check, or an array of prefs to check
175 * @returns {Boolean|Array}
176 * whether or not the pref has a value; or, if the caller provided
177 * an array of pref names, an array of booleans indicating whether
178 * or not the prefs have values
180 Preferences.has = function (prefName) {
181 if (Array.isArray(prefName)) {
182 return prefName.map(this.has, this);
186 this._prefBranch.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID
191 * Whether or not the given pref has a user-set value. This is different
192 * from |has| because it returns true only if the value of the pref is a user-
193 * set value, while |has| returns true if the value of the pref is a default
194 * value or a user-set value.
196 * @param prefName {String|Array}
197 * the pref to check, or an array of prefs to check
199 * @returns {Boolean|Array}
200 * whether or not the pref has a user-set value; or, if the caller
201 * provided an array of pref names, an array of booleans indicating
202 * whether or not the prefs have user-set values
204 Preferences.isSet = function (prefName) {
205 if (Array.isArray(prefName)) {
206 return prefName.map(this.isSet, this);
209 return this.has(prefName) && this._prefBranch.prefHasUserValue(prefName);
213 * Whether or not the given pref has a user-set value. Use isSet instead,
214 * which is equivalent.
217 Preferences.modified = function (prefName) {
218 return this.isSet(prefName);
221 Preferences.reset = function (prefName) {
222 if (Array.isArray(prefName)) {
223 prefName.map(v => this.reset(v));
227 this._prefBranch.clearUserPref(prefName);
231 * Lock a pref so it can't be changed.
233 * @param prefName {String|Array}
234 * the pref to lock, or an array of prefs to lock
236 Preferences.lock = function (prefName) {
237 if (Array.isArray(prefName)) {
238 prefName.map(this.lock, this);
241 this._prefBranch.lockPref(prefName);
245 * Unlock a pref so it can be changed.
247 * @param prefName {String|Array}
248 * the pref to lock, or an array of prefs to lock
250 Preferences.unlock = function (prefName) {
251 if (Array.isArray(prefName)) {
252 prefName.map(this.unlock, this);
255 this._prefBranch.unlockPref(prefName);
259 * Whether or not the given pref is locked against changes.
261 * @param prefName {String|Array}
262 * the pref to check, or an array of prefs to check
264 * @returns {Boolean|Array}
265 * whether or not the pref has a user-set value; or, if the caller
266 * provided an array of pref names, an array of booleans indicating
267 * whether or not the prefs have user-set values
269 Preferences.locked = function (prefName) {
270 if (Array.isArray(prefName)) {
271 return prefName.map(this.locked, this);
274 return this._prefBranch.prefIsLocked(prefName);
278 * Start observing a pref.
280 * The callback can be a function or any object that implements nsIObserver.
281 * When the callback is a function and thisObject is provided, it gets called
282 * as a method of thisObject.
284 * @param prefName {String}
285 * the name of the pref to observe
287 * @param callback {Function|Object}
288 * the code to notify when the pref changes;
290 * @param thisObject {Object} [optional]
291 * the object to use as |this| when calling a Function callback;
293 * @returns the wrapped observer
295 Preferences.observe = function (prefName, callback, thisObject) {
296 let fullPrefName = this._branchStr + (prefName || "");
298 let observer = new PrefObserver(fullPrefName, callback, thisObject);
299 Preferences._prefBranch.addObserver(fullPrefName, observer, true);
300 observers.push(observer);
306 * Stop observing a pref.
308 * You must call this method with the same prefName, callback, and thisObject
309 * with which you originally registered the observer. However, you don't have
310 * to call this method on the same exact instance of Preferences; you can call
311 * it on any instance. For example, the following code first starts and then
312 * stops observing the "foo.bar.baz" preference:
314 * let observer = function() {...};
315 * Preferences.observe("foo.bar.baz", observer);
316 * new Preferences("foo.bar.").ignore("baz", observer);
318 * @param prefName {String}
319 * the name of the pref being observed
321 * @param callback {Function|Object}
322 * the code being notified when the pref changes
324 * @param thisObject {Object} [optional]
325 * the object being used as |this| when calling a Function callback
327 Preferences.ignore = function (prefName, callback, thisObject) {
328 let fullPrefName = this._branchStr + (prefName || "");
330 // This seems fairly inefficient, but I'm not sure how much better we can
331 // make it. We could index by fullBranch, but we can't index by callback
332 // or thisObject, as far as I know, since the keys to JavaScript hashes
333 // (a.k.a. objects) can apparently only be primitive values.
334 let [observer] = observers.filter(
336 v.prefName == fullPrefName &&
337 v.callback == callback &&
338 v.thisObject == thisObject
342 Preferences._prefBranch.removeObserver(fullPrefName, observer);
343 observers.splice(observers.indexOf(observer), 1);
346 `Attempt to stop observing a preference "${prefName}" that's not being observed`
351 Preferences.resetBranch = function (prefBranch = "") {
352 this.reset(this._prefBranch.getChildList(prefBranch));
356 * A string identifying the branch of the preferences tree to which this
357 * instance provides access.
360 Preferences._branchStr = "";
363 * The cached preferences branch object this instance encapsulates, or null.
364 * Do not use! Use _prefBranch below instead.
367 Preferences._cachedPrefBranch = null;
370 * The preferences branch object for this instance.
373 Object.defineProperty(Preferences, "_prefBranch", {
374 get: function _prefBranch() {
375 if (!this._cachedPrefBranch) {
376 let prefSvc = Services.prefs;
377 this._cachedPrefBranch = this._defaultBranch
378 ? prefSvc.getDefaultBranch(this._branchStr)
379 : prefSvc.getBranch(this._branchStr);
381 return this._cachedPrefBranch;
387 // Constructor-based access (Preferences.get(...) and set) is preferred over
388 // instance-based access (new Preferences().get(...) and set) and when using the
389 // root preferences branch, as it's desirable not to allocate the extra object.
390 // But both forms are acceptable.
391 Preferences.prototype = Preferences;
394 * A cache of pref observers.
396 * We use this to remove observers when a caller calls Preferences::ignore.
398 * All Preferences instances share this object, because we want callers to be
399 * able to remove an observer using a different Preferences object than the one
400 * with which they added it. That means we have to identify the observers
401 * in this object by their complete pref name, not just their name relative to
402 * the root branch of the Preferences object with which they were created.
406 function PrefObserver(prefName, callback, thisObject) {
407 this.prefName = prefName;
408 this.callback = callback;
409 this.thisObject = thisObject;
412 PrefObserver.prototype = {
413 QueryInterface: ChromeUtils.generateQI([
415 "nsISupportsWeakReference",
418 observe(subject, topic, data) {
419 // The pref service only observes whole branches, but we only observe
420 // individual preferences, so we check here that the pref that changed
421 // is the exact one we're observing (and not some sub-pref on the branch).
422 if (data != this.prefName) {
426 if (typeof this.callback == "function") {
427 let prefValue = Preferences.get(this.prefName);
429 if (this.thisObject) {
430 this.callback.call(this.thisObject, prefValue);
432 this.callback(prefValue);
435 // typeof this.callback == "object" (nsIObserver)
436 this.callback.observe(subject, topic, data);
441 function isObject(val) {
442 // We can't check for |val.constructor == Object| here, since the value
443 // might be from a different context whose Object constructor is not the same
444 // as ours, so instead we match based on the name of the constructor.
446 typeof val != "undefined" &&
448 typeof val == "object" &&
449 val.constructor.name == "Object"