tor-browser

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

webconsole.js (12860B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "Utils",
     10  "resource://devtools/client/webconsole/utils.js",
     11  true
     12 );
     13 loader.lazyRequireGetter(
     14  this,
     15  "WebConsoleUI",
     16  "resource://devtools/client/webconsole/webconsole-ui.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "gDevTools",
     22  "resource://devtools/client/framework/devtools.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "openDocLink",
     28  "resource://devtools/client/shared/link.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "DevToolsUtils",
     34  "resource://devtools/shared/DevToolsUtils.js"
     35 );
     36 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     37 const Telemetry = require("resource://devtools/client/shared/telemetry.js");
     38 
     39 var gHudId = 0;
     40 const isMacOS = Services.appinfo.OS === "Darwin";
     41 
     42 /**
     43 * A WebConsole instance is an interactive console initialized *per target*
     44 * that displays console log data as well as provides an interactive terminal to
     45 * manipulate the target's document content.
     46 *
     47 * This object only wraps the iframe that holds the Web Console UI. This is
     48 * meant to be an integration point between the Firefox UI and the Web Console
     49 * UI and features.
     50 */
     51 class WebConsole {
     52  /**
     53   * @class
     54   * @param object toolbox
     55   *        The toolbox where the web console is displayed.
     56   * @param object commands
     57   *        The commands object with all interfaces defined from devtools/shared/commands/
     58   * @param nsIDOMWindow iframeWindow
     59   *        The window where the web console UI is already loaded.
     60   * @param nsIDOMWindow chromeWindow
     61   *        The window of the web console owner.
     62   * @param bool isBrowserConsole
     63   */
     64  constructor(
     65    toolbox,
     66    commands,
     67    iframeWindow,
     68    chromeWindow,
     69    isBrowserConsole = false
     70  ) {
     71    this.toolbox = toolbox;
     72    this.commands = commands;
     73    this.iframeWindow = iframeWindow;
     74    this.chromeWindow = chromeWindow;
     75    this.hudId = "hud_" + ++gHudId;
     76    this.browserWindow = DevToolsUtils.getTopWindow(this.chromeWindow);
     77    this.isBrowserConsole = isBrowserConsole;
     78 
     79    // On the browser console, where we don't have a toolbox, we instantiate a dedicated Telemetry instance.
     80    this.telemetry = toolbox?.telemetry || new Telemetry();
     81 
     82    const element = this.browserWindow.document.documentElement;
     83    if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
     84      this.browserWindow = Services.wm.getMostRecentWindow(
     85        gDevTools.chromeWindowType
     86      );
     87    }
     88    this.ui = new WebConsoleUI(this);
     89    this._destroyer = null;
     90 
     91    EventEmitter.decorate(this);
     92  }
     93 
     94  recordEvent(event, extra = {}) {
     95    this.telemetry.recordEvent(event, "webconsole", null, extra);
     96  }
     97 
     98  get currentTarget() {
     99    return this.commands.targetCommand.targetFront;
    100  }
    101 
    102  get resourceCommand() {
    103    return this.commands.resourceCommand;
    104  }
    105 
    106  /**
    107   * Getter for the window that can provide various utilities that the web
    108   * console makes use of, like opening links, managing popups, etc.  In
    109   * most cases, this will be |this.browserWindow|, but in some uses (such as
    110   * the Browser Toolbox), there is no browser window, so an alternative window
    111   * hosts the utilities there.
    112   *
    113   * @type nsIDOMWindow
    114   */
    115  get chromeUtilsWindow() {
    116    if (this.browserWindow) {
    117      return this.browserWindow;
    118    }
    119    return DevToolsUtils.getTopWindow(this.chromeWindow);
    120  }
    121 
    122  get gViewSourceUtils() {
    123    return this.chromeUtilsWindow.gViewSourceUtils;
    124  }
    125 
    126  getFrontByID(id) {
    127    return this.commands.client.getFrontByID(id);
    128  }
    129 
    130  /**
    131   * Initialize the Web Console instance.
    132   *
    133   * @param {boolean} emitCreatedEvent: Defaults to true. If false is passed,
    134   *        We won't be sending the 'web-console-created' event.
    135   *
    136   * @return object
    137   *         A promise for the initialization.
    138   */
    139  async init(emitCreatedEvent = true) {
    140    await this.ui.init();
    141 
    142    // This event needs to be fired later in the case of the BrowserConsole
    143    if (emitCreatedEvent) {
    144      const id = Utils.supportsString(this.hudId);
    145      Services.obs.notifyObservers(id, "web-console-created");
    146    }
    147  }
    148 
    149  /**
    150   * The JSTerm object that manages the console's input.
    151   *
    152   * @see webconsole.js::JSTerm
    153   * @type object
    154   */
    155  get jsterm() {
    156    return this.ui ? this.ui.jsterm : null;
    157  }
    158 
    159  /**
    160   * Get the value from the input field.
    161   *
    162   * @returns {string | null} returns null if there's no input.
    163   */
    164  getInputValue() {
    165    if (!this.jsterm) {
    166      return null;
    167    }
    168 
    169    return this.jsterm._getValue();
    170  }
    171 
    172  inputHasSelection() {
    173    const { editor } = this.jsterm || {};
    174    return editor && !!editor.getSelection();
    175  }
    176 
    177  getInputSelection() {
    178    if (!this.jsterm || !this.jsterm.editor) {
    179      return null;
    180    }
    181    return this.jsterm.editor.getSelection();
    182  }
    183 
    184  /**
    185   * Sets the value of the input field (command line)
    186   *
    187   * @param {string} newValue: The new value to set.
    188   */
    189  setInputValue(newValue) {
    190    if (!this.jsterm) {
    191      return;
    192    }
    193 
    194    this.jsterm._setValue(newValue);
    195  }
    196 
    197  focusInput() {
    198    return this.jsterm && this.jsterm.focus();
    199  }
    200 
    201  /**
    202   * Open a link in a new tab.
    203   *
    204   * @param string link
    205   *        The URL you want to open in a new tab.
    206   */
    207  openLink(link, e = {}) {
    208    openDocLink(link, {
    209      relatedToCurrent: true,
    210      inBackground: isMacOS ? e.metaKey : e.ctrlKey,
    211    });
    212    if (e && typeof e.stopPropagation === "function") {
    213      e.stopPropagation();
    214    }
    215  }
    216 
    217  /**
    218   * Open a link in Firefox's view source.
    219   *
    220   * @param string sourceURL
    221   *        The URL of the file.
    222   * @param integer sourceLine
    223   *        The line number which should be highlighted.
    224   */
    225  viewSource(sourceURL, sourceLine) {
    226    this.gViewSourceUtils.viewSource({
    227      URL: sourceURL,
    228      lineNumber: sourceLine || -1,
    229    });
    230  }
    231 
    232  /**
    233   * Tries to open a JavaScript file related to the web page for the web console
    234   * instance in the Script Debugger. If the file is not found, it is opened in
    235   * source view instead.
    236   *
    237   * Manually handle the case where toolbox does not exist (Browser Console).
    238   *
    239   * @param string sourceURL
    240   *        The URL of the file.
    241   * @param integer sourceLine
    242   *        The line number which you want to place the caret.
    243   * @param integer sourceColumn
    244   *        The column number which you want to place the caret.
    245   */
    246  async viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
    247    const { toolbox } = this;
    248    if (!toolbox) {
    249      this.viewSource(sourceURL, sourceLine, sourceColumn);
    250      return;
    251    }
    252 
    253    await toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
    254    this.ui.emitForTests("source-in-debugger-opened");
    255  }
    256 
    257  /**
    258   * Retrieve information about the JavaScript debugger's currently selected stackframe.
    259   * is used to allow the Web Console to evaluate code in the selected stackframe.
    260   *
    261   * @return {string}
    262   *         The Frame Actor ID.
    263   *         If the debugger is not open or if it's not paused, then |null| is
    264   *         returned.
    265   */
    266  getSelectedFrameActorID() {
    267    const { toolbox } = this;
    268    if (!toolbox) {
    269      return null;
    270    }
    271    const panel = toolbox.getPanel("jsdebugger");
    272 
    273    if (!panel) {
    274      return null;
    275    }
    276 
    277    return panel.getSelectedFrameActorID();
    278  }
    279 
    280  /**
    281   * Given an expression, returns an object containing a new expression, mapped by the
    282   * parser worker to provide additional feature for the user (top-level await,
    283   * original languages mapping, …).
    284   *
    285   * @param {string} expression: The input to maybe map.
    286   * @returns {object | null}
    287   *          Returns null if the input can't be mapped.
    288   *          If it can, returns an object containing the following:
    289   *            - {String} expression: The mapped expression
    290   *            - {Object} mapped: An object containing the different mapping that could
    291   *                               be done and if they were applied on the input.
    292   *                               At the moment, contains `await`, `bindings` and
    293   *                               `originalExpression`.
    294   */
    295  getMappedExpression(expression) {
    296    const { toolbox } = this;
    297 
    298    // We need to check if the debugger is open, since it may perform a variable name
    299    // substitution for sourcemapped script (i.e. evaluated `myVar.trim()` might need to
    300    // be transformed into `a.trim()`).
    301    const panel = toolbox && toolbox.getPanel("jsdebugger");
    302    if (panel) {
    303      return panel.getMappedExpression(expression);
    304    }
    305 
    306    if (expression.includes("await ")) {
    307      const shouldMapBindings = false;
    308      const shouldMapAwait = true;
    309      const res = this.parserWorker.mapExpression(
    310        expression,
    311        null,
    312        null,
    313        shouldMapBindings,
    314        shouldMapAwait
    315      );
    316      return res;
    317    }
    318 
    319    return null;
    320  }
    321 
    322  getMappedVariables() {
    323    const { toolbox } = this;
    324    return toolbox?.getPanel("jsdebugger")?.getMappedVariables();
    325  }
    326 
    327  get parserWorker() {
    328    // If we have a toolbox, we could reuse the parser already instantiated for the debugger.
    329    // Note that we won't have a toolbox when running the Browser Console...
    330    if (this.toolbox) {
    331      return this.toolbox.parserWorker;
    332    }
    333 
    334    if (this._parserWorker) {
    335      return this._parserWorker;
    336    }
    337 
    338    const {
    339      ParserDispatcher,
    340    } = require("resource://devtools/client/debugger/src/workers/parser/index.js");
    341 
    342    this._parserWorker = new ParserDispatcher();
    343    return this._parserWorker;
    344  }
    345 
    346  /**
    347   * Retrieves the current selection from the Inspector, if such a selection
    348   * exists. This is used to pass the ID of the selected actor to the Web
    349   * Console server for the $0 helper.
    350   *
    351   * @return object|null
    352   *         A Selection referring to the currently selected node in the
    353   *         Inspector.
    354   *         If the inspector was never opened, or no node was ever selected,
    355   *         then |null| is returned.
    356   */
    357  getInspectorSelection() {
    358    const { toolbox } = this;
    359    if (!toolbox) {
    360      return null;
    361    }
    362    const panel = toolbox.getPanel("inspector");
    363    if (!panel || !panel.selection) {
    364      return null;
    365    }
    366    return panel.selection;
    367  }
    368 
    369  async onViewSourceInDebugger({ id, url, line, column }) {
    370    if (this.toolbox) {
    371      await this.toolbox.viewSourceInDebugger(url, line, column, id);
    372 
    373      this.recordEvent("jump_to_source");
    374      this.emitForTests("source-in-debugger-opened");
    375    }
    376  }
    377 
    378  async onViewSourceInStyleEditor({ url, line, column }) {
    379    if (!this.toolbox) {
    380      return;
    381    }
    382    await this.toolbox.viewSourceInStyleEditorByURL(url, line, column);
    383    this.recordEvent("jump_to_source");
    384  }
    385 
    386  async openNetworkPanel(requestId) {
    387    if (!this.toolbox) {
    388      return;
    389    }
    390    const netmonitor = await this.toolbox.selectTool("netmonitor");
    391    await netmonitor.panelWin.Netmonitor.inspectRequest(requestId);
    392  }
    393 
    394  getHighlighter() {
    395    if (!this.toolbox) {
    396      return null;
    397    }
    398 
    399    if (this._highlighter) {
    400      return this._highlighter;
    401    }
    402 
    403    this._highlighter = this.toolbox.getHighlighter();
    404    return this._highlighter;
    405  }
    406 
    407  async resendNetworkRequest(requestId) {
    408    if (!this.toolbox) {
    409      return;
    410    }
    411 
    412    const api = await this.toolbox.getNetMonitorAPI();
    413    await api.resendRequest(requestId);
    414  }
    415 
    416  async openNodeInInspector(grip) {
    417    if (!this.toolbox) {
    418      return;
    419    }
    420 
    421    const onSelectInspector = this.toolbox.selectTool(
    422      "inspector",
    423      "inspect_dom"
    424    );
    425 
    426    const onNodeFront = this.toolbox.target
    427      .getFront("inspector")
    428      .then(inspectorFront => inspectorFront.getNodeFrontFromNodeGrip(grip));
    429 
    430    const [nodeFront, inspectorPanel] = await Promise.all([
    431      onNodeFront,
    432      onSelectInspector,
    433    ]);
    434 
    435    const onInspectorUpdated = inspectorPanel.once("inspector-updated");
    436    const onNodeFrontSet = this.toolbox.selection.setNodeFront(nodeFront, {
    437      reason: "console",
    438    });
    439 
    440    await Promise.all([onNodeFrontSet, onInspectorUpdated]);
    441  }
    442 
    443  /**
    444   * Destroy the object. Call this method to avoid memory leaks when the Web
    445   * Console is closed.
    446   *
    447   * @return object
    448   *         A promise object that is resolved once the Web Console is closed.
    449   */
    450  destroy() {
    451    if (!this.hudId) {
    452      return;
    453    }
    454 
    455    if (this.ui) {
    456      this.ui.destroy();
    457    }
    458 
    459    if (this._parserWorker) {
    460      this._parserWorker.stop();
    461      this._parserWorker = null;
    462    }
    463 
    464    const id = Utils.supportsString(this.hudId);
    465    Services.obs.notifyObservers(id, "web-console-destroyed");
    466    this.hudId = null;
    467 
    468    this.emit("destroyed");
    469  }
    470 }
    471 
    472 module.exports = WebConsole;