Preferences.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3. *
  4. * The contents of this file are subject to the Mozilla Public License Version
  5. * 1.1 (the "License"); you may not use this file except in compliance with
  6. * the License. You may obtain a copy of the License at
  7. * http://www.mozilla.org/MPL/
  8. *
  9. * Software distributed under the License is distributed on an "AS IS" basis,
  10. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11. * for the specific language governing rights and limitations under the
  12. * License.
  13. *
  14. * The Original Code is Preferences.
  15. *
  16. * The Initial Developer of the Original Code is Mozilla.
  17. * Portions created by the Initial Developer are Copyright (C) 2008
  18. * the Initial Developer. All Rights Reserved.
  19. *
  20. * Contributor(s):
  21. * Myk Melez <myk@mozilla.org>
  22. * Daniel Aquino <mr.danielaquino@gmail.com>
  23. *
  24. * Alternatively, the contents of this file may be used under the terms of
  25. * either the GNU General Public License Version 2 or later (the "GPL"), or
  26. * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27. * in which case the provisions of the GPL or the LGPL are applicable instead
  28. * of those above. If you wish to allow use of your version of this file only
  29. * under the terms of either the GPL or the LGPL, and not to allow others to
  30. * use your version of this file under the terms of the MPL, indicate your
  31. * decision by deleting the provisions above and replace them with the notice
  32. * and other provisions required by the GPL or the LGPL. If you do not delete
  33. * the provisions above, a recipient may use your version of this file under
  34. * the terms of any one of the MPL, the GPL or the LGPL.
  35. *
  36. * ***** END LICENSE BLOCK ***** */
  37. let EXPORTED_SYMBOLS = ["Preferences"];
  38. const Cc = Components.classes;
  39. const Ci = Components.interfaces;
  40. const Cr = Components.results;
  41. const Cu = Components.utils;
  42. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  43. // The minimum and maximum integers that can be set as preferences.
  44. // The range of valid values is narrower than the range of valid JS values
  45. // because the native preferences code treats integers as NSPR PRInt32s,
  46. // which are 32-bit signed integers on all platforms.
  47. const MAX_INT = Math.pow(2, 31) - 1;
  48. const MIN_INT = -MAX_INT;
  49. function Preferences(args) {
  50. if (isObject(args)) {
  51. if (args.branch)
  52. this._prefBranch = args.branch;
  53. if (args.site)
  54. this._site = args.site;
  55. }
  56. else if (args)
  57. this._prefBranch = args;
  58. }
  59. Preferences.prototype = {
  60. /**
  61. * Get the value of a pref, if any; otherwise return the default value.
  62. *
  63. * @param prefName {String|Array}
  64. * the pref to get, or an array of prefs to get
  65. *
  66. * @param defaultValue
  67. * the default value, if any, for prefs that don't have one
  68. *
  69. * @returns the value of the pref, if any; otherwise the default value
  70. */
  71. get: function(prefName, defaultValue) {
  72. if (isArray(prefName))
  73. return prefName.map(function(v) this.get(v, defaultValue), this);
  74. if (this._site)
  75. return this._siteGet(prefName, defaultValue);
  76. else
  77. return this._get(prefName, defaultValue);
  78. },
  79. _get: function(prefName, defaultValue) {
  80. switch (this._prefSvc.getPrefType(prefName)) {
  81. case Ci.nsIPrefBranch.PREF_STRING:
  82. return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data;
  83. case Ci.nsIPrefBranch.PREF_INT:
  84. return this._prefSvc.getIntPref(prefName);
  85. case Ci.nsIPrefBranch.PREF_BOOL:
  86. return this._prefSvc.getBoolPref(prefName);
  87. case Ci.nsIPrefBranch.PREF_INVALID:
  88. return defaultValue;
  89. default:
  90. // This should never happen.
  91. throw "Error getting pref " + prefName + "; its value's type is " +
  92. this._prefSvc.getPrefType(prefName) + ", which I don't know " +
  93. "how to handle.";
  94. }
  95. },
  96. _siteGet: function(prefName, defaultValue) {
  97. let value = this._contentPrefSvc.getPref(this._site, this._prefBranch + prefName);
  98. return typeof value != "undefined" ? value : defaultValue;
  99. },
  100. /**
  101. * Set a preference to a value.
  102. *
  103. * You can set multiple prefs by passing an object as the only parameter.
  104. * In that case, this method will treat the properties of the object
  105. * as preferences to set, where each property name is the name of a pref
  106. * and its corresponding property value is the value of the pref.
  107. *
  108. * @param prefName {String|Object}
  109. * the name of the pref to set; or an object containing a set
  110. * of prefs to set
  111. *
  112. * @param prefValue {String|Number|Boolean}
  113. * the value to which to set the pref
  114. *
  115. * Note: Preferences cannot store non-integer numbers or numbers outside
  116. * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number,
  117. * store it as a string by calling toString() on the number before passing
  118. * it to this method, i.e.:
  119. * Preferences.set("pi", 3.14159.toString())
  120. * Preferences.set("big", Math.pow(2, 31).toString()).
  121. */
  122. set: function(prefName, prefValue) {
  123. if (isObject(prefName)) {
  124. for (let [name, value] in Iterator(prefName))
  125. this.set(name, value);
  126. return;
  127. }
  128. if (this._site)
  129. this._siteSet(prefName, prefValue);
  130. else
  131. this._set(prefName, prefValue);
  132. },
  133. _set: function(prefName, prefValue) {
  134. let prefType;
  135. if (typeof prefValue != "undefined" && prefValue != null)
  136. prefType = prefValue.constructor.name;
  137. switch (prefType) {
  138. case "String":
  139. {
  140. let string = Cc["@mozilla.org/supports-string;1"].
  141. createInstance(Ci.nsISupportsString);
  142. string.data = prefValue;
  143. this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string);
  144. }
  145. break;
  146. case "Number":
  147. // We throw if the number is outside the range, since the result
  148. // will never be what the consumer wanted to store, but we only warn
  149. // if the number is non-integer, since the consumer might not mind
  150. // the loss of precision.
  151. if (prefValue > MAX_INT || prefValue < MIN_INT)
  152. throw("you cannot set the " + prefName + " pref to the number " +
  153. prefValue + ", as number pref values must be in the signed " +
  154. "32-bit integer range -(2^31-1) to 2^31-1. To store numbers " +
  155. "outside that range, store them as strings.");
  156. this._prefSvc.setIntPref(prefName, prefValue);
  157. if (prefValue % 1 != 0)
  158. Cu.reportError("Warning: setting the " + prefName + " pref to the " +
  159. "non-integer number " + prefValue + " converted it " +
  160. "to the integer number " + this.get(prefName) +
  161. "; to retain fractional precision, store non-integer " +
  162. "numbers as strings.");
  163. break;
  164. case "Boolean":
  165. this._prefSvc.setBoolPref(prefName, prefValue);
  166. break;
  167. default:
  168. throw "can't set pref " + prefName + " to value '" + prefValue +
  169. "'; it isn't a String, Number, or Boolean";
  170. }
  171. },
  172. _siteSet: function(prefName, prefValue) {
  173. this._contentPrefSvc.setPref(this._site, this._prefBranch + prefName, prefValue);
  174. },
  175. /**
  176. * Whether or not the given pref has a value. This is different from isSet
  177. * because it returns true whether the value of the pref is a default value
  178. * or a user-set value, while isSet only returns true if the value
  179. * is a user-set value.
  180. *
  181. * @param prefName {String|Array}
  182. * the pref to check, or an array of prefs to check
  183. *
  184. * @returns {Boolean|Array}
  185. * whether or not the pref has a value; or, if the caller provided
  186. * an array of pref names, an array of booleans indicating whether
  187. * or not the prefs have values
  188. */
  189. has: function(prefName) {
  190. if (isArray(prefName))
  191. return prefName.map(this.has, this);
  192. if (this._site)
  193. return this._siteHas(prefName);
  194. else
  195. return this._has(prefName);
  196. },
  197. _has: function(prefName) {
  198. return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID);
  199. },
  200. _siteHas: function(prefName) {
  201. return this._contentPrefSvc.hasPref(this._site, this._prefBranch + prefName);
  202. },
  203. /**
  204. * Whether or not the given pref has a user-set value. This is different
  205. * from |has| because it returns true only if the value of the pref is a user-
  206. * set value, while |has| returns true if the value of the pref is a default
  207. * value or a user-set value.
  208. *
  209. * @param prefName {String|Array}
  210. * the pref to check, or an array of prefs to check
  211. *
  212. * @returns {Boolean|Array}
  213. * whether or not the pref has a user-set value; or, if the caller
  214. * provided an array of pref names, an array of booleans indicating
  215. * whether or not the prefs have user-set values
  216. */
  217. isSet: function(prefName) {
  218. if (isArray(prefName))
  219. return prefName.map(this.isSet, this);
  220. return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName));
  221. },
  222. /**
  223. * Whether or not the given pref has a user-set value. Use isSet instead,
  224. * which is equivalent.
  225. * @deprecated
  226. */
  227. modified: function(prefName) { return this.isSet(prefName) },
  228. reset: function(prefName) {
  229. if (isArray(prefName)) {
  230. prefName.map(function(v) this.reset(v), this);
  231. return;
  232. }
  233. if (this._site)
  234. this._siteReset(prefName);
  235. else
  236. this._reset(prefName);
  237. },
  238. _reset: function(prefName) {
  239. try {
  240. this._prefSvc.clearUserPref(prefName);
  241. }
  242. catch(ex) {
  243. // The pref service throws NS_ERROR_UNEXPECTED when the caller tries
  244. // to reset a pref that doesn't exist or is already set to its default
  245. // value. This interface fails silently in those cases, so callers
  246. // can unconditionally reset a pref without having to check if it needs
  247. // resetting first or trap exceptions after the fact. It passes through
  248. // other exceptions, however, so callers know about them, since we don't
  249. // know what other exceptions might be thrown and what they might mean.
  250. if (ex.result != Cr.NS_ERROR_UNEXPECTED)
  251. throw ex;
  252. }
  253. },
  254. _siteReset: function(prefName) {
  255. return this._contentPrefSvc.removePref(this._site, this._prefBranch + prefName);
  256. },
  257. /**
  258. * Lock a pref so it can't be changed.
  259. *
  260. * @param prefName {String|Array}
  261. * the pref to lock, or an array of prefs to lock
  262. */
  263. lock: function(prefName) {
  264. if (isArray(prefName))
  265. prefName.map(this.lock, this);
  266. this._prefSvc.lockPref(prefName);
  267. },
  268. /**
  269. * Unlock a pref so it can be changed.
  270. *
  271. * @param prefName {String|Array}
  272. * the pref to lock, or an array of prefs to lock
  273. */
  274. unlock: function(prefName) {
  275. if (isArray(prefName))
  276. prefName.map(this.unlock, this);
  277. this._prefSvc.unlockPref(prefName);
  278. },
  279. /**
  280. * Whether or not the given pref is locked against changes.
  281. *
  282. * @param prefName {String|Array}
  283. * the pref to check, or an array of prefs to check
  284. *
  285. * @returns {Boolean|Array}
  286. * whether or not the pref has a user-set value; or, if the caller
  287. * provided an array of pref names, an array of booleans indicating
  288. * whether or not the prefs have user-set values
  289. */
  290. locked: function(prefName) {
  291. if (isArray(prefName))
  292. return prefName.map(this.locked, this);
  293. return this._prefSvc.prefIsLocked(prefName);
  294. },
  295. /**
  296. * Start observing a pref.
  297. *
  298. * The callback can be a function or any object that implements nsIObserver.
  299. * When the callback is a function and thisObject is provided, it gets called
  300. * as a method of thisObject.
  301. *
  302. * @param prefName {String}
  303. * the name of the pref to observe
  304. *
  305. * @param callback {Function|Object}
  306. * the code to notify when the pref changes;
  307. *
  308. * @param thisObject {Object} [optional]
  309. * the object to use as |this| when calling a Function callback;
  310. *
  311. * @returns the wrapped observer
  312. */
  313. observe: function(prefName, callback, thisObject) {
  314. let fullPrefName = this._prefBranch + (prefName || "");
  315. let observer = new PrefObserver(fullPrefName, callback, thisObject);
  316. Preferences._prefSvc.addObserver(fullPrefName, observer, true);
  317. observers.push(observer);
  318. return observer;
  319. },
  320. /**
  321. * Stop observing a pref.
  322. *
  323. * You must call this method with the same prefName, callback, and thisObject
  324. * with which you originally registered the observer. However, you don't have
  325. * to call this method on the same exact instance of Preferences; you can call
  326. * it on any instance. For example, the following code first starts and then
  327. * stops observing the "foo.bar.baz" preference:
  328. *
  329. * let observer = function() {...};
  330. * Preferences.observe("foo.bar.baz", observer);
  331. * new Preferences("foo.bar.").ignore("baz", observer);
  332. *
  333. * @param prefName {String}
  334. * the name of the pref being observed
  335. *
  336. * @param callback {Function|Object}
  337. * the code being notified when the pref changes
  338. *
  339. * @param thisObject {Object} [optional]
  340. * the object being used as |this| when calling a Function callback
  341. */
  342. ignore: function(prefName, callback, thisObject) {
  343. let fullPrefName = this._prefBranch + (prefName || "");
  344. // This seems fairly inefficient, but I'm not sure how much better we can
  345. // make it. We could index by fullBranch, but we can't index by callback
  346. // or thisObject, as far as I know, since the keys to JavaScript hashes
  347. // (a.k.a. objects) can apparently only be primitive values.
  348. let [observer] = observers.filter(function(v) v.prefName == fullPrefName &&
  349. v.callback == callback &&
  350. v.thisObject == thisObject);
  351. if (observer) {
  352. Preferences._prefSvc.removeObserver(fullPrefName, observer);
  353. observers.splice(observers.indexOf(observer), 1);
  354. }
  355. },
  356. resetBranch: function(prefBranch) {
  357. try {
  358. this._prefSvc.resetBranch(prefBranch);
  359. }
  360. catch(ex) {
  361. // The current implementation of nsIPrefBranch in Mozilla
  362. // doesn't implement resetBranch, so we do it ourselves.
  363. if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED)
  364. this.reset(this._prefSvc.getChildList(prefBranch, []));
  365. else
  366. throw ex;
  367. }
  368. },
  369. /**
  370. * The branch of the preferences tree to which this instance provides access.
  371. * @private
  372. */
  373. _prefBranch: "",
  374. site: function(site) {
  375. if (!(site instanceof Ci.nsIURI))
  376. site = this._ioSvc.newURI("http://" + site, null, null);
  377. return new Preferences({ branch: this._prefBranch, site: site });
  378. },
  379. /**
  380. * Preferences Service
  381. * @private
  382. */
  383. get _prefSvc() {
  384. let prefSvc = Cc["@mozilla.org/preferences-service;1"].
  385. getService(Ci.nsIPrefService).
  386. getBranch(this._prefBranch).
  387. QueryInterface(Ci.nsIPrefBranch2);
  388. this.__defineGetter__("_prefSvc", function() prefSvc);
  389. return this._prefSvc;
  390. },
  391. /**
  392. * IO Service
  393. * @private
  394. */
  395. get _ioSvc() {
  396. let ioSvc = Cc["@mozilla.org/network/io-service;1"].
  397. getService(Ci.nsIIOService);
  398. this.__defineGetter__("_ioSvc", function() ioSvc);
  399. return this._ioSvc;
  400. },
  401. /**
  402. * Site Preferences Service
  403. * @private
  404. */
  405. get _contentPrefSvc() {
  406. let contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"].
  407. getService(Ci.nsIContentPrefService);
  408. this.__defineGetter__("_contentPrefSvc", function() contentPrefSvc);
  409. return this._contentPrefSvc;
  410. }
  411. };
  412. // Give the constructor the same prototype as its instances, so users can access
  413. // preferences directly via the constructor without having to create an instance
  414. // first.
  415. Preferences.__proto__ = Preferences.prototype;
  416. /**
  417. * A cache of pref observers.
  418. *
  419. * We use this to remove observers when a caller calls Preferences::ignore.
  420. *
  421. * All Preferences instances share this object, because we want callers to be
  422. * able to remove an observer using a different Preferences object than the one
  423. * with which they added it. That means we have to identify the observers
  424. * in this object by their complete pref name, not just their name relative to
  425. * the root branch of the Preferences object with which they were created.
  426. */
  427. let observers = [];
  428. function PrefObserver(prefName, callback, thisObject) {
  429. this.prefName = prefName;
  430. this.callback = callback;
  431. this.thisObject = thisObject;
  432. }
  433. PrefObserver.prototype = {
  434. QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
  435. observe: function(subject, topic, data) {
  436. // The pref service only observes whole branches, but we only observe
  437. // individual preferences, so we check here that the pref that changed
  438. // is the exact one we're observing (and not some sub-pref on the branch).
  439. if (data != this.prefName)
  440. return;
  441. if (typeof this.callback == "function") {
  442. let prefValue = Preferences.get(this.prefName);
  443. if (this.thisObject)
  444. this.callback.call(this.thisObject, prefValue);
  445. else
  446. this.callback(prefValue);
  447. }
  448. else // typeof this.callback == "object" (nsIObserver)
  449. this.callback.observe(subject, topic, data);
  450. }
  451. };
  452. function isArray(val) {
  453. // We can't check for |val.constructor == Array| here, since the value
  454. // might be from a different context whose Array constructor is not the same
  455. // as ours, so instead we match based on the name of the constructor.
  456. return (typeof val != "undefined" && val != null && typeof val == "object" &&
  457. val.constructor.name == "Array");
  458. }
  459. function isObject(val) {
  460. // We can't check for |val.constructor == Object| here, since the value
  461. // might be from a different context whose Object constructor is not the same
  462. // as ours, so instead we match based on the name of the constructor.
  463. return (typeof val != "undefined" && val != null && typeof val == "object" &&
  464. val.constructor.name == "Object");
  465. }