tor-browser

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

XPCOMUtils.sys.mjs (14014B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * vim: sw=2 ts=2 sts=2 et filetype=javascript
      3 * This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      8 
      9 let global = Cu.getGlobalForObject({});
     10 
     11 // Some global imports expose additional symbols; for example,
     12 // `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel`
     13 // and `MessagePort`. This table maps those extra symbols to the main
     14 // import name.
     15 const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = {
     16  MessagePort: "MessageChannel",
     17 };
     18 
     19 /**
     20 * Redefines the given property on the given object with the given
     21 * value. This can be used to redefine getter properties which do not
     22 * implement setters.
     23 */
     24 function redefine(object, prop, value) {
     25  Object.defineProperty(object, prop, {
     26    configurable: true,
     27    enumerable: true,
     28    value,
     29    writable: true,
     30  });
     31  return value;
     32 }
     33 
     34 /**
     35 * XPCOMUtils contains helpers to make lazily loading scripts, modules, prefs
     36 * and XPCOM services more ergonomic for JS consumers.
     37 *
     38 * @class
     39 */
     40 export var XPCOMUtils = {
     41  /**
     42   * Defines a getter on a specified object for a script.  The script will not
     43   * be loaded until first use.
     44   *
     45   * @param {object} aObject
     46   *        The object to define the lazy getter on.
     47   * @param {string|string[]} aNames
     48   *        The name of the getter to define on aObject for the script.
     49   *        This can be a string if the script exports only one symbol,
     50   *        or an array of strings if the script can be first accessed
     51   *        from several different symbols.
     52   * @param {string} aResource
     53   *        The URL used to obtain the script.
     54   */
     55  defineLazyScriptGetter(aObject, aNames, aResource) {
     56    if (!Array.isArray(aNames)) {
     57      aNames = [aNames];
     58    }
     59    for (let name of aNames) {
     60      Object.defineProperty(aObject, name, {
     61        get() {
     62          XPCOMUtils._scriptloader.loadSubScript(aResource, aObject);
     63          return aObject[name];
     64        },
     65        set(value) {
     66          redefine(aObject, name, value);
     67        },
     68        configurable: true,
     69        enumerable: true,
     70      });
     71    }
     72  },
     73 
     74  /**
     75   * Overrides the scriptloader definition for tests to help with globals
     76   * tracking. Should only be used for tests.
     77   *
     78   * @param {object} aObject
     79   *        The alternative script loader object to use.
     80   */
     81  overrideScriptLoaderForTests(aObject) {
     82    Cu.crashIfNotInAutomation();
     83    delete this._scriptloader;
     84    this._scriptloader = aObject;
     85  },
     86 
     87  /**
     88   * Defines a getter property on the given object for each of the given
     89   * global names as accepted by Cu.importGlobalProperties. These
     90   * properties are imported into the shared system global, and then
     91   * copied onto the given object, no matter which global the object
     92   * belongs to.
     93   *
     94   * @param {object} aObject
     95   *        The object on which to define the properties.
     96   * @param {string[]} aNames
     97   *        The list of global properties to define.
     98   */
     99  defineLazyGlobalGetters(aObject, aNames) {
    100    for (let name of aNames) {
    101      ChromeUtils.defineLazyGetter(aObject, name, () => {
    102        if (!(name in global)) {
    103          let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name;
    104          // eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars
    105          Cu.importGlobalProperties([importName]);
    106        }
    107        return global[name];
    108      });
    109    }
    110  },
    111 
    112  /**
    113   * Defines a getter on a specified object for a service.  The service will not
    114   * be obtained until first use.
    115   *
    116   * @param {object} aObject
    117   *        The object to define the lazy getter on.
    118   * @param {string} aName
    119   *        The name of the getter to define on aObject for the service.
    120   * @param {string} aContract
    121   *        The contract used to obtain the service.
    122   * @param {nsIID} aInterface
    123   *        The interface or name of interface to query the service to.
    124   */
    125  defineLazyServiceGetter(aObject, aName, aContract, aInterface) {
    126    ChromeUtils.defineLazyGetter(aObject, aName, () => {
    127      return Cc[aContract].getService(aInterface);
    128    });
    129  },
    130 
    131  /**
    132   * @typedef {{[key: string]: [string, nsIID]}} ServicesDetail
    133   *   Details of the services by name. The first item in the value array is the
    134   *   contract ID, the second is the nsIID for the interface of the service.
    135   */
    136 
    137  /**
    138   * Defines a lazy service getter on a specified object for each
    139   * property in the given object.
    140   *
    141   * @param {object} aObject
    142   *        The object to define the lazy getter on.
    143   * @param {ServicesDetail} aServices
    144   *        An object with a property for each service to be
    145   *        imported.
    146   */
    147  defineLazyServiceGetters(aObject, aServices) {
    148    for (let [name, service] of Object.entries(aServices)) {
    149      // Note: This is hot code, and cross-compartment array wrappers
    150      // are not JIT-friendly to destructuring or spread operators, so
    151      // we need to use indexed access instead.
    152      this.defineLazyServiceGetter(aObject, name, service[0], service[1]);
    153    }
    154  },
    155 
    156  /**
    157   * Defines a getter on a specified object for preference value. The
    158   * preference is read the first time that the property is accessed,
    159   * and is thereafter kept up-to-date using a preference observer.
    160   *
    161   * @param {object} aObject
    162   *        The object to define the lazy getter on.
    163   * @param {string} aName
    164   *        The name of the getter property to define on aObject.
    165   * @param {string} aPreference
    166   *        The name of the preference to read.
    167   * @param {any} aDefaultPrefValue
    168   *        The default value to use, if the preference is not defined.
    169   *        This is the default value of the pref, before applying aTransform.
    170   * @param {Function} aOnUpdate
    171   *        A function to call upon update. Receives as arguments
    172   *         `(aPreference, previousValue, newValue)`
    173   * @param {Function} aTransform
    174   *        An optional function to transform the value.  If provided,
    175   *        this function receives the new preference value as an argument
    176   *        and its return value is used by the getter.
    177   */
    178  defineLazyPreferenceGetter(
    179    aObject,
    180    aName,
    181    aPreference,
    182    aDefaultPrefValue = null,
    183    aOnUpdate = null,
    184    aTransform = val => val
    185  ) {
    186    if (AppConstants.DEBUG && aDefaultPrefValue !== null) {
    187      let prefType = Services.prefs.getPrefType(aPreference);
    188      if (prefType != Ci.nsIPrefBranch.PREF_INVALID) {
    189        // The pref may get defined after the lazy getter is called
    190        // at which point the code here won't know the expected type.
    191        let prefTypeForDefaultValue = {
    192          boolean: Ci.nsIPrefBranch.PREF_BOOL,
    193          number: Ci.nsIPrefBranch.PREF_INT,
    194          string: Ci.nsIPrefBranch.PREF_STRING,
    195        }[typeof aDefaultPrefValue];
    196        if (prefTypeForDefaultValue != prefType) {
    197          throw new Error(
    198            `Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}`
    199          );
    200        }
    201      }
    202    }
    203 
    204    // Note: We need to keep a reference to this observer alive as long
    205    // as aObject is alive. This means that all of our getters need to
    206    // explicitly close over the variable that holds the object, and we
    207    // cannot define a value in place of a getter after we read the
    208    // preference.
    209    let observer = {
    210      QueryInterface: XPCU_lazyPreferenceObserverQI,
    211 
    212      value: undefined,
    213 
    214      observe(subject, topic, data) {
    215        if (data == aPreference) {
    216          if (aOnUpdate) {
    217            let previous = this.value;
    218 
    219            // Fetch and cache value.
    220            this.value = undefined;
    221            let latest = lazyGetter();
    222            aOnUpdate(data, previous, latest);
    223          } else {
    224            // Empty cache, next call to the getter will cause refetch.
    225            this.value = undefined;
    226          }
    227        }
    228      },
    229    };
    230 
    231    let defineGetter = get => {
    232      Object.defineProperty(aObject, aName, {
    233        configurable: true,
    234        enumerable: true,
    235        get,
    236      });
    237    };
    238 
    239    function lazyGetter() {
    240      if (observer.value === undefined) {
    241        let prefValue;
    242        switch (Services.prefs.getPrefType(aPreference)) {
    243          case Ci.nsIPrefBranch.PREF_STRING:
    244            prefValue = Services.prefs.getStringPref(aPreference);
    245            break;
    246 
    247          case Ci.nsIPrefBranch.PREF_INT:
    248            prefValue = Services.prefs.getIntPref(aPreference);
    249            break;
    250 
    251          case Ci.nsIPrefBranch.PREF_BOOL:
    252            prefValue = Services.prefs.getBoolPref(aPreference);
    253            break;
    254 
    255          case Ci.nsIPrefBranch.PREF_INVALID:
    256            prefValue = aDefaultPrefValue;
    257            break;
    258 
    259          default:
    260            // This should never happen.
    261            throw new Error(
    262              `Error getting pref ${aPreference}; its value's type is ` +
    263                `${Services.prefs.getPrefType(aPreference)}, which I don't ` +
    264                `know how to handle.`
    265            );
    266        }
    267 
    268        observer.value = aTransform(prefValue);
    269      }
    270      return observer.value;
    271    }
    272 
    273    defineGetter(() => {
    274      Services.prefs.addObserver(aPreference, observer, true);
    275 
    276      defineGetter(lazyGetter);
    277      return lazyGetter();
    278    });
    279  },
    280 
    281  /**
    282   * Defines properties on the given object which lazily import
    283   * an ES module or run another utility getter when accessed.
    284   *
    285   * Use this version when you need to define getters on the
    286   * global `this`, or any other object you can't assign to:
    287   *
    288   *    @example
    289   *    XPCOMUtils.defineLazy(this, {
    290   *      AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
    291   *      verticalTabs: { pref: "sidebar.verticalTabs", default: false },
    292   *      MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
    293   *      expensiveThing: () => fetch_or_compute(),
    294   *    });
    295   *
    296   * Additionally, the given object is also returned, which enables
    297   * type-friendly composition:
    298   *
    299   *    @example
    300   *    const existing = {
    301   *      someProps: new Widget(),
    302   *    };
    303   *    const combined = XPCOMUtils.defineLazy(existing, {
    304   *      expensiveThing: () => fetch_or_compute(),
    305   *    });
    306   *
    307   * The `combined` variable is the same object reference as `existing`,
    308   * but TypeScript also knows about lazy getters defined on it.
    309   *
    310   * Since you probably don't want aliases, you can use it like this to,
    311   * for example, define (static) lazy getters on a class:
    312   *
    313   *    @example
    314   *    const Widget = XPCOMUtils.defineLazy(
    315   *      class Widget {
    316   *        static normalProp = 3;
    317   *      },
    318   *      {
    319   *        verticalTabs: { pref: "sidebar.verticalTabs", default: false },
    320   *      }
    321   *    );
    322   *
    323   * @template {LazyDefinition} const L, T
    324   *
    325   * @param {T} lazy
    326   * The object to define the getters on.
    327   *
    328   * @param {L} definition
    329   * Each key:value property defines type and parameters for getters.
    330   *
    331   *  - "resource://module" string
    332   *    @see ChromeUtils.defineESModuleGetters
    333   *
    334   *  - () => value
    335   *    @see ChromeUtils.defineLazyGetter
    336   *
    337   *  - { service: "contract", iid?: nsIID }
    338   *    @see XPCOMUtils.defineLazyServiceGetter
    339   *
    340   *  - { pref: "name", default?, onUpdate?, transform? }
    341   *    @see XPCOMUtils.defineLazyPreferenceGetter
    342   *
    343   * @param {ImportESModuleOptionsDictionary} [options]
    344   * When importing ESModules in devtools and worker contexts,
    345   * the third parameter is required.
    346   */
    347  defineLazy(lazy, definition, options) {
    348    let modules = {};
    349 
    350    for (let [key, val] of Object.entries(definition)) {
    351      if (typeof val === "string") {
    352        modules[key] = val;
    353      } else if (typeof val === "function") {
    354        ChromeUtils.defineLazyGetter(lazy, key, val);
    355      } else if ("service" in val) {
    356        XPCOMUtils.defineLazyServiceGetter(lazy, key, val.service, val.iid);
    357      } else if ("pref" in val) {
    358        XPCOMUtils.defineLazyPreferenceGetter(
    359          lazy,
    360          key,
    361          val.pref,
    362          val.default,
    363          val.onUpdate,
    364          val.transform
    365        );
    366      } else {
    367        throw new Error(`Unkown LazyDefinition for ${key}`);
    368      }
    369    }
    370 
    371    ChromeUtils.defineESModuleGetters(lazy, modules, options);
    372    return /** @type {T & DeclaredLazy<L>} */ (lazy);
    373  },
    374 
    375  /**
    376   * @see XPCOMUtils.defineLazy
    377   * A shorthand for above which always returns a new lazy object.
    378   * Use this version if you have a global `lazy` const with all the getters:
    379   *
    380   *    @example
    381   *    const lazy = XPCOMUtils.declareLazy({
    382   *      AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
    383   *      verticalTabs: { pref: "sidebar.verticalTabs", default: false },
    384   *      MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
    385   *      expensiveThing: () => fetch_or_compute(),
    386   *    });
    387   *
    388   * @template {LazyDefinition} const L
    389   * @param {L} declaration
    390   * @param {ImportESModuleOptionsDictionary} [options]
    391   */
    392  declareLazy(declaration, options) {
    393    return XPCOMUtils.defineLazy({}, declaration, options);
    394  },
    395 
    396  /**
    397   * Defines a non-writable property on an object.
    398   *
    399   * @param {object} aObj
    400   *        The object to define the property on.
    401   *
    402   * @param {string} aName
    403   *        The name of the non-writable property to define on aObject.
    404   *
    405   * @param {any} aValue
    406   *        The value of the non-writable property.
    407   */
    408  defineConstant(aObj, aName, aValue) {
    409    Object.defineProperty(aObj, aName, {
    410      value: aValue,
    411      enumerable: true,
    412      writable: false,
    413    });
    414  },
    415 };
    416 
    417 ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => {
    418  return Services.scriptloader;
    419 });
    420 
    421 var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([
    422  "nsIObserver",
    423  "nsISupportsWeakReference",
    424 ]);