diff options
Diffstat (limited to 'devtools/server/actors/webconsole.js')
-rw-r--r-- | devtools/server/actors/webconsole.js | 2346 |
1 files changed, 2346 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js new file mode 100644 index 000000000..9712ff32d --- /dev/null +++ b/devtools/server/actors/webconsole.js @@ -0,0 +1,2346 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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"; + +const Services = require("Services"); +const { Cc, Ci, Cu } = require("chrome"); +const { DebuggerServer, ActorPool } = require("devtools/server/main"); +const { EnvironmentActor } = require("devtools/server/actors/environment"); +const { ThreadActor } = require("devtools/server/actors/script"); +const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const ErrorDocs = require("devtools/server/actors/errordocs"); + +loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true); +loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true); +loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true); +loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true); +loader.lazyRequireGetter(this, "events", "sdk/event/core"); +loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true); +loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true); +loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true); +loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true); + +for (let name of ["WebConsoleUtils", "ConsoleServiceListener", + "ConsoleAPIListener", "addWebConsoleCommands", + "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) { + Object.defineProperty(this, name, { + get: function (prop) { + if (prop == "WebConsoleUtils") { + prop = "Utils"; + } + if (isWorker) { + return require("devtools/server/actors/utils/webconsole-worker-utils")[prop]; + } else { + return require("devtools/server/actors/utils/webconsole-utils")[prop]; + } + }.bind(null, name), + configurable: true, + enumerable: true + }); +} + +/** + * The WebConsoleActor implements capabilities needed for the Web Console + * feature. + * + * @constructor + * @param object aConnection + * The connection to the client, DebuggerServerConnection. + * @param object [aParentActor] + * Optional, the parent actor. + */ +function WebConsoleActor(aConnection, aParentActor) +{ + this.conn = aConnection; + this.parentActor = aParentActor; + + this._actorPool = new ActorPool(this.conn); + this.conn.addActorPool(this._actorPool); + + this._prefs = {}; + + this.dbg = this.parentActor.makeDebugger(); + + this._netEvents = new Map(); + this._gripDepth = 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); + events.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", false); + } + + this.traits = { + customNetworkRequest: !this._parentIsContentActor, + evaluateJSAsync: true, + transferredResponseSize: true, + selectedObjectActor: true, // 44+ + }; +} + +WebConsoleActor.prototype = +{ + /** + * 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, + + /** + * Actor pool for all of the actors we send to the client. + * @private + * @type object + * @see ActorPool + */ + _actorPool: null, + + /** + * Web Console-related preferences. + * @private + * @type object + */ + _prefs: null, + + /** + * Holds a map between nsIChannel objects and NetworkEventActors for requests + * created with sendHTTPRequest. + * + * @private + * @type Map + */ + _netEvents: null, + + /** + * Holds a set of all currently registered listeners. + * + * @private + * @type Set + */ + _listeners: null, + + /** + * The debugger server connection instance. + * @type object + */ + conn: null, + + /** + * List of supported features by the console actor. + * @type object + */ + traits: null, + + /** + * Boolean getter that tells if the parent actor is a ContentActor. + * + * @private + * @type boolean + */ + get _parentIsContentActor() { + return "ContentActor" in DebuggerServer && + this.parentActor instanceof DebuggerServer.ContentActor; + }, + + /** + * The window or sandbox we work with. + * Note that even if it is named `window` it refers to the current + * global we are debugging, which can be a Sandbox for addons + * or browser content toolbox. + * + * @type nsIDOMWindow or Sandbox + */ + get window() { + if (this.parentActor.isRootActor) { + return this._getWindowForBrowserConsole(); + } + return this.parentActor.window; + }, + + /** + * 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 WCA__getWindowForBrowserConsole() + { + // 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. + let 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", false); + } + + 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 WCA__handleNewWindow(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 evalWindow is used at the scope for JS evaluation. + _evalWindow: null, + get evalWindow() { + return this._evalWindow || this.window; + }, + + set evalWindow(aWindow) { + this._evalWindow = aWindow; + + if (!this._progressListenerActive) { + events.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 tab actor. We use the progress listener to clear + * this.evalWindow on page navigation. + * + * @private + * @type boolean + */ + _progressListenerActive: false, + + /** + * The ConsoleServiceListener instance. + * @type object + */ + consoleServiceListener: null, + + /** + * The ConsoleAPIListener instance. + */ + consoleAPIListener: null, + + /** + * The NetworkMonitor instance. + */ + networkMonitor: null, + + /** + * The NetworkMonitor instance living in the same (child) process. + */ + networkMonitorChild: null, + + /** + * The ConsoleProgressListener instance. + */ + consoleProgressListener: null, + + /** + * The ConsoleReflowListener instance. + */ + consoleReflowListener: null, + + /** + * The Web Console Commands names cache. + * @private + * @type array + */ + _webConsoleCommandsCache: null, + + actorPrefix: "console", + + get globalDebugObject() { + return this.parentActor.threadActor.globalDebugObject; + }, + + grip: function WCA_grip() + { + return { actor: this.actorID }; + }, + + hasNativeConsoleAPI: function WCA_hasNativeConsoleAPI(aWindow) { + let isNative = false; + try { + // We are very explicitly examining the "console" property of + // the non-Xrayed object here. + let console = aWindow.wrappedJSObject.console; + isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE + } + catch (ex) { } + return isNative; + }, + + _findProtoChain: ThreadActor.prototype._findProtoChain, + _removeFromProtoChain: ThreadActor.prototype._removeFromProtoChain, + + /** + * Destroy the current WebConsoleActor instance. + */ + disconnect: function WCA_disconnect() + { + if (this.consoleServiceListener) { + this.consoleServiceListener.destroy(); + this.consoleServiceListener = null; + } + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + if (this.networkMonitor) { + this.networkMonitor.destroy(); + this.networkMonitor = null; + } + if (this.networkMonitorChild) { + this.networkMonitorChild.destroy(); + this.networkMonitorChild = null; + } + if (this.stackTraceCollector) { + this.stackTraceCollector.destroy(); + this.stackTraceCollector = null; + } + if (this.consoleProgressListener) { + this.consoleProgressListener.destroy(); + this.consoleProgressListener = null; + } + if (this.consoleReflowListener) { + this.consoleReflowListener.destroy(); + this.consoleReflowListener = null; + } + if (this.serverLoggingListener) { + this.serverLoggingListener.destroy(); + this.serverLoggingListener = null; + } + + events.off(this.parentActor, "changed-toplevel-document", + this._onChangedToplevelDocument); + + this.conn.removeActorPool(this._actorPool); + + if (this.parentActor.isRootActor) { + Services.obs.removeObserver(this._onObserverNotification, + "last-pb-context-exited"); + } + + this._actorPool = null; + this._webConsoleCommandsCache = null; + this._lastConsoleInputEvaluation = null; + this._evalWindow = null; + this._netEvents.clear(); + this.dbg.enabled = false; + 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 aEnvironment + * The lexical environment we want to extract. + * @return The EnvironmentActor for aEnvironment or undefined for host + * functions or functions scoped to a non-debuggee global. + */ + createEnvironmentActor: function WCA_createEnvironmentActor(aEnvironment) { + if (!aEnvironment) { + return undefined; + } + + if (aEnvironment.actor) { + return aEnvironment.actor; + } + + let actor = new EnvironmentActor(aEnvironment, this); + this._actorPool.addActor(actor); + aEnvironment.actor = actor; + + return actor; + }, + + /** + * Create a grip for the given value. + * + * @param mixed aValue + * @return object + */ + createValueGrip: function WCA_createValueGrip(aValue) + { + return createValueGrip(aValue, this._actorPool, this.objectGrip); + }, + + /** + * Make a debuggee value for the given value. + * + * @param mixed aValue + * The value you want to get a debuggee value for. + * @param boolean aUseObjectGlobal + * If |true| the object global is determined and added as a debuggee, + * otherwise |this.window| is used when makeDebuggeeValue() is invoked. + * @return object + * Debuggee value for |aValue|. + */ + makeDebuggeeValue: function WCA_makeDebuggeeValue(aValue, aUseObjectGlobal) + { + if (aUseObjectGlobal && typeof aValue == "object") { + try { + let global = Cu.getGlobalForObject(aValue); + let dbgGlobal = this.dbg.makeGlobalObjectReference(global); + return dbgGlobal.makeDebuggeeValue(aValue); + } + catch (ex) { + // The above can throw an exception if aValue is not an actual object + // or 'Object in compartment marked as invisible to Debugger' + } + } + let dbgGlobal = this.dbg.makeGlobalObjectReference(this.window); + return dbgGlobal.makeDebuggeeValue(aValue); + }, + + /** + * Create a grip for the given object. + * + * @param object aObject + * The object you want. + * @param object aPool + * An ActorPool where the new actor instance is added. + * @param object + * The object grip. + */ + objectGrip: function WCA_objectGrip(aObject, aPool) + { + let actor = new ObjectActor(aObject, { + 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), + getGlobalDebugObject: () => this.globalDebugObject + }); + aPool.addActor(actor); + return actor.grip(); + }, + + /** + * Create a grip for the given string. + * + * @param string aString + * The string you want to create the grip for. + * @param object aPool + * An ActorPool where the new actor instance is added. + * @return object + * A LongStringActor object that wraps the given string. + */ + longStringGrip: function WCA_longStringGrip(aString, aPool) + { + let actor = new LongStringActor(aString); + aPool.addActor(actor); + return actor.grip(); + }, + + /** + * Create a long string grip if needed for the given string. + * + * @private + * @param string aString + * The string you want to create a long string grip for. + * @return string|object + * A string is returned if |aString| is not a long string. + * A LongStringActor grip is returned if |aString| is a long string. + */ + _createStringGrip: function NEA__createStringGrip(aString) + { + if (aString && stringIsLong(aString)) { + return this.longStringGrip(aString, this._actorPool); + } + return aString; + }, + + /** + * Get an object actor by its ID. + * + * @param string aActorID + * @return object + */ + getActorByID: function WCA_getActorByID(aActorID) + { + return this._actorPool.get(aActorID); + }, + + /** + * Release an actor. + * + * @param object aActor + * The actor instance you want to release. + */ + releaseActor: function WCA_releaseActor(aActor) + { + this._actorPool.removeActor(aActor.actorID); + }, + + /** + * Returns the latest web console input evaluation. + * This is undefined if no evaluations have been completed. + * + * @return object + */ + getLastConsoleInputEvaluation: function WCU_getLastConsoleInputEvaluation() + { + return this._lastConsoleInputEvaluation; + }, + + // Request handlers for known packet types. + + /** + * Handler for the "startListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response object which holds the startedListeners array. + */ + onStartListeners: function WCA_onStartListeners(aRequest) + { + // XXXworkers: Not handling the Console API yet for workers (Bug 1209353). + if (isWorker) { + aRequest.listeners = []; + } + + let startedListeners = []; + let window = !this.parentActor.isRootActor ? this.window : null; + let appId = null; + let messageManager = null; + + if (this._parentIsContentActor) { + appId = this.parentActor.docShell.appId; + messageManager = this.parentActor.messageManager; + } + + while (aRequest.listeners.length > 0) { + let listener = aRequest.listeners.shift(); + switch (listener) { + case "PageError": + if (!this.consoleServiceListener) { + this.consoleServiceListener = + new ConsoleServiceListener(window, this); + this.consoleServiceListener.init(); + } + startedListeners.push(listener); + break; + case "ConsoleAPI": + if (!this.consoleAPIListener) { + // Create the consoleAPIListener (and apply the filtering options defined + // in the parent actor). + this.consoleAPIListener = + new ConsoleAPIListener(window, this, + this.parentActor.consoleAPIListenerOptions); + this.consoleAPIListener.init(); + } + startedListeners.push(listener); + break; + case "NetworkActivity": + if (!this.networkMonitor) { + // Create a StackTraceCollector that's going to be shared both by the + // NetworkMonitorChild (getting messages about requests from parent) and + // by the NetworkMonitor that directly watches service workers requests. + this.stackTraceCollector = new StackTraceCollector({ window, appId }); + this.stackTraceCollector.init(); + + let processBoundary = Services.appinfo.processType != + Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + if ((appId || messageManager) && processBoundary) { + // Start a network monitor in the parent process to listen to + // most requests than happen in parent + this.networkMonitor = + new NetworkMonitorChild(appId, this.parentActor.outerWindowID, + messageManager, this.conn, this); + this.networkMonitor.init(); + // Spawn also one in the child to listen to service workers + this.networkMonitorChild = new NetworkMonitor({ window }, this); + this.networkMonitorChild.init(); + } else { + this.networkMonitor = new NetworkMonitor({ window }, this); + this.networkMonitor.init(); + } + } + startedListeners.push(listener); + break; + case "FileActivity": + if (this.window instanceof Ci.nsIDOMWindow) { + if (!this.consoleProgressListener) { + this.consoleProgressListener = + new ConsoleProgressListener(this.window, this); + } + this.consoleProgressListener.startMonitor(this.consoleProgressListener. + MONITOR_FILE_ACTIVITY); + startedListeners.push(listener); + } + break; + case "ReflowActivity": + if (!this.consoleReflowListener) { + this.consoleReflowListener = + new ConsoleReflowListener(this.window, this); + } + startedListeners.push(listener); + break; + case "ServerLogging": + if (!this.serverLoggingListener) { + this.serverLoggingListener = + new ServerLoggingListener(this.window, this); + } + startedListeners.push(listener); + break; + } + } + + // Update the live list of running listeners + startedListeners.forEach(this._listeners.add, this._listeners); + + return { + startedListeners: startedListeners, + nativeConsoleAPI: this.hasNativeConsoleAPI(this.window), + traits: this.traits, + }; + }, + + /** + * Handler for the "stopListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response packet to send to the client: holds the + * stoppedListeners array. + */ + onStopListeners: function WCA_onStopListeners(aRequest) + { + let stoppedListeners = []; + + // If no specific listeners are requested to be detached, we stop all + // listeners. + let toDetach = aRequest.listeners || + ["PageError", "ConsoleAPI", "NetworkActivity", + "FileActivity", "ServerLogging"]; + + while (toDetach.length > 0) { + let listener = toDetach.shift(); + switch (listener) { + case "PageError": + if (this.consoleServiceListener) { + this.consoleServiceListener.destroy(); + this.consoleServiceListener = null; + } + stoppedListeners.push(listener); + break; + case "ConsoleAPI": + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + stoppedListeners.push(listener); + break; + case "NetworkActivity": + if (this.networkMonitor) { + this.networkMonitor.destroy(); + this.networkMonitor = null; + } + if (this.networkMonitorChild) { + this.networkMonitorChild.destroy(); + this.networkMonitorChild = null; + } + if (this.stackTraceCollector) { + this.stackTraceCollector.destroy(); + this.stackTraceCollector = null; + } + stoppedListeners.push(listener); + break; + case "FileActivity": + if (this.consoleProgressListener) { + this.consoleProgressListener.stopMonitor(this.consoleProgressListener. + MONITOR_FILE_ACTIVITY); + this.consoleProgressListener = null; + } + stoppedListeners.push(listener); + break; + case "ReflowActivity": + if (this.consoleReflowListener) { + this.consoleReflowListener.destroy(); + this.consoleReflowListener = null; + } + stoppedListeners.push(listener); + break; + case "ServerLogging": + if (this.serverLoggingListener) { + this.serverLoggingListener.destroy(); + this.serverLoggingListener = null; + } + stoppedListeners.push(listener); + 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 object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response packet to send to the client: it holds the cached + * messages array. + */ + onGetCachedMessages: function WCA_onGetCachedMessages(aRequest) + { + let types = aRequest.messageTypes; + if (!types) { + return { + error: "missingParameter", + message: "The messageTypes parameter is missing.", + }; + } + + let messages = []; + + while (types.length > 0) { + let type = types.shift(); + switch (type) { + case "ConsoleAPI": { + if (!this.consoleAPIListener) { + break; + } + + // See `window` definition. It isn't always a DOM Window. + let requestStartTime = this.window && this.window.performance ? + this.window.performance.timing.requestStart : 0; + + let cache = this.consoleAPIListener + .getCachedMessages(!this.parentActor.isRootActor); + cache.forEach((aMessage) => { + // Filter out messages that came from a ServiceWorker but happened + // before the page was requested. + if (aMessage.innerID === "ServiceWorker" && + requestStartTime > aMessage.timeStamp) { + return; + } + + let message = this.prepareConsoleMessageForRemote(aMessage); + message._type = type; + messages.push(message); + }); + break; + } + case "PageError": { + if (!this.consoleServiceListener) { + break; + } + let cache = this.consoleServiceListener + .getCachedMessages(!this.parentActor.isRootActor); + cache.forEach((aMessage) => { + let message = null; + if (aMessage instanceof Ci.nsIScriptError) { + message = this.preparePageErrorForRemote(aMessage); + message._type = type; + } + else { + message = { + _type: "LogMessage", + message: this._createStringGrip(aMessage.message), + timeStamp: aMessage.timeStamp, + }; + } + messages.push(message); + }); + break; + } + } + } + + return { + from: this.actorID, + messages: messages, + }; + }, + + /** + * Handler for the "evaluateJSAsync" request. This method evaluates the given + * JavaScript string and sends back a packet with a unique ID. + * The result will be returned later as an unsolicited `evaluationResult`, + * that can be associated back to this request via the `resultID` field. + * + * @param object aRequest + * 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. + */ + onEvaluateJSAsync: function WCA_onEvaluateJSAsync(aRequest) + { + // We want to be able to run console commands without waiting + // for the first to return (see Bug 1088861). + + // First, send a response packet with the id only. + let resultID = Date.now(); + this.conn.send({ + from: this.actorID, + resultID: resultID + }); + + // Then, execute the script that may pause. + let response = this.onEvaluateJS(aRequest); + response.resultID = resultID; + + // Finally, send an unsolicited evaluationResult packet with + // the normal return value + this.conn.sendActorEvent(this.actorID, "evaluationResult", response); + }, + + /** + * Handler for the "evaluateJS" request. This method evaluates the given + * JavaScript string and sends back the result. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The evaluation response packet. + */ + onEvaluateJS: function WCA_onEvaluateJS(aRequest) + { + let input = aRequest.text; + let timestamp = Date.now(); + + let evalOptions = { + bindObjectActor: aRequest.bindObjectActor, + frameActor: aRequest.frameActor, + url: aRequest.url, + selectedNodeActor: aRequest.selectedNodeActor, + selectedObjectActor: aRequest.selectedObjectActor, + }; + + let evalInfo = this.evalWithDebugger(input, evalOptions); + let evalResult = evalInfo.result; + let helperResult = evalInfo.helperResult; + + let result, errorDocURL, errorMessage, errorGrip = null, frame = null; + if (evalResult) { + if ("return" in evalResult) { + result = evalResult.return; + } else if ("yield" in evalResult) { + result = evalResult.yield; + } else if ("throw" in evalResult) { + let error = evalResult.throw; + + errorGrip = this.createValueGrip(error); + + 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") { + errorMessage = error.unsafeDereference().toString(); + } + } + } + + // It is possible that we won't have permission to unwrap an + // object and retrieve its errorMessageName. + try { + errorDocURL = ErrorDocs.GetURL(error); + } catch (ex) {} + + try { + let line = error.errorLineNumber; + let 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) {} + } + } + + // If a value is encountered that the debugger server doesn't support yet, + // the console should remain functional. + let resultGrip; + try { + resultGrip = this.createValueGrip(result); + } catch (e) { + errorMessage = e; + } + + this._lastConsoleInputEvaluation = result; + + return { + from: this.actorID, + input: input, + result: resultGrip, + timestamp: timestamp, + exception: errorGrip, + exceptionMessage: this._createStringGrip(errorMessage), + exceptionDocURL: errorDocURL, + frame, + helperResult: helperResult, + }; + }, + + /** + * The Autocomplete request handler. + * + * @param object aRequest + * The request message - what input to autocomplete. + * @return object + * The response message - matched properties. + */ + onAutocomplete: function WCA_onAutocomplete(aRequest) + { + let frameActorId = aRequest.frameActor; + let dbgObject = null; + let environment = null; + let hadDebuggee = false; + + // This is the case of the paused debugger + if (frameActorId) { + let frameActor = this.conn.getActor(frameActorId); + try { + // Need to try/catch since accessing frame.environment + // can throw "Debugger.Frame is not live" + let frame = frameActor.frame; + environment = frame.environment; + } catch (e) { + DevToolsUtils.reportException("onAutocomplete", + Error("The frame actor was not found: " + frameActorId)); + } + } + // This is the general case (non-paused debugger) + else { + hadDebuggee = this.dbg.hasDebuggee(this.evalWindow); + dbgObject = this.dbg.addDebuggee(this.evalWindow); + } + + let result = JSPropertyProvider(dbgObject, environment, aRequest.text, + aRequest.cursor, frameActorId) || {}; + + if (!hadDebuggee && dbgObject) { + this.dbg.removeDebuggee(this.evalWindow); + } + + let matches = result.matches || []; + let reqText = aRequest.text.substr(0, aRequest.cursor); + + // We consider '$' as alphanumerc because it is used in the names of some + // helper functions. + let lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText); + if (!lastNonAlphaIsDot) { + if (!this._webConsoleCommandsCache) { + let helpers = { + sandbox: Object.create(null) + }; + addWebConsoleCommands(helpers); + this._webConsoleCommandsCache = + Object.getOwnPropertyNames(helpers.sandbox); + } + matches = matches.concat(this._webConsoleCommandsCache + .filter(n => n.startsWith(result.matchProp))); + } + + return { + from: this.actorID, + matches: matches.sort(), + matchProp: result.matchProp, + }; + }, + + /** + * The "clearMessagesCache" request handler. + */ + onClearMessagesCache: function WCA_onClearMessagesCache() + { + // TODO: Bug 717611 - Web Console clear button does not clear cached errors + let windowId = !this.parentActor.isRootActor ? + WebConsoleUtils.getInnerWindowId(this.window) : null; + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage); + ConsoleAPIStorage.clearEvents(windowId); + + CONSOLE_WORKER_IDS.forEach((aId) => { + ConsoleAPIStorage.clearEvents(aId); + }); + + if (this.parentActor.isRootActor) { + Services.console.logStringMessage(null); // for the Error Console + Services.console.reset(); + } + return {}; + }, + + /** + * The "getPreferences" request handler. + * + * @param object aRequest + * The request message - which preferences need to be retrieved. + * @return object + * The response message - a { key: value } object map. + */ + onGetPreferences: function WCA_onGetPreferences(aRequest) + { + let prefs = Object.create(null); + for (let key of aRequest.preferences) { + prefs[key] = this._prefs[key]; + } + return { preferences: prefs }; + }, + + /** + * The "setPreferences" request handler. + * + * @param object aRequest + * The request message - which preferences need to be updated. + */ + onSetPreferences: function WCA_onSetPreferences(aRequest) + { + for (let key in aRequest.preferences) { + this._prefs[key] = aRequest.preferences[key]; + + if (this.networkMonitor) { + if (key == "NetworkMonitor.saveRequestAndResponseBodies") { + this.networkMonitor.saveRequestAndResponseBodies = this._prefs[key]; + if (this.networkMonitorChild) { + this.networkMonitorChild.saveRequestAndResponseBodies = + this._prefs[key]; + } + } else if (key == "NetworkMonitor.throttleData") { + this.networkMonitor.throttleData = this._prefs[key]; + if (this.networkMonitorChild) { + this.networkMonitorChild.throttleData = this._prefs[key]; + } + } + } + } + return { updated: Object.keys(aRequest.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 aDebuggerGlobal + * 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 (aDebuggerGlobal) + { + let helpers = { + window: this.evalWindow, + chromeWindow: this.chromeWindow.bind(this), + makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal), + createValueGrip: this.createValueGrip.bind(this), + sandbox: Object.create(null), + helperResult: null, + consoleActor: this, + }; + addWebConsoleCommands(helpers); + + let evalWindow = this.evalWindow; + 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], evalWindow, { allowCrossOriginArguments: true }); + } + for (let name in helpers.sandbox) { + let 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 = aDebuggerGlobal.makeDebuggeeValue(desc.value); + } + Object.defineProperty(helpers.sandbox, name, desc); + } + return helpers; + }, + + /** + * Evaluates a string using the debugger API. + * + * To allow the variables view to update properties from the Web Console we + * provide the "bindObjectActor" mechanism: the Web Console tells the + * ObjectActor ID for which it desires to evaluate an expression. The + * Debugger.Object pointed at by the actor ID is bound such that it is + * available during expression evaluation (executeInGlobalWithBindings()). + * + * Example: + * _self['foobar'] = 'test' + * where |_self| refers to the desired object. + * + * The |frameActor| property allows the Web Console client to provide the + * frame actor ID, such that the expression can be evaluated in the + * user-selected stack frame. + * + * For the above to work we need the debugger and the Web Console to share + * a connection, otherwise the Web Console actor will not find the frame + * actor. + * + * The Debugger.Frame comes from the jsdebugger's Debugger instance, which + * is different from the Web Console's Debugger instance. This means that + * for evaluation to work, we need to create a new instance for the Web + * Console Commands helpers - they need to be Debugger.Objects coming from the + * jsdebugger's Debugger instance. + * + * When |bindObjectActor| is used objects can come from different iframes, + * from different domains. To avoid permission-related errors when objects + * come from a different window, we also determine the object's own global, + * such that evaluation happens in the context of that global. This means that + * evaluation will happen in the object's iframe, rather than the top level + * window. + * + * @param string aString + * String to evaluate. + * @param object [aOptions] + * Options for evaluation: + * - bindObjectActor: the ObjectActor ID to use for evaluation. + * |evalWithBindings()| will be called with one additional binding: + * |_self| which will point to the Debugger.Object of the given + * ObjectActor. + * - selectedObjectActor: Like bindObjectActor, but executes with the + * top level window as the global. + * - frameActor: the FrameActor ID to use for evaluation. The given + * debugger frame is used for evaluation, instead of the global window. + * - selectedNodeActor: the NodeActor ID of the currently selected node + * in the Inspector (or null, if there is no selection). This is used + * for helper functions that make reference to the currently selected + * node, like $0. + * - url: the url to evaluate the script as. Defaults to + * "debugger eval code". + * @return object + * An object that holds the following properties: + * - dbg: the debugger where the string was evaluated. + * - frame: (optional) the frame where the string was evaluated. + * - window: the Debugger.Object for the global where the string was + * evaluated. + * - result: the result of the evaluation. + * - helperResult: any result coming from a Web Console commands + * function. + */ + evalWithDebugger: function WCA_evalWithDebugger(aString, aOptions = {}) + { + let trimmedString = aString.trim(); + // The help function needs to be easy to guess, so we make the () optional. + if (trimmedString == "help" || trimmedString == "?") { + aString = "help()"; + } + + // Add easter egg for console.mihai(). + if (trimmedString == "console.mihai()" || trimmedString == "console.mihai();") { + aString = "\"http://incompleteness.me/blog/2015/02/09/console-dot-mihai/\""; + } + + // Find the Debugger.Frame of the given FrameActor. + let frame = null, frameActor = null; + if (aOptions.frameActor) { + frameActor = this.conn.getActor(aOptions.frameActor); + if (frameActor) { + frame = frameActor.frame; + } + else { + DevToolsUtils.reportException("evalWithDebugger", + Error("The frame actor was not found: " + aOptions.frameActor)); + } + } + + // If we've been given a frame actor in whose scope we should evaluate the + // expression, be sure to use that frame's Debugger (that is, the JavaScript + // debugger's Debugger) for the whole operation, not the console's Debugger. + // (One Debugger will treat a different Debugger's Debugger.Object instances + // as ordinary objects, not as references to be followed, so mixing + // debuggers causes strange behaviors.) + let dbg = frame ? frameActor.threadActor.dbg : this.dbg; + let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow); + + // If we have an object to bind to |_self|, create a Debugger.Object + // referring to that object, belonging to dbg. + let bindSelf = null; + if (aOptions.bindObjectActor || aOptions.selectedObjectActor) { + let objActor = this.getActorByID(aOptions.bindObjectActor || + aOptions.selectedObjectActor); + if (objActor) { + let jsObj = objActor.obj.unsafeDereference(); + // If we use the makeDebuggeeValue method of jsObj's own global, then + // we'll get a D.O that sees jsObj as viewed from its own compartment - + // that is, without wrappers. The evalWithBindings call will then wrap + // jsObj appropriately for the evaluation compartment. + let global = Cu.getGlobalForObject(jsObj); + let _dbgWindow = dbg.makeGlobalObjectReference(global); + bindSelf = dbgWindow.makeDebuggeeValue(jsObj); + + if (aOptions.bindObjectActor) { + dbgWindow = _dbgWindow; + } + } + } + + // Get the Web Console commands for the given debugger window. + let helpers = this._getWebConsoleCommands(dbgWindow); + let bindings = helpers.sandbox; + if (bindSelf) { + bindings._self = bindSelf; + } + + if (aOptions.selectedNodeActor) { + let actor = this.conn.getActor(aOptions.selectedNodeActor); + if (actor) { + helpers.selectedNode = actor.rawNode; + } + } + + // Check if the Debugger.Frame or Debugger.Object for the global include + // $ or $$. We will not overwrite these functions with the Web Console + // commands. + let found$ = false, found$$ = false; + if (frame) { + let env = frame.environment; + if (env) { + found$ = !!env.find("$"); + found$$ = !!env.find("$$"); + } + } + else { + found$ = !!dbgWindow.getOwnPropertyDescriptor("$"); + found$$ = !!dbgWindow.getOwnPropertyDescriptor("$$"); + } + + let $ = null, $$ = null; + if (found$) { + $ = bindings.$; + delete bindings.$; + } + if (found$$) { + $$ = bindings.$$; + delete bindings.$$; + } + + // Ready to evaluate the string. + helpers.evalInput = aString; + + let evalOptions; + if (typeof aOptions.url == "string") { + evalOptions = { url: aOptions.url }; + } + + // If the debugger object is changed from the last evaluation, + // adopt this._lastConsoleInputEvaluation value in the new debugger, + // to prevents "Debugger.Object belongs to a different Debugger" exceptions + // related to the $_ bindings. + if (this._lastConsoleInputEvaluation && + this._lastConsoleInputEvaluation.global !== dbgWindow) { + this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue( + this._lastConsoleInputEvaluation + ); + } + + let result; + + if (frame) { + result = frame.evalWithBindings(aString, bindings, evalOptions); + } + else { + result = dbgWindow.executeInGlobalWithBindings(aString, bindings, evalOptions); + // Attempt to initialize any declarations found in the evaluated string + // since they may now be stuck in an "initializing" state due to the + // error. Already-initialized bindings will be ignored. + if ("throw" in result) { + let ast; + // Parse errors will raise an exception. We can/should ignore the error + // since it's already being handled elsewhere and we are only interested + // in initializing bindings. + try { + ast = Parser.reflectionAPI.parse(aString); + } catch (ex) { + ast = {"body": []}; + } + for (let line of ast.body) { + // Only let and const declarations put bindings into an + // "initializing" state. + if (!(line.kind == "let" || line.kind == "const")) + continue; + + let identifiers = []; + for (let decl of line.declarations) { + switch (decl.id.type) { + case "Identifier": + // let foo = bar; + identifiers.push(decl.id.name); + break; + case "ArrayPattern": + // let [foo, bar] = [1, 2]; + // let [foo=99, bar] = [1, 2]; + for (let e of decl.id.elements) { + if (e.type == "Identifier") { + identifiers.push(e.name); + } else if (e.type == "AssignmentExpression") { + identifiers.push(e.left.name); + } + } + break; + case "ObjectPattern": + // let {bilbo, my} = {bilbo: "baggins", my: "precious"}; + // let {blah: foo} = {blah: yabba()} + // let {blah: foo=99} = {blah: yabba()} + for (let prop of decl.id.properties) { + // key + if (prop.key.type == "Identifier") + identifiers.push(prop.key.name); + // value + if (prop.value.type == "Identifier") { + identifiers.push(prop.value.name); + } else if (prop.value.type == "AssignmentExpression") { + identifiers.push(prop.value.left.name); + } + } + break; + } + } + + for (let name of identifiers) + dbgWindow.forceLexicalInitializationByName(name); + } + } + } + + let helperResult = helpers.helperResult; + delete helpers.evalInput; + delete helpers.helperResult; + delete helpers.selectedNode; + + if ($) { + bindings.$ = $; + } + if ($$) { + bindings.$$ = $$; + } + + if (bindings._self) { + delete bindings._self; + } + + return { + result: result, + helperResult: helperResult, + dbg: dbg, + frame: frame, + window: dbgWindow, + }; + }, + + // 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 aMessage + * The message we need to send to the client. + */ + onConsoleServiceMessage: function WCA_onConsoleServiceMessage(aMessage) + { + let packet; + if (aMessage instanceof Ci.nsIScriptError) { + packet = { + from: this.actorID, + type: "pageError", + pageError: this.preparePageErrorForRemote(aMessage), + }; + } + else { + packet = { + from: this.actorID, + type: "logMessage", + message: this._createStringGrip(aMessage.message), + timeStamp: aMessage.timeStamp, + }; + } + this.conn.send(packet); + }, + + /** + * Prepare an nsIScriptError to be sent to the client. + * + * @param nsIScriptError aPageError + * The page error we need to send to the client. + * @return object + * The object you can send to the remote client. + */ + preparePageErrorForRemote: function WCA_preparePageErrorForRemote(aPageError) + { + let stack = null; + // Convert stack objects to the JSON attributes expected by client code + if (aPageError.stack) { + stack = []; + let s = aPageError.stack; + while (s !== null) { + stack.push({ + filename: s.source, + lineNumber: s.line, + columnNumber: s.column, + functionName: s.functionDisplayName + }); + s = s.parent; + } + } + let lineText = aPageError.sourceLine; + if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) { + lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + } + + return { + errorMessage: this._createStringGrip(aPageError.errorMessage), + errorMessageName: aPageError.errorMessageName, + exceptionDocURL: ErrorDocs.GetURL(aPageError), + sourceName: aPageError.sourceName, + lineText: lineText, + lineNumber: aPageError.lineNumber, + columnNumber: aPageError.columnNumber, + category: aPageError.category, + timeStamp: aPageError.timeStamp, + warning: !!(aPageError.flags & aPageError.warningFlag), + error: !!(aPageError.flags & aPageError.errorFlag), + exception: !!(aPageError.flags & aPageError.exceptionFlag), + strict: !!(aPageError.flags & aPageError.strictFlag), + info: !!(aPageError.flags & aPageError.infoFlag), + private: aPageError.isFromPrivateWindow, + stacktrace: stack + }; + }, + + /** + * 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 aMessage + * The console API call we need to send to the remote client. + */ + onConsoleAPICall: function WCA_onConsoleAPICall(aMessage) + { + let packet = { + from: this.actorID, + type: "consoleAPICall", + message: this.prepareConsoleMessageForRemote(aMessage), + }; + this.conn.send(packet); + }, + + /** + * Handler for network events. This method is invoked when a new network event + * is about to be recorded. + * + * @see NetworkEventActor + * @see NetworkMonitor from webconsole/utils.js + * + * @param object aEvent + * The initial network request event information. + * @return object + * A new NetworkEventActor is returned. This is used for tracking the + * network request and response. + */ + onNetworkEvent: function WCA_onNetworkEvent(aEvent) + { + let actor = this.getNetworkEventActor(aEvent.channelId); + actor.init(aEvent); + + let packet = { + from: this.actorID, + type: "networkEvent", + eventActor: actor.grip() + }; + + this.conn.send(packet); + + return actor; + }, + + /** + * Get the NetworkEventActor for a nsIHttpChannel, if it exists, + * otherwise create a new one. + * + * @param string channelId + * The id of the channel for the network event. + * @return object + * The NetworkEventActor for the given channel. + */ + getNetworkEventActor: function WCA_getNetworkEventActor(channelId) { + let actor = this._netEvents.get(channelId); + if (actor) { + // delete from map as we should only need to do this check once + this._netEvents.delete(channelId); + return actor; + } + + actor = new NetworkEventActor(this); + this._actorPool.addActor(actor); + return actor; + }, + + /** + * Send a new HTTP request from the target's window. + * + * @param object message + * Object with 'request' - the HTTP request details. + */ + onSendHTTPRequest(message) { + let { url, method, headers, body } = message.request; + + // Set the loadingNode and loadGroup to the target document - otherwise the + // request won't show up in the opened netmonitor. + let doc = this.window.document; + + let channel = NetUtil.newChannel({ + uri: NetUtil.newURI(url), + loadingNode: doc, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + contentPolicyType: 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; + + for (let {name, value} of headers) { + channel.setRequestHeader(name, value, false); + } + + if (body) { + channel.QueryInterface(Ci.nsIUploadChannel2); + let 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, () => {}); + + let actor = this.getNetworkEventActor(channel.channelId); + + // map channel to actor so we can associate future events with it + this._netEvents.set(channel.channelId, actor); + + return { + from: this.actorID, + eventActor: actor.grip() + }; + }, + + /** + * Handler for file activity. This method sends the file request information + * to the remote Web Console client. + * + * @see ConsoleProgressListener + * @param string aFileURI + * The requested file URI. + */ + onFileActivity: function WCA_onFileActivity(aFileURI) + { + let packet = { + from: this.actorID, + type: "fileActivity", + uri: aFileURI, + }; + this.conn.send(packet); + }, + + /** + * Handler for reflow activity. This method forwards reflow events to the + * remote Web Console client. + * + * @see ConsoleReflowListener + * @param Object aReflowInfo + */ + onReflowActivity: function WCA_onReflowActivity(aReflowInfo) + { + let packet = { + from: this.actorID, + type: "reflowActivity", + interruptible: aReflowInfo.interruptible, + start: aReflowInfo.start, + end: aReflowInfo.end, + sourceURL: aReflowInfo.sourceURL, + sourceLine: aReflowInfo.sourceLine, + functionName: aReflowInfo.functionName + }; + + this.conn.send(packet); + }, + + /** + * Handler for server logging. This method forwards log events to the + * remote Web Console client. + * + * @see ServerLoggingListener + * @param object aMessage + * The console API call on the server we need to send to the remote client. + */ + onServerLogCall: function WCA_onServerLogCall(aMessage) + { + // Clone all data into the content scope (that's where + // passed arguments comes from). + let msg = Cu.cloneInto(aMessage, this.window); + + // All arguments within the message need to be converted into + // debuggees to properly send it to the client side. + // Use the default target: this.window as the global object + // since that's the correct scope for data in the message. + // The 'false' argument passed into prepareConsoleMessageForRemote() + // ensures that makeDebuggeeValue uses content debuggee. + // See also: + // * makeDebuggeeValue() + // * prepareConsoleMessageForRemote() + msg = this.prepareConsoleMessageForRemote(msg, false); + + let packet = { + from: this.actorID, + type: "serverLogCall", + message: msg, + }; + + this.conn.send(packet); + }, + + // 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 aMessage + * 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.window| is used when makeDebuggeeValue() is invoked. + * @return object + * The object that can be sent to the remote client. + */ + prepareConsoleMessageForRemote: + function WCA_prepareConsoleMessageForRemote(aMessage, aUseObjectGlobal = true) + { + let result = WebConsoleUtils.cloneObject(aMessage); + + result.workerType = WebConsoleUtils.getWorkerType(result) || "none"; + + delete result.wrappedJSObject; + delete result.ID; + delete result.innerID; + delete result.consoleID; + + result.arguments = Array.map(aMessage.arguments || [], (aObj) => { + let dbgObj = this.makeDebuggeeValue(aObj, aUseObjectGlobal); + return this.createValueGrip(dbgObj); + }); + + result.styles = Array.map(aMessage.styles || [], (aString) => { + return this.createValueGrip(aString); + }); + + result.category = aMessage.category || "webdev"; + + return result; + }, + + /** + * Find the XUL window that owns the content window. + * + * @return Window + * The XUL window that owns the content window. + */ + chromeWindow: function WCA_chromeWindow() + { + let window = null; + try { + window = this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + } + catch (ex) { + // The above can fail because chromeEventHandler is not available for all + // kinds of |this.window|. + } + + return window; + }, + + /** + * Notification observer for the "last-pb-context-exited" topic. + * + * @private + * @param object aSubject + * Notification subject - in this case it is the inner window ID that + * was destroyed. + * @param string aTopic + * Notification topic. + */ + _onObserverNotification: function WCA__onObserverNotification(aSubject, aTopic) + { + switch (aTopic) { + case "last-pb-context-exited": + this.conn.send({ + from: this.actorID, + type: "lastPrivateContextExited", + }); + break; + } + }, + + /** + * The "will-navigate" progress listener. This is used to clear the current + * eval scope. + */ + _onWillNavigate: function WCA__onWillNavigate({ window, isTopLevel }) + { + if (isTopLevel) { + this._evalWindow = null; + events.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 WCA__onChangedToplevelDocument() + { + // Convert the Set to an Array + let listeners = [...this._listeners]; + + // Unregister existing listener on the previous document + // (pass a copy of the array as it will shift from it) + this.onStopListeners({listeners: listeners.slice()}); + + // This method is called after this.window is changed, + // so we register new listener on this new window + this.onStartListeners({listeners: listeners}); + + // Also reset the cached top level chrome window being targeted + this._lastChromeWindow = null; + }, +}; + +WebConsoleActor.prototype.requestTypes = +{ + startListeners: WebConsoleActor.prototype.onStartListeners, + stopListeners: WebConsoleActor.prototype.onStopListeners, + getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages, + evaluateJS: WebConsoleActor.prototype.onEvaluateJS, + evaluateJSAsync: WebConsoleActor.prototype.onEvaluateJSAsync, + autocomplete: WebConsoleActor.prototype.onAutocomplete, + clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache, + getPreferences: WebConsoleActor.prototype.onGetPreferences, + setPreferences: WebConsoleActor.prototype.onSetPreferences, + sendHTTPRequest: WebConsoleActor.prototype.onSendHTTPRequest +}; + +exports.WebConsoleActor = WebConsoleActor; + +/** + * Creates an actor for a network event. + * + * @constructor + * @param object webConsoleActor + * The parent WebConsoleActor instance for this object. + */ +function NetworkEventActor(webConsoleActor) { + this.parent = webConsoleActor; + this.conn = this.parent.conn; + + this._request = { + method: null, + url: null, + httpVersion: null, + headers: [], + cookies: [], + headersSize: null, + postData: {}, + }; + + this._response = { + headers: [], + cookies: [], + content: {}, + }; + + this._timings = {}; + + // Keep track of LongStringActors owned by this NetworkEventActor. + this._longStringActors = new Set(); +} + +NetworkEventActor.prototype = +{ + _request: null, + _response: null, + _timings: null, + _longStringActors: null, + + actorPrefix: "netEvent", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function NEA_grip() + { + return { + actor: this.actorID, + startedDateTime: this._startedDateTime, + timeStamp: Date.parse(this._startedDateTime), + url: this._request.url, + method: this._request.method, + isXHR: this._isXHR, + cause: this._cause, + fromCache: this._fromCache, + fromServiceWorker: this._fromServiceWorker, + private: this._private, + }; + }, + + /** + * Releases this actor from the pool. + */ + release: function NEA_release() + { + for (let grip of this._longStringActors) { + let actor = this.parent.getActorByID(grip.actor); + if (actor) { + this.parent.releaseActor(actor); + } + } + this._longStringActors = new Set(); + + if (this.channel) { + this.parent._netEvents.delete(this.channel); + } + this.parent.releaseActor(this); + }, + + /** + * Handle a protocol request to release a grip. + */ + onRelease: function NEA_onRelease() + { + this.release(); + return {}; + }, + + /** + * Set the properties of this actor based on it's corresponding + * network event. + * + * @param object aNetworkEvent + * The network event associated with this actor. + */ + init: function NEA_init(aNetworkEvent) + { + this._startedDateTime = aNetworkEvent.startedDateTime; + this._isXHR = aNetworkEvent.isXHR; + this._cause = aNetworkEvent.cause; + this._fromCache = aNetworkEvent.fromCache; + this._fromServiceWorker = aNetworkEvent.fromServiceWorker; + + for (let prop of ["method", "url", "httpVersion", "headersSize"]) { + this._request[prop] = aNetworkEvent[prop]; + } + + this._discardRequestBody = aNetworkEvent.discardRequestBody; + this._discardResponseBody = aNetworkEvent.discardResponseBody; + this._private = aNetworkEvent.private; + }, + + /** + * The "getRequestHeaders" packet type handler. + * + * @return object + * The response packet - network request headers. + */ + onGetRequestHeaders: function NEA_onGetRequestHeaders() + { + return { + from: this.actorID, + headers: this._request.headers, + headersSize: this._request.headersSize, + rawHeaders: this._request.rawHeaders, + }; + }, + + /** + * The "getRequestCookies" packet type handler. + * + * @return object + * The response packet - network request cookies. + */ + onGetRequestCookies: function NEA_onGetRequestCookies() + { + return { + from: this.actorID, + cookies: this._request.cookies, + }; + }, + + /** + * The "getRequestPostData" packet type handler. + * + * @return object + * The response packet - network POST data. + */ + onGetRequestPostData: function NEA_onGetRequestPostData() + { + return { + from: this.actorID, + postData: this._request.postData, + postDataDiscarded: this._discardRequestBody, + }; + }, + + /** + * The "getSecurityInfo" packet type handler. + * + * @return object + * The response packet - connection security information. + */ + onGetSecurityInfo: function NEA_onGetSecurityInfo() + { + return { + from: this.actorID, + securityInfo: this._securityInfo, + }; + }, + + /** + * The "getResponseHeaders" packet type handler. + * + * @return object + * The response packet - network response headers. + */ + onGetResponseHeaders: function NEA_onGetResponseHeaders() + { + return { + from: this.actorID, + headers: this._response.headers, + headersSize: this._response.headersSize, + rawHeaders: this._response.rawHeaders, + }; + }, + + /** + * The "getResponseCookies" packet type handler. + * + * @return object + * The response packet - network response cookies. + */ + onGetResponseCookies: function NEA_onGetResponseCookies() + { + return { + from: this.actorID, + cookies: this._response.cookies, + }; + }, + + /** + * The "getResponseContent" packet type handler. + * + * @return object + * The response packet - network response content. + */ + onGetResponseContent: function NEA_onGetResponseContent() + { + return { + from: this.actorID, + content: this._response.content, + contentDiscarded: this._discardResponseBody, + }; + }, + + /** + * The "getEventTimings" packet type handler. + * + * @return object + * The response packet - network event timings. + */ + onGetEventTimings: function NEA_onGetEventTimings() + { + return { + from: this.actorID, + timings: this._timings, + totalTime: this._totalTime + }; + }, + + /** **************************************************************** + * Listeners for new network event data coming from NetworkMonitor. + ******************************************************************/ + + /** + * Add network request headers. + * + * @param array aHeaders + * The request headers array. + * @param string aRawHeaders + * The raw headers source. + */ + addRequestHeaders: function NEA_addRequestHeaders(aHeaders, aRawHeaders) + { + this._request.headers = aHeaders; + this._prepareHeaders(aHeaders); + + var rawHeaders = this.parent._createStringGrip(aRawHeaders); + if (typeof rawHeaders == "object") { + this._longStringActors.add(rawHeaders); + } + this._request.rawHeaders = rawHeaders; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestHeaders", + headers: aHeaders.length, + headersSize: this._request.headersSize, + }; + + this.conn.send(packet); + }, + + /** + * Add network request cookies. + * + * @param array aCookies + * The request cookies array. + */ + addRequestCookies: function NEA_addRequestCookies(aCookies) + { + this._request.cookies = aCookies; + this._prepareHeaders(aCookies); + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestCookies", + cookies: aCookies.length, + }; + + this.conn.send(packet); + }, + + /** + * Add network request POST data. + * + * @param object aPostData + * The request POST data. + */ + addRequestPostData: function NEA_addRequestPostData(aPostData) + { + this._request.postData = aPostData; + aPostData.text = this.parent._createStringGrip(aPostData.text); + if (typeof aPostData.text == "object") { + this._longStringActors.add(aPostData.text); + } + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestPostData", + dataSize: aPostData.text.length, + discardRequestBody: this._discardRequestBody, + }; + + this.conn.send(packet); + }, + + /** + * Add the initial network response information. + * + * @param object aInfo + * The response information. + * @param string aRawHeaders + * The raw headers source. + */ + addResponseStart: function NEA_addResponseStart(aInfo, aRawHeaders) + { + var rawHeaders = this.parent._createStringGrip(aRawHeaders); + if (typeof rawHeaders == "object") { + this._longStringActors.add(rawHeaders); + } + this._response.rawHeaders = rawHeaders; + + this._response.httpVersion = aInfo.httpVersion; + this._response.status = aInfo.status; + this._response.statusText = aInfo.statusText; + this._response.headersSize = aInfo.headersSize; + this._discardResponseBody = aInfo.discardResponseBody; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseStart", + response: aInfo + }; + + this.conn.send(packet); + }, + + /** + * Add connection security information. + * + * @param object info + * The object containing security information. + */ + addSecurityInfo: function NEA_addSecurityInfo(info) + { + this._securityInfo = info; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "securityInfo", + state: info.state, + }; + + this.conn.send(packet); + }, + + /** + * Add network response headers. + * + * @param array aHeaders + * The response headers array. + */ + addResponseHeaders: function NEA_addResponseHeaders(aHeaders) + { + this._response.headers = aHeaders; + this._prepareHeaders(aHeaders); + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseHeaders", + headers: aHeaders.length, + headersSize: this._response.headersSize, + }; + + this.conn.send(packet); + }, + + /** + * Add network response cookies. + * + * @param array aCookies + * The response cookies array. + */ + addResponseCookies: function NEA_addResponseCookies(aCookies) + { + this._response.cookies = aCookies; + this._prepareHeaders(aCookies); + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseCookies", + cookies: aCookies.length, + }; + + this.conn.send(packet); + }, + + /** + * Add network response content. + * + * @param object aContent + * The response content. + * @param boolean aDiscardedResponseBody + * Tells if the response content was recorded or not. + */ + addResponseContent: + function NEA_addResponseContent(aContent, aDiscardedResponseBody) + { + this._response.content = aContent; + aContent.text = this.parent._createStringGrip(aContent.text); + if (typeof aContent.text == "object") { + this._longStringActors.add(aContent.text); + } + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseContent", + mimeType: aContent.mimeType, + contentSize: aContent.size, + encoding: aContent.encoding, + transferredSize: aContent.transferredSize, + discardResponseBody: aDiscardedResponseBody, + }; + + this.conn.send(packet); + }, + + /** + * Add network event timing information. + * + * @param number aTotal + * The total time of the network event. + * @param object aTimings + * Timing details about the network event. + */ + addEventTimings: function NEA_addEventTimings(aTotal, aTimings) + { + this._totalTime = aTotal; + this._timings = aTimings; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "eventTimings", + totalTime: aTotal + }; + + this.conn.send(packet); + }, + + /** + * Prepare the headers array to be sent to the client by using the + * LongStringActor for the header values, when needed. + * + * @private + * @param array aHeaders + */ + _prepareHeaders: function NEA__prepareHeaders(aHeaders) + { + for (let header of aHeaders) { + header.value = this.parent._createStringGrip(header.value); + if (typeof header.value == "object") { + this._longStringActors.add(header.value); + } + } + }, +}; + +NetworkEventActor.prototype.requestTypes = +{ + "release": NetworkEventActor.prototype.onRelease, + "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders, + "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies, + "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData, + "getResponseHeaders": NetworkEventActor.prototype.onGetResponseHeaders, + "getResponseCookies": NetworkEventActor.prototype.onGetResponseCookies, + "getResponseContent": NetworkEventActor.prototype.onGetResponseContent, + "getEventTimings": NetworkEventActor.prototype.onGetEventTimings, + "getSecurityInfo": NetworkEventActor.prototype.onGetSecurityInfo, +}; |