/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* global XPCNativeWrapper */
const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
const { webconsoleSpec } = require("devtools/shared/specs/webconsole");

const Services = require("Services");
const { Cc, Ci, Cu } = require("chrome");
const { DevToolsServer } = require("devtools/server/devtools-server");
const { ThreadActor } = require("devtools/server/actors/thread");
const { ObjectActor } = require("devtools/server/actors/object");
const { LongStringActor } = require("devtools/server/actors/string");
const {
  createValueGrip,
  isArray,
  stringIsLong,
} = require("devtools/server/actors/object/utils");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const ErrorDocs = require("devtools/server/actors/errordocs");

loader.lazyRequireGetter(
  this,
  "evalWithDebugger",
  "devtools/server/actors/webconsole/eval-with-debugger",
  true
);
loader.lazyRequireGetter(
  this,
  "NetworkMonitorActor",
  "devtools/server/actors/network-monitor/network-monitor",
  true
);
loader.lazyRequireGetter(
  this,
  "ConsoleFileActivityListener",
  "devtools/server/actors/webconsole/listeners/console-file-activity",
  true
);
loader.lazyRequireGetter(
  this,
  "StackTraceCollector",
  "devtools/server/actors/network-monitor/stack-trace-collector",
  true
);
loader.lazyRequireGetter(
  this,
  "JSPropertyProvider",
  "devtools/shared/webconsole/js-property-provider",
  true
);
loader.lazyRequireGetter(
  this,
  "NetUtil",
  "resource://gre/modules/NetUtil.jsm",
  true
);
loader.lazyRequireGetter(
  this,
  ["isCommand", "validCommands"],
  "devtools/server/actors/webconsole/commands",
  true
);
loader.lazyRequireGetter(
  this,
  "createMessageManagerMocks",
  "devtools/server/actors/webconsole/message-manager-mock",
  true
);
loader.lazyRequireGetter(
  this,
  ["addWebConsoleCommands", "CONSOLE_WORKER_IDS", "WebConsoleUtils"],
  "devtools/server/actors/webconsole/utils",
  true
);
loader.lazyRequireGetter(
  this,
  "EnvironmentActor",
  "devtools/server/actors/environment",
  true
);
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(
  this,
  "MESSAGE_CATEGORY",
  "devtools/shared/constants",
  true
);
loader.lazyRequireGetter(
  this,
  "NetworkUtils",
  "devtools/server/actors/network-monitor/utils/network-utils"
);

// Generated by /devtools/shared/webconsole/GenerateReservedWordsJS.py
loader.lazyRequireGetter(
  this,
  "RESERVED_JS_KEYWORDS",
  "devtools/shared/webconsole/reserved-js-words"
);

// Overwrite implemented listeners for workers so that we don't attempt
// to load an unsupported module.
if (isWorker) {
  loader.lazyRequireGetter(
    this,
    ["ConsoleAPIListener", "ConsoleServiceListener"],
    "devtools/server/actors/webconsole/worker-listeners",
    true
  );
} else {
  loader.lazyRequireGetter(
    this,
    "ConsoleAPIListener",
    "devtools/server/actors/webconsole/listeners/console-api",
    true
  );
  loader.lazyRequireGetter(
    this,
    "ConsoleServiceListener",
    "devtools/server/actors/webconsole/listeners/console-service",
    true
  );
  loader.lazyRequireGetter(
    this,
    "ConsoleReflowListener",
    "devtools/server/actors/webconsole/listeners/console-reflow",
    true
  );
  loader.lazyRequireGetter(
    this,
    "ContentProcessListener",
    "devtools/server/actors/webconsole/listeners/content-process",
    true
  );
  loader.lazyRequireGetter(
    this,
    "DocumentEventsListener",
    "devtools/server/actors/webconsole/listeners/document-events",
    true
  );
}
loader.lazyRequireGetter(
  this,
  "ObjectUtils",
  "devtools/server/actors/object/utils"
);

function isObject(value) {
  return Object(value) === value;
}

/**
 * The WebConsoleActor implements capabilities needed for the Web Console
 * feature.
 *
 * @constructor
 * @param object connection
 *        The connection to the client, DevToolsServerConnection.
 * @param object [parentActor]
 *        Optional, the parent actor.
 */
