summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webconsole.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/webconsole.js')
-rw-r--r--devtools/server/actors/webconsole.js2215
1 files changed, 2215 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js
new file mode 100644
index 0000000000..8c302cfd02
--- /dev/null
+++ b/devtools/server/actors/webconsole.js
@@ -0,0 +1,2215 @@
+/* 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,
+ "stringToCauseType",
+ "devtools/server/actors/network-monitor/network-observer",
+ true
+);
+
+// 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(
+ this.onConsoleAPICall
+ );
+ }
+ 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.
+ */
+ onConsoleAPICall: function(message) {
+ this.emit("consoleAPICall", {
+ message: this.prepareConsoleMessageForRemote(message),
+ });
+ },
+
+ /**
+ * 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) {
+ 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:
+ 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;