/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Preferences. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Myk Melez * Daniel Aquino * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ let EXPORTED_SYMBOLS = ["Preferences"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.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 = Math.pow(2, 31) - 1; const MIN_INT = -MAX_INT; function Preferences(args) { if (isObject(args)) { if (args.branch) this._prefBranch = args.branch; } else if (args) this._prefBranch = args; this.isDefaultBranch = false; } Preferences.prototype = { /** * 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 * * @returns the value of the pref, if any; otherwise the default value */ get: function(prefName, defaultValue) { if (isArray(prefName)) return prefName.map(v => this.get(v, defaultValue)); return this._get(prefName, defaultValue); }, // In all cases below, the preference might exist as a user pref, but not // have a default value. In those cases, get* throws. Return the default value. _get: function(prefName, defaultValue) { switch (this._prefSvc.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_STRING: try { return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data; } catch (ex) { if (this.isDefaultBranch) return defaultValue; else return this._prefSvc.getCharPref(prefName); } case Ci.nsIPrefBranch.PREF_INT: try { return this._prefSvc.getIntPref(prefName); } catch (ex) { return defaultValue; } case Ci.nsIPrefBranch.PREF_BOOL: try { return this._prefSvc.getBoolPref(prefName); } catch (ex) { return defaultValue; } case Ci.nsIPrefBranch.PREF_INVALID: return defaultValue; default: // This should never happen. throw "Error getting pref " + prefName + "; its value's type is " + this._prefSvc.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()). */ set: function(prefName, prefValue) { if (isObject(prefName)) { for (let [name, value] in Iterator(prefName)) this.set(name, value); return; } this._set(prefName, prefValue); }, _set: function(prefName, prefValue) { let prefType; if (typeof prefValue != "undefined" && prefValue != null) prefType = prefValue.constructor.name; var existingPrefType = this._prefSvc.getPrefType(prefName); if (existingPrefType != Ci.nsIPrefBranch.PREF_INVALID) { // convert if (existingPrefType == Ci.nsIPrefBranch.PREF_INT && prefType == "String") { prefValue = parseInt(prefValue); if (isNaN(prefValue)) throw "Incompatible pref value type - " + prefName; prefType = "Number"; } else if (existingPrefType == Ci.nsIPrefBranch.PREF_BOOL && prefType == "String") { if (prefValue == "true") prefValue = true; else if (prefValue == "false") prefValue = false; else throw "Incompatible pref value type - " + prefName; prefType = "Boolean"; } else if (existingPrefType == Ci.nsIPrefBranch.PREF_BOOL && prefType == "Number") { prefValue = prefValue != 0; prefType = "Boolean"; } } switch (prefType) { case "String": { try { this._prefSvc.setStringPref(prefName, prefValue); } catch (e) { try { let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); string.data = prefValue; this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); } catch (e2) { Components.utils.reportError(e2); } } } 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."); try { this._prefSvc.setIntPref(prefName, prefValue); } catch (e) { throw new Error(e.toString() + " - " + prefName); } 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._prefSvc.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 */ has: function(prefName) { if (isArray(prefName)) return prefName.map(this.has, this); return this._has(prefName); }, _has: function(prefName) { return (this._prefSvc.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 */ isSet: function(prefName) { if (isArray(prefName)) return prefName.map(this.isSet, this); return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); }, /** * Whether or not the given pref has a user-set value. Use isSet instead, * which is equivalent. * @deprecated */ modified: function(prefName) { return this.isSet(prefName) }, reset: function(prefName) { if (isArray(prefName)) { prefName.map(v => this.reset(v)); return; } this._reset(prefName); }, _reset: function(prefName) { try { this._prefSvc.clearUserPref(prefName); } catch(ex) { // The pref service throws NS_ERROR_UNEXPECTED when the caller tries // to reset a pref that doesn't exist or is already set to its default // value. This interface fails silently in those cases, so callers // can unconditionally reset a pref without having to check if it needs // resetting first or trap exceptions after the fact. It passes through // other exceptions, however, so callers know about them, since we don't // know what other exceptions might be thrown and what they might mean. if (ex.result != Cr.NS_ERROR_UNEXPECTED) throw ex; } }, /** * If you need to know the default values, without resetting the actual * user prefs, you can use this. * @returns {Preferences} a new Preferences object, which accesses * the defaults rather than the user prefs. * *Only* call get() on this. * If you call set(), you will modify the defaults, so don't do that! */ get defaults() { // nsIPrefService let defaultBranch = Services.prefs. getDefaultBranch(this._prefBranch). QueryInterface(Ci.nsIPrefBranch); let prefs = new Preferences(this._prefBranch); // override. nasty, but this is internal, so OK. Object.defineProperty(prefs, "_prefSvc", { get: function() { return defaultBranch; } }); prefs.isDefaultBranch = true; return prefs; }, /** * Lock a pref so it can't be changed. * * @param prefName {String|Array} * the pref to lock, or an array of prefs to lock * @param prefValue {String} (optional) * default value of pref to lock only works if prefName isn't an array */ lock: function(prefName, prefValue) { if (isArray(prefName)) prefName.map(this.lock, this); else if (typeof prefValue != "undefined") this.defaults.set(prefName, prefValue); this._prefSvc.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 */ unlock: function(prefName) { if (isArray(prefName)) prefName.map(this.unlock, this); this._prefSvc.unlockPref(prefName); }, /** * Whether or not the given pref is locked against changes and * if it is set to the passedi n value * * @param prefName {String|Array} * the pref to check, or an array of prefs to check * @param prefValue {String|Number|Boolean}} * the pref value to compare against * * @returns {Boolean|Array} * whether or not the pref is locked; or, if the caller * provided an array of pref names, an array of booleans indicating * whether or not the prefs are locked * If a pref value was specified returns whether or not the pref * was locked and equal to the passed in value. */ locked: function(prefName, prefValue) { if (isArray(prefName)) return prefName.map(this.locked, this); if (prefValue) return this._prefSvc.prefIsLocked(prefName) && (this.get(prefName) == prefValue); else return this._prefSvc.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 */ observe: function(prefName, callback, thisObject) { let fullPrefName = this._prefBranch + (prefName || ""); let observer = new PrefObserver(fullPrefName, callback, thisObject); Preferences._prefSvc.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 */ ignore: function(prefName, callback, thisObject) { let fullPrefName = this._prefBranch + (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._prefSvc.removeObserver(fullPrefName, observer); observers.splice(observers.indexOf(observer), 1); } }, /** * Same as observe(), but automatically unregisters itself when * the window closes, saving you from writing an unload handler and * calling ignore(). * @param win {nsIDOMWindow} your |window| */ observeAuto: function(win, prefName, callback, thisObject) { if (!win instanceof Ci.nsIDOMWindow) throw "Need your |window| as first parameter"; this.observe(prefName, callback, thisObject); var self = this; win.addEventListener("unload", function() { self.ignore(prefName, callback, thisObject); }, false); win = null; // don't let closure hold on to window unnecessarily }, resetBranch: function(prefBranch) { try { this._prefSvc.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._prefSvc.getChildList(prefBranch, [])); else throw ex; } }, /** * Returns all child prefs of this pref branch. * This equals nsIPrefBranch.getChildList(). * This allows you to do e.g. * var myPrefs = new Preferences("extensions.cooler."); * var contents = myPrefs.branch("contents."); * for each (let prefname in contents.childPrefNames()) * dump("have " + contents.get(prefname) + " " + prefname + "\n"); * * @returns {Array of String} The names of the children, * without the base pref branch, but with subbranch. */ childPrefNames : function() { return this._prefSvc.getChildList("", []); }, /** * Returns an nsIPrefBranch for the pref branch that this object stands for. * You can use this to use functions that are not supported here. * @returns {nsIPrefBranch} */ get mozillaPrefBranch() { return this._prefSvc; }, /** * Returns the base pref name that this object stands for. * E.g. "extensions.yourcooler."; * @returns {String} */ get prefBranchName() { return this._prefBranch; }, /** * Returns an Preferences object for an sub pref branch * underneath the current pref branch. * @param subbranch {String} Will be appended to the * current pref branch. Don't forget the trailing dot, * where necessary. * E.g. "contents." * @returns {Preferences} */ branch : function(subbranch) { return new Preferences(this._prefBranch + subbranch); }, /** * The branch of the preferences tree to which this instance provides access. * @private */ _prefBranch: "", /** * Preferences Service * @private */ get _prefSvc() { // nsIPrefService let prefSvc = Services.prefs. getBranch(this._prefBranch). QueryInterface(Ci.nsIPrefBranch); Object.defineProperty(this, "_prefSvc", { get: function() { return prefSvc; } }); return this._prefSvc; } }; // Give the constructor the same prototype as its instances, so users can access // preferences directly via the constructor without having to create an instance // first. Preferences.__proto__ = Preferences.prototype; /** * 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. */ let 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 != this.prefName) return; if (typeof this.callback == "function") { let prefValue = Preferences.get(this.prefName); 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 isArray(val) { // We can't check for |val.constructor == Array| here, since the value // might be from a different context whose Array 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 == "Array"); } 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"); }