tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Services.js (16501B)


      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/. */
      4 
      5 "use strict";
      6 
      7 /* globals localStorage, window */
      8 
      9 // XXX: This file is a copy of the Services shim from devtools-services.
     10 // See https://github.com/firefox-devtools/devtools-core/blob/a9263b4c3f88ea42879a36cdc3ca8217b4a528ea/packages/devtools-services/index.js
     11 // Many Jest tests in the debugger rely on preferences, but can't use Services.
     12 // This fixture is probably doing too much and should be reduced to the minimum
     13 // needed to pass the tests.
     14 
     15 /* eslint-disable mozilla/valid-services */
     16 
     17 // Some constants from nsIPrefBranch.idl.
     18 const PREF_INVALID = 0;
     19 const PREF_STRING = 32;
     20 const PREF_INT = 64;
     21 const PREF_BOOL = 128;
     22 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
     23 
     24 // We prefix all our local storage items with this.
     25 const PREFIX = "Services.prefs:";
     26 
     27 /**
     28 * Create a new preference branch.  This object conforms largely to
     29 * nsIPrefBranch and nsIPrefService, though it only implements the
     30 * subset needed by devtools.  A preference branch can hold child
     31 * preferences while also holding a preference value itself.
     32 */
     33 class PrefBranch {
     34  /**
     35   * @param {PrefBranch} parent the parent branch, or null for the root
     36   *        branch.
     37   * @param {string} name the base name of this branch
     38   * @param {string} fullName the fully-qualified name of this branch
     39   */
     40  constructor(parent, name, fullName) {
     41    this._parent = parent;
     42    this._name = name;
     43    this._fullName = fullName;
     44    this._observers = {};
     45    this._children = {};
     46 
     47    // Properties used when this branch has a value as well.
     48    this._defaultValue = null;
     49    this._hasUserValue = false;
     50    this._userValue = null;
     51    this._type = PREF_INVALID;
     52  }
     53  PREF_INVALID = PREF_INVALID;
     54  PREF_STRING = PREF_STRING;
     55  PREF_INT = PREF_INT;
     56  PREF_BOOL = PREF_BOOL;
     57 
     58  /** @see nsIPrefBranch.root.  */
     59  get root() {
     60    return this._fullName;
     61  }
     62 
     63  /** @see nsIPrefBranch.getPrefType.  */
     64  getPrefType(prefName) {
     65    return this._findPref(prefName)._type;
     66  }
     67 
     68  /** @see nsIPrefBranch.getBoolPref.  */
     69  getBoolPref(prefName, defaultValue) {
     70    try {
     71      const thePref = this._findPref(prefName);
     72      if (thePref._type !== PREF_BOOL) {
     73        throw new Error(`${prefName} does not have bool type`);
     74      }
     75      return thePref._get();
     76    } catch (e) {
     77      if (typeof defaultValue !== "undefined") {
     78        return defaultValue;
     79      }
     80      throw e;
     81    }
     82  }
     83 
     84  /** @see nsIPrefBranch.setBoolPref.  */
     85  setBoolPref(prefName, value) {
     86    if (typeof value !== "boolean") {
     87      throw new Error("non-bool passed to setBoolPref");
     88    }
     89    const thePref = this._findOrCreatePref(prefName, value, true, value);
     90    if (thePref._type !== PREF_BOOL) {
     91      throw new Error(`${prefName} does not have bool type`);
     92    }
     93    thePref._set(value);
     94  }
     95 
     96  /** @see nsIPrefBranch.getCharPref.  */
     97  getCharPref(prefName, defaultValue) {
     98    try {
     99      const thePref = this._findPref(prefName);
    100      if (thePref._type !== PREF_STRING) {
    101        throw new Error(`${prefName} does not have string type`);
    102      }
    103      return thePref._get();
    104    } catch (e) {
    105      if (typeof defaultValue !== "undefined") {
    106        return defaultValue;
    107      }
    108      throw e;
    109    }
    110  }
    111 
    112  /** @see nsIPrefBranch.getStringPref.  */
    113  getStringPref() {
    114    return this.getCharPref.apply(this, arguments);
    115  }
    116 
    117  /** @see nsIPrefBranch.setCharPref.  */
    118  setCharPref(prefName, value) {
    119    if (typeof value !== "string") {
    120      throw new Error("non-string passed to setCharPref");
    121    }
    122    const thePref = this._findOrCreatePref(prefName, value, true, value);
    123    if (thePref._type !== PREF_STRING) {
    124      throw new Error(`${prefName} does not have string type`);
    125    }
    126    thePref._set(value);
    127  }
    128 
    129  /** @see nsIPrefBranch.setStringPref.  */
    130  setStringPref() {
    131    return this.setCharPref.apply(this, arguments);
    132  }
    133 
    134  /** @see nsIPrefBranch.getIntPref.  */
    135  getIntPref(prefName, defaultValue) {
    136    try {
    137      const thePref = this._findPref(prefName);
    138      if (thePref._type !== PREF_INT) {
    139        throw new Error(`${prefName} does not have int type`);
    140      }
    141      return thePref._get();
    142    } catch (e) {
    143      if (typeof defaultValue !== "undefined") {
    144        return defaultValue;
    145      }
    146      throw e;
    147    }
    148  }
    149 
    150  /** @see nsIPrefBranch.setIntPref.  */
    151  setIntPref(prefName, value) {
    152    if (typeof value !== "number") {
    153      throw new Error("non-number passed to setIntPref");
    154    }
    155    const thePref = this._findOrCreatePref(prefName, value, true, value);
    156    if (thePref._type !== PREF_INT) {
    157      throw new Error(`${prefName} does not have int type`);
    158    }
    159    thePref._set(value);
    160  }
    161 
    162  /** @see nsIPrefBranch.clearUserPref */
    163  clearUserPref(prefName) {
    164    const thePref = this._findPref(prefName);
    165    thePref._clearUserValue();
    166  }
    167 
    168  /** @see nsIPrefBranch.prefHasUserValue */
    169  prefHasUserValue(prefName) {
    170    const thePref = this._findPref(prefName);
    171    return thePref._hasUserValue;
    172  }
    173 
    174  /** @see nsIPrefBranch.addObserver */
    175  addObserver(domain, observer, holdWeak) {
    176    if (holdWeak) {
    177      throw new Error("shim prefs only supports strong observers");
    178    }
    179 
    180    if (!(domain in this._observers)) {
    181      this._observers[domain] = [];
    182    }
    183    this._observers[domain].push(observer);
    184  }
    185 
    186  /** @see nsIPrefBranch.removeObserver */
    187  removeObserver(domain, observer) {
    188    if (!(domain in this._observers)) {
    189      return;
    190    }
    191    const index = this._observers[domain].indexOf(observer);
    192    if (index >= 0) {
    193      this._observers[domain].splice(index, 1);
    194    }
    195  }
    196 
    197  /** @see nsIPrefService.savePrefFile */
    198  savePrefFile(file) {
    199    if (file) {
    200      throw new Error("shim prefs only supports null file in savePrefFile");
    201    }
    202    // Nothing to do - this implementation always writes back.
    203  }
    204 
    205  /** @see nsIPrefService.getBranch */
    206  getBranch(prefRoot) {
    207    if (!prefRoot) {
    208      return this;
    209    }
    210    if (prefRoot.endsWith(".")) {
    211      prefRoot = prefRoot.slice(0, -1);
    212    }
    213    // This is a bit weird since it could erroneously return a pref,
    214    // not a pref branch.
    215    return this._findPref(prefRoot);
    216  }
    217 
    218  /**
    219   * Return this preference's current value.
    220   *
    221   * @return {Any} The current value of this preference.  This may
    222   *         return a string, a number, or a boolean depending on the
    223   *         preference's type.
    224   */
    225  _get() {
    226    if (this._hasUserValue) {
    227      return this._userValue;
    228    }
    229    return this._defaultValue;
    230  }
    231 
    232  /**
    233   * Set the preference's value.  The new value is assumed to be a
    234   * user value.  After setting the value, this function emits a
    235   * change notification.
    236   *
    237   * @param {Any} value the new value
    238   */
    239  _set(value) {
    240    if (!this._hasUserValue || value !== this._userValue) {
    241      this._userValue = value;
    242      this._hasUserValue = true;
    243      this._saveAndNotify();
    244    }
    245  }
    246 
    247  /**
    248   * Set the default value for this preference, and emit a
    249   * notification if this results in a visible change.
    250   *
    251   * @param {Any} value the new default value
    252   */
    253  _setDefault(value) {
    254    if (this._defaultValue !== value) {
    255      this._defaultValue = value;
    256      if (!this._hasUserValue) {
    257        this._saveAndNotify();
    258      }
    259    }
    260  }
    261 
    262  /**
    263   * If this preference has a user value, clear it.  If a change was
    264   * made, emit a change notification.
    265   */
    266  _clearUserValue() {
    267    if (this._hasUserValue) {
    268      this._userValue = null;
    269      this._hasUserValue = false;
    270      this._saveAndNotify();
    271    }
    272  }
    273 
    274  /**
    275   * Helper function to write the preference's value to local storage
    276   * and then emit a change notification.
    277   */
    278  _saveAndNotify() {
    279    const store = {
    280      type: this._type,
    281      defaultValue: this._defaultValue,
    282      hasUserValue: this._hasUserValue,
    283      userValue: this._userValue,
    284    };
    285 
    286    localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store));
    287    this._parent._notify(this._name);
    288  }
    289 
    290  /**
    291   * Change this preference's value without writing it back to local
    292   * storage.  This is used to handle changes to local storage that
    293   * were made externally.
    294   *
    295   * @param {number} type one of the PREF_* values
    296   * @param {Any} userValue the user value to use if the pref does not exist
    297   * @param {Any} defaultValue the default value to use if the pref
    298   *        does not exist
    299   * @param {boolean} hasUserValue if a new pref is created, whether
    300   *        the default value is also a user value
    301   * @param {object} store the new value of the preference.  It should
    302   *        be of the form {type, defaultValue, hasUserValue, userValue};
    303   *        where |type| is one of the PREF_* type constants; |defaultValue|
    304   *        and |userValue| are the default and user values, respectively;
    305   *        and |hasUserValue| is a boolean indicating whether the user value
    306   *        is valid
    307   */
    308  _storageUpdated(type, userValue, hasUserValue, defaultValue) {
    309    this._type = type;
    310    this._defaultValue = defaultValue;
    311    this._hasUserValue = hasUserValue;
    312    this._userValue = userValue;
    313    // There's no need to write this back to local storage, since it
    314    // came from there; and this avoids infinite event loops.
    315    this._parent._notify(this._name);
    316  }
    317 
    318  /**
    319   * Helper function to find either a Preference or PrefBranch object
    320   * given its name.  If the name is not found, throws an exception.
    321   *
    322   * @param {string} prefName the fully-qualified preference name
    323   * @return {object} Either a Preference or PrefBranch object
    324   */
    325  _findPref(prefName) {
    326    const branchNames = prefName.split(".");
    327    let branch = this;
    328 
    329    for (const branchName of branchNames) {
    330      branch = branch._children[branchName];
    331      if (!branch) {
    332        // throw new Error(`could not find pref branch ${ prefName}`);
    333        return false;
    334      }
    335    }
    336 
    337    return branch;
    338  }
    339 
    340  /**
    341   * Helper function to notify any observers when a preference has
    342   * changed.  This will also notify the parent branch for further
    343   * reporting.
    344   *
    345   * @param {string} relativeName the name of the updated pref,
    346   *        relative to this branch
    347   */
    348  _notify(relativeName) {
    349    for (const domain in this._observers) {
    350      if (
    351        relativeName === domain ||
    352        domain === "" ||
    353        (domain.endsWith(".") && relativeName.startsWith(domain))
    354      ) {
    355        // Allow mutation while walking.
    356        const localList = this._observers[domain].slice();
    357        for (const observer of localList) {
    358          try {
    359            if ("observe" in observer) {
    360              observer.observe(
    361                this,
    362                NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
    363                relativeName
    364              );
    365            } else {
    366              // Function-style observer -- these aren't mentioned in
    367              // the IDL, but they're accepted and devtools uses them.
    368              observer(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, relativeName);
    369            }
    370          } catch (e) {
    371            console.error(e);
    372          }
    373        }
    374      }
    375    }
    376 
    377    if (this._parent) {
    378      this._parent._notify(`${this._name}.${relativeName}`);
    379    }
    380  }
    381 
    382  /**
    383   * Helper function to create a branch given an array of branch names
    384   * representing the path of the new branch.
    385   *
    386   * @param {Array} branchList an array of strings, one per component
    387   *        of the branch to be created
    388   * @return {PrefBranch} the new branch
    389   */
    390  _createBranch(branchList) {
    391    let parent = this;
    392    for (const branch of branchList) {
    393      if (!parent._children[branch]) {
    394        const isParentRoot = !parent._parent;
    395        const branchName = (isParentRoot ? "" : `${parent.root}.`) + branch;
    396        parent._children[branch] = new PrefBranch(parent, branch, branchName);
    397      }
    398      parent = parent._children[branch];
    399    }
    400    return parent;
    401  }
    402 
    403  /**
    404   * Create a new preference.  The new preference is assumed to be in
    405   * local storage already, and the new value is taken from there.
    406   *
    407   * @param {string} keyName the full-qualified name of the preference.
    408   *        This is also the name of the key in local storage.
    409   * @param {Any} userValue the user value to use if the pref does not exist
    410   * @param {boolean} hasUserValue if a new pref is created, whether
    411   *        the default value is also a user value
    412   * @param {Any} defaultValue the default value to use if the pref
    413   *        does not exist
    414   * @param {boolean} init if true, then this call is initialization
    415   *        from local storage and should override the default prefs
    416   */
    417  _findOrCreatePref(
    418    keyName,
    419    userValue,
    420    hasUserValue,
    421    defaultValue,
    422    init = false
    423  ) {
    424    const branch = this._createBranch(keyName.split("."));
    425 
    426    if (hasUserValue && typeof userValue !== typeof defaultValue) {
    427      throw new Error(`inconsistent values when creating ${keyName}`);
    428    }
    429 
    430    let type;
    431    switch (typeof defaultValue) {
    432      case "boolean":
    433        type = PREF_BOOL;
    434        break;
    435      case "number":
    436        type = PREF_INT;
    437        break;
    438      case "string":
    439        type = PREF_STRING;
    440        break;
    441      default:
    442        throw new Error(`unhandled argument type: ${typeof defaultValue}`);
    443    }
    444 
    445    if (init || branch._type === PREF_INVALID) {
    446      branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
    447    } else if (branch._type !== type) {
    448      throw new Error(`attempt to change type of pref ${keyName}`);
    449    }
    450 
    451    return branch;
    452  }
    453 
    454  getKeyName(keyName) {
    455    if (keyName.startsWith(PREFIX)) {
    456      return keyName.slice(PREFIX.length);
    457    }
    458 
    459    return keyName;
    460  }
    461 
    462  /**
    463   * Helper function that is called when local storage changes.  This
    464   * updates the preferences and notifies pref observers as needed.
    465   *
    466   * @param {StorageEvent} event the event representing the local
    467   *        storage change
    468   */
    469  _onStorageChange(event) {
    470    if (event.storageArea !== localStorage) {
    471      return;
    472    }
    473 
    474    const key = this.getKeyName(event.key);
    475 
    476    // Ignore delete events.  Not clear what's correct.
    477    if (key === null || event.newValue === null) {
    478      return;
    479    }
    480 
    481    const { type, userValue, hasUserValue, defaultValue } = JSON.parse(
    482      event.newValue
    483    );
    484    if (event.oldValue === null) {
    485      this._findOrCreatePref(key, userValue, hasUserValue, defaultValue);
    486    } else {
    487      const thePref = this._findPref(key);
    488      thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
    489    }
    490  }
    491 
    492  /**
    493   * Helper function to initialize the root PrefBranch.
    494   */
    495  _initializeRoot() {
    496    if (Services._defaultPrefsEnabled) {
    497      /* eslint-disable no-eval */
    498      // let devtools = require("raw!prefs!devtools/client/preferences/devtools");
    499      // eval(devtools);
    500      // let all = require("raw!prefs!modules/libpref/init/all");
    501      // eval(all);
    502      /* eslint-enable no-eval */
    503    }
    504 
    505    // Read the prefs from local storage and create the local
    506    // representations.
    507    for (let i = 0; i < localStorage.length; ++i) {
    508      const keyName = localStorage.key(i);
    509      if (keyName.startsWith(PREFIX)) {
    510        const { userValue, hasUserValue, defaultValue } = JSON.parse(
    511          localStorage.getItem(keyName)
    512        );
    513        this._findOrCreatePref(
    514          keyName.slice(PREFIX.length),
    515          userValue,
    516          hasUserValue,
    517          defaultValue,
    518          true
    519        );
    520      }
    521    }
    522 
    523    this._onStorageChange = this._onStorageChange.bind(this);
    524    window.addEventListener("storage", this._onStorageChange);
    525  }
    526 }
    527 
    528 const Services = {
    529  _prefs: null,
    530 
    531  _defaultPrefsEnabled: true,
    532 
    533  get prefs() {
    534    if (!this._prefs) {
    535      this._prefs = new PrefBranch(null, "", "");
    536      this._prefs._initializeRoot();
    537    }
    538    return this._prefs;
    539  },
    540 
    541  appinfo: "",
    542  locale: {
    543    appLocalesAsLangTags: ["en-US", "en"],
    544  },
    545  obs: { addObserver: () => {} },
    546  strings: {
    547    createBundle() {
    548      return {
    549        GetStringFromName() {
    550          return "NodeTest";
    551        },
    552      };
    553    },
    554  },
    555  intl: {
    556    stringHasRTLChars: () => false,
    557  },
    558 };
    559 
    560 function pref(name, value) {
    561  // eslint-disable-next-line mozilla/valid-services-property
    562  const thePref = Services.prefs._findOrCreatePref(name, value, true, value);
    563  thePref._setDefault(value);
    564 }
    565 
    566 module.exports = Services;
    567 Services.pref = pref;
    568 Services.uuid = { generateUUID: () => {} };
    569 Services.dns = {};