const WebConsoleActor = ActorClassWithSpec(webconsoleSpec, {
  initialize: function(connection, parentActor) {
    Actor.prototype.initialize.call(this, connection);
    this.conn = connection;
    this.parentActor = parentActor;

    this._prefs = {};
    this.dbg = this.parentActor.dbg;

    this._gripDepth = 0;
    this._evalCounter = 0;
    this._listeners = new Set();
    this._lastConsoleInputEvaluation = undefined;

    this.objectGrip = this.objectGrip.bind(this);
    this._onWillNavigate = this._onWillNavigate.bind(this);
    this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(
      this
    );
    this.onConsoleServiceMessage = this.onConsoleServiceMessage.bind(this);
    this.onConsoleAPICall = this.onConsoleAPICall.bind(this);
    this.onDocumentEvent = this.onDocumentEvent.bind(this);

    EventEmitter.on(
      this.parentActor,
      "changed-toplevel-document",
      this._onChangedToplevelDocument
    );
    this._onObserverNotification = this._onObserverNotification.bind(this);
    if (this.parentActor.isRootActor) {
      Services.obs.addObserver(
        this._onObserverNotification,
        "last-pb-context-exited"
      );
    }

    this.traits = {
      // Supports retrieving blocked urls
      blockedUrls: true,
    };
  },
  /**
   * Debugger instance.
   *
   * @see jsdebugger.jsm
   */
  dbg: null,

  /**
   * This is used by the ObjectActor to keep track of the depth of grip() calls.
   * @private
   * @type number
   */
  _gripDepth: null,

  /**
   * Web Console-related preferences.
   * @private
   * @type object
   */
  _prefs: null,

  /**
   * Holds a set of all currently registered listeners.
   *
   * @private
   * @type Set
   */
  _listeners: null,

  /**
   * The devtools server connection instance.
   * @type object
   */
  conn: null,

  /**
   * List of supported features by the console actor.
   * @type object
   */
  traits: null,

  /**
   * The global we work with (this can be a Window, a Worker global or even a Sandbox
   * for processes and addons).
   *
   * @type nsIDOMWindow, WorkerGlobalScope or Sandbox
   */
  get global() {
    if (this.parentActor.isRootActor) {
      return this._getWindowForBrowserConsole();
    }
    return this.parentActor.window || this.parentActor.workerGlobal;
  },

  /**
   * Get a window to use for the browser console.
   *
   * @private
   * @return nsIDOMWindow
   *         The window to use, or null if no window could be found.
   */
  _getWindowForBrowserConsole: function() {
    // Check if our last used chrome window is still live.
    let window = this._lastChromeWindow && this._lastChromeWindow.get();
    // If not, look for a new one.
    if (!window || window.closed) {
      window = this.parentActor.window;
      if (!window) {
        // Try to find the Browser Console window to use instead.
        window = Services.wm.getMostRecentWindow("devtools:webconsole");
        // We prefer the normal chrome window over the console window,
        // so we'll look for those windows in order to replace our reference.
        const onChromeWindowOpened = () => {
          // We'll look for this window when someone next requests window()
          Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened");
          this._lastChromeWindow = null;
        };
        Services.obs.addObserver(onChromeWindowOpened, "domwindowopened");
      }

      this._handleNewWindow(window);
    }

    return window;
  },

  /**
   * Store a newly found window on the actor to be used in the future.
   *
   * @private
   * @param nsIDOMWindow window
   *        The window to store on the actor (can be null).
   */
  _handleNewWindow: function(window) {
    if (window) {
      if (this._hadChromeWindow) {
        Services.console.logStringMessage("Webconsole context has changed");
      }
      this._lastChromeWindow = Cu.getWeakReference(window);
      this._hadChromeWindow = true;
    } else {
      this._lastChromeWindow = null;
    }
  },

  /**
   * Whether we've been using a window before.
   *
   * @private
   * @type boolean
   */
  _hadChromeWindow: false,

  /**
   * A weak reference to the last chrome window we used to work with.
   *
   * @private
   * @type nsIWeakReference
   */
  _lastChromeWindow: null,

  // The evalGlobal is used at the scope for JS evaluation.
  _evalGlobal: null,
  get evalGlobal() {
    return this._evalGlobal || this.global;
  },

  set evalGlobal(global) {
    this._evalGlobal = global;

    if (!this._progressListenerActive) {
      EventEmitter.on(this.parentActor, "will-navigate", this._onWillNavigate);
      this._progressListenerActive = true;
    }
  },

  /**
   * Flag used to track if we are listening for events from the progress
   * listener of the target actor. We use the progress listener to clear
   * this.evalGlobal on page navigation.
   *
   * @private
   * @type boolean
   */
  _progressListenerActive: false,

  /**
   * The ConsoleServiceListener instance.
   * @type object
   */
  consoleServiceListener: null,

  /**
   * The ConsoleAPIListener instance.
   */
  consoleAPIListener: null,

  /**
   * The ConsoleFileActivityListener instance.
   */
  consoleFileActivityListener: null,

  /**
   * The ConsoleReflowListener instance.
   */
  consoleReflowListener: null,

  /**
   * The Web Console Commands names cache.
   * @private
   * @type array
   */
  _webConsoleCommandsCache: null,

  grip: function() {
    return { actor: this.actorID };
  },

  hasNativeConsoleAPI: function(window) {
    if (isWorker || !(window instanceof Ci.nsIDOMWindow)) {
      // We can only use XPCNativeWrapper on non-worker nsIDOMWindow.
      return true;
    }

    let isNative = false;
    try {
      // We are very explicitly examining the "console" property of
      // the non-Xrayed object here.
      const console = window.wrappedJSObject.console;
      // In xpcshell tests, console ends up being undefined and XPCNativeWrapper
      // crashes in debug builds.
      if (console) {
        isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
      }
    } catch (ex) {
      // ignored
    }
    return isNative;
  },

  _findProtoChain: ThreadActor.prototype._findProtoChain,
  _removeFromProtoChain: ThreadActor.prototype._removeFromProtoChain,

  /**
   * Destroy the current WebConsoleActor instance.
   */
  destroy() {
    this.stopListeners();
    Actor.prototype.destroy.call(this);

    EventEmitter.off(
      this.parentActor,
      "changed-toplevel-document",
      this._onChangedToplevelDocument
    );

    if (this.parentActor.isRootActor) {
      Services.obs.removeObserver(
        this._onObserverNotification,
        "last-pb-context-exited"
      );
    }

    this._webConsoleCommandsCache = null;
    this._lastConsoleInputEvaluation = null;
    this._evalGlobal = null;
    this.dbg = null;
    this.conn = null;
  },

  /**
   * Create and return an environment actor that corresponds to the provided
   * Debugger.Environment. This is a straightforward clone of the ThreadActor's
   * method except that it stores the environment actor in the web console
   * actor's pool.
   *
   * @param Debugger.Environment environment
   *        The lexical environment we want to extract.
   * @return The EnvironmentActor for |environment| or |undefined| for host
   *         functions or functions scoped to a non-debuggee global.
   */
  createEnvironmentActor: function(environment) {
    if (!environment) {
      return undefined;
    }

    if (environment.actor) {
      return environment.actor;
    }

    const actor = new EnvironmentActor(environment, this);
    this.manage(actor);
    environment.actor = actor;

    return actor;
  },

  /**
   * Create a grip for the given value.
   *
   * @param mixed value
   * @return object
   */
  createValueGrip: function(value) {
    return createValueGrip(value, this, this.objectGrip);
  },

  /**
   * Make a debuggee value for the given value.
   *
   * @param mixed value
   *        The value you want to get a debuggee value for.
   * @param boolean useObjectGlobal
   *        If |true| the object global is determined and added as a debuggee,
   *        otherwise |this.global| is used when makeDebuggeeValue() is invoked.
   * @return object
   *         Debuggee value for |value|.
   */
  makeDebuggeeValue: function(value, useObjectGlobal) {
    if (useObjectGlobal && isObject(value)) {
      try {
        const global = Cu.getGlobalForObject(value);
        const dbgGlobal = this.dbg.makeGlobalObjectReference(global);
        return dbgGlobal.makeDebuggeeValue(value);
      } catch (ex) {
        // The above can throw an exception if value is not an actual object
        // or 'Object in compartment marked as invisible to Debugger'
      }
    }
    const dbgGlobal = this.dbg.makeGlobalObjectReference(this.global);
    return dbgGlobal.makeDebuggeeValue(value);
  },

  /**
   * Create a grip for the given object.
   *
   * @param object object
   *        The object you want.
   * @param object pool
   *        A Pool where the new actor instance is added.
   * @param object
   *        The object grip.
   */
  objectGrip: function(object, pool) {
    const actor = new ObjectActor(
      object,
      {
        thread: this.parentActor.threadActor,
        getGripDepth: () => this._gripDepth,
        incrementGripDepth: () => this._gripDepth++,
        decrementGripDepth: () => this._gripDepth--,
        createValueGrip: v => this.createValueGrip(v),
        createEnvironmentActor: env => this.createEnvironmentActor(env),
      },
      this.conn
    );
    pool.manage(actor);
    return actor.form();
  },

  /**
   * Create a grip for the given string.
   *
   * @param string string
   *        The string you want to create the grip for.
   * @param object pool
   *        A Pool where the new actor instance is added.
   * @return object
   *         A LongStringActor object that wraps the given string.
   */
  longStringGrip: function(string, pool) {
    const actor = new LongStringActor(this.conn, string);
    pool.manage(actor);
    return actor.form();
  },

  /**
   * Create a long string grip if needed for the given string.
   *
   * @private
   * @param string string
   *        The string you want to create a long string grip for.
   * @return string|object
   *         A string is returned if |string| is not a long string.
   *         A LongStringActor grip is returned if |string| is a long string.
   */
  _createStringGrip: function(string) {
    if (string && stringIsLong(string)) {
      return this.longStringGrip(string, this);
    }
    return string;
  },

  /**
   * Returns the latest web console input evaluation.
   * This is undefined if no evaluations have been completed.
   *
   * @return object
   */
  getLastConsoleInputEvaluation: function() {
    return this._lastConsoleInputEvaluation;
  },

  /**
   * Preprocess a debugger object (e.g. return the `boundTargetFunction`
   * debugger object if the given debugger object is a bound function).
   *
   * This method is called by both the `inspect` binding implemented
   * for the webconsole and the one implemented for the devtools API
   * `browser.devtools.inspectedWindow.eval`.
   */
  preprocessDebuggerObject(dbgObj) {
    // Returns the bound target function on a bound function.
    if (dbgObj?.isBoundFunction && dbgObj?.boundTargetFunction) {
      return dbgObj.boundTargetFunction;
    }

    return dbgObj;
  },

  /**
   * This helper is used by the WebExtensionInspectedWindowActor to
   * inspect an object in the developer toolbox.
   *
   * NOTE: shared parts related to preprocess the debugger object (between
   * this function and the `inspect` webconsole command defined in
   * "devtools/server/actor/webconsole/utils.js") should be added to
   * the webconsole actors' `preprocessDebuggerObject` method.
   */
  inspectObject(dbgObj, inspectFromAnnotation) {
    dbgObj = this.preprocessDebuggerObject(dbgObj);
    this.emit("inspectObject", {
      objectActor: this.createValueGrip(dbgObj),
      inspectFromAnnotation,
    });
  },

  // Request handlers for known packet types.

  /**
   * Handler for the "startListeners" request.
   *
   * @param array listeners
   *        An array of events to start sent by the Web Console client.
   * @return object
   *        The response object which holds the startedListeners array.
   */
  // eslint-disable-next-line complexity
  startListeners: async function(listeners) {
    const startedListeners = [];
    const global = !this.parentActor.isRootActor ? this.global : null;

    for (const event of listeners) {
      switch (event) {
        case "PageError":
          // Workers don't support this message type yet
          if (isWorker) {
            break;
          }
          if (!this.consoleServiceListener) {
            this.consoleServiceListener = new ConsoleServiceListener(
              global,
              this.onConsoleServiceMessage
            );
            this.consoleServiceListener.init();
          }
          startedListeners.push(event);
          break;
        case "ConsoleAPI":
          if (!this.consoleAPIListener) {
            // Create the consoleAPIListener
            // (and apply the filtering options defined in the parent actor).
            this.consoleAPIListener = new ConsoleAPIListener(
              global,
              this.onConsoleAPICall,
              this.parentActor.consoleAPIListenerOptions
            );
            this.consoleAPIListener.init();
          }
          startedListeners.push(event);
          break;
        case "NetworkActivity":
          // Workers don't support this message type
          if (isWorker) {
            break;
          }
          if (!this.netmonitors) {
            // Instanciate fake message managers used for service worker's netmonitor
            // when running in the content process, and for netmonitor running in the
            // same process when running in the parent process.
            // `createMessageManagerMocks` returns a couple of connected messages
            // managers that pass messages to each other to simulate the process
            // boundary. We will use the first one for the webconsole-actor and the
            // second one will be used by the netmonitor-actor.
            const [mmMockParent, mmMockChild] = createMessageManagerMocks();

            // Maintain the list of message manager we should message to/listen from
            // to support the netmonitor instances, also records actorID of each
            // NetworkMonitorActor.
            // Array of `{ messageManager, parentProcess }`.
            // Where `parentProcess` is true for the netmonitor actor instanciated in the
            // parent process.
            this.netmonitors = [];

            // Check if the actor is running in a content process
            const isInContentProcess =
              Services.appinfo.processType !=
                Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT &&
              this.parentActor.messageManager;
            if (isInContentProcess) {
              // Start a network monitor in the parent process to listen to
              // most requests that happen in parent. This one will communicate through
              // `messageManager`.
              await this.conn.spawnActorInParentProcess(this.actorID, {
                module:
                  "devtools/server/actors/network-monitor/network-monitor",
                constructor: "NetworkMonitorActor",
                args: [{ browserId: this.parentActor.browserId }, this.actorID],
              });
              this.netmonitors.push({
                messageManager: this.parentActor.messageManager,
                parentProcess: true,
              });
            }

            // When the console actor runs in the parent process, Netmonitor can be ran
            // in the process and communicate through `messageManagerMock`.
            // And while it runs in the content process, we also spawn one in the content
            // to listen to requests that happen in the content process (for instance
            // service workers requests)
            new NetworkMonitorActor(
              this.conn,
              { window: global },
              this.actorID,
              mmMockParent
            );

            this.netmonitors.push({
              messageManager: mmMockChild,
              parentProcess: !isInContentProcess,
            });

            // Create a StackTraceCollector that's going to be shared both by
            // the NetworkMonitorActor running in the same process for service worker
            // requests, as well with the NetworkMonitorActor running in the parent
            // process. It will communicate via message manager for this one.
            this.stackTraceCollector = new StackTraceCollector(
              { window: global },
              this.netmonitors
            );
            this.stackTraceCollector.init();
          }
          startedListeners.push(event);
          break;
        case "FileActivity":
          // Workers don't support this message type
          if (isWorker) {
            break;
          }
          if (this.global instanceof Ci.nsIDOMWindow) {
            if (!this.consoleFileActivityListener) {
              this.consoleFileActivityListener = new ConsoleFileActivityListener(
                this.global,
                this
              );
            }
            this.consoleFileActivityListener.startMonitor();
            startedListeners.push(event);
          }
          break;
        case "ReflowActivity":
          // Workers don't support this message type
          if (isWorker) {
            break;
          }
          if (!this.consoleReflowListener) {
            this.consoleReflowListener = new ConsoleReflowListener(
              this.global,
              this
            );
          }
          startedListeners.push(event);
          break;
        case "ContentProcessMessages":
          // Workers don't support this message type
          if (isWorker) {
            break;
          }
          if (!this.contentProcessListener) {
            this.contentProcessListener = new ContentProcessListener(message =>
              this.onConsoleAPICall(message, { clonedFromContentProcess: true })
            );
          }
          startedListeners.push(event);
          break;
        case "DocumentEvents":
          // Workers don't support this message type
          if (isWorker) {
            break;
          }
          if (!this.documentEventsListener) {
            this.documentEventsListener = new DocumentEventsListener(
              this.parentActor
            );
            this.documentEventsListener.on("*", this.onDocumentEvent);
            this.documentEventsListener.listen();
          }
          startedListeners.push(event);
          break;
      }
    }

    // Update the live list of running listeners
    startedListeners.forEach(this._listeners.add, this._listeners);

    return {
      startedListeners: startedListeners,
      nativeConsoleAPI: this.hasNativeConsoleAPI(this.global),
      traits: this.traits,
    };
  },

  /**
   * Handler for the "stopListeners" request.
   *
   * @param array listeners
   *        An array of events to stop sent by the Web Console client.
   * @return object
   *        The response packet to send to the client: holds the
   *        stoppedListeners array.
   */
  stopListeners: function(listeners) {
    const stoppedListeners = [];

    // If no specific listeners are requested to be detached, we stop all
    // listeners.
    const eventsToDetach = listeners || [
      "PageError",
      "ConsoleAPI",
      "NetworkActivity",
      "FileActivity",
      "ReflowActivity",
      "ContentProcessMessages",
      "DocumentEvents",
    ];

    for (const event of eventsToDetach) {
      switch (event) {
        case "PageError":
          if (this.consoleServiceListener) {
            this.consoleServiceListener.destroy();
            this.consoleServiceListener = null;
          }
          stoppedListeners.push(event);
          break;
        case "ConsoleAPI":
          if (this.consoleAPIListener) {
            this.consoleAPIListener.destroy();
            this.consoleAPIListener = null;
          }
          stoppedListeners.push(event);
          break;
        case "NetworkActivity":
          if (this.netmonitors) {
            for (const { messageManager } of this.netmonitors) {
              messageManager.sendAsyncMessage("debug:destroy-network-monitor", {
                actorID: this.actorID,
              });
            }
            this.netmonitors = null;
          }
          if (this.stackTraceCollector) {
            this.stackTraceCollector.destroy();
            this.stackTraceCollector = null;
          }
          stoppedListeners.push(event);
          break;
        case "FileActivity":
          if (this.consoleFileActivityListener) {
            this.consoleFileActivityListener.stopMonitor();
            this.consoleFileActivityListener = null;
          }
          stoppedListeners.push(event);
          break;
        case "ReflowActivity":
          if (this.consoleReflowListener) {
            this.consoleReflowListener.destroy();
            this.consoleReflowListener = null;
          }
          stoppedListeners.push(event);
          break;
        case "ContentProcessMessages":
          if (this.contentProcessListener) {
            this.contentProcessListener.destroy();
            this.contentProcessListener = null;
          }
          stoppedListeners.push(event);
          break;
        case "DocumentEvents":
          if (this.documentEventsListener) {
            this.documentEventsListener.destroy();
            this.documentEventsListener = null;
          }
          stoppedListeners.push(event);
          break;
      }
    }

    // Update the live list of running listeners
    stoppedListeners.forEach(this._listeners.delete, this._listeners);

    return { stoppedListeners: stoppedListeners };
  },

  /**
   * Handler for the "getCachedMessages" request. This method sends the cached
   * error messages and the window.console API calls to the client.
   *
   * @param array messageTypes
   *        An array of message types sent by the Web Console client.
   * @return object
   *         The response packet to send to the client: it holds the cached
   *         messages array.
   */
  getCachedMessages: function(messageTypes) {
    if (!messageTypes) {
      return {
        error: "missingParameter",
        message: "The messageTypes parameter is missing.",
      };
    }

    const messages = [];

    const consoleServiceCachedMessages =
      messageTypes.includes("PageError") || messageTypes.includes("LogMessage")
        ? this.consoleServiceListener?.getCachedMessages(
            !this.parentActor.isRootActor
          )
        : null;

    for (const type of messageTypes) {
      switch (type) {
        case "ConsoleAPI": {
          if (!this.consoleAPIListener) {
            break;
          }

          // this.global might not be a window (can be a worker global or a Sandbox),
          // and in such case performance isn't defined
          const winStartTime = this.global?.performance?.timing
            ?.navigationStart;

          const cache = this.consoleAPIListener.getCachedMessages(
            !this.parentActor.isRootActor
          );
          cache.forEach(cachedMessage => {
            // Filter out messages that came from a ServiceWorker but happened
            // before the page was requested.
            if (
              cachedMessage.innerID === "ServiceWorker" &&
              winStartTime > cachedMessage.timeStamp
            ) {
              return;
            }

            messages.push({
              message: this.prepareConsoleMessageForRemote(cachedMessage),
              type: "consoleAPICall",
            });
          });
          break;
        }

        case "PageError": {
          if (!consoleServiceCachedMessages) {
            break;
          }

          for (const cachedMessage of consoleServiceCachedMessages) {
            if (!(cachedMessage instanceof Ci.nsIScriptError)) {
              continue;
            }

            messages.push({
              pageError: this.preparePageErrorForRemote(cachedMessage),
              type: "pageError",
            });
          }
          break;
        }

        case "LogMessage": {
          if (!consoleServiceCachedMessages) {
            break;
          }

          for (const cachedMessage of consoleServiceCachedMessages) {
            if (cachedMessage instanceof Ci.nsIScriptError) {
              continue;
            }

            messages.push({
              message: this._createStringGrip(cachedMessage.message),
              timeStamp: cachedMessage.timeStamp,
              type: "logMessage",
            });
          }
          break;
        }
      }
    }

    return {
      messages: messages,
    };
  },

  /**
   * Handler for the "evaluateJSAsync" request. This method evaluates a given
   * JavaScript string with an associated `resultID`.
   *
   * The result will be returned later as an unsolicited `evaluationResult`,
   * that can be associated back to this request via the `resultID` field.
   *
   * @param object request
   *        The JSON request object received from the Web Console client.
   * @return object
   *         The response packet to send to with the unique id in the
   *         `resultID` field.
   */
  evaluateJSAsync: async function(request) {
    const startTime = Date.now();
    // Use Date instead of UUID as this code is used by workers, which
    // don't have access to the UUID XPCOM component.
    // Also use a counter in order to prevent mixing up response when calling
    // evaluateJSAsync during the same millisecond.
    const resultID = startTime + "-" + this._evalCounter++;

    // Execute the evaluation in the next event loop in order to immediately
    // reply with the resultID.
    DevToolsUtils.executeSoon(async () => {
      try {
        // Execute the script that may pause.
        let response = await this.evaluateJS(request);
        // Wait for any potential returned Promise.
        response = await this._maybeWaitForResponseResult(response);
        // Set the timestamp only now, so any messages logged in the expression will come
        // before the result. Add an extra millisecond so the result has a different timestamp
        // than the console message it might have emitted (unlike the evaluation result,
        // console api messages are throttled before being handled by the webconsole client,
        // which might cause some ordering issue).
        response.timestamp = Date.now() + 1;
        // Finally, emit an unsolicited evaluationResult packet with the evaluation result.
        this.emit("evaluationResult", {
          type: "evaluationResult",
          resultID,
          startTime,
          ...response,
        });
        return;
      } catch (e) {
        const message = `Encountered error while waiting for Helper Result: ${e}\n${e.stack}`;
        DevToolsUtils.reportException("evaluateJSAsync", Error(message));
      }
    });
    return { resultID };
  },

  /**
   * In order to have asynchronous commands (e.g. screenshot, top-level await, …) ,
   * we have to be able to handle promises. This method handles waiting for the promise,
   * and then returns the result.
   *
   * @private
   * @param object response
   *         The response packet to send to with the unique id in the
   *         `resultID` field, and potentially a promise in the `helperResult` or in the
   *         `awaitResult` field.
   *
   * @return object
   *         The updated response object.
   */
  _maybeWaitForResponseResult: async function(response) {
    if (!response) {
      return response;
    }

    const thenable = obj => obj && typeof obj.then === "function";
    const waitForHelperResult =
      response.helperResult && thenable(response.helperResult);
    const waitForAwaitResult =
      response.awaitResult && thenable(response.awaitResult);

    if (!waitForAwaitResult && !waitForHelperResult) {
      return response;
    }

    // Wait for asynchronous command completion before sending back the response
    if (waitForHelperResult) {
      response.helperResult = await response.helperResult;
    } else if (waitForAwaitResult) {
      let result;
      try {
        result = await response.awaitResult;

        // `createValueGrip` expect a debuggee value, while here we have the raw object.
        // We need to call `makeDebuggeeValue` on it to make it work.
        const dbgResult = this.makeDebuggeeValue(result);
        response.result = this.createValueGrip(dbgResult);
      } catch (e) {
        // The promise was rejected. We let the engine handle this as it will report a
        // `uncaught exception` error.
        response.topLevelAwaitRejected = true;
      }

      // Remove the promise from the response object.
      delete response.awaitResult;
    }

    return response;
  },

  /**
   * Handler for the "evaluateJS" request. This method evaluates the given
   * JavaScript string and sends back the result.
   *
   * @param object request
   *        The JSON request object received from the Web Console client.
   * @return object
   *         The evaluation response packet.
   */
  evaluateJS: function(request) {
    const input = request.text;

    const evalOptions = {
      frameActor: request.frameActor,
      url: request.url,
      innerWindowID: request.innerWindowID,
      selectedNodeActor: request.selectedNodeActor,
      selectedObjectActor: request.selectedObjectActor,
      eager: request.eager,
      bindings: request.bindings,
      lineNumber: request.lineNumber,
    };

    const { mapped } = request;

    // Set a flag on the thread actor which indicates an evaluation is being
    // done for the client. This can affect how debugger handlers behave.
    this.parentActor.threadActor.insideClientEvaluation = evalOptions;

    const evalInfo = evalWithDebugger(input, evalOptions, this);

    this.parentActor.threadActor.insideClientEvaluation = null;

    return new Promise((resolve, reject) => {
      // Queue up a task to run in the next tick so any microtask created by the evaluated
      // expression has the time to be run.
      // e.g. in :
      // ```
      // const promiseThenCb = result => "result: " + result;
      // new Promise(res => res("hello")).then(promiseThenCb)
      // ```
      // we want`promiseThenCb` to have run before handling the result.
      DevToolsUtils.executeSoon(() => {
        try {
          const result = this.prepareEvaluationResult(
            evalInfo,
            input,
            request.eager,
            mapped
          );
          resolve(result);
        } catch (err) {
          reject(err);
        }
      });
    });
  },

  // eslint-disable-next-line complexity
  prepareEvaluationResult: function(evalInfo, input, eager, mapped) {
    const evalResult = evalInfo.result;
    const helperResult = evalInfo.helperResult;

    let result,
      errorDocURL,
      errorMessage,
      errorNotes = null,
      errorGrip = null,
      frame = null,
      awaitResult,
      errorMessageName,
      exceptionStack;
    if (evalResult) {
      if ("return" in evalResult) {
        result = evalResult.return;
        if (
          mapped?.await &&
          result &&
          result.class === "Promise" &&
          typeof result.unsafeDereference === "function"
        ) {
          awaitResult = result.unsafeDereference();
        }
      } else if ("yield" in evalResult) {
        result = evalResult.yield;
      } else if ("throw" in evalResult) {
        const error = evalResult.throw;
        errorGrip = this.createValueGrip(error);

        exceptionStack = this.prepareStackForRemote(evalResult.stack);

        if (exceptionStack) {
          // Set the frame based on the topmost stack frame for the exception.
          const {
            filename: source,
            sourceId,
            lineNumber: line,
            columnNumber: column,
          } = exceptionStack[0];
          frame = { source, sourceId, line, column };

          exceptionStack = WebConsoleUtils.removeFramesAboveDebuggerEval(
            exceptionStack
          );
        }

        errorMessage = String(error);
        if (typeof error === "object" && error !== null) {
          try {
            errorMessage = DevToolsUtils.callPropertyOnObject(
              error,
              "toString"
            );
          } catch (e) {
            // If the debuggee is not allowed to access the "toString" property
            // of the error object, calling this property from the debuggee's
            // compartment will fail. The debugger should show the error object
            // as it is seen by the debuggee, so this behavior is correct.
            //
            // Unfortunately, we have at least one test that assumes calling the
            // "toString" property of an error object will succeed if the
            // debugger is allowed to access it, regardless of whether the
            // debuggee is allowed to access it or not.
            //
            // To accomodate these tests, if calling the "toString" property
            // from the debuggee compartment fails, we rewrap the error object
            // in the debugger's compartment, and then call the "toString"
            // property from there.
            if (typeof error.unsafeDereference === "function") {
              const rawError = error.unsafeDereference();
              errorMessage = rawError ? rawError.toString() : "";
            }
          }
        }

        // It is possible that we won't have permission to unwrap an
        // object and retrieve its errorMessageName.
        try {
          errorDocURL = ErrorDocs.GetURL(error);
          errorMessageName = error.errorMessageName;
        } catch (ex) {
          // ignored
        }

        try {
          const line = error.errorLineNumber;
          const column = error.errorColumnNumber;

          if (typeof line === "number" && typeof column === "number") {
            // Set frame only if we have line/column numbers.
            frame = {
              source: "debugger eval code",
              line,
              column,
            };
          }
        } catch (ex) {
          // ignored
        }

        try {
          const notes = error.errorNotes;
          if (notes?.length) {
            errorNotes = [];
            for (const note of notes) {
              errorNotes.push({
                messageBody: this._createStringGrip(note.message),
                frame: {
                  source: note.fileName,
                  line: note.lineNumber,
                  column: note.columnNumber,
                },
              });
            }
          }
        } catch (ex) {
          // ignored
        }
      }
    }

    // If a value is encountered that the devtools server doesn't support yet,
    // the console should remain functional.
    let resultGrip;
    if (!awaitResult) {
      try {
        const objectActor = this.parentActor.threadActor.getThreadLifetimeObject(
          result
        );
        if (objectActor) {
          resultGrip = this.parentActor.threadActor.createValueGrip(result);
        } else {
          resultGrip = this.createValueGrip(result);
        }
      } catch (e) {
        errorMessage = e;
      }
    }

    // Don't update _lastConsoleInputEvaluation in eager evaluation, as it would interfere
    // with the $_ command.
    if (!eager) {
      if (!awaitResult) {
        this._lastConsoleInputEvaluation = result;
      } else {
        // If we evaluated a top-level await expression, we want to assign its result to the
        // _lastConsoleInputEvaluation only when the promise resolves, and only if it
        // resolves. If the promise rejects, we don't re-assign _lastConsoleInputEvaluation,
        // it will keep its previous value.

        const p = awaitResult.then(res => {
          this._lastConsoleInputEvaluation = this.makeDebuggeeValue(res);
        });

        // If the top level await was already rejected (e.g. `await Promise.reject("bleh")`),
        // catch the resulting promise of awaitResult.then.
        // If we don't do that, the new Promise will also be rejected, and since it's
        // unhandled, it will generate an error.
        // We don't want to do that for pending promise (e.g. `await new Promise((res, rej) => setTimeout(rej,250))`),
        // as the the Promise rejection will be considered as handled, and the "Uncaught (in promise)"
        // message wouldn't be emitted.
        const { state } = ObjectUtils.getPromiseState(evalResult.return);
        if (state === "rejected") {
          p.catch(() => {});
        }
      }
    }

    return {
      input: input,
      result: resultGrip,
      awaitResult,
      exception: errorGrip,
      exceptionMessage: this._createStringGrip(errorMessage),
      exceptionDocURL: errorDocURL,
      exceptionStack,
      hasException: errorGrip !== null,
      errorMessageName,
      frame,
      helperResult: helperResult,
      notes: errorNotes,
    };
  },

  /**
   * The Autocomplete request handler.
   *
   * @param string text
   *        The request message - what input to autocomplete.
   * @param number cursor
   *        The cursor position at the moment of starting autocomplete.
   * @param string frameActor
   *        The frameactor id of the current paused frame.
   * @param string selectedNodeActor
   *        The actor id of the currently selected node.
   * @param array authorizedEvaluations
   *        Array of the properties access which can be executed by the engine.
   * @return object
   *         The response message - matched properties.
   */
  autocomplete: function(
    text,
    cursor,
    frameActorId,
    selectedNodeActor,
    authorizedEvaluations,
    expressionVars = []
  ) {
    let dbgObject = null;
    let environment = null;
    let matches = [];
    let matchProp;
    let isElementAccess;

    const reqText = text.substr(0, cursor);

    if (isCommand(reqText)) {
      const commandsCache = this._getWebConsoleCommandsCache();
      matchProp = reqText;
      matches = validCommands
        .filter(
          c =>
            `:${c}`.startsWith(reqText) &&
            commandsCache.find(n => `:${n}`.startsWith(reqText))
        )
        .map(c => `:${c}`);
    } else {
      // This is the case of the paused debugger
      if (frameActorId) {
        const frameActor = this.conn.getActor(frameActorId);
        try {
          // Need to try/catch since accessing frame.environment
          // can throw "Debugger.Frame is not live"
          const frame = frameActor.frame;
          environment = frame.environment;
        } catch (e) {
          DevToolsUtils.reportException(
            "autocomplete",
            Error("The frame actor was not found: " + frameActorId)
          );
        }
      } else {
        dbgObject = this.dbg.addDebuggee(this.evalGlobal);
      }

      const result = JSPropertyProvider({
        dbgObject,
        environment,
        inputValue: text,
        cursor,
        webconsoleActor: this,
        selectedNodeActor,
        authorizedEvaluations,
        expressionVars,
      });

      if (result === null) {
        return {
          matches: null,
        };
      }

      if (result && result.isUnsafeGetter === true) {
        return {
          isUnsafeGetter: true,
          getterPath: result.getterPath,
        };
      }

      matches = result.matches || new Set();
      matchProp = result.matchProp || "";
      isElementAccess = result.isElementAccess;

      // We consider '$' as alphanumeric because it is used in the names of some
      // helper functions; we also consider whitespace as alphanum since it should not
      // be seen as break in the evaled string.
      const lastNonAlphaIsDot = /[.][a-zA-Z0-9$\s]*$/.test(reqText);

      // We only return commands and keywords when we are not dealing with a property or
      // element access.
      if (matchProp && !lastNonAlphaIsDot && !isElementAccess) {
        this._getWebConsoleCommandsCache().forEach(n => {
          // filter out `screenshot` command as it is inaccessible without the `:` prefix
          if (n !== "screenshot" && n.startsWith(result.matchProp)) {
            matches.add(n);
          }
        });

        for (const keyword of RESERVED_JS_KEYWORDS) {
          if (keyword.startsWith(result.matchProp)) {
            matches.add(keyword);
          }
        }
      }

      // Sort the results in order to display lowercased item first (e.g. we want to
      // display `document` then `Document` as we loosely match the user input if the
      // first letter was lowercase).
      const firstMeaningfulCharIndex = isElementAccess ? 1 : 0;
      matches = Array.from(matches).sort((a, b) => {
        const aFirstMeaningfulChar = a[firstMeaningfulCharIndex];
        const bFirstMeaningfulChar = b[firstMeaningfulCharIndex];
        const lA =
          aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
        const lB =
          bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
        if (lA === lB) {
          if (a === matchProp) {
            return -1;
          }
          if (b === matchProp) {
            return 1;
          }
          return a.localeCompare(b);
        }
        return lA ? -1 : 1;
      });
    }

    return {
      matches,
      matchProp,
      isElementAccess: isElementAccess === true,
    };
  },

  /**
   * The "clearMessagesCache" request handler.
   */
  clearMessagesCache: function() {
    if (isWorker) {
      // At the moment there is no mechanism available to clear the Console API cache for
      // a given worker target (See https://bugzilla.mozilla.org/show_bug.cgi?id=1674336).
      // Worker messages from the console service (e.g. error) are emitted from the main
      // thread, so this cache will be cleared when the associated document target cache
      // is cleared.
      return;
    }

    const windowId = !this.parentActor.isRootActor
      ? WebConsoleUtils.getInnerWindowId(this.global)
      : null;

    const ConsoleAPIStorage = Cc[
      "@mozilla.org/consoleAPI-storage;1"
    ].getService(Ci.nsIConsoleAPIStorage);
    ConsoleAPIStorage.clearEvents(windowId);

    CONSOLE_WORKER_IDS.forEach(id => {
      ConsoleAPIStorage.clearEvents(id);
    });

    if (this.parentActor.isRootActor || !this.global) {
      // If were dealing with the root actor (e.g. the browser console), we want
      // to remove all cached messages, not only the ones specific to a window.
      Services.console.reset();
    } else {
      WebConsoleUtils.getInnerWindowIDsForFrames(this.global).forEach(id =>
        Services.console.resetWindow(id)
      );
    }
  },

  /**
   * The "getPreferences" request handler.
   *
   * @param array preferences
   *        The preferences that need to be retrieved.
   * @return object
   *         The response message - a { key: value } object map.
   */
  getPreferences: function(preferences) {
    const prefs = Object.create(null);
    for (const key of preferences) {
      prefs[key] = this._prefs[key];
    }
    return { preferences: prefs };
  },

  /**
   * The "setPreferences" request handler.
   *
   * @param object preferences
   *        The preferences that need to be updated.
   */
  setPreferences: function(preferences) {
    for (const key in preferences) {
      this._prefs[key] = preferences[key];

      if (this.netmonitors) {
        if (key == "NetworkMonitor.saveRequestAndResponseBodies") {
          for (const { messageManager } of this.netmonitors) {
            messageManager.sendAsyncMessage("debug:netmonitor-preference", {
              saveRequestAndResponseBodies: this._prefs[key],
            });
          }
        } else if (key == "NetworkMonitor.throttleData") {
          for (const { messageManager } of this.netmonitors) {
            messageManager.sendAsyncMessage("debug:netmonitor-preference", {
              throttleData: this._prefs[key],
            });
          }
        }
      }
    }
    return { updated: Object.keys(preferences) };
  },

  // End of request handlers.

  /**
   * Create an object with the API we expose to the Web Console during
   * JavaScript evaluation.
   * This object inherits properties and methods from the Web Console actor.
   *
   * @private
   * @param object debuggerGlobal
   *        A Debugger.Object that wraps a content global. This is used for the
   *        Web Console Commands.
   * @return object
   *         The same object as |this|, but with an added |sandbox| property.
   *         The sandbox holds methods and properties that can be used as
   *         bindings during JS evaluation.
   */
  _getWebConsoleCommands: function(debuggerGlobal) {
    const helpers = {
      window: this.evalGlobal,
      makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal),
      createValueGrip: this.createValueGrip.bind(this),
      preprocessDebuggerObject: this.preprocessDebuggerObject.bind(this),
      sandbox: Object.create(null),
      helperResult: null,
      consoleActor: this,
    };
    addWebConsoleCommands(helpers);

    const evalGlobal = this.evalGlobal;
    function maybeExport(obj, name) {
      if (typeof obj[name] != "function") {
        return;
      }

      // By default, chrome-implemented functions that are exposed to content
      // refuse to accept arguments that are cross-origin for the caller. This
      // is generally the safe thing, but causes problems for certain console
      // helpers like cd(), where we users sometimes want to pass a cross-origin
      // window. To circumvent this restriction, we use exportFunction along
      // with a special option designed for this purpose. See bug 1051224.
      obj[name] = Cu.exportFunction(obj[name], evalGlobal, {
        allowCrossOriginArguments: true,
      });
    }
    for (const name in helpers.sandbox) {
      const desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);

      // Workers don't have access to Cu so won't be able to exportFunction.
      if (!isWorker) {
        maybeExport(desc, "get");
        maybeExport(desc, "set");
        maybeExport(desc, "value");
      }
      if (desc.value) {
        // Make sure the helpers can be used during eval.
        desc.value = debuggerGlobal.makeDebuggeeValue(desc.value);
      }
      Object.defineProperty(helpers.sandbox, name, desc);
    }
    return helpers;
  },

  _getWebConsoleCommandsCache: function() {
    if (!this._webConsoleCommandsCache) {
      const helpers = {
        sandbox: Object.create(null),
      };
      addWebConsoleCommands(helpers);
      this._webConsoleCommandsCache = Object.getOwnPropertyNames(
        helpers.sandbox
      );
    }
    return this._webConsoleCommandsCache;
  },

  // Event handlers for various listeners.

  /**
   * Handler for messages received from the ConsoleServiceListener. This method
   * sends the nsIConsoleMessage to the remote Web Console client.
   *
   * @param nsIConsoleMessage message
   *        The message we need to send to the client.
   */
  onConsoleServiceMessage: function(message) {
    if (message instanceof Ci.nsIScriptError) {
      this.emit("pageError", {
        pageError: this.preparePageErrorForRemote(message),
      });
    } else {
      this.emit("logMessage", {
        message: this._createStringGrip(message.message),
        timeStamp: message.timeStamp,
      });
    }
  },

  getActorIdForInternalSourceId(id) {
    const actor = this.parentActor.sourcesManager.getSourceActorByInternalSourceId(
      id
    );
    return actor ? actor.actorID : null;
  },

  /**
   * Prepare a SavedFrame stack to be sent to the client.
   *
   * @param SavedFrame errorStack
   *        Stack for an error we need to send to the client.
   * @return object
   *         The object you can send to the remote client.
   */
  prepareStackForRemote(errorStack) {
    // Convert stack objects to the JSON attributes expected by client code
    // Bug 1348885: If the global from which this error came from has been
    // nuked, stack is going to be a dead wrapper.
    if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) {
      return null;
    }
    const stack = [];
    let s = errorStack;
    while (s) {
      stack.push({
        filename: s.source,
        sourceId: this.getActorIdForInternalSourceId(s.sourceId),
        lineNumber: s.line,
        columnNumber: s.column,
        functionName: s.functionDisplayName,
        asyncCause: s.asyncCause ? s.asyncCause : undefined,
      });
      s = s.parent || s.asyncParent;
    }
    return stack;
  },

  /**
   * Prepare an nsIScriptError to be sent to the client.
   *
   * @param nsIScriptError pageError
   *        The page error we need to send to the client.
   * @return object
   *         The object you can send to the remote client.
   */
  preparePageErrorForRemote: function(pageError) {
    const stack = this.prepareStackForRemote(pageError.stack);
    let lineText = pageError.sourceLine;
    if (
      lineText &&
      lineText.length > DevToolsServer.LONG_STRING_INITIAL_LENGTH
    ) {
      lineText = lineText.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
    }

    let notesArray = null;
    const notes = pageError.notes;
    if (notes?.length) {
      notesArray = [];
      for (let i = 0, len = notes.length; i < len; i++) {
        const note = notes.queryElementAt(i, Ci.nsIScriptErrorNote);
        notesArray.push({
          messageBody: this._createStringGrip(note.errorMessage),
          frame: {
            source: note.sourceName,
            sourceId: this.getActorIdForInternalSourceId(note.sourceId),
            line: note.lineNumber,
            column: note.columnNumber,
          },
        });
      }
    }

    // If there is no location information in the error but we have a stack,
    // fill in the location with the first frame on the stack.
    let { sourceName, sourceId, lineNumber, columnNumber } = pageError;
    if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
      sourceName = stack[0].filename;
      sourceId = stack[0].sourceId;
      lineNumber = stack[0].lineNumber;
      columnNumber = stack[0].columnNumber;
    }

    const isCSSMessage = pageError.category === MESSAGE_CATEGORY.CSS_PARSER;

    const result = {
      errorMessage: this._createStringGrip(pageError.errorMessage),
      errorMessageName: isCSSMessage ? undefined : pageError.errorMessageName,
      exceptionDocURL: ErrorDocs.GetURL(pageError),
      sourceName,
      sourceId: this.getActorIdForInternalSourceId(sourceId),
      lineText,
      lineNumber,
      columnNumber,
      category: pageError.category,
      innerWindowID: pageError.innerWindowID,
      timeStamp: pageError.timeStamp,
      warning: !!(pageError.flags & pageError.warningFlag),
      error: !(pageError.flags & (pageError.warningFlag | pageError.infoFlag)),
      info: !!(pageError.flags & pageError.infoFlag),
      private: pageError.isFromPrivateWindow,
      stacktrace: stack,
      notes: notesArray,
      chromeContext: pageError.isFromChromeContext,
      isPromiseRejection: isCSSMessage
        ? undefined
        : pageError.isPromiseRejection,
      isForwardedFromContentProcess: pageError.isForwardedFromContentProcess,
      cssSelectors: isCSSMessage ? pageError.cssSelectors : undefined,
    };

    // If the pageError does have an exception object, we want to return the grip for it,
    // but only if we do manage to get the grip, as we're checking the property on the
    // client to render things differently.
    if (pageError.hasException) {
      try {
        const obj = this.makeDebuggeeValue(pageError.exception, true);
        if (obj?.class !== "DeadObject") {
          result.exception = this.createValueGrip(obj);
          result.hasException = true;
        }
      } catch (e) {}
    }

    return result;
  },

  /**
   * Handler for window.console API calls received from the ConsoleAPIListener.
   * This method sends the object to the remote Web Console client.
   *
   * @see ConsoleAPIListener
   * @param object message
   *        The console API call we need to send to the remote client.
   * @param object extraProperties
   *        an object whose properties will be folded in the packet that is emitted.
   */
  onConsoleAPICall: function(message, extraProperties = {}) {
    this.emit("consoleAPICall", {
      message: this.prepareConsoleMessageForRemote(message),
      ...extraProperties,
    });
  },

  /**
   * Handler for the DocumentEventsListener.
   *
   * @see DocumentEventsListener
   * @param {String} name
   *        The document event name that either of followings.
   *        - dom-loading
   *        - dom-interactive
   *        - dom-complete
   * @param {Number} time
   *        The time that the event is fired.
   */
  onDocumentEvent: function(name, { time }) {
    // will-navigate event has been added in Fx91 and is only expected to be used
    // by DOCUMENT_EVENT watcher. For toolbox still not using watcher actor and DOCUMENT_EVENT watcher
    // will-navigate will be emitted based on target actor's will-navigate events.
    if (name == "will-navigate") {
      return;
    }
    this.emit("documentEvent", {
      name,
      time,
    });
  },

  /**
   * Send a new HTTP request from the target's window.
   *
   * @param object request
   *        The details of the HTTP request.
   */
  async sendHTTPRequest(request) {
    const { url, method, headers, body, cause } = request;
    // Set the loadingNode and loadGroup to the target document - otherwise the
    // request won't show up in the opened netmonitor.
    const doc = this.global.document;

    const channel = NetUtil.newChannel({
      uri: NetUtil.newURI(url),
      loadingNode: doc,
      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      contentPolicyType:
        NetworkUtils.stringToCauseType(cause.type) ||
        Ci.nsIContentPolicy.TYPE_OTHER,
    });

    channel.QueryInterface(Ci.nsIHttpChannel);

    channel.loadGroup = doc.documentLoadGroup;
    channel.loadFlags |=
      Ci.nsIRequest.LOAD_BYPASS_CACHE |
      Ci.nsIRequest.INHIBIT_CACHING |
      Ci.nsIRequest.LOAD_ANONYMOUS;

    channel.requestMethod = method;
    if (headers) {
      for (const { name, value } of headers) {
        if (name.toLowerCase() == "referer") {
          // The referer header and referrerInfo object should always match. So
          // if we want to set the header from privileged context, we should set
          // referrerInfo. The referrer header will get set internally.
          channel.setNewReferrerInfo(
            value,
            Ci.nsIReferrerInfo.UNSAFE_URL,
            true
          );
        } else {
          channel.setRequestHeader(name, value, false);
        }
      }
    }

    if (body) {
      channel.QueryInterface(Ci.nsIUploadChannel2);
      const bodyStream = Cc[
        "@mozilla.org/io/string-input-stream;1"
      ].createInstance(Ci.nsIStringInputStream);
      bodyStream.setData(body, body.length);
      channel.explicitSetUploadStream(bodyStream, null, -1, method, false);
    }

    NetUtil.asyncFetch(channel, () => {});

    if (!this.netmonitors) {
      return null;
    }
    const { channelId } = channel;
    // Only query the NetworkMonitorActor running in the parent process, where the
    // request will be done. There always is one listener running in the parent process,
    // see startListeners.
    const netmonitor = this.netmonitors.filter(
      ({ parentProcess }) => parentProcess
    )[0];
    const { messageManager } = netmonitor;
    return new Promise(resolve => {
      const onMessage = ({ data }) => {
        if (data.channelId == channelId) {
          messageManager.removeMessageListener(
            "debug:get-network-event-actor:response",
            onMessage
          );
          resolve({
            eventActor: data.actor,
          });
        }
      };
      messageManager.addMessageListener(
        "debug:get-network-event-actor:response",
        onMessage
      );
      messageManager.sendAsyncMessage("debug:get-network-event-actor:request", {
        channelId,
      });
    });
  },

  /**
   * Send a message to all the netmonitor message managers, and resolve when
   * all of them replied with the expected responseName message.
   *
   * @param {String} messageName
   *        Name of the message to send via the netmonitor message managers.
   * @param {String} responseName
   *        Name of the message that should be received when the message has
   *        been processed by the netmonitor instance.
   * @param {Object} args
   *        argument object passed with the initial message.
   */
  async _sendMessageToNetmonitors(messageName, responseName, args) {
    if (!this.netmonitors) {
      return null;
    }
    const results = await Promise.all(
      this.netmonitors.map(({ messageManager }) => {
        const onResponseReceived = new Promise(resolve => {
          messageManager.addMessageListener(responseName, function onResponse(
            response
          ) {
            messageManager.removeMessageListener(responseName, onResponse);
            resolve(response);
          });
        });
        messageManager.sendAsyncMessage(messageName, args);
        return onResponseReceived;
      })
    );

    return results;
  },

  /**
   * Block a request based on certain filtering options.
   *
   * Currently, an exact URL match is the only supported filter type.
   * In the future, there may be other types of filters, such as domain.
   * For now, ignore anything other than URL.
   *
   * @param object filter
   *   An object containing a `url` key with a URL to block.
   */
  async blockRequest(filter) {
    await this._sendMessageToNetmonitors(
      "debug:block-request",
      "debug:block-request:response",
      { filter }
    );

    return {};
  },

  /**
   * Unblock a request based on certain filtering options.
   *
   * Currently, an exact URL match is the only supported filter type.
   * In the future, there may be other types of filters, such as domain.
   * For now, ignore anything other than URL.
   *
   * @param object filter
   *   An object containing a `url` key with a URL to unblock.
   */
  async unblockRequest(filter) {
    await this._sendMessageToNetmonitors(
      "debug:unblock-request",
      "debug:unblock-request:response",
      { filter }
    );

    return {};
  },

  /*
   * Gets the list of blocked request urls as per the backend
   */
  async getBlockedUrls() {
    const responses =
      (await this._sendMessageToNetmonitors(
        "debug:get-blocked-urls",
        "debug:get-blocked-urls:response"
      )) || [];
    if (!responses || responses.length == 0) {
      return [];
    }

    return Array.from(
      new Set(
        responses
          .filter(response => response.data)
          .map(response => response.data)
      )
    );
  },

  /**
   * Sets the list of blocked request URLs as provided by the netmonitor frontend
   *
   * This match will be a (String).includes match, not an exact URL match
   *
   * @param object filter
   *   An object containing a `url` key with a URL to unblock.
   */
  async setBlockedUrls(urls) {
    await this._sendMessageToNetmonitors(
      "debug:set-blocked-urls",
      "debug:set-blocked-urls:response",
      { urls }
    );

    return {};
  },

  /**
   * Handler for file activity. This method sends the file request information
   * to the remote Web Console client.
   *
   * @see ConsoleFileActivityListener
   * @param string fileURI
   *        The requested file URI.
   */
  onFileActivity: function(fileURI) {
    this.emit("fileActivity", {
      uri: fileURI,
    });
  },

  // End of event handlers for various listeners.

  /**
   * Prepare a message from the console API to be sent to the remote Web Console
   * instance.
   *
   * @param object message
   *        The original message received from console-api-log-event.
   * @param boolean aUseObjectGlobal
   *        If |true| the object global is determined and added as a debuggee,
   *        otherwise |this.global| is used when makeDebuggeeValue() is invoked.
   * @return object
   *         The object that can be sent to the remote client.
   */
  prepareConsoleMessageForRemote: function(message, useObjectGlobal = true) {
    const result = WebConsoleUtils.cloneObject(message);

    result.workerType = WebConsoleUtils.getWorkerType(result) || "none";
    result.sourceId = this.getActorIdForInternalSourceId(result.sourceId);

    delete result.wrappedJSObject;
    delete result.ID;
    delete result.innerID;
    delete result.consoleID;

    if (result.stacktrace) {
      result.stacktrace = result.stacktrace.map(frame => {
        return {
          ...frame,
          sourceId: this.getActorIdForInternalSourceId(frame.sourceId),
        };
      });
    }

    result.arguments = (message.arguments || []).map(obj => {
      const dbgObj = this.makeDebuggeeValue(obj, useObjectGlobal);
      return this.createValueGrip(dbgObj);
    });

    result.styles = (message.styles || []).map(string => {
      return this.createValueGrip(string);
    });

    if (result.level === "table") {
      const tableItems = this._getConsoleTableMessageItems(result);
      if (tableItems) {
        result.arguments[0].ownProperties = tableItems;
        result.arguments[0].preview = null;
      }

      // Only return the 2 first params.
      result.arguments = result.arguments.slice(0, 2);
    }

    result.category = message.category || "webdev";
    result.innerWindowID = message.innerID;

    return result;
  },

  /**
   * Return the properties needed to display the appropriate table for a given
   * console.table call.
   * This function does a little more than creating an ObjectActor for the first
   * parameter of the message. When layout out the console table in the output, we want
   * to be able to look into sub-properties so the table can have a different layout (
   * for arrays of arrays, objects with objects properties, arrays of objects, …).
   * So here we need to retrieve the properties of the first parameter, and also all the
   * sub-properties we might need.
   *
   * @param {Object} result: The console.table message.
   * @returns {Object} An object containing the properties of the first argument of the
   *                   console.table call.
   */
  _getConsoleTableMessageItems: function(result) {
    if (
      !result ||
      !Array.isArray(result.arguments) ||
      result.arguments.length == 0
    ) {
      return null;
    }

    const [tableItemGrip] = result.arguments;
    const dataType = tableItemGrip.class;
    const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
    const ignoreNonIndexedProperties = isArray(tableItemGrip);

    const tableItemActor = this.getActorByID(tableItemGrip.actor);
    if (!tableItemActor) {
      return null;
    }

    // Retrieve the properties (or entries for Set/Map) of the console table first arg.
    const iterator = needEntries
      ? tableItemActor.enumEntries()
      : tableItemActor.enumProperties({
          ignoreNonIndexedProperties,
        });
    const { ownProperties } = iterator.all();

    // The iterator returns a descriptor for each property, wherein the value could be
    // in one of those sub-property.
    const descriptorKeys = ["safeGetterValues", "getterValue", "value"];

    Object.values(ownProperties).forEach(desc => {
      if (typeof desc !== "undefined") {
        descriptorKeys.forEach(key => {
          if (desc && desc.hasOwnProperty(key)) {
            const grip = desc[key];

            // We need to load sub-properties as well to render the table in a nice way.
            const actor = grip && this.getActorByID(grip.actor);
            if (actor) {
              const res = actor
                .enumProperties({
                  ignoreNonIndexedProperties: isArray(grip),
                })
                .all();
              if (res?.ownProperties) {
                desc[key].ownProperties = res.ownProperties;
              }
            }
          }
        });
      }
    });

    return ownProperties;
  },

  /**
   * Notification observer for the "last-pb-context-exited" topic.
   *
   * @private
   * @param object subject
   *        Notification subject - in this case it is the inner window ID that
   *        was destroyed.
   * @param string topic
   *        Notification topic.
   */
  _onObserverNotification: function(subject, topic) {
    if (topic === "last-pb-context-exited") {
      this.emit("lastPrivateContextExited");
    }
  },

  /**
   * The "will-navigate" progress listener. This is used to clear the current
   * eval scope.
   */
  _onWillNavigate: function({ window, isTopLevel }) {
    if (isTopLevel) {
      this._evalGlobal = null;
      EventEmitter.off(this.parentActor, "will-navigate", this._onWillNavigate);
      this._progressListenerActive = false;
    }
  },

  /**
   * This listener is called when we switch to another frame,
   * mostly to unregister previous listeners and start listening on the new document.
   */
  _onChangedToplevelDocument: function() {
    // Convert the Set to an Array
    const listeners = [...this._listeners];

    // Unregister existing listener on the previous document
    // (pass a copy of the array as it will shift from it)
    this.stopListeners(listeners.slice());

    // This method is called after this.global is changed,
    // so we register new listener on this new global
    this.startListeners(listeners);

    // Also reset the cached top level chrome window being targeted
    this._lastChromeWindow = null;
  },
});

exports.WebConsoleActor = WebConsoleActor;
