tor-browser

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

devtools-browser.js (19873B)


      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 /**
      8 * This is the main module loaded in Firefox desktop that handles browser
      9 * windows and coordinates devtools around each window.
     10 *
     11 * This module is loaded lazily by devtools-clhandler.js, once the first
     12 * browser window is ready (i.e. fired browser-delayed-startup-finished event)
     13 */
     14 
     15 const lazy = {};
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  BrowserToolboxLauncher:
     18    "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
     19 });
     20 
     21 const {
     22  gDevTools,
     23 } = require("resource://devtools/client/framework/devtools.js");
     24 const {
     25  getTheme,
     26  addThemeObserver,
     27  removeThemeObserver,
     28 } = require("resource://devtools/client/shared/theme.js");
     29 
     30 // Load toolbox lazily as it needs gDevTools to be fully initialized
     31 loader.lazyRequireGetter(
     32  this,
     33  "Toolbox",
     34  "resource://devtools/client/framework/toolbox.js",
     35  true
     36 );
     37 loader.lazyRequireGetter(
     38  this,
     39  "DevToolsServer",
     40  "resource://devtools/server/devtools-server.js",
     41  true
     42 );
     43 loader.lazyRequireGetter(
     44  this,
     45  "BrowserMenus",
     46  "resource://devtools/client/framework/browser-menus.js"
     47 );
     48 loader.lazyRequireGetter(
     49  this,
     50  "appendStyleSheet",
     51  "resource://devtools/client/shared/stylesheet-utils.js",
     52  true
     53 );
     54 loader.lazyRequireGetter(
     55  this,
     56  "ResponsiveUIManager",
     57  "resource://devtools/client/responsive/manager.js"
     58 );
     59 
     60 const BROWSER_STYLESHEET_URL = "chrome://devtools/skin/devtools-browser.css";
     61 
     62 const DEVTOOLS_F12_ENABLED_PREF = "devtools.f12_enabled";
     63 
     64 /**
     65 * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
     66 * Firefox instance.
     67 */
     68 var gDevToolsBrowser = (exports.gDevToolsBrowser = {
     69  /**
     70   * A record of the windows whose menus we altered, so we can undo the changes
     71   * as the window is closed
     72   */
     73  _trackedBrowserWindows: new Set(),
     74 
     75  /**
     76   * WeakMap keeping track of the devtools-browser stylesheets loaded in the various
     77   * tracked windows.
     78   */
     79  _browserStyleSheets: new WeakMap(),
     80 
     81  /**
     82   * This function is for the benefit of Tools:DevToolbox in
     83   * browser/base/content/browser-sets.inc and should not be used outside
     84   * of there
     85   */
     86  // used by browser-sets.inc, command
     87  toggleToolboxCommand(gBrowser, startTime) {
     88    const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
     89 
     90    // If a toolbox exists, using toggle from the Main window :
     91    // - should close a docked toolbox
     92    // - should focus a windowed toolbox
     93    const isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
     94    if (isDocked) {
     95      gDevTools.closeToolboxForTab(gBrowser.selectedTab);
     96    } else {
     97      gDevTools.showToolboxForTab(gBrowser.selectedTab, { startTime });
     98    }
     99  },
    100 
    101  /**
    102   * This function ensures the right commands are enabled in a window,
    103   * depending on their relevant prefs. It gets run when a window is registered,
    104   * or when any of the devtools prefs change.
    105   */
    106  updateCommandAvailability(win) {
    107    const doc = win.document;
    108 
    109    function toggleMenuItem(id, isEnabled) {
    110      const cmd = doc.getElementById(id);
    111      cmd.hidden = !isEnabled;
    112      if (isEnabled) {
    113        cmd.removeAttribute("disabled");
    114      } else {
    115        cmd.setAttribute("disabled", "true");
    116      }
    117    }
    118 
    119    // Enable Browser Toolbox?
    120    const chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
    121    const devtoolsRemoteEnabled = Services.prefs.getBoolPref(
    122      "devtools.debugger.remote-enabled"
    123    );
    124    const remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
    125    toggleMenuItem("menu_browserToolbox", remoteEnabled);
    126 
    127    if (Services.prefs.getBoolPref("devtools.policy.disabled", false)) {
    128      toggleMenuItem("menu_devToolbox", false);
    129      toggleMenuItem("menu_devtools_remotedebugging", false);
    130      toggleMenuItem("menu_browserToolbox", false);
    131      toggleMenuItem("menu_browserConsole", false);
    132      toggleMenuItem("menu_responsiveUI", false);
    133      toggleMenuItem("menu_eyedropper", false);
    134      toggleMenuItem("extensionsForDevelopers", false);
    135    }
    136  },
    137 
    138  /**
    139   * This function makes sure that the "devtoolstheme" attribute is set on the
    140   * browser window to make it possible to change colors on elements in the
    141   * browser (like the splitter between the toolbox and web content).
    142   */
    143  updateDevtoolsThemeAttribute(win) {
    144    // Set an attribute on root element of each window to make it possible
    145    // to change colors based on the selected devtools theme.
    146    let devtoolsTheme = getTheme();
    147    if (devtoolsTheme != "dark") {
    148      devtoolsTheme = "light";
    149    }
    150    win.document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
    151  },
    152 
    153  observe(subject, topic, prefName) {
    154    switch (topic) {
    155      case "browser-delayed-startup-finished":
    156        this._registerBrowserWindow(subject);
    157        break;
    158      case "nsPref:changed":
    159        if (prefName.endsWith("enabled")) {
    160          for (const win of this._trackedBrowserWindows) {
    161            this.updateCommandAvailability(win);
    162          }
    163        }
    164        break;
    165      case "quit-application":
    166        gDevToolsBrowser.destroy({ shuttingDown: true });
    167        break;
    168      case "devtools:loader:destroy":
    169        // This event is fired when the devtools loader unloads, which happens
    170        // only when the add-on workflow ask devtools to be reloaded.
    171        if (subject.wrappedJSObject == require("@loader/unload")) {
    172          gDevToolsBrowser.destroy({ shuttingDown: false });
    173        }
    174        break;
    175    }
    176  },
    177 
    178  _observersRegistered: false,
    179 
    180  /**
    181   * This function is for the benefit of Tools:{toolId} commands,
    182   * triggered from the WebDeveloper menu and keyboard shortcuts.
    183   *
    184   * selectToolCommand's behavior:
    185   * - if the current page is about:devtools-toolbox
    186   *   we select the targeted tool
    187   * - if the toolbox is closed,
    188   *   we open the toolbox and select the tool
    189   * - if the toolbox is open, and the targeted tool is not selected,
    190   *   we select it
    191   * - if the toolbox is open, and the targeted tool is selected,
    192   *   and the host is NOT a window, we close the toolbox
    193   * - if the toolbox is open, and the targeted tool is selected,
    194   *   and the host is a window, we raise the toolbox window
    195   *
    196   * Used when: - registering a new tool
    197   *            - new xul window, to add menu items
    198   */
    199  async selectToolCommand(win, toolId, startTime) {
    200    if (gDevToolsBrowser._isAboutDevtoolsToolbox(win)) {
    201      const toolbox = gDevToolsBrowser._getAboutDevtoolsToolbox(win);
    202      await toolbox.selectTool(toolId, "key_shortcut");
    203      return;
    204    }
    205 
    206    const tab = win.gBrowser.selectedTab;
    207    const toolbox = gDevTools.getToolboxForTab(tab);
    208    const toolDefinition = gDevTools.getToolDefinition(toolId);
    209 
    210    if (
    211      toolbox &&
    212      (toolbox.currentToolId == toolId ||
    213        (toolId == "webconsole" && toolbox.splitConsole))
    214    ) {
    215      toolbox.fireCustomKey(toolId);
    216 
    217      if (
    218        toolDefinition.preventClosingOnKey ||
    219        toolbox.hostType == Toolbox.HostType.WINDOW
    220      ) {
    221        if (!toolDefinition.preventRaisingOnKey) {
    222          await toolbox.raise();
    223        }
    224      } else {
    225        await toolbox.destroy();
    226      }
    227      gDevTools.emit("select-tool-command", toolId);
    228    } else {
    229      await gDevTools
    230        .showToolboxForTab(tab, {
    231          raise: !toolDefinition.preventRaisingOnKey,
    232          startTime,
    233          toolId,
    234        })
    235        .then(newToolbox => {
    236          newToolbox.fireCustomKey(toolId);
    237          gDevTools.emit("select-tool-command", toolId);
    238        });
    239    }
    240  },
    241 
    242  /**
    243   * Called by devtools/client/devtools-startup.js when a key shortcut is pressed
    244   *
    245   * @param  {Window} window
    246   *         The top level browser window from which the key shortcut is pressed.
    247   * @param  {object} key
    248   *         Key object describing the key shortcut being pressed. It comes
    249   *         from devtools-startup.js's KeyShortcuts array. The useful fields here
    250   *         are:
    251   *         - `toolId` used to identify a toolbox's panel like inspector or webconsole,
    252   *         - `id` used to identify any other key shortcuts like about:debugging
    253   * @param {number} startTime
    254   *        Optional, indicates the time at which the key event fired. This is a
    255   *        `ChromeUtils.now()` timing.
    256   */
    257  async onKeyShortcut(window, key, startTime) {
    258    // Avoid to open devtools when the about:devtools-toolbox page is showing
    259    // on the window now.
    260    if (
    261      gDevToolsBrowser._isAboutDevtoolsToolbox(window) &&
    262      (key.id === "toggleToolbox" || key.id === "toggleToolboxF12")
    263    ) {
    264      return;
    265    }
    266 
    267    // If this is a toolbox's panel key shortcut, delegate to selectToolCommand
    268    if (key.toolId) {
    269      await gDevToolsBrowser.selectToolCommand(window, key.toolId, startTime);
    270      return;
    271    }
    272    // Otherwise implement all other key shortcuts individually here
    273    switch (key.id) {
    274      case "toggleToolbox":
    275        gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime);
    276        break;
    277      case "toggleToolboxF12":
    278        if (Services.prefs.getBoolPref(DEVTOOLS_F12_ENABLED_PREF, true)) {
    279          gDevToolsBrowser.toggleToolboxCommand(window.gBrowser, startTime);
    280        }
    281        break;
    282      case "browserToolbox":
    283        lazy.BrowserToolboxLauncher.init();
    284        break;
    285      case "browserConsole": {
    286        const {
    287          BrowserConsoleManager,
    288        } = require("resource://devtools/client/webconsole/browser-console-manager.js");
    289        BrowserConsoleManager.openBrowserConsoleOrFocus();
    290        break;
    291      }
    292      case "responsiveDesignMode":
    293        ResponsiveUIManager.toggle(window, window.gBrowser.selectedTab, {
    294          trigger: "shortcut",
    295        });
    296        break;
    297      case "javascriptTracingToggle": {
    298        const toolbox = gDevTools.getToolboxForTab(window.gBrowser.selectedTab);
    299        if (!toolbox) {
    300          break;
    301        }
    302        await toolbox.commands.tracerCommand.toggle();
    303        break;
    304      }
    305    }
    306  },
    307 
    308  /**
    309   * Open a tab on "about:debugging", optionally pre-select a given tab.
    310   */
    311  // Used by browser-sets.inc, command
    312  openAboutDebugging(gBrowser, hash) {
    313    const url = "about:debugging" + (hash ? "#" + hash : "");
    314    gBrowser.selectedTab = gBrowser.addTrustedTab(url);
    315  },
    316 
    317  /**
    318   * Add the devtools-browser stylesheet to browser window's document. Returns a promise.
    319   *
    320   * @param  {Window} win
    321   *         The window on which the stylesheet should be added.
    322   * @return {Promise} promise that resolves when the stylesheet is loaded (or rejects
    323   *         if it fails to load).
    324   */
    325  loadBrowserStyleSheet(win) {
    326    if (this._browserStyleSheets.has(win)) {
    327      return Promise.resolve();
    328    }
    329 
    330    const doc = win.document;
    331    const { styleSheet, loadPromise } = appendStyleSheet(
    332      doc,
    333      BROWSER_STYLESHEET_URL
    334    );
    335    this._browserStyleSheets.set(win, styleSheet);
    336    return loadPromise;
    337  },
    338 
    339  /**
    340   * Add this DevTools's presence to a browser window's document
    341   *
    342   * @param {HTMLDocument} doc
    343   *        The document to which devtools should be hooked to.
    344   */
    345  _registerBrowserWindow(win) {
    346    if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
    347      return;
    348    }
    349    if (!win.document.getElementById("menuWebDeveloperPopup")) {
    350      // Menus etc. set up here are browser specific.
    351      return;
    352    }
    353    gDevToolsBrowser._trackedBrowserWindows.add(win);
    354    BrowserMenus.addMenus(win.document);
    355 
    356    this.updateCommandAvailability(win);
    357    this.updateDevtoolsThemeAttribute(win);
    358    if (!this._observersRegistered) {
    359      this._observersRegistered = true;
    360      Services.prefs.addObserver("devtools.", this);
    361      this._onThemeChanged = this._onThemeChanged.bind(this);
    362      addThemeObserver(this._onThemeChanged);
    363    }
    364 
    365    win.addEventListener("unload", this);
    366 
    367    const tabContainer = win.gBrowser.tabContainer;
    368    tabContainer.addEventListener("TabSelect", this);
    369  },
    370 
    371  _onThemeChanged() {
    372    for (const win of this._trackedBrowserWindows) {
    373      this.updateDevtoolsThemeAttribute(win);
    374    }
    375  },
    376 
    377  /**
    378   * Add the menuitem for a tool to all open browser windows.
    379   *
    380   * @param {object} toolDefinition
    381   *        properties of the tool to add
    382   */
    383  _addToolToWindows(toolDefinition) {
    384    // No menu item or global shortcut is required for options panel.
    385    if (!toolDefinition.inMenu) {
    386      return;
    387    }
    388 
    389    // Skip if the tool is disabled.
    390    try {
    391      if (
    392        toolDefinition.visibilityswitch &&
    393        !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)
    394      ) {
    395        return;
    396      }
    397    } catch (e) {
    398      // Prevent breaking everything if the pref doesn't exists.
    399    }
    400 
    401    // We need to insert the new tool in the right place, which means knowing
    402    // the tool that comes before the tool that we're trying to add
    403    const allDefs = gDevTools.getToolDefinitionArray();
    404    let prevDef;
    405    for (const def of allDefs) {
    406      if (!def.inMenu) {
    407        continue;
    408      }
    409      if (def === toolDefinition) {
    410        break;
    411      }
    412      prevDef = def;
    413    }
    414 
    415    for (const win of gDevToolsBrowser._trackedBrowserWindows) {
    416      BrowserMenus.insertToolMenuElements(
    417        win.document,
    418        toolDefinition,
    419        prevDef
    420      );
    421      // If we are on a page where devtools menu items are hidden such as
    422      // about:devtools-toolbox, we need to call _updateMenuItems to update the
    423      // visibility of the newly created menu item.
    424      gDevToolsBrowser._updateMenuItems(win);
    425    }
    426  },
    427 
    428  hasToolboxOpened(win) {
    429    const tab = win.gBrowser.selectedTab;
    430    for (const commands of gDevTools._toolboxesPerCommands.keys()) {
    431      if (commands.descriptorFront.localTab == tab) {
    432        return true;
    433      }
    434    }
    435    return false;
    436  },
    437 
    438  /**
    439   * Update developer tools menu items and the "Toggle Tools" checkbox. This is
    440   * called when a toolbox is created or destroyed.
    441   */
    442  _updateMenu() {
    443    for (const win of gDevToolsBrowser._trackedBrowserWindows) {
    444      gDevToolsBrowser._updateMenuItems(win);
    445    }
    446  },
    447 
    448  /**
    449   * Update developer tools menu items and the "Toggle Tools" checkbox of XULWindow.
    450   *
    451   * @param {XULWindow} win
    452   */
    453  _updateMenuItems(win) {
    454    const menu = win.document.getElementById("menu_devToolbox");
    455 
    456    // Hide the "Toggle Tools" menu item if we are on about:devtools-toolbox.
    457    menu.hidden =
    458      gDevToolsBrowser._isAboutDevtoolsToolbox(win) ||
    459      Services.prefs.getBoolPref("devtools.policy.disabled", false);
    460 
    461    // Add a checkmark for the "Toggle Tools" menu item if a toolbox is already opened.
    462    const hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
    463    if (hasToolbox) {
    464      menu.setAttribute("checked", "true");
    465    } else {
    466      menu.removeAttribute("checked");
    467    }
    468  },
    469 
    470  /**
    471   * Check whether the window is showing about:devtools-toolbox page or not.
    472   *
    473   * @param {XULWindow} win
    474   * @return {boolean} true: about:devtools-toolbox is showing
    475   *                   false: otherwise
    476   */
    477  _isAboutDevtoolsToolbox(win) {
    478    const currentURI = win.gBrowser.currentURI;
    479    return (
    480      currentURI.scheme === "about" &&
    481      currentURI.filePath === "devtools-toolbox"
    482    );
    483  },
    484 
    485  /**
    486   * Retrieve the Toolbox instance loaded in the current page if the page is
    487   * about:devtools-toolbox, null otherwise.
    488   *
    489   * @param {XULWindow} win
    490   *        The chrome window containing about:devtools-toolbox. Will match
    491   *        toolbox.topWindow.
    492   * @return {Toolbox} The toolbox instance loaded in about:devtools-toolbox
    493   */
    494  _getAboutDevtoolsToolbox(win) {
    495    if (!gDevToolsBrowser._isAboutDevtoolsToolbox(win)) {
    496      return null;
    497    }
    498    return gDevTools.getToolboxes().find(toolbox => toolbox.topWindow === win);
    499  },
    500 
    501  /**
    502   * Remove the menuitem for a tool to all open browser windows.
    503   *
    504   * @param {string} toolId
    505   *        id of the tool to remove
    506   */
    507  _removeToolFromWindows(toolId) {
    508    for (const win of gDevToolsBrowser._trackedBrowserWindows) {
    509      BrowserMenus.removeToolFromMenu(toolId, win.document);
    510    }
    511  },
    512 
    513  /**
    514   * Called on browser unload to remove menu entries, toolboxes and event
    515   * listeners from the closed browser window.
    516   *
    517   * @param  {XULWindow} win
    518   *         The window containing the menu entry
    519   */
    520  _forgetBrowserWindow(win) {
    521    if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
    522      return;
    523    }
    524    gDevToolsBrowser._trackedBrowserWindows.delete(win);
    525    win.removeEventListener("unload", this);
    526 
    527    BrowserMenus.removeMenus(win.document);
    528 
    529    // Destroy toolboxes for closed window
    530    for (const [commands, toolbox] of gDevTools._toolboxesPerCommands) {
    531      if (
    532        commands.descriptorFront.localTab?.ownerDocument?.defaultView == win
    533      ) {
    534        toolbox.destroy();
    535      }
    536    }
    537 
    538    const styleSheet = this._browserStyleSheets.get(win);
    539    if (styleSheet) {
    540      styleSheet.remove();
    541      this._browserStyleSheets.delete(win);
    542    }
    543 
    544    const tabContainer = win.gBrowser.tabContainer;
    545    tabContainer.removeEventListener("TabSelect", this);
    546  },
    547 
    548  handleEvent(event) {
    549    switch (event.type) {
    550      case "TabSelect":
    551        gDevToolsBrowser._updateMenu();
    552        break;
    553      case "unload":
    554        // top-level browser window unload
    555        gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
    556        break;
    557    }
    558  },
    559 
    560  /**
    561   * Either the DevTools Loader has been destroyed by the add-on contribution
    562   * workflow, or firefox is shutting down.
    563 
    564   * @param {boolean} shuttingDown
    565   *        True if firefox is currently shutting down. We may prevent doing
    566   *        some cleanups to speed it up. Otherwise everything need to be
    567   *        cleaned up in order to be able to load devtools again.
    568   */
    569  destroy({ shuttingDown }) {
    570    Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
    571    removeThemeObserver(this._onThemeChanged);
    572    Services.obs.removeObserver(
    573      gDevToolsBrowser,
    574      "browser-delayed-startup-finished"
    575    );
    576    Services.obs.removeObserver(gDevToolsBrowser, "quit-application");
    577    Services.obs.removeObserver(gDevToolsBrowser, "devtools:loader:destroy");
    578 
    579    for (const win of gDevToolsBrowser._trackedBrowserWindows) {
    580      gDevToolsBrowser._forgetBrowserWindow(win);
    581    }
    582 
    583    // Remove scripts loaded in content process to support the Browser Content Toolbox.
    584    DevToolsServer.removeContentServerScript();
    585 
    586    gDevTools.destroy({ shuttingDown });
    587  },
    588 });
    589 
    590 // Handle all already registered tools,
    591 gDevTools
    592  .getToolDefinitionArray()
    593  .forEach(def => gDevToolsBrowser._addToolToWindows(def));
    594 // and the new ones.
    595 gDevTools.on("tool-registered", function (toolId) {
    596  const toolDefinition = gDevTools._tools.get(toolId);
    597  // If the tool has been registered globally, add to all the
    598  // available windows.
    599  if (toolDefinition) {
    600    gDevToolsBrowser._addToolToWindows(toolDefinition);
    601  }
    602 });
    603 
    604 gDevTools.on("tool-unregistered", function (toolId) {
    605  gDevToolsBrowser._removeToolFromWindows(toolId);
    606 });
    607 
    608 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenu);
    609 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenu);
    610 
    611 Services.obs.addObserver(gDevToolsBrowser, "quit-application");
    612 Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
    613 // Watch for module loader unload. Fires when the tools are reloaded.
    614 Services.obs.addObserver(gDevToolsBrowser, "devtools:loader:destroy");
    615 
    616 // Fake end of browser window load event for all already opened windows
    617 // that is already fully loaded.
    618 for (const win of Services.wm.getEnumerator(gDevTools.chromeWindowType)) {
    619  if (win.gBrowserInit?.delayedStartupFinished) {
    620    gDevToolsBrowser._registerBrowserWindow(win);
    621  }
    622 }