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