ext/Preferences.js

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
}