tor-browser

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

toolbox-host-manager.js (12967B)


      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 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
      8 const L10N = new LocalizationHelper(
      9  "devtools/client/locales/toolbox.properties"
     10 );
     11 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     12 const { DOMHelpers } = require("resource://devtools/shared/dom-helpers.js");
     13 
     14 // The min-width of toolbox and browser toolbox.
     15 const WIDTH_CHEVRON_AND_MEATBALL = 50;
     16 const WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE = 74;
     17 const ZOOM_VALUE_PREF = "devtools.toolbox.zoomValue";
     18 
     19 loader.lazyRequireGetter(
     20  this,
     21  "Toolbox",
     22  "resource://devtools/client/framework/toolbox.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "Hosts",
     28  "resource://devtools/client/framework/toolbox-hosts.js",
     29  true
     30 );
     31 
     32 /**
     33 * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
     34 *
     35 * This component handles iframe creation within Firefox, in which we are loading
     36 * the toolbox document. Then both the chrome and the toolbox document communicate
     37 * via "message" events.
     38 *
     39 * Messages sent by the toolbox to the chrome:
     40 * - switch-host:
     41 *   Order to display the toolbox in another host (side, bottom, window, or the
     42 *   previously used one)
     43 * - raise-host:
     44 *   Focus the tools
     45 * - set-host-title:
     46 *   When using the window host, update the window title
     47 *
     48 * Messages sent by the chrome to the toolbox:
     49 * - switched-host:
     50 *   The `switch-host` command sent by the toolbox is done
     51 */
     52 
     53 const LAST_HOST = "devtools.toolbox.host";
     54 const PREVIOUS_HOST = "devtools.toolbox.previousHost";
     55 let ID_COUNTER = 1;
     56 
     57 class ToolboxHostManager {
     58  constructor(commands, hostType, hostOptions) {
     59    this.commands = commands;
     60 
     61    // When debugging a local tab, we keep a reference of the current tab into which the toolbox is displayed.
     62    // This will only change from the descriptor's localTab when we start debugging popups (i.e. window.open).
     63    this.currentTab = this.commands.descriptorFront.localTab;
     64 
     65    // Keep the previously instantiated Host for all tabs where we displayed the Toolbox.
     66    // This will only be useful when we start debugging popups (i.e. window.open).
     67    // This is used to re-use the previous host instance when we re-select the original tab
     68    // we were debugging before the popup opened.
     69    this.hostPerTab = new Map();
     70 
     71    this.frameId = ID_COUNTER++;
     72 
     73    if (!hostType) {
     74      hostType = Services.prefs.getCharPref(LAST_HOST);
     75      if (!Hosts[hostType]) {
     76        // If the preference value is unexpected, restore to the default value.
     77        Services.prefs.clearUserPref(LAST_HOST);
     78        hostType = Services.prefs.getCharPref(LAST_HOST);
     79      }
     80    }
     81    this.eventController = new AbortController();
     82    this.host = this.createHost(hostType, hostOptions);
     83    this.hostType = hostType;
     84    // List of event which are collected when a new host is created for a popup
     85    // from `switchHostToTab` method.
     86    this.collectPendingMessages = null;
     87    this.setMinWidthWithZoom = this.setMinWidthWithZoom.bind(this);
     88    this._onMessage = this._onMessage.bind(this);
     89    Services.prefs.addObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
     90  }
     91  /**
     92   * Create a Toolbox
     93   *
     94   * @param {string} toolId
     95   *        The id of the tool to show
     96   * @param {object} toolOptions
     97   *        Options that will be passed to the tool init function
     98   * @returns {Toolbox}
     99   */
    100  async create(toolId, toolOptions) {
    101    await this.host.create();
    102    if (this.currentTab) {
    103      this.hostPerTab.set(this.currentTab, this.host);
    104    }
    105 
    106    this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
    107    this.host.frame.ownerDocument.defaultView.addEventListener(
    108      "message",
    109      this._onMessage,
    110      { signal: this.eventController.signal }
    111    );
    112 
    113    const toolbox = new Toolbox({
    114      commands: this.commands,
    115      selectedTool: toolId,
    116      selectedToolOptions: toolOptions,
    117      hostType: this.host.type,
    118      contentWindow: this.host.frame.contentWindow,
    119      frameId: this.frameId,
    120    });
    121    toolbox.once("destroyed", this._onToolboxDestroyed.bind(this));
    122 
    123    // Prevent reloading the toolbox when loading the tools in a tab
    124    // (e.g. from about:debugging)
    125    const location = this.host.frame.contentWindow.location;
    126    if (!location.href.startsWith("about:devtools-toolbox")) {
    127      this.host.frame.setAttribute("src", "about:devtools-toolbox");
    128    }
    129 
    130    this.setMinWidthWithZoom();
    131    return toolbox;
    132  }
    133 
    134  setMinWidthWithZoom() {
    135    const zoomValue = parseFloat(Services.prefs.getCharPref(ZOOM_VALUE_PREF));
    136 
    137    if (isNaN(zoomValue)) {
    138      return;
    139    }
    140 
    141    if (
    142      this.hostType === Toolbox.HostType.LEFT ||
    143      this.hostType === Toolbox.HostType.RIGHT
    144    ) {
    145      this.host.frame.style.minWidth =
    146        WIDTH_CHEVRON_AND_MEATBALL_AND_CLOSE * zoomValue + "px";
    147    } else if (
    148      this.hostType === Toolbox.HostType.WINDOW ||
    149      this.hostType === Toolbox.HostType.PAGE ||
    150      this.hostType === Toolbox.HostType.BROWSERTOOLBOX
    151    ) {
    152      this.host.frame.style.minWidth =
    153        WIDTH_CHEVRON_AND_MEATBALL * zoomValue + "px";
    154    }
    155  }
    156 
    157  _onToolboxDestroyed() {
    158    // Delay self-destruction to let the debugger complete async destruction.
    159    // Otherwise it throws when running browser_dbg-breakpoints-in-evaled-sources.js
    160    // because the promise middleware delay each promise action using setTimeout...
    161    DevToolsUtils.executeSoon(() => {
    162      this.destroy();
    163    });
    164  }
    165 
    166  _onMessage(event) {
    167    if (!event.data) {
    168      return;
    169    }
    170    const msg = event.data;
    171    // Toolbox document is still chrome and disallow identifying message
    172    // origin via event.source as it is null. So use a custom id.
    173    if (msg.frameId != this.frameId) {
    174      return;
    175    }
    176    if (this.collectPendingMessages) {
    177      this.collectPendingMessages.push(event);
    178      return;
    179    }
    180    switch (msg.name) {
    181      case "switch-host":
    182        this.switchHost(msg.hostType);
    183        break;
    184      case "switch-host-to-tab":
    185        this.switchHostToTab(msg.tabBrowsingContextID);
    186        break;
    187      case "raise-host":
    188        this.host.raise();
    189        this.postMessage({
    190          name: "host-raised",
    191        });
    192        break;
    193      case "set-host-title":
    194        this.host.setTitle(msg.title);
    195        break;
    196    }
    197  }
    198 
    199  postMessage(data) {
    200    const window = this.host.frame.contentWindow;
    201    window.postMessage(data, "*");
    202  }
    203 
    204  destroy() {
    205    Services.prefs.removeObserver(ZOOM_VALUE_PREF, this.setMinWidthWithZoom);
    206    this.eventController.abort();
    207    this.eventController = null;
    208    this.destroyHost();
    209    // When we are debugging popup, we created host for each popup opened
    210    // in some other tabs. Ensure destroying them here.
    211    for (const host of this.hostPerTab.values()) {
    212      host.destroy();
    213    }
    214    this.hostPerTab.clear();
    215    this.host = null;
    216    this.hostType = null;
    217    this.commands = null;
    218  }
    219 
    220  /**
    221   * Create a host object based on the given host type.
    222   *
    223   * Warning: bottom and sidebar hosts require that the toolbox target provides
    224   * a reference to the attached tab. Not all Targets have a tab property -
    225   * make sure you correctly mix and match hosts and targets.
    226   *
    227   * @param {string} hostType
    228   *        The host type of the new host object
    229   *
    230   * @return {Host} host
    231   *        The created host object
    232   */
    233  createHost(hostType, options) {
    234    if (!Hosts[hostType]) {
    235      throw new Error("Unknown hostType: " + hostType);
    236    }
    237    const newHost = new Hosts[hostType](this.currentTab, options);
    238    return newHost;
    239  }
    240 
    241  /**
    242   * Migrate the toolbox to a new host, while keeping it fully functional.
    243   * The toolbox's iframe will be moved as-is to the new host.
    244   *
    245   * @param {string} hostType
    246   *        The new type of host to spawn
    247   * @param {boolean} destroyPreviousHost
    248   *        Defaults to true. If false is passed, we will avoid destroying
    249   *        the previous host. This is helpful for popup debugging,
    250   *        where we migrate the toolbox between two tabs. In this scenario
    251   *        we are reusing previously instantiated hosts. This is especially
    252   *        useful when we close the current tab and have to have an
    253   *        already instantiated host to migrate to. If we don't have one,
    254   *        the toolbox iframe will already be destroyed before we have a chance
    255   *        to migrate it.
    256   */
    257  async switchHost(hostType, destroyPreviousHost = true) {
    258    if (hostType == "previous") {
    259      // Switch to the last used host for the toolbox UI.
    260      // This is determined by the devtools.toolbox.previousHost pref.
    261      hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
    262 
    263      // Handle the case where the previous host happens to match the current
    264      // host. If so, switch to bottom if it's not already used, and right side if not.
    265      if (hostType === this.hostType) {
    266        if (hostType === Toolbox.HostType.BOTTOM) {
    267          hostType = Toolbox.HostType.RIGHT;
    268        } else {
    269          hostType = Toolbox.HostType.BOTTOM;
    270        }
    271      }
    272    }
    273    const iframe = this.host.frame;
    274    const newHost = this.createHost(hostType);
    275    const newIframe = await newHost.create();
    276 
    277    // Load a blank document in the host frame. The new iframe must have a valid
    278    // document before using swapFrameLoaders().
    279    await new Promise(resolve => {
    280      newIframe.setAttribute("src", "about:blank");
    281      DOMHelpers.onceDOMReady(newIframe.contentWindow, resolve);
    282    });
    283 
    284    // change toolbox document's parent to the new host
    285    newIframe.swapFrameLoaders(iframe);
    286 
    287    // swapFrameLoaders ends up disabling the new frame activeness,
    288    // so ensure we set the expected state at the end of this method
    289    iframe.docShellIsActive = false;
    290    newIframe.docShellIsActive = true;
    291 
    292    if (destroyPreviousHost) {
    293      this.destroyHost();
    294    }
    295 
    296    if (
    297      this.hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
    298      this.hostType !== Toolbox.HostType.PAGE
    299    ) {
    300      Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
    301    }
    302    this.host = newHost;
    303    if (this.currentTab) {
    304      this.hostPerTab.set(this.currentTab, newHost);
    305    }
    306    this.hostType = hostType;
    307    this.host.setTitle(this.host.frame.contentWindow.document.title);
    308    this.host.frame.ownerDocument.defaultView.addEventListener(
    309      "message",
    310      this._onMessage,
    311      { signal: this.eventController.signal }
    312    );
    313 
    314    this.setMinWidthWithZoom();
    315 
    316    if (
    317      hostType !== Toolbox.HostType.BROWSERTOOLBOX &&
    318      hostType !== Toolbox.HostType.PAGE
    319    ) {
    320      Services.prefs.setCharPref(LAST_HOST, hostType);
    321    }
    322 
    323    // Tell the toolbox the host changed
    324    this.postMessage({
    325      name: "switched-host",
    326      hostType,
    327    });
    328  }
    329 
    330  /**
    331   * When we are debugging popup, we are moving around the toolbox between original tab
    332   * and popup tabs. This method will only move the host to a new tab, while
    333   * keeping the same host type.
    334   *
    335   * @param {string} tabBrowsingContextID
    336   *        The ID of the browsing context of the tab we want to move to.
    337   */
    338  async switchHostToTab(tabBrowsingContextID) {
    339    const { gBrowser } = this.host.frame.ownerDocument.defaultView;
    340 
    341    const previousTab = this.currentTab;
    342    const newTab = gBrowser.tabs.find(
    343      tab => tab.linkedBrowser.browsingContext.id == tabBrowsingContextID
    344    );
    345    // Note that newTab will be undefined when the popup opens in a new top level window.
    346    if (newTab && newTab != previousTab) {
    347      this.currentTab = newTab;
    348      const newHost = this.hostPerTab.get(this.currentTab);
    349      if (newHost) {
    350        newHost.frame.swapFrameLoaders(this.host.frame);
    351        this.host = newHost;
    352      } else {
    353        // Ensure collecting any message sent by the toolbox in order to emit them
    354        // on the newly created host, which is created asynchronously
    355        const pendingMessages = [];
    356        this.collectPendingMessages = pendingMessages;
    357        await this.switchHost(this.hostType, false);
    358        this.collectPendingMessages = null;
    359        for (const message of pendingMessages) {
    360          this._onMessage(message);
    361        }
    362      }
    363      previousTab.addEventListener(
    364        "TabSelect",
    365        event => {
    366          this.switchHostToTab(event.target.linkedBrowser.browsingContext.id);
    367        },
    368        { once: true, signal: this.eventController.signal }
    369      );
    370    }
    371 
    372    this.postMessage({
    373      name: "switched-host-to-tab",
    374      browsingContextID: tabBrowsingContextID,
    375    });
    376  }
    377 
    378  /**
    379   * Destroy the current host, and remove event listeners from its frame.
    380   *
    381   * @return {promise} to be resolved when the host is destroyed.
    382   */
    383  destroyHost() {
    384    return this.host.destroy();
    385  }
    386 }
    387 
    388 exports.ToolboxHostManager = ToolboxHostManager;