tor-browser

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

webconsole.js (53865B)


      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 /* global clearConsoleEvents */
      6 
      7 "use strict";
      8 
      9 const { Actor } = require("resource://devtools/shared/protocol.js");
     10 const {
     11  webconsoleSpec,
     12 } = require("resource://devtools/shared/specs/webconsole.js");
     13 
     14 const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
     15 const {
     16  LongStringActor,
     17 } = require("resource://devtools/server/actors/string.js");
     18 const {
     19  createValueGrip,
     20  isArray,
     21  stringIsLong,
     22 } = require("resource://devtools/server/actors/object/utils.js");
     23 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     24 const ErrorDocs = require("resource://devtools/server/actors/errordocs.js");
     25 const Targets = require("resource://devtools/server/actors/targets/index.js");
     26 
     27 loader.lazyRequireGetter(
     28  this,
     29  "evalWithDebugger",
     30  "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
     31  true
     32 );
     33 loader.lazyRequireGetter(
     34  this,
     35  "ConsoleFileActivityListener",
     36  "resource://devtools/server/actors/webconsole/listeners/console-file-activity.js",
     37  true
     38 );
     39 loader.lazyRequireGetter(
     40  this,
     41  "jsPropertyProvider",
     42  "resource://devtools/shared/webconsole/js-property-provider.js",
     43  true
     44 );
     45 loader.lazyRequireGetter(
     46  this,
     47  ["isCommand"],
     48  "resource://devtools/server/actors/webconsole/commands/parser.js",
     49  true
     50 );
     51 loader.lazyRequireGetter(
     52  this,
     53  ["CONSOLE_WORKER_IDS", "WebConsoleUtils"],
     54  "resource://devtools/server/actors/webconsole/utils.js",
     55  true
     56 );
     57 loader.lazyRequireGetter(
     58  this,
     59  ["WebConsoleCommandsManager"],
     60  "resource://devtools/server/actors/webconsole/commands/manager.js",
     61  true
     62 );
     63 loader.lazyRequireGetter(
     64  this,
     65  "EventEmitter",
     66  "resource://devtools/shared/event-emitter.js"
     67 );
     68 loader.lazyRequireGetter(
     69  this,
     70  "MESSAGE_CATEGORY",
     71  "resource://devtools/shared/constants.js",
     72  true
     73 );
     74 
     75 // Generated by /devtools/shared/webconsole/GenerateReservedWordsJS.py
     76 loader.lazyRequireGetter(
     77  this,
     78  "RESERVED_JS_KEYWORDS",
     79  "resource://devtools/shared/webconsole/reserved-js-words.js"
     80 );
     81 
     82 // Overwrite implemented listeners for workers so that we don't attempt
     83 // to load an unsupported module.
     84 if (isWorker) {
     85  loader.lazyRequireGetter(
     86    this,
     87    ["ConsoleAPIListener", "ConsoleServiceListener"],
     88    "resource://devtools/server/actors/webconsole/worker-listeners.js",
     89    true
     90  );
     91 } else {
     92  loader.lazyRequireGetter(
     93    this,
     94    "ConsoleAPIListener",
     95    "resource://devtools/server/actors/webconsole/listeners/console-api.js",
     96    true
     97  );
     98  loader.lazyRequireGetter(
     99    this,
    100    "ConsoleServiceListener",
    101    "resource://devtools/server/actors/webconsole/listeners/console-service.js",
    102    true
    103  );
    104  loader.lazyRequireGetter(
    105    this,
    106    "ConsoleReflowListener",
    107    "resource://devtools/server/actors/webconsole/listeners/console-reflow.js",
    108    true
    109  );
    110  loader.lazyRequireGetter(
    111    this,
    112    "DocumentEventsListener",
    113    "resource://devtools/server/actors/webconsole/listeners/document-events.js",
    114    true
    115  );
    116 }
    117 loader.lazyRequireGetter(
    118  this,
    119  "ObjectUtils",
    120  "resource://devtools/server/actors/object/utils.js"
    121 );
    122 
    123 function isObject(value) {
    124  return Object(value) === value;
    125 }
    126 
    127 /**
    128 * The WebConsoleActor implements capabilities needed for the Web Console
    129 * feature.
    130 *
    131 * @class
    132 * @param object connection
    133 *        The connection to the client, DevToolsServerConnection.
    134 * @param object [targetActor]
    135 *        Optional, the parent actor.
    136 */
    137 class WebConsoleActor extends Actor {
    138  constructor(connection, targetActor) {
    139    super(connection, webconsoleSpec);
    140 
    141    this.targetActor = targetActor;
    142 
    143    this.dbg = this.targetActor.dbg;
    144 
    145    this._gripDepth = 0;
    146    this._evalCounter = 0;
    147    this._listeners = new Set();
    148    this._lastConsoleInputEvaluation = undefined;
    149 
    150    this._onWillNavigate = this._onWillNavigate.bind(this);
    151    this._onChangedToplevelDocument =
    152      this._onChangedToplevelDocument.bind(this);
    153    this.onConsoleServiceMessage = this.onConsoleServiceMessage.bind(this);
    154    this.onConsoleAPICall = this.onConsoleAPICall.bind(this);
    155    this.onDocumentEvent = this.onDocumentEvent.bind(this);
    156 
    157    this.targetActor.on(
    158      "changed-toplevel-document",
    159      this._onChangedToplevelDocument
    160    );
    161  }
    162 
    163  /**
    164   * Debugger instance.
    165   *
    166   * @see jsdebugger.sys.mjs
    167   */
    168  dbg = null;
    169 
    170  /**
    171   * This is used by the ObjectActor to keep track of the depth of grip() calls.
    172   *
    173   * @private
    174   * @type number
    175   */
    176  _gripDepth = null;
    177 
    178  /**
    179   * Holds a set of all currently registered listeners.
    180   *
    181   * @private
    182   * @type Set
    183   */
    184  _listeners = null;
    185 
    186  /**
    187   * The global we work with (this can be a Window, a Worker global or even a Sandbox
    188   * for processes and addons).
    189   *
    190   * @type nsIDOMWindow, WorkerGlobalScope or Sandbox
    191   */
    192  get global() {
    193    if (this.targetActor.isRootActor) {
    194      return this._getWindowForBrowserConsole();
    195    }
    196    return this.targetActor.targetGlobal;
    197  }
    198 
    199  /**
    200   * Get a window to use for the browser console.
    201   *
    202   * (note that is is also used for browser toolbox and webextension
    203   *  i.e. all targets flagged with isRootActor=true)
    204   *
    205   * @private
    206   * @return nsIDOMWindow
    207   *         The window to use, or null if no window could be found.
    208   */
    209  _getWindowForBrowserConsole() {
    210    // Check if our last used chrome window is still live.
    211    let window = this._lastChromeWindow && this._lastChromeWindow.get();
    212    // If not, look for a new one.
    213    // In case of WebExtension reload of the background page, the last
    214    // chrome window might be a dead wrapper, from which we can't check for window.closed.
    215    if (!window || Cu.isDeadWrapper(window) || window.closed) {
    216      window = this.targetActor.window;
    217      if (!window) {
    218        // Try to find the Browser Console window to use instead.
    219        window = Services.wm.getMostRecentWindow("devtools:webconsole");
    220        // We prefer the normal chrome window over the console window,
    221        // so we'll look for those windows in order to replace our reference.
    222        const onChromeWindowOpened = () => {
    223          // We'll look for this window when someone next requests window()
    224          Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened");
    225          this._lastChromeWindow = null;
    226        };
    227        Services.obs.addObserver(onChromeWindowOpened, "domwindowopened");
    228      }
    229 
    230      this._handleNewWindow(window);
    231    }
    232 
    233    return window;
    234  }
    235 
    236  /**
    237   * Store a newly found window on the actor to be used in the future.
    238   *
    239   * @private
    240   * @param nsIDOMWindow window
    241   *        The window to store on the actor (can be null).
    242   */
    243  _handleNewWindow(window) {
    244    if (window) {
    245      if (this._hadChromeWindow) {
    246        Services.console.logStringMessage("Webconsole context has changed");
    247      }
    248      this._lastChromeWindow = Cu.getWeakReference(window);
    249      this._hadChromeWindow = true;
    250    } else {
    251      this._lastChromeWindow = null;
    252    }
    253  }
    254 
    255  /**
    256   * Whether we've been using a window before.
    257   *
    258   * @private
    259   * @type boolean
    260   */
    261  _hadChromeWindow = false;
    262 
    263  /**
    264   * A weak reference to the last chrome window we used to work with.
    265   *
    266   * @private
    267   * @type nsIWeakReference
    268   */
    269  _lastChromeWindow = null;
    270 
    271  // The evalGlobal is used at the scope for JS evaluation.
    272  _evalGlobal = null;
    273  get evalGlobal() {
    274    return this._evalGlobal || this.global;
    275  }
    276 
    277  set evalGlobal(global) {
    278    this._evalGlobal = global;
    279 
    280    if (!this._progressListenerActive) {
    281      this.targetActor.on("will-navigate", this._onWillNavigate);
    282      this._progressListenerActive = true;
    283    }
    284  }
    285 
    286  /**
    287   * Flag used to track if we are listening for events from the progress
    288   * listener of the target actor. We use the progress listener to clear
    289   * this.evalGlobal on page navigation.
    290   *
    291   * @private
    292   * @type boolean
    293   */
    294  _progressListenerActive = false;
    295 
    296  /**
    297   * The ConsoleServiceListener instance.
    298   *
    299   * @type object
    300   */
    301  consoleServiceListener = null;
    302 
    303  /**
    304   * The ConsoleAPIListener instance.
    305   */
    306  consoleAPIListener = null;
    307 
    308  /**
    309   * The ConsoleFileActivityListener instance.
    310   */
    311  consoleFileActivityListener = null;
    312 
    313  /**
    314   * The ConsoleReflowListener instance.
    315   */
    316  consoleReflowListener = null;
    317 
    318  grip() {
    319    return { actor: this.actorID };
    320  }
    321 
    322  _findProtoChain = ThreadActor.prototype._findProtoChain;
    323  _removeFromProtoChain = ThreadActor.prototype._removeFromProtoChain;
    324 
    325  /**
    326   * Destroy the current WebConsoleActor instance.
    327   */
    328  destroy() {
    329    this.stopListeners();
    330    super.destroy();
    331 
    332    this.targetActor.off(
    333      "changed-toplevel-document",
    334      this._onChangedToplevelDocument
    335    );
    336 
    337    this._lastConsoleInputEvaluation = null;
    338    this._evalGlobal = null;
    339    this.dbg = null;
    340  }
    341 
    342  /**
    343   * Create a grip for the given value.
    344   *
    345   * @param mixed value
    346   * @param object objectActorAttributes
    347   *        See createValueGrip in devtools/server/actors/object/utils.js
    348   * @return object
    349   */
    350  createValueGrip(value, objectActorAttributes = {}) {
    351    return createValueGrip(
    352      this.targetActor.threadActor,
    353      value,
    354      this.targetActor.objectsPool,
    355      0,
    356      objectActorAttributes
    357    );
    358  }
    359 
    360  /**
    361   * Make a debuggee value for the given value.
    362   *
    363   * @param mixed value
    364   *        The value you want to get a debuggee value for.
    365   * @param boolean useObjectGlobal
    366   *        If |true| the object global is determined and added as a debuggee,
    367   *        otherwise |this.global| is used when makeDebuggeeValue() is invoked.
    368   * @return object
    369   *         Debuggee value for |value|.
    370   */
    371  makeDebuggeeValue(value, useObjectGlobal) {
    372    if (useObjectGlobal && isObject(value)) {
    373      try {
    374        const global = Cu.getGlobalForObject(value);
    375        const dbgGlobal = this.dbg.makeGlobalObjectReference(global);
    376        return dbgGlobal.makeDebuggeeValue(value);
    377      } catch (ex) {
    378        // The above can throw an exception if value is not an actual object
    379        // or 'Object in compartment marked as invisible to Debugger'
    380      }
    381    }
    382    const dbgGlobal = this.dbg.makeGlobalObjectReference(this.global);
    383    return dbgGlobal.makeDebuggeeValue(value);
    384  }
    385 
    386  /**
    387   * Create a grip for the given string.
    388   *
    389   * @param string string
    390   *        The string you want to create the grip for.
    391   * @param object pool
    392   *        A Pool where the new actor instance is added.
    393   * @return object
    394   *         A LongStringActor object that wraps the given string.
    395   */
    396  longStringGrip(string, pool) {
    397    const actor = new LongStringActor(this.conn, string);
    398    pool.manage(actor);
    399    return actor.form();
    400  }
    401 
    402  /**
    403   * Create a long string grip if needed for the given string.
    404   *
    405   * @private
    406   * @param string string
    407   *        The string you want to create a long string grip for.
    408   * @return string|object
    409   *         A string is returned if |string| is not a long string.
    410   *         A LongStringActor grip is returned if |string| is a long string.
    411   */
    412  _createStringGrip(string) {
    413    if (string && stringIsLong(string)) {
    414      return this.longStringGrip(string, this);
    415    }
    416    return string;
    417  }
    418 
    419  /**
    420   * Returns the latest web console input evaluation.
    421   * This is undefined if no evaluations have been completed.
    422   *
    423   * @return object
    424   */
    425  getLastConsoleInputEvaluation() {
    426    return this._lastConsoleInputEvaluation;
    427  }
    428 
    429  /**
    430   * Preprocess a debugger object (e.g. return the `boundTargetFunction`
    431   * debugger object if the given debugger object is a bound function).
    432   *
    433   * This method is called by both the `inspect` binding implemented
    434   * for the webconsole and the one implemented for the devtools API
    435   * `browser.devtools.inspectedWindow.eval`.
    436   */
    437  preprocessDebuggerObject(dbgObj) {
    438    // Returns the bound target function on a bound function.
    439    if (dbgObj?.isBoundFunction && dbgObj?.boundTargetFunction) {
    440      return dbgObj.boundTargetFunction;
    441    }
    442 
    443    return dbgObj;
    444  }
    445 
    446  /**
    447   * This helper is used by the WebExtensionInspectedWindowActor to
    448   * inspect an object in the developer toolbox.
    449   *
    450   * NOTE: shared parts related to preprocess the debugger object (between
    451   * this function and the `inspect` webconsole command defined in
    452   * "devtools/server/actor/webconsole/utils.js") should be added to
    453   * the webconsole actors' `preprocessDebuggerObject` method.
    454   */
    455  inspectObject(dbgObj, inspectFromAnnotation) {
    456    dbgObj = this.preprocessDebuggerObject(dbgObj);
    457    this.emit("inspectObject", {
    458      objectActor: this.createValueGrip(dbgObj),
    459      inspectFromAnnotation,
    460    });
    461  }
    462 
    463  // Request handlers for known packet types.
    464 
    465  /**
    466   * Handler for the "startListeners" request.
    467   *
    468   * @param array listeners
    469   *        An array of events to start sent by the Web Console client.
    470   * @return object
    471   *        The response object which holds the startedListeners array.
    472   */
    473  // eslint-disable-next-line complexity
    474  async startListeners(listeners) {
    475    const startedListeners = [];
    476    const global = !this.targetActor.isRootActor ? this.global : null;
    477    const isTargetActorContentProcess =
    478      this.targetActor.targetType === Targets.TYPES.PROCESS;
    479 
    480    for (const event of listeners) {
    481      switch (event) {
    482        case "PageError":
    483          // Workers don't support this message type yet
    484          if (isWorker) {
    485            break;
    486          }
    487          if (!this.consoleServiceListener) {
    488            this.consoleServiceListener = new ConsoleServiceListener(
    489              global,
    490              this.onConsoleServiceMessage,
    491              {
    492                matchExactWindow: this.targetActor.ignoreSubFrames,
    493              }
    494            );
    495            this.consoleServiceListener.init();
    496          }
    497          startedListeners.push(event);
    498          break;
    499        case "ConsoleAPI":
    500          if (!this.consoleAPIListener) {
    501            // Create the consoleAPIListener
    502            // (and apply the filtering options defined in the parent actor).
    503            this.consoleAPIListener = new ConsoleAPIListener(
    504              global,
    505              this.onConsoleAPICall,
    506              {
    507                matchExactWindow: this.targetActor.ignoreSubFrames,
    508              }
    509            );
    510            this.consoleAPIListener.init();
    511          }
    512          startedListeners.push(event);
    513          break;
    514        case "NetworkActivity": {
    515          // Workers don't support this message type
    516          if (isWorker) {
    517            break;
    518          }
    519          // Bug 1807650 removed this in favor of the new Watcher/Resources APIs
    520          const errorMessage =
    521            "NetworkActivity is no longer supported. " +
    522            "Instead use Watcher actor's watchResources and listen to NETWORK_EVENT resource";
    523          dump(errorMessage + "\n");
    524          throw new Error(errorMessage);
    525        }
    526        case "FileActivity":
    527          // Workers don't support this message type
    528          if (isWorker) {
    529            break;
    530          }
    531          if (this.global instanceof Ci.nsIDOMWindow) {
    532            if (!this.consoleFileActivityListener) {
    533              this.consoleFileActivityListener =
    534                new ConsoleFileActivityListener(this.global, this);
    535            }
    536            this.consoleFileActivityListener.startMonitor();
    537            startedListeners.push(event);
    538          }
    539          break;
    540        case "ReflowActivity":
    541          // Workers don't support this message type
    542          if (isWorker) {
    543            break;
    544          }
    545          if (!this.consoleReflowListener) {
    546            this.consoleReflowListener = new ConsoleReflowListener(
    547              this.global,
    548              this
    549            );
    550          }
    551          startedListeners.push(event);
    552          break;
    553        case "DocumentEvents":
    554          // Workers don't support this message type
    555          if (isWorker || isTargetActorContentProcess) {
    556            break;
    557          }
    558          if (!this.documentEventsListener) {
    559            this.documentEventsListener = new DocumentEventsListener(
    560              this.targetActor
    561            );
    562 
    563            this.documentEventsListener.on("dom-loading", data =>
    564              this.onDocumentEvent("dom-loading", data)
    565            );
    566            this.documentEventsListener.on("dom-interactive", data =>
    567              this.onDocumentEvent("dom-interactive", data)
    568            );
    569            this.documentEventsListener.on("dom-complete", data =>
    570              this.onDocumentEvent("dom-complete", data)
    571            );
    572 
    573            this.documentEventsListener.listen();
    574          }
    575          startedListeners.push(event);
    576          break;
    577      }
    578    }
    579 
    580    // Update the live list of running listeners
    581    startedListeners.forEach(this._listeners.add, this._listeners);
    582 
    583    return {
    584      startedListeners,
    585    };
    586  }
    587 
    588  /**
    589   * Handler for the "stopListeners" request.
    590   *
    591   * @param array listeners
    592   *        An array of events to stop sent by the Web Console client.
    593   * @return object
    594   *        The response packet to send to the client: holds the
    595   *        stoppedListeners array.
    596   */
    597  stopListeners(listeners) {
    598    const stoppedListeners = [];
    599 
    600    // If no specific listeners are requested to be detached, we stop all
    601    // listeners.
    602    const eventsToDetach = listeners || [
    603      "PageError",
    604      "ConsoleAPI",
    605      "FileActivity",
    606      "ReflowActivity",
    607      "DocumentEvents",
    608    ];
    609 
    610    for (const event of eventsToDetach) {
    611      switch (event) {
    612        case "PageError":
    613          if (this.consoleServiceListener) {
    614            this.consoleServiceListener.destroy();
    615            this.consoleServiceListener = null;
    616          }
    617          stoppedListeners.push(event);
    618          break;
    619        case "ConsoleAPI":
    620          if (this.consoleAPIListener) {
    621            this.consoleAPIListener.destroy();
    622            this.consoleAPIListener = null;
    623          }
    624          stoppedListeners.push(event);
    625          break;
    626        case "FileActivity":
    627          if (this.consoleFileActivityListener) {
    628            this.consoleFileActivityListener.stopMonitor();
    629            this.consoleFileActivityListener = null;
    630          }
    631          stoppedListeners.push(event);
    632          break;
    633        case "ReflowActivity":
    634          if (this.consoleReflowListener) {
    635            this.consoleReflowListener.destroy();
    636            this.consoleReflowListener = null;
    637          }
    638          stoppedListeners.push(event);
    639          break;
    640        case "DocumentEvents":
    641          if (this.documentEventsListener) {
    642            this.documentEventsListener.destroy();
    643            this.documentEventsListener = null;
    644          }
    645          stoppedListeners.push(event);
    646          break;
    647      }
    648    }
    649 
    650    // Update the live list of running listeners
    651    stoppedListeners.forEach(this._listeners.delete, this._listeners);
    652 
    653    return { stoppedListeners };
    654  }
    655 
    656  /**
    657   * Handler for the "getCachedMessages" request. This method sends the cached
    658   * error messages and the window.console API calls to the client.
    659   *
    660   * @param array messageTypes
    661   *        An array of message types sent by the Web Console client.
    662   * @return object
    663   *         The response packet to send to the client: it holds the cached
    664   *         messages array.
    665   */
    666  getCachedMessages(messageTypes) {
    667    if (!messageTypes) {
    668      return {
    669        error: "missingParameter",
    670        message: "The messageTypes parameter is missing.",
    671      };
    672    }
    673 
    674    const messages = [];
    675 
    676    const consoleServiceCachedMessages =
    677      messageTypes.includes("PageError") || messageTypes.includes("LogMessage")
    678        ? this.consoleServiceListener?.getCachedMessages(
    679            !this.targetActor.isRootActor
    680          )
    681        : null;
    682 
    683    for (const type of messageTypes) {
    684      switch (type) {
    685        case "ConsoleAPI": {
    686          if (!this.consoleAPIListener) {
    687            break;
    688          }
    689 
    690          // this.global might not be a window (can be a worker global or a Sandbox),
    691          // and in such case performance isn't defined
    692          const winStartTime =
    693            this.global?.performance?.timing?.navigationStart;
    694 
    695          const cache = this.consoleAPIListener.getCachedMessages(
    696            !this.targetActor.isRootActor
    697          );
    698          cache.forEach(cachedMessage => {
    699            // Filter out messages that came from a ServiceWorker but happened
    700            // before the page was requested.
    701            if (
    702              cachedMessage.innerID === "ServiceWorker" &&
    703              winStartTime > cachedMessage.timeStamp
    704            ) {
    705              return;
    706            }
    707 
    708            messages.push({
    709              message: this.prepareConsoleMessageForRemote(cachedMessage),
    710              type: "consoleAPICall",
    711            });
    712          });
    713          break;
    714        }
    715 
    716        case "PageError": {
    717          if (!consoleServiceCachedMessages) {
    718            break;
    719          }
    720 
    721          for (const cachedMessage of consoleServiceCachedMessages) {
    722            if (!(cachedMessage instanceof Ci.nsIScriptError)) {
    723              continue;
    724            }
    725 
    726            messages.push({
    727              pageError: this.preparePageErrorForRemote(cachedMessage),
    728              type: "pageError",
    729            });
    730          }
    731          break;
    732        }
    733 
    734        case "LogMessage": {
    735          if (!consoleServiceCachedMessages) {
    736            break;
    737          }
    738 
    739          for (const cachedMessage of consoleServiceCachedMessages) {
    740            if (cachedMessage instanceof Ci.nsIScriptError) {
    741              continue;
    742            }
    743 
    744            messages.push({
    745              message: this._createStringGrip(cachedMessage.message),
    746              timeStamp: cachedMessage.microSecondTimeStamp / 1000,
    747              type: "logMessage",
    748            });
    749          }
    750          break;
    751        }
    752      }
    753    }
    754 
    755    return {
    756      messages,
    757    };
    758  }
    759 
    760  /**
    761   * Handler for the "evaluateJSAsync" request. This method evaluates a given
    762   * JavaScript string with an associated `resultID`.
    763   *
    764   * The result will be returned later as an unsolicited `evaluationResult`,
    765   * that can be associated back to this request via the `resultID` field.
    766   *
    767   * @param object request
    768   *        The JSON request object received from the Web Console client.
    769   * @return object
    770   *         The response packet to send to with the unique id in the
    771   *         `resultID` field.
    772   */
    773  async evaluateJSAsync(request) {
    774    const startTime = ChromeUtils.dateNow();
    775    // Use  a timestamp instead of a UUID as this code is used by workers, which
    776    // don't have access to the UUID XPCOM component.
    777    // Also use a counter in order to prevent mixing up response when calling
    778    // at the exact same time.
    779    const resultID = startTime + "-" + this._evalCounter++;
    780 
    781    // Execute the evaluation in the next event loop in order to immediately
    782    // reply with the resultID.
    783    //
    784    // The console input should be evaluated with micro task level != 0,
    785    // so that microtask checkpoint isn't performed while evaluating it.
    786    DevToolsUtils.executeSoonWithMicroTask(async () => {
    787      try {
    788        // Execute the script that may pause.
    789        let response = await this.evaluateJS(request);
    790        // Wait for any potential returned Promise.
    791        response = await this._maybeWaitForResponseResult(response);
    792 
    793        // Set the timestamp only now, so any messages logged in the expression (e.g. console.log)
    794        // can be appended before the result message (unlike the evaluation result, other
    795        // console resources are throttled before being handled by the webconsole client,
    796        // which might cause some ordering issue).
    797        // Use ChromeUtils.dateNow() as it gives us a higher precision than Date.now().
    798        response.timestamp = ChromeUtils.dateNow();
    799        // Finally, emit an unsolicited evaluationResult packet with the evaluation result.
    800        this.emit("evaluationResult", {
    801          type: "evaluationResult",
    802          resultID,
    803          startTime,
    804          ...response,
    805        });
    806      } catch (e) {
    807        const message = `Encountered error while waiting for Helper Result: ${e}\n${e.stack}`;
    808        DevToolsUtils.reportException("evaluateJSAsync", Error(message));
    809      }
    810    });
    811    return { resultID };
    812  }
    813 
    814  /**
    815   * In order to support async evaluations (e.g. top-level await, …),
    816   * we have to be able to handle promises. This method handles waiting for the promise,
    817   * and then returns the result.
    818   *
    819   * @private
    820   * @param object response
    821   *         The response packet to send to with the unique id in the
    822   *         `resultID` field, and potentially a promise in the `helperResult` or in the
    823   *         `awaitResult` field.
    824   *
    825   * @return object
    826   *         The updated response object.
    827   */
    828  async _maybeWaitForResponseResult(response) {
    829    if (!response?.awaitResult) {
    830      return response;
    831    }
    832 
    833    let result;
    834    try {
    835      result = await response.awaitResult;
    836 
    837      // `createValueGrip` expect a debuggee value, while here we have the raw object.
    838      // We need to call `makeDebuggeeValue` on it to make it work.
    839      const dbgResult = this.makeDebuggeeValue(result);
    840      response.result = this.createValueGrip(dbgResult);
    841    } catch (e) {
    842      // The promise was rejected. We let the engine handle this as it will report a
    843      // `uncaught exception` error.
    844      response.topLevelAwaitRejected = true;
    845    }
    846 
    847    // Remove the promise from the response object.
    848    delete response.awaitResult;
    849 
    850    return response;
    851  }
    852 
    853  /**
    854   * Handler for the "evaluateJS" request. This method evaluates the given
    855   * JavaScript string and sends back the result.
    856   *
    857   * @param object request
    858   *        The JSON request object received from the Web Console client.
    859   * @return object
    860   *         The evaluation response packet.
    861   */
    862  evaluateJS(request) {
    863    const input = request.text;
    864 
    865    const evalOptions = {
    866      frameActor: request.frameActor,
    867      url: request.url,
    868      innerWindowID: request.innerWindowID,
    869      selectedNodeActor: request.selectedNodeActor,
    870      selectedObjectActor: request.selectedObjectActor,
    871      eager: request.eager,
    872      bindings: request.bindings,
    873      lineNumber: request.lineNumber,
    874      // This flag is set to true in most cases as we consider most evaluations as internal and:
    875      // * prevent any breakpoint from being triggerred when evaluating the JS input
    876      // * prevent spawning Debugger.Source for the evaluated JS and showing it in Debugger UI
    877      // This is only set to false when evaluating the console input.
    878      disableBreaks: !!request.disableBreaks,
    879      // Optional flag, to be set to true when Console Commands should override local symbols with
    880      // the same name. Like if the page defines `$`, the evaluated string will use the `$` implemented
    881      // by the console command instead of the page's function.
    882      preferConsoleCommandsOverLocalSymbols:
    883        !!request.preferConsoleCommandsOverLocalSymbols,
    884    };
    885 
    886    const { mapped } = request;
    887 
    888    // Set a flag on the thread actor which indicates an evaluation is being
    889    // done for the client. This is used to disable all types of breakpoints for all sources
    890    // via `disabledBreaks`. When this flag is used, `reportExceptionsWhenBreaksAreDisabled`
    891    // allows to still pause on exceptions.
    892    this.targetActor.threadActor.insideClientEvaluation = evalOptions;
    893 
    894    let evalInfo;
    895    try {
    896      evalInfo = evalWithDebugger(input, evalOptions, this);
    897    } finally {
    898      this.targetActor.threadActor.insideClientEvaluation = null;
    899    }
    900 
    901    return new Promise((resolve, reject) => {
    902      // Queue up a task to run in the next tick so any microtask created by the evaluated
    903      // expression has the time to be run.
    904      // e.g. in :
    905      // ```
    906      // const promiseThenCb = result => "result: " + result;
    907      // new Promise(res => res("hello")).then(promiseThenCb)
    908      // ```
    909      // we want`promiseThenCb` to have run before handling the result.
    910      DevToolsUtils.executeSoon(() => {
    911        try {
    912          const result = this.prepareEvaluationResult(
    913            evalInfo,
    914            input,
    915            request.eager,
    916            mapped,
    917            request.evalInTracer
    918          );
    919          resolve(result);
    920        } catch (err) {
    921          reject(err);
    922        }
    923      });
    924    });
    925  }
    926 
    927  // eslint-disable-next-line complexity
    928  prepareEvaluationResult(evalInfo, input, eager, mapped, evalInTracer) {
    929    const evalResult = evalInfo.result;
    930    const helperResult = evalInfo.helperResult;
    931 
    932    let result,
    933      errorDocURL,
    934      errorMessage,
    935      errorNotes = null,
    936      errorGrip = null,
    937      frame = null,
    938      awaitResult,
    939      errorMessageName,
    940      exceptionStack;
    941    if (evalResult) {
    942      if ("return" in evalResult) {
    943        result = evalResult.return;
    944        if (
    945          mapped?.await &&
    946          result &&
    947          result.class === "Promise" &&
    948          typeof result.unsafeDereference === "function"
    949        ) {
    950          awaitResult = result.unsafeDereference();
    951        }
    952      } else if ("yield" in evalResult) {
    953        result = evalResult.yield;
    954      } else if ("throw" in evalResult) {
    955        const error = evalResult.throw;
    956        const allowSideEffect = !eager;
    957        errorGrip = this.createValueGrip(error, { allowSideEffect });
    958 
    959        exceptionStack = this.prepareStackForRemote(evalResult.stack);
    960 
    961        if (exceptionStack) {
    962          exceptionStack =
    963            WebConsoleUtils.removeFramesAboveDebuggerEval(exceptionStack);
    964 
    965          // Set the frame based on the topmost stack frame for the exception.
    966          if (exceptionStack && exceptionStack.length) {
    967            const {
    968              filename: source,
    969              sourceId,
    970              lineNumber: line,
    971              columnNumber: column,
    972            } = exceptionStack[0];
    973            frame = { source, sourceId, line, column };
    974          }
    975        }
    976 
    977        errorMessage = String(error);
    978        if (allowSideEffect && typeof error === "object" && error !== null) {
    979          try {
    980            errorMessage = DevToolsUtils.callPropertyOnObject(
    981              error,
    982              "toString"
    983            );
    984          } catch (e) {
    985            // If the debuggee is not allowed to access the "toString" property
    986            // of the error object, calling this property from the debuggee's
    987            // compartment will fail. The debugger should show the error object
    988            // as it is seen by the debuggee, so this behavior is correct.
    989            //
    990            // Unfortunately, we have at least one test that assumes calling the
    991            // "toString" property of an error object will succeed if the
    992            // debugger is allowed to access it, regardless of whether the
    993            // debuggee is allowed to access it or not.
    994            //
    995            // To accomodate these tests, if calling the "toString" property
    996            // from the debuggee compartment fails, we rewrap the error object
    997            // in the debugger's compartment, and then call the "toString"
    998            // property from there.
    999            if (typeof error.unsafeDereference === "function") {
   1000              const rawError = error.unsafeDereference();
   1001              errorMessage = rawError ? rawError.toString() : "";
   1002            }
   1003          }
   1004        }
   1005 
   1006        // It is possible that we won't have permission to unwrap an
   1007        // object and retrieve its errorMessageName.
   1008        try {
   1009          errorDocURL = ErrorDocs.GetURL(error);
   1010          errorMessageName = error.errorMessageName;
   1011        } catch (ex) {
   1012          // ignored
   1013        }
   1014 
   1015        try {
   1016          const line = error.errorLineNumber;
   1017          const column = error.errorColumnNumber;
   1018 
   1019          if (
   1020            !frame &&
   1021            typeof line === "number" &&
   1022            typeof column === "number"
   1023          ) {
   1024            // Set frame only if we have line/column numbers.
   1025            frame = {
   1026              source: "debugger eval code",
   1027              line,
   1028              column,
   1029            };
   1030          }
   1031        } catch (ex) {
   1032          // ignored
   1033        }
   1034 
   1035        try {
   1036          const notes = error.errorNotes;
   1037          if (notes?.length) {
   1038            errorNotes = [];
   1039            for (const note of notes) {
   1040              errorNotes.push({
   1041                messageBody: this._createStringGrip(note.message),
   1042                frame: {
   1043                  source: note.fileName,
   1044                  line: note.lineNumber,
   1045                  column: note.columnNumber,
   1046                },
   1047              });
   1048            }
   1049          }
   1050        } catch (ex) {
   1051          // ignored
   1052        }
   1053      }
   1054    }
   1055    // If a value is encountered that the devtools server doesn't support yet,
   1056    // the console should remain functional.
   1057    let resultGrip;
   1058    if (!awaitResult) {
   1059      try {
   1060        const objectActor =
   1061          this.targetActor.threadActor.getThreadLifetimeObject(result);
   1062        if (evalInTracer) {
   1063          const tracerActor = this.targetActor.getTargetScopedActor("tracer");
   1064          resultGrip = tracerActor.createValueGrip(result);
   1065        } else if (objectActor) {
   1066          resultGrip = this.targetActor.threadActor.createValueGrip(result);
   1067        } else {
   1068          resultGrip = this.createValueGrip(result);
   1069        }
   1070      } catch (e) {
   1071        errorMessage = e;
   1072      }
   1073    }
   1074 
   1075    // Don't update _lastConsoleInputEvaluation in eager evaluation, as it would interfere
   1076    // with the $_ command.
   1077    if (!eager) {
   1078      if (!awaitResult) {
   1079        this._lastConsoleInputEvaluation = result;
   1080      } else {
   1081        // If we evaluated a top-level await expression, we want to assign its result to the
   1082        // _lastConsoleInputEvaluation only when the promise resolves, and only if it
   1083        // resolves. If the promise rejects, we don't re-assign _lastConsoleInputEvaluation,
   1084        // it will keep its previous value.
   1085 
   1086        const p = awaitResult.then(res => {
   1087          this._lastConsoleInputEvaluation = this.makeDebuggeeValue(res);
   1088        });
   1089 
   1090        // If the top level await was already rejected (e.g. `await Promise.reject("bleh")`),
   1091        // catch the resulting promise of awaitResult.then.
   1092        // If we don't do that, the new Promise will also be rejected, and since it's
   1093        // unhandled, it will generate an error.
   1094        // We don't want to do that for pending promise (e.g. `await new Promise((res, rej) => setTimeout(rej,250))`),
   1095        // as the the Promise rejection will be considered as handled, and the "Uncaught (in promise)"
   1096        // message wouldn't be emitted.
   1097        const { state } = ObjectUtils.getPromiseState(evalResult.return);
   1098        if (state === "rejected") {
   1099          p.catch(() => {});
   1100        }
   1101      }
   1102    }
   1103 
   1104    return {
   1105      input,
   1106      result: resultGrip,
   1107      awaitResult,
   1108      exception: errorGrip,
   1109      exceptionMessage: this._createStringGrip(errorMessage),
   1110      exceptionDocURL: errorDocURL,
   1111      exceptionStack,
   1112      hasException: errorGrip !== null,
   1113      errorMessageName,
   1114      frame,
   1115      helperResult,
   1116      notes: errorNotes,
   1117    };
   1118  }
   1119 
   1120  /**
   1121   * The Autocomplete request handler.
   1122   *
   1123   * @param string text
   1124   *        The request message - what input to autocomplete.
   1125   * @param number cursor
   1126   *        The cursor position at the moment of starting autocomplete.
   1127   * @param string frameActor
   1128   *        The frameactor id of the current paused frame.
   1129   * @param string selectedNodeActor
   1130   *        The actor id of the currently selected node.
   1131   * @param array authorizedEvaluations
   1132   *        Array of the properties access which can be executed by the engine.
   1133   * @return object
   1134   *         The response message - matched properties.
   1135   */
   1136  autocomplete(
   1137    text,
   1138    cursor,
   1139    frameActorId,
   1140    selectedNodeActor,
   1141    authorizedEvaluations,
   1142    expressionVars = []
   1143  ) {
   1144    let dbgObject = null;
   1145    let environment = null;
   1146    let matches = [];
   1147    let matchProp;
   1148    let isElementAccess;
   1149 
   1150    const reqText = text.substr(0, cursor);
   1151 
   1152    if (isCommand(reqText)) {
   1153      matchProp = reqText;
   1154      matches = WebConsoleCommandsManager.getAllColonCommandNames()
   1155        .filter(c => `:${c}`.startsWith(reqText))
   1156        .map(c => `:${c}`);
   1157    } else {
   1158      // This is the case of the paused debugger
   1159      if (frameActorId) {
   1160        const frameActor = this.conn.getActor(frameActorId);
   1161        try {
   1162          // Need to try/catch since accessing frame.environment
   1163          // can throw "Debugger.Frame is not live"
   1164          const frame = frameActor.frame;
   1165          environment = frame.environment;
   1166        } catch (e) {
   1167          DevToolsUtils.reportException(
   1168            "autocomplete",
   1169            Error("The frame actor was not found: " + frameActorId)
   1170          );
   1171        }
   1172      } else {
   1173        dbgObject = this.dbg.addDebuggee(this.evalGlobal);
   1174      }
   1175 
   1176      const result = jsPropertyProvider({
   1177        dbgObject,
   1178        environment,
   1179        frameActorId,
   1180        inputValue: text,
   1181        cursor,
   1182        webconsoleActor: this,
   1183        selectedNodeActor,
   1184        authorizedEvaluations,
   1185        expressionVars,
   1186      });
   1187 
   1188      if (result === null) {
   1189        return {
   1190          matches: null,
   1191        };
   1192      }
   1193 
   1194      if (result && result.isUnsafeGetter === true) {
   1195        return {
   1196          isUnsafeGetter: true,
   1197          getterPath: result.getterPath,
   1198        };
   1199      }
   1200 
   1201      matches = result.matches || new Set();
   1202      matchProp = result.matchProp || "";
   1203      isElementAccess = result.isElementAccess;
   1204 
   1205      // We consider '$' as alphanumeric because it is used in the names of some
   1206      // helper functions; we also consider whitespace as alphanum since it should not
   1207      // be seen as break in the evaled string.
   1208      const lastNonAlphaIsDot = /[.][a-zA-Z0-9$\s]*$/.test(reqText);
   1209 
   1210      // We only return commands and keywords when we are not dealing with a property or
   1211      // element access.
   1212      if (matchProp && !lastNonAlphaIsDot && !isElementAccess) {
   1213        const colonOnlyCommands =
   1214          WebConsoleCommandsManager.getColonOnlyCommandNames();
   1215        for (const name of WebConsoleCommandsManager.getAllCommandNames()) {
   1216          // Filter out commands like `screenshot` as it is inaccessible without the `:` prefix
   1217          if (
   1218            !colonOnlyCommands.includes(name) &&
   1219            name.startsWith(result.matchProp)
   1220          ) {
   1221            matches.add(name);
   1222          }
   1223        }
   1224 
   1225        for (const keyword of RESERVED_JS_KEYWORDS) {
   1226          if (keyword.startsWith(result.matchProp)) {
   1227            matches.add(keyword);
   1228          }
   1229        }
   1230      }
   1231 
   1232      // Sort the results in order to display lowercased item first (e.g. we want to
   1233      // display `document` then `Document` as we loosely match the user input if the
   1234      // first letter was lowercase).
   1235      const firstMeaningfulCharIndex = isElementAccess ? 1 : 0;
   1236      matches = Array.from(matches).sort((a, b) => {
   1237        const aFirstMeaningfulChar = a[firstMeaningfulCharIndex];
   1238        const bFirstMeaningfulChar = b[firstMeaningfulCharIndex];
   1239        const lA =
   1240          aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
   1241        const lB =
   1242          bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
   1243        if (lA === lB) {
   1244          if (a === matchProp) {
   1245            return -1;
   1246          }
   1247          if (b === matchProp) {
   1248            return 1;
   1249          }
   1250          return a.localeCompare(b);
   1251        }
   1252        return lA ? -1 : 1;
   1253      });
   1254    }
   1255 
   1256    return {
   1257      matches,
   1258      matchProp,
   1259      isElementAccess: isElementAccess === true,
   1260    };
   1261  }
   1262 
   1263  /**
   1264   * The "clearMessagesCacheAsync" request handler.
   1265   */
   1266  clearMessagesCacheAsync() {
   1267    if (isWorker) {
   1268      // Defined on WorkerScope
   1269      clearConsoleEvents();
   1270      return;
   1271    }
   1272 
   1273    const windowId = !this.targetActor.isRootActor
   1274      ? WebConsoleUtils.getInnerWindowId(this.global)
   1275      : null;
   1276 
   1277    const ConsoleAPIStorage = Cc[
   1278      "@mozilla.org/consoleAPI-storage;1"
   1279    ].getService(Ci.nsIConsoleAPIStorage);
   1280    ConsoleAPIStorage.clearEvents(windowId);
   1281 
   1282    CONSOLE_WORKER_IDS.forEach(id => {
   1283      ConsoleAPIStorage.clearEvents(id);
   1284    });
   1285 
   1286    if (this.targetActor.isRootActor || !this.global) {
   1287      // If were dealing with the root actor (e.g. the browser console), we want
   1288      // to remove all cached messages, not only the ones specific to a window.
   1289      Services.console.reset();
   1290    } else if (this.targetActor.ignoreSubFrames) {
   1291      Services.console.resetWindow(windowId);
   1292    } else {
   1293      WebConsoleUtils.getInnerWindowIDsForFrames(this.global).forEach(id =>
   1294        Services.console.resetWindow(id)
   1295      );
   1296    }
   1297  }
   1298 
   1299  // End of request handlers.
   1300 
   1301  // Event handlers for various listeners.
   1302 
   1303  /**
   1304   * Handler for messages received from the ConsoleServiceListener. This method
   1305   * sends the nsIConsoleMessage to the remote Web Console client.
   1306   *
   1307   * @param nsIConsoleMessage message
   1308   *        The message we need to send to the client.
   1309   */
   1310  onConsoleServiceMessage(message) {
   1311    if (message instanceof Ci.nsIScriptError) {
   1312      this.emit("pageError", {
   1313        pageError: this.preparePageErrorForRemote(message),
   1314      });
   1315    } else {
   1316      this.emit("logMessage", {
   1317        message: this._createStringGrip(message.message),
   1318        timeStamp: message.microSecondTimeStamp / 1000,
   1319      });
   1320    }
   1321  }
   1322 
   1323  getActorIdForInternalSourceId(id) {
   1324    const actor =
   1325      this.targetActor.sourcesManager.getSourceActorByInternalSourceId(id);
   1326    return actor ? actor.actorID : null;
   1327  }
   1328 
   1329  /**
   1330   * Prepare a SavedFrame stack to be sent to the client.
   1331   *
   1332   * @param SavedFrame errorStack
   1333   *        Stack for an error we need to send to the client.
   1334   * @return object
   1335   *         The object you can send to the remote client.
   1336   */
   1337  prepareStackForRemote(errorStack) {
   1338    // Convert stack objects to the JSON attributes expected by client code
   1339    // Bug 1348885: If the global from which this error came from has been
   1340    // nuked, stack is going to be a dead wrapper.
   1341    if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) {
   1342      return null;
   1343    }
   1344    const stack = [];
   1345    let s = errorStack;
   1346    while (s) {
   1347      stack.push({
   1348        filename: s.source,
   1349        sourceId: this.getActorIdForInternalSourceId(s.sourceId),
   1350        lineNumber: s.line,
   1351        columnNumber: s.column,
   1352        functionName: s.functionDisplayName,
   1353        asyncCause: s.asyncCause ? s.asyncCause : undefined,
   1354      });
   1355      s = s.parent || s.asyncParent;
   1356    }
   1357    return stack;
   1358  }
   1359 
   1360  /**
   1361   * Prepare an nsIScriptError to be sent to the client.
   1362   *
   1363   * @param nsIScriptError pageError
   1364   *        The page error we need to send to the client.
   1365   * @return object
   1366   *         The object you can send to the remote client.
   1367   */
   1368  preparePageErrorForRemote(pageError) {
   1369    const stack = this.prepareStackForRemote(pageError.stack);
   1370    let notesArray = null;
   1371    const notes = pageError.notes;
   1372    if (notes?.length) {
   1373      notesArray = [];
   1374      for (let i = 0, len = notes.length; i < len; i++) {
   1375        const note = notes.queryElementAt(i, Ci.nsIScriptErrorNote);
   1376        notesArray.push({
   1377          messageBody: this._createStringGrip(note.errorMessage),
   1378          frame: {
   1379            source: note.sourceName,
   1380            sourceId: this.getActorIdForInternalSourceId(note.sourceId),
   1381            line: note.lineNumber,
   1382            column: note.columnNumber,
   1383          },
   1384        });
   1385      }
   1386    }
   1387 
   1388    // If there is no location information in the error but we have a stack,
   1389    // fill in the location with the first frame on the stack.
   1390    let { sourceName, sourceId, lineNumber, columnNumber } = pageError;
   1391    if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
   1392      sourceName = stack[0].filename;
   1393      sourceId = stack[0].sourceId;
   1394      lineNumber = stack[0].lineNumber;
   1395      columnNumber = stack[0].columnNumber;
   1396    }
   1397 
   1398    const isCSSMessage = pageError.category === MESSAGE_CATEGORY.CSS_PARSER;
   1399 
   1400    const result = {
   1401      errorMessage: this._createStringGrip(pageError.errorMessage),
   1402      errorMessageName: isCSSMessage ? undefined : pageError.errorMessageName,
   1403      exceptionDocURL: ErrorDocs.GetURL(pageError),
   1404      sourceName,
   1405      sourceId: this.getActorIdForInternalSourceId(sourceId),
   1406      lineNumber,
   1407      columnNumber,
   1408      category: pageError.category,
   1409      innerWindowID: pageError.innerWindowID,
   1410      timeStamp: pageError.microSecondTimeStamp / 1000,
   1411      warning: !!(pageError.flags & pageError.warningFlag),
   1412      error: !(pageError.flags & (pageError.warningFlag | pageError.infoFlag)),
   1413      info: !!(pageError.flags & pageError.infoFlag),
   1414      private: pageError.isFromPrivateWindow,
   1415      stacktrace: stack,
   1416      notes: notesArray,
   1417      chromeContext: pageError.isFromChromeContext,
   1418      isPromiseRejection: isCSSMessage
   1419        ? undefined
   1420        : pageError.isPromiseRejection,
   1421      isForwardedFromContentProcess: pageError.isForwardedFromContentProcess,
   1422      cssSelectors: isCSSMessage ? pageError.cssSelectors : undefined,
   1423    };
   1424 
   1425    // If the pageError does have an exception object, we want to return the grip for it,
   1426    // but only if we do manage to get the grip, as we're checking the property on the
   1427    // client to render things differently.
   1428    if (pageError.hasException) {
   1429      try {
   1430        const obj = this.makeDebuggeeValue(pageError.exception, true);
   1431        if (obj?.class !== "DeadObject") {
   1432          result.exception = this.createValueGrip(obj);
   1433          result.hasException = true;
   1434        }
   1435      } catch (e) {}
   1436    }
   1437 
   1438    return result;
   1439  }
   1440 
   1441  /**
   1442   * Handler for window.console API calls received from the ConsoleAPIListener.
   1443   * This method sends the object to the remote Web Console client.
   1444   *
   1445   * @see ConsoleAPIListener
   1446   * @param object message
   1447   *        The console API call we need to send to the remote client.
   1448   * @param object extraProperties
   1449   *        an object whose properties will be folded in the packet that is emitted.
   1450   */
   1451  onConsoleAPICall(message, extraProperties = {}) {
   1452    this.emit("consoleAPICall", {
   1453      message: this.prepareConsoleMessageForRemote(message),
   1454      ...extraProperties,
   1455    });
   1456  }
   1457 
   1458  /**
   1459   * Handler for the DocumentEventsListener.
   1460   *
   1461   * @see DocumentEventsListener
   1462   * @param {string} name
   1463   *        The document event name that either of followings.
   1464   *        - dom-loading
   1465   *        - dom-interactive
   1466   *        - dom-complete
   1467   * @param {number} time
   1468   *        The time that the event is fired.
   1469   * @param {boolean} hasNativeConsoleAPI
   1470   *        Tells if the window.console object is native or overwritten by script in the page.
   1471   *        Only passed when `name` is "dom-complete" (see devtools/server/actors/webconsole/listeners/document-events.js).
   1472   */
   1473  onDocumentEvent(name, { time, hasNativeConsoleAPI }) {
   1474    this.emit("documentEvent", {
   1475      name,
   1476      time,
   1477      hasNativeConsoleAPI,
   1478    });
   1479  }
   1480 
   1481  /**
   1482   * Handler for file activity. This method sends the file request information
   1483   * to the remote Web Console client.
   1484   *
   1485   * @see ConsoleFileActivityListener
   1486   * @param string fileURI
   1487   *        The requested file URI.
   1488   */
   1489  onFileActivity(fileURI) {
   1490    this.emit("fileActivity", {
   1491      uri: fileURI,
   1492    });
   1493  }
   1494 
   1495  // End of event handlers for various listeners.
   1496 
   1497  /**
   1498   * Prepare a message from the console API to be sent to the remote Web Console
   1499   * instance.
   1500   *
   1501   * @param object message
   1502   *        The original message received from the console storage listener.
   1503   * @param boolean aUseObjectGlobal
   1504   *        If |true| the object global is determined and added as a debuggee,
   1505   *        otherwise |this.global| is used when makeDebuggeeValue() is invoked.
   1506   * @return object
   1507   *         The object that can be sent to the remote client.
   1508   */
   1509  prepareConsoleMessageForRemote(message, useObjectGlobal = true) {
   1510    const result = {
   1511      arguments: message.arguments
   1512        ? message.arguments.map(obj => {
   1513            const dbgObj = this.makeDebuggeeValue(obj, useObjectGlobal);
   1514            return this.createValueGrip(dbgObj);
   1515          })
   1516        : [],
   1517      chromeContext: message.chromeContext,
   1518      columnNumber: message.columnNumber,
   1519      filename: message.filename,
   1520      level: message.level,
   1521      lineNumber: message.lineNumber,
   1522      // messages emitted from Console.sys.mjs don't have a microSecondTimeStamp property
   1523      timeStamp: message.microSecondTimeStamp
   1524        ? message.microSecondTimeStamp / 1000
   1525        : message.timeStamp,
   1526      sourceId: this.getActorIdForInternalSourceId(message.sourceId),
   1527      category: message.category || "webdev",
   1528      innerWindowID: message.innerID,
   1529    };
   1530 
   1531    // It only make sense to include the following properties in the message when they have
   1532    // a meaningful value. Otherwise we simply don't include them so we save cycles in JSActor communication.
   1533    if (message.counter) {
   1534      result.counter = message.counter;
   1535    }
   1536    if (message.private) {
   1537      result.private = message.private;
   1538    }
   1539    if (message.prefix) {
   1540      result.prefix = message.prefix;
   1541    }
   1542 
   1543    if (message.stacktrace) {
   1544      result.stacktrace = message.stacktrace.map(frame => {
   1545        return {
   1546          ...frame,
   1547          sourceId: this.getActorIdForInternalSourceId(frame.sourceId),
   1548        };
   1549      });
   1550    }
   1551 
   1552    if (message.styles && message.styles.length) {
   1553      result.styles = message.styles.map(string => {
   1554        return this.createValueGrip(string);
   1555      });
   1556    }
   1557 
   1558    if (message.timer) {
   1559      result.timer = message.timer;
   1560    }
   1561 
   1562    if (message.level === "table") {
   1563      const tableItems = this._getConsoleTableMessageItems(result);
   1564      if (tableItems) {
   1565        result.arguments[0].ownProperties = tableItems;
   1566        result.arguments[0].preview = null;
   1567      }
   1568 
   1569      // Only return the 2 first params.
   1570      result.arguments = result.arguments.slice(0, 2);
   1571    }
   1572 
   1573    return result;
   1574  }
   1575 
   1576  /**
   1577   * Return the properties needed to display the appropriate table for a given
   1578   * console.table call.
   1579   * This function does a little more than creating an ObjectActor for the first
   1580   * parameter of the message. When layout out the console table in the output, we want
   1581   * to be able to look into sub-properties so the table can have a different layout (
   1582   * for arrays of arrays, objects with objects properties, arrays of objects, …).
   1583   * So here we need to retrieve the properties of the first parameter, and also all the
   1584   * sub-properties we might need.
   1585   *
   1586   * @param {object} result: The console.table message.
   1587   * @returns {object} An object containing the properties of the first argument of the
   1588   *                   console.table call.
   1589   */
   1590  _getConsoleTableMessageItems(result) {
   1591    if (
   1592      !result ||
   1593      !Array.isArray(result.arguments) ||
   1594      !result.arguments.length
   1595    ) {
   1596      return null;
   1597    }
   1598 
   1599    const [tableItemGrip] = result.arguments;
   1600    const dataType = tableItemGrip.class;
   1601    const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
   1602    const ignoreNonIndexedProperties = isArray(tableItemGrip);
   1603 
   1604    const tableItemActor = this.targetActor.objectsPool.getActorByID(
   1605      tableItemGrip.actor
   1606    );
   1607    if (!tableItemActor) {
   1608      return null;
   1609    }
   1610 
   1611    // Retrieve the properties (or entries for Set/Map) of the console table first arg.
   1612    const iterator = needEntries
   1613      ? tableItemActor.enumEntries()
   1614      : tableItemActor.enumProperties({
   1615          ignoreNonIndexedProperties,
   1616        });
   1617    const { ownProperties } = iterator.all();
   1618 
   1619    // The iterator returns a descriptor for each property, wherein the value could be
   1620    // in one of those sub-property.
   1621    const descriptorKeys = ["safeGetterValues", "getterValue", "value"];
   1622 
   1623    Object.values(ownProperties).forEach(desc => {
   1624      if (typeof desc !== "undefined") {
   1625        descriptorKeys.forEach(key => {
   1626          if (desc && desc.hasOwnProperty(key)) {
   1627            const grip = desc[key];
   1628 
   1629            // We need to load sub-properties as well to render the table in a nice way.
   1630            const actor =
   1631              grip && this.targetActor.objectsPool.getActorByID(grip.actor);
   1632            if (actor && typeof actor.enumProperties === "function") {
   1633              const res = actor
   1634                .enumProperties({
   1635                  ignoreNonIndexedProperties: isArray(grip),
   1636                })
   1637                .all();
   1638              if (res?.ownProperties) {
   1639                desc[key].ownProperties = res.ownProperties;
   1640              }
   1641            }
   1642          }
   1643        });
   1644      }
   1645    });
   1646 
   1647    return ownProperties;
   1648  }
   1649 
   1650  /**
   1651   * The "will-navigate" progress listener. This is used to clear the current
   1652   * eval scope.
   1653   */
   1654  _onWillNavigate({ isTopLevel }) {
   1655    if (isTopLevel) {
   1656      this._evalGlobal = null;
   1657      this.targetActor.off("will-navigate", this._onWillNavigate);
   1658      this._progressListenerActive = false;
   1659    }
   1660  }
   1661 
   1662  /**
   1663   * This listener is called when we switch to another frame,
   1664   * mostly to unregister previous listeners and start listening on the new document.
   1665   */
   1666  _onChangedToplevelDocument() {
   1667    // Convert the Set to an Array
   1668    const listeners = [...this._listeners];
   1669 
   1670    // Unregister existing listener on the previous document
   1671    // (pass a copy of the array as it will shift from it)
   1672    this.stopListeners(listeners.slice());
   1673 
   1674    // This method is called after this.global is changed,
   1675    // so we register new listener on this new global
   1676    this.startListeners(listeners);
   1677 
   1678    // Also reset the cached top level chrome window being targeted
   1679    this._lastChromeWindow = null;
   1680  }
   1681 }
   1682 
   1683 exports.WebConsoleActor = WebConsoleActor;