tor-browser

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

DevToolsShim.sys.mjs (9862B)


      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 const lazy = {};
      6 ChromeUtils.defineLazyGetter(lazy, "DevToolsStartup", () => {
      7  return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
      8    Ci.nsICommandLineHandler
      9  ).wrappedJSObject;
     10 });
     11 
     12 // We don't want to spend time initializing the full loader here so we create
     13 // our own lazy require.
     14 ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
     15  const { require } = ChromeUtils.importESModule(
     16    "resource://devtools/shared/loader/Loader.sys.mjs"
     17  );
     18  // eslint-disable-next-line no-shadow
     19  const Telemetry = require("devtools/client/shared/telemetry");
     20 
     21  return Telemetry;
     22 });
     23 
     24 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
     25 
     26 function removeItem(array, callback) {
     27  const index = array.findIndex(callback);
     28  if (index >= 0) {
     29    array.splice(index, 1);
     30  }
     31 }
     32 
     33 /**
     34 * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
     35 * that work whether Devtools are enabled or not.
     36 *
     37 * It can be used to start listening to devtools events before DevTools are ready. As soon
     38 * as DevTools are ready, the DevToolsShim will forward all the requests received until
     39 * then to the real DevTools instance.
     40 */
     41 export const DevToolsShim = {
     42  _gDevTools: null,
     43  listeners: [],
     44 
     45  get telemetry() {
     46    if (!this._telemetry) {
     47      this._telemetry = new lazy.Telemetry();
     48    }
     49    return this._telemetry;
     50  },
     51 
     52  /**
     53   * Returns true if DevTools are enabled. This now only depends on the policy.
     54   * TODO: Merge isEnabled and isDisabledByPolicy.
     55   *
     56   * @returns {boolean}
     57   */
     58  isEnabled() {
     59    return !this.isDisabledByPolicy();
     60  },
     61 
     62  /**
     63   * Returns true if the devtools are completely disabled and can not be enabled. All
     64   * entry points should return without throwing, initDevTools should never be called.
     65   *
     66   * @returns {boolean}
     67   */
     68  isDisabledByPolicy() {
     69    return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
     70  },
     71 
     72  /**
     73   * Check if DevTools have already been initialized.
     74   *
     75   * @returns {boolean} true if DevTools are initialized.
     76   */
     77  isInitialized() {
     78    return !!this._gDevTools;
     79  },
     80 
     81  /**
     82   * Returns the array of the existing toolboxes. This method is part of the compatibility
     83   * layer for webextensions.
     84   *
     85   * @returns {Array<Toolbox>}
     86   *   An array of toolboxes.
     87   */
     88  getToolboxes() {
     89    if (this.isInitialized()) {
     90      return this._gDevTools.getToolboxes();
     91    }
     92 
     93    return [];
     94  },
     95 
     96  /**
     97   * Register an instance of gDevTools. Should be called by DevTools during startup.
     98   *
     99   * @param {DevTools} gDevTools - A DevTools instance (from client/framework/devtools)
    100   */
    101  register(gDevTools) {
    102    this._gDevTools = gDevTools;
    103    this._onDevToolsRegistered();
    104    this._gDevTools.emit("devtools-registered");
    105  },
    106 
    107  /**
    108   * Unregister the current instance of gDevTools. Should be called by DevTools during
    109   * shutdown.
    110   */
    111  unregister() {
    112    if (this.isInitialized()) {
    113      this._gDevTools.emit("devtools-unregistered");
    114      this._gDevTools = null;
    115    }
    116  },
    117 
    118  /**
    119   * The following methods can be called before DevTools are initialized:
    120   * - on
    121   * - off
    122   *
    123   * If DevTools are not initialized when calling the method, DevToolsShim will call the
    124   * appropriate method as soon as a gDevTools instance is registered.
    125   */
    126 
    127  /**
    128   * This method is used by browser/components/extensions/ext-devtools.js for the events:
    129   * - toolbox-ready
    130   * - toolbox-destroyed
    131   *
    132   * @param {string} event
    133   * @param {Function} listener
    134   */
    135  on(event, listener) {
    136    if (this.isInitialized()) {
    137      this._gDevTools.on(event, listener);
    138    } else {
    139      this.listeners.push([event, listener]);
    140    }
    141  },
    142 
    143  /**
    144   * This method is currently only used by devtools code, but is kept here for consistency
    145   * with on().
    146   *
    147   * @param {string} event
    148   * @param {Function} listener
    149   */
    150  off(event, listener) {
    151    if (this.isInitialized()) {
    152      this._gDevTools.off(event, listener);
    153    } else {
    154      removeItem(this.listeners, ([e, l]) => e === event && l === listener);
    155    }
    156  },
    157 
    158  /**
    159   * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
    160   *
    161   * @param {object} state - A SessionStore state object that gets modified by reference
    162   */
    163  saveDevToolsSession(state) {
    164    if (!this.isInitialized()) {
    165      return;
    166    }
    167 
    168    this._gDevTools.saveDevToolsSession(state);
    169  },
    170 
    171  /**
    172   * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session.
    173   * Will always be called, even if the session does not contain DevTools related items.
    174   *
    175   * @param {object} session
    176   */
    177  restoreDevToolsSession(session) {
    178    if (!this.isEnabled()) {
    179      return;
    180    }
    181 
    182    const { browserConsole, browserToolbox } = session;
    183    const hasDevToolsData = browserConsole || browserToolbox;
    184    if (!hasDevToolsData) {
    185      // Do not initialize DevTools unless there is DevTools specific data in the session.
    186      return;
    187    }
    188 
    189    this.initDevTools("SessionRestore");
    190    this._gDevTools.restoreDevToolsSession(session);
    191  },
    192 
    193  isDevToolsUser() {
    194    return lazy.DevToolsStartup.isDevToolsUser();
    195  },
    196 
    197  /**
    198   * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
    199   * context menu item.
    200   *
    201   * @param {XULTab} tab
    202   *        The browser tab on which inspect accessibility was used.
    203   * @param {ElementIdentifier} domReference
    204   *        Identifier generated by ContentDOMReference. It is a unique pair of
    205   *        BrowsingContext ID and a numeric ID.
    206   * @returns {Promise} a promise that resolves when the accessible node is selected in the
    207   *         accessibility inspector or that resolves immediately if DevTools are not
    208   *         enabled.
    209   */
    210  inspectA11Y(tab, domReference) {
    211    if (!this.isEnabled()) {
    212      return Promise.resolve();
    213    }
    214 
    215    // Record the timing at which this event started in order to compute later in
    216    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
    217    // i.e. especially take `DevToolsStartup.initDevTools` into account.
    218    const startTime = ChromeUtils.now();
    219 
    220    this.initDevTools("ContextMenu");
    221 
    222    return this._gDevTools.inspectA11Y(tab, domReference, startTime);
    223  },
    224 
    225  /**
    226   * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
    227   * context menu item.
    228   *
    229   * @param {XULTab} tab
    230   *        The browser tab on which inspect node was used.
    231   * @param {ElementIdentifier} domReference
    232   *        Identifier generated by ContentDOMReference. It is a unique pair of
    233   *        BrowsingContext ID and a numeric ID.
    234   * @returns {Promise} a promise that resolves when the node is selected in the inspector
    235   *         markup view or that resolves immediately if DevTools are not enabled.
    236   */
    237  inspectNode(tab, domReference) {
    238    if (!this.isEnabled()) {
    239      return Promise.resolve();
    240    }
    241 
    242    // Record the timing at which this event started in order to compute later in
    243    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
    244    // i.e. especially take `DevToolsStartup.initDevTools` into account.
    245    const startTime = ChromeUtils.now();
    246 
    247    this.initDevTools("ContextMenu");
    248 
    249    return this._gDevTools.inspectNode(tab, domReference, startTime);
    250  },
    251 
    252  _onDevToolsRegistered() {
    253    // Register all pending event listeners on the real gDevTools object.
    254    for (const [event, listener] of this.listeners) {
    255      this._gDevTools.on(event, listener);
    256    }
    257 
    258    this.listeners = [];
    259  },
    260 
    261  /**
    262   * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
    263   * not enabled.
    264   *
    265   * @param {string} [reason]
    266   *        optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
    267   *        in toolkit/components/telemetry/Histograms.json
    268   *        and devtools:entry_point in devtools/client/shared/metrics.yaml
    269   */
    270  initDevTools(reason) {
    271    if (!this.isEnabled()) {
    272      throw new Error("DevTools are not enabled and can not be initialized.");
    273    }
    274 
    275    if (reason) {
    276      const window = Services.wm.getMostRecentBrowserWindow();
    277 
    278      this.telemetry.addEventProperty(
    279        window,
    280        "open",
    281        "tools",
    282        null,
    283        "shortcut",
    284        ""
    285      );
    286      this.telemetry.addEventProperty(
    287        window,
    288        "open",
    289        "tools",
    290        null,
    291        "entrypoint",
    292        reason
    293      );
    294    }
    295 
    296    if (!this.isInitialized()) {
    297      lazy.DevToolsStartup.initDevTools(reason);
    298    }
    299  },
    300 };
    301 
    302 /**
    303 * Compatibility layer for webextensions.
    304 *
    305 * Those methods are called only after a DevTools webextension was loaded in DevTools,
    306 * therefore DevTools should always be available when they are called.
    307 */
    308 const webExtensionsMethods = [
    309  "createCommandsForTabForWebExtension",
    310  "getTheme",
    311  "openBrowserConsole",
    312 ];
    313 
    314 /**
    315 * Compatibility layer for other third parties.
    316 */
    317 const otherToolMethods = [
    318  // gDevTools.showToolboxForTab is used by wptrunner to start devtools
    319  // https://github.com/web-platform-tests/wpt
    320  // And also, Quick Actions on URL bar.
    321  "showToolboxForTab",
    322  // Used for Quick Actions on URL bar.
    323  "hasToolboxForTab",
    324  // Used for Quick Actions test.
    325  "getToolboxForTab",
    326 ];
    327 
    328 for (const method of [...webExtensionsMethods, ...otherToolMethods]) {
    329  DevToolsShim[method] = function () {
    330    if (!this.isEnabled()) {
    331      throw new Error(
    332        "Could not call a DevToolsShim webextension method ('" +
    333          method +
    334          "'): DevTools are not initialized."
    335      );
    336    }
    337 
    338    this.initDevTools();
    339    return this._gDevTools[method].apply(this._gDevTools, arguments);
    340  };
    341 }