gecko-dev/devtools/server/actors/webconsole.js

2152 строки
66 KiB
JavaScript

/* 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
);
}
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),
sources: () =>
DevToolsUtils.reportException(
"WebConsoleActor",
Error("sources not yet implemented")
),
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 = 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.
response.timestamp = Date.now();
// 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.
*/
// eslint-disable-next-line complexity
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;
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 (!request.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.
awaitResult.then(res => {
this._lastConsoleInputEvaluation = this.makeDebuggeeValue(res);
});
}
}
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() {
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.sources.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) {
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;