gecko/browser/devtools/webconsole/HUDService-content.js

2539 lines
77 KiB
JavaScript

/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* vim: set ft=javascript 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";
// This code is appended to the browser content script.
(function _HUDServiceContent() {
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let tempScope = {};
Cu.import("resource://gre/modules/XPCOMUtils.jsm", tempScope);
Cu.import("resource://gre/modules/Services.jsm", tempScope);
Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", tempScope);
Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope);
Cu.import("resource:///modules/NetworkHelper.jsm", tempScope);
Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
let XPCOMUtils = tempScope.XPCOMUtils;
let Services = tempScope.Services;
let gConsoleStorage = tempScope.ConsoleAPIStorage;
let WebConsoleUtils = tempScope.WebConsoleUtils;
let l10n = WebConsoleUtils.l10n;
let JSPropertyProvider = tempScope.JSPropertyProvider;
let NetworkHelper = tempScope.NetworkHelper;
let NetUtil = tempScope.NetUtil;
tempScope = null;
let activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
let _alive = true; // Track if this content script should still be alive.
/**
* The Web Console content instance manager.
*/
let Manager = {
get window() content,
hudId: null,
_sequence: 0,
_messageListeners: ["WebConsole:Init", "WebConsole:EnableFeature",
"WebConsole:DisableFeature", "WebConsole:SetPreferences",
"WebConsole:GetPreferences", "WebConsole:Destroy"],
_messageHandlers: null,
_enabledFeatures: null,
_prefs: { },
/**
* Getter for a unique ID for the current Web Console content instance.
*/
get sequenceId() "HUDContent-" + (++this._sequence),
/**
* Initialize the Web Console manager.
*/
init: function Manager_init()
{
this._enabledFeatures = [];
this._messageHandlers = {};
this._messageListeners.forEach(function(aName) {
addMessageListener(aName, this);
}, this);
// Need to track the owner XUL window to listen to the unload and TabClose
// events, to avoid memory leaks.
let xulWindow = this._xulWindow();
xulWindow.addEventListener("unload", this._onXULWindowClose, false);
let tabContainer = xulWindow.gBrowser.tabContainer;
tabContainer.addEventListener("TabClose", this._onTabClose, false);
// Need to track private browsing change and quit application notifications,
// again to avoid memory leaks. The Web Console main process cannot notify
// this content script when the XUL window close, tab close, private
// browsing change and quit application events happen, so we must call
// Manager.destroy() on our own.
Services.obs.addObserver(this, "private-browsing-change-granted", false);
Services.obs.addObserver(this, "quit-application-granted", false);
},
/**
* The message handler. This method forwards all the remote messages to the
* appropriate code.
*/
receiveMessage: function Manager_receiveMessage(aMessage)
{
if (!_alive || !aMessage.json) {
return;
}
if (aMessage.name == "WebConsole:Init" && !this.hudId) {
this._onInit(aMessage.json);
return;
}
if (aMessage.json.hudId != this.hudId) {
return;
}
switch (aMessage.name) {
case "WebConsole:EnableFeature":
this.enableFeature(aMessage.json.feature, aMessage.json);
break;
case "WebConsole:DisableFeature":
this.disableFeature(aMessage.json.feature);
break;
case "WebConsole:GetPreferences":
this.handleGetPreferences(aMessage.json);
break;
case "WebConsole:SetPreferences":
this.handleSetPreferences(aMessage.json);
break;
case "WebConsole:Destroy":
this.destroy();
break;
default: {
let handler = this._messageHandlers[aMessage.name];
handler && handler(aMessage.json);
break;
}
}
},
/**
* Observe notifications from the nsIObserverService.
*
* @param mixed aSubject
* @param string aTopic
* @param mixed aData
*/
observe: function Manager_observe(aSubject, aTopic, aData)
{
if (_alive && (aTopic == "quit-application-granted" ||
(aTopic == "private-browsing-change-granted" &&
(aData == "enter" || aData == "exit")))) {
this.destroy();
}
},
/**
* The manager initialization code. This method is called when the Web Console
* remote process initializes the content process (this code!).
*
* @param object aMessage
* The object received from the remote process. The WebConsole:Init
* message properties:
* - hudId - (required) the remote Web Console instance ID.
* - features - (optional) array of features you want to enable from
* the start. For each feature you enable you can pass feature-specific
* options in a property on the JSON object you send with the same name
* as the feature. See this.enableFeature() for the list of available
* features.
* - preferences - (optional) an object of preferences you want to set.
* Use keys for preference names and values for preference values.
* - cachedMessages - (optional) an array of cached messages you want
* to receive. See this._sendCachedMessages() for the list of available
* message types.
*
* Example message:
* {
* hudId: "foo1",
* features: ["JSTerm", "ConsoleAPI"],
* ConsoleAPI: { ... }, // ConsoleAPI-specific options
* cachedMessages: ["ConsoleAPI"],
* preferences: {"foo.bar": true},
* }
*/
_onInit: function Manager_onInit(aMessage)
{
this.hudId = aMessage.hudId;
if (aMessage.preferences) {
this.handleSetPreferences({ preferences: aMessage.preferences });
}
if (aMessage.features) {
aMessage.features.forEach(function(aFeature) {
this.enableFeature(aFeature, aMessage[aFeature]);
}, this);
}
if (aMessage.cachedMessages) {
this._sendCachedMessages(aMessage.cachedMessages);
}
},
/**
* Add a remote message handler. This is used by other components of the Web
* Console content script.
*
* @param string aName
* Message name to listen for.
* @param function aCallback
* Function to execute when the message is received. This function is
* given the JSON object that came from the remote Web Console
* instance.
* Only one callback per message name is allowed!
*/
addMessageHandler: function Manager_addMessageHandler(aName, aCallback)
{
if (aName in this._messageHandlers) {
Cu.reportError("Web Console content script: addMessageHandler() called for an existing message handler: " + aName);
return;
}
this._messageHandlers[aName] = aCallback;
addMessageListener(aName, this);
},
/**
* Remove the message handler for the given name.
*
* @param string aName
* Message name for the handler you want removed.
*/
removeMessageHandler: function Manager_removeMessageHandler(aName)
{
if (!(aName in this._messageHandlers)) {
return;
}
delete this._messageHandlers[aName];
removeMessageListener(aName, this);
},
/**
* Send a message to the remote Web Console instance.
*
* @param string aName
* The name of the message you want to send.
* @param object aMessage
* The message object you want to send.
*/
sendMessage: function Manager_sendMessage(aName, aMessage)
{
aMessage.hudId = this.hudId;
if (!("id" in aMessage)) {
aMessage.id = this.sequenceId;
}
sendAsyncMessage(aName, aMessage);
},
/**
* Enable a feature in the Web Console content script. A feature is generally
* a set of observers/listeners that are added in the content process. This
* content script exposes the data via the message manager for the features
* you enable.
*
* Supported features:
* - JSTerm - a JavaScript "terminal" which allows code execution.
* - ConsoleAPI - support for routing the window.console API to the remote
* process.
* - PageError - route all the nsIScriptErrors from the nsIConsoleService
* to the remote process.
* - NetworkMonitor - log all the network activity and send HAR-like
* messages to the remote Web Console process.
* - LocationChange - log page location changes. See
* ConsoleProgressListener.
*
* @param string aFeature
* One of the supported features.
* @param object [aMessage]
* Optional JSON message object coming from the remote Web Console
* instance. This can be used for feature-specific options.
*/
enableFeature: function Manager_enableFeature(aFeature, aMessage)
{
if (this._enabledFeatures.indexOf(aFeature) != -1) {
return;
}
switch (aFeature) {
case "JSTerm":
JSTerm.init(aMessage);
break;
case "ConsoleAPI":
ConsoleAPIObserver.init(aMessage);
break;
case "PageError":
ConsoleListener.init(aMessage);
break;
case "NetworkMonitor":
NetworkMonitor.init(aMessage);
break;
case "LocationChange":
ConsoleProgressListener.startMonitor(ConsoleProgressListener
.MONITOR_LOCATION_CHANGE);
ConsoleProgressListener.sendLocation(this.window.location.href,
this.window.document.title);
break;
default:
Cu.reportError("Web Console content: unknown feature " + aFeature);
break;
}
this._enabledFeatures.push(aFeature);
},
/**
* Disable a Web Console content script feature.
*
* @see this.enableFeature
* @param string aFeature
* One of the supported features - see this.enableFeature() for the
* list of supported features.
*/
disableFeature: function Manager_disableFeature(aFeature)
{
let index = this._enabledFeatures.indexOf(aFeature);
if (index == -1) {
return;
}
this._enabledFeatures.splice(index, 1);
switch (aFeature) {
case "JSTerm":
JSTerm.destroy();
break;
case "ConsoleAPI":
ConsoleAPIObserver.destroy();
break;
case "PageError":
ConsoleListener.destroy();
break;
case "NetworkMonitor":
NetworkMonitor.destroy();
break;
case "LocationChange":
ConsoleProgressListener.stopMonitor(ConsoleProgressListener
.MONITOR_LOCATION_CHANGE);
break;
default:
Cu.reportError("Web Console content: unknown feature " + aFeature);
break;
}
},
/**
* Handle the "WebConsole:GetPreferences" messages from the remote Web Console
* instance.
*
* @param object aMessage
* The JSON object of the remote message. This object holds one
* property: preferences. The |preferences| value must be an array of
* preference names you want to retrieve the values for.
* A "WebConsole:Preferences" message is sent back to the remote Web
* Console instance. The message holds a |preferences| object which has
* key names for preference names and values for each preference value.
*/
handleGetPreferences: function Manager_handleGetPreferences(aMessage)
{
let prefs = {};
aMessage.preferences.forEach(function(aName) {
prefs[aName] = this.getPreference(aName);
}, this);
this.sendMessage("WebConsole:Preferences", {preferences: prefs});
},
/**
* Handle the "WebConsole:SetPreferences" messages from the remote Web Console
* instance.
*
* @param object aMessage
* The JSON object of the remote message. This object holds one
* property: preferences. The |preferences| value must be an object of
* preference names as keys and preference values as object values, for
* each preference you want to change.
*/
handleSetPreferences: function Manager_handleSetPreferences(aMessage)
{
for (let key in aMessage.preferences) {
this.setPreference(key, aMessage.preferences[key]);
}
},
/**
* Retrieve a preference.
*
* @param string aName
* Preference name.
* @return mixed|null
* Preference value. Null is returned if the preference does not
* exist.
*/
getPreference: function Manager_getPreference(aName)
{
return aName in this._prefs ? this._prefs[aName] : null;
},
/**
* Set a preference to a new value.
*
* @param string aName
* Preference name.
* @param mixed aValue
* Preference value.
*/
setPreference: function Manager_setPreference(aName, aValue)
{
this._prefs[aName] = aValue;
},
/**
* Send the cached messages to the remote Web Console instance.
*
* @private
* @param array aMessageTypes
* An array that lists which kinds of messages you want. Supported
* message types: "ConsoleAPI" and "PageError".
*/
_sendCachedMessages: function Manager__sendCachedMessages(aMessageTypes)
{
let messages = [];
while (aMessageTypes.length > 0) {
switch (aMessageTypes.shift()) {
case "ConsoleAPI":
messages.push.apply(messages, ConsoleAPIObserver.getCachedMessages());
break;
case "PageError":
messages.push.apply(messages, ConsoleListener.getCachedMessages());
break;
}
}
messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
this.sendMessage("WebConsole:CachedMessages", {messages: messages});
},
/**
* The XUL window "unload" event handler which destroys this content script
* instance.
* @private
*/
_onXULWindowClose: function Manager__onXULWindowClose()
{
if (_alive) {
Manager.destroy();
}
},
/**
* The "TabClose" event handler which destroys this content script
* instance, if needed.
* @private
*/
_onTabClose: function Manager__onTabClose(aEvent)
{
let tab = aEvent.target;
if (_alive && tab.linkedBrowser.contentWindow === Manager.window) {
Manager.destroy();
}
},
/**
* Find the XUL window that owns the content script.
* @private
* @return Window
* The XUL window that owns the content script.
*/
_xulWindow: function Manager__xulWindow()
{
return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell)
.chromeEventHandler.ownerDocument.defaultView;
},
/**
* Destroy the Web Console content script instance.
*/
destroy: function Manager_destroy()
{
Services.obs.removeObserver(this, "private-browsing-change-granted");
Services.obs.removeObserver(this, "quit-application-granted");
_alive = false;
let xulWindow = this._xulWindow();
xulWindow.removeEventListener("unload", this._onXULWindowClose, false);
let tabContainer = xulWindow.gBrowser.tabContainer;
tabContainer.removeEventListener("TabClose", this._onTabClose, false);
this._messageListeners.forEach(function(aName) {
removeMessageListener(aName, this);
}, this);
this._enabledFeatures.slice().forEach(this.disableFeature, this);
this.hudId = null;
this._messageHandlers = null;
Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = NetworkMonitor =
NetworkResponseListener = ConsoleProgressListener = null;
XPCOMUtils = gConsoleStorage = WebConsoleUtils = l10n = JSPropertyProvider =
null;
},
};
///////////////////////////////////////////////////////////////////////////////
// JavaScript Terminal
///////////////////////////////////////////////////////////////////////////////
/**
* JSTerm helper functions.
*
* Defines a set of functions ("helper functions") that are available from the
* Web Console but not from the web page.
*
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*/
function JSTermHelper(aJSTerm)
{
/**
* Find a node by ID.
*
* @param string aId
* The ID of the element you want.
* @return nsIDOMNode or null
* The result of calling document.querySelector(aSelector).
*/
aJSTerm.sandbox.$ = function JSTH_$(aSelector)
{
return aJSTerm.window.document.querySelector(aSelector);
};
/**
* Find the nodes matching a CSS selector.
*
* @param string aSelector
* A string that is passed to window.document.querySelectorAll.
* @return nsIDOMNodeList
* Returns the result of document.querySelectorAll(aSelector).
*/
aJSTerm.sandbox.$$ = function JSTH_$$(aSelector)
{
return aJSTerm.window.document.querySelectorAll(aSelector);
};
/**
* Runs an xPath query and returns all matched nodes.
*
* @param string aXPath
* xPath search query to execute.
* @param [optional] nsIDOMNode aContext
* Context to run the xPath query on. Uses window.document if not set.
* @returns array of nsIDOMNode
*/
aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext)
{
let nodes = [];
let doc = aJSTerm.window.document;
let aContext = aContext || doc;
try {
let results = doc.evaluate(aXPath, aContext, null,
Ci.nsIDOMXPathResult.ANY_TYPE, null);
let node;
while (node = results.iterateNext()) {
nodes.push(node);
}
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
return nodes;
};
/**
* Returns the currently selected object in the highlighter.
*
* Warning: this implementation crosses the process boundaries! This is not
* usable within a remote browser. To implement this feature correctly we need
* support for remote inspection capabilities within the Inspector as well.
*
* @return nsIDOMElement|null
* The DOM element currently selected in the highlighter.
*/
Object.defineProperty(aJSTerm.sandbox, "$0", {
get: function() {
try {
return Manager._xulWindow().InspectorUI.selection;
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
},
enumerable: true,
configurable: false
});
/**
* Clears the output of the JSTerm.
*/
aJSTerm.sandbox.clear = function JSTH_clear()
{
aJSTerm.helperEvaluated = true;
Manager.sendMessage("JSTerm:ClearOutput", {});
};
/**
* Returns the result of Object.keys(aObject).
*
* @param object aObject
* Object to return the property names from.
* @returns array of string
*/
aJSTerm.sandbox.keys = function JSTH_keys(aObject)
{
return Object.keys(WebConsoleUtils.unwrap(aObject));
};
/**
* Returns the values of all properties on aObject.
*
* @param object aObject
* Object to display the values from.
* @returns array of string
*/
aJSTerm.sandbox.values = function JSTH_values(aObject)
{
let arrValues = [];
let obj = WebConsoleUtils.unwrap(aObject);
try {
for (let prop in obj) {
arrValues.push(obj[prop]);
}
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
return arrValues;
};
/**
* Opens a help window in MDN.
*/
aJSTerm.sandbox.help = function JSTH_help()
{
aJSTerm.helperEvaluated = true;
aJSTerm.window.open(
"https://developer.mozilla.org/docs/Tools/Web_Console/Helpers",
"help", "");
};
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*
* @param object aObject
* Object to inspect.
*/
aJSTerm.sandbox.inspect = function JSTH_inspect(aObject)
{
if (!WebConsoleUtils.isObjectInspectable(aObject)) {
return aObject;
}
aJSTerm.helperEvaluated = true;
let message = {
input: aJSTerm._evalInput,
objectCacheId: Manager.sequenceId,
};
message.resultObject =
aJSTerm.prepareObjectForRemote(WebConsoleUtils.unwrap(aObject),
message.objectCacheId);
Manager.sendMessage("JSTerm:InspectObject", message);
};
/**
* Prints aObject to the output.
*
* @param object aObject
* Object to print to the output.
* @return string
*/
aJSTerm.sandbox.pprint = function JSTH_pprint(aObject)
{
aJSTerm.helperEvaluated = true;
if (aObject === null || aObject === undefined || aObject === true ||
aObject === false) {
aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError"));
return;
}
else if (typeof aObject == "function") {
aJSTerm.helperRawOutput = true;
return aObject + "\n";
}
aJSTerm.helperRawOutput = true;
let output = [];
let pairs = WebConsoleUtils.namesAndValuesOf(WebConsoleUtils.unwrap(aObject));
pairs.forEach(function(aPair) {
output.push(aPair.name + ": " + aPair.value);
});
return " " + output.join("\n ");
};
/**
* Print a string to the output, as-is.
*
* @param string aString
* A string you want to output.
* @returns void
*/
aJSTerm.sandbox.print = function JSTH_print(aString)
{
aJSTerm.helperEvaluated = true;
aJSTerm.helperRawOutput = true;
return String(aString);
};
}
/**
* The JavaScript terminal is meant to allow remote code execution for the Web
* Console.
*/
let JSTerm = {
get window() Manager.window,
get console() this.window.console,
/**
* The Cu.Sandbox() object where code is evaluated.
*/
sandbox: null,
_sandboxLocation: null,
_messageHandlers: {},
/**
* Evaluation result objects are cached in this object. The chrome process can
* request any object based on its ID.
*/
_objectCache: null,
/**
* Initialize the JavaScript terminal feature.
*
* @param object aMessage
* Options for JSTerm sent from the remote Web Console instance. This
* object holds the following properties:
*
* - notifyNonNativeConsoleAPI - boolean that tells if you want to be
* notified if the window.console API object in the page is not the
* native one (if the page overrides it).
* A "JSTerm:NonNativeConsoleAPI" message will be sent if this is the
* case.
*/
init: function JST_init(aMessage)
{
this._objectCache = {};
this._messageHandlers = {
"JSTerm:EvalRequest": this.handleEvalRequest,
"JSTerm:GetEvalObject": this.handleGetEvalObject,
"JSTerm:Autocomplete": this.handleAutocomplete,
"JSTerm:ClearObjectCache": this.handleClearObjectCache,
};
for (let name in this._messageHandlers) {
let handler = this._messageHandlers[name].bind(this);
Manager.addMessageHandler(name, handler);
}
if (aMessage && aMessage.notifyNonNativeConsoleAPI) {
let consoleObject = WebConsoleUtils.unwrap(this.window).console;
if (!("__mozillaConsole__" in consoleObject)) {
Manager.sendMessage("JSTerm:NonNativeConsoleAPI", {});
}
}
},
/**
* Handler for the "JSTerm:EvalRequest" remote message. This method evaluates
* user input in the JavaScript sandbox and sends the result back to the
* remote process. The "JSTerm:EvalResult" message includes the following
* data:
* - id - the same ID as the EvalRequest (for tracking purposes).
* - input - the JS string that was evaluated.
* - resultString - the evaluation result converted to a string formatted
* for display.
* - timestamp - timestamp when evaluation occurred (Date.now(),
* milliseconds since the UNIX epoch).
* - inspectable - boolean that tells if the evaluation result object can be
* inspected or not.
* - error - the evaluation exception object (if any).
* - errorMessage - the exception object converted to a string (if any error
* occurred).
* - helperResult - boolean that tells if a JSTerm helper was evaluated.
* - helperRawOutput - boolean that tells if the helper evaluation result
* should be displayed as raw output.
*
* If the result object is inspectable then two additional properties are
* included:
* - childrenCacheId - tells where child objects are cached. This is the
* same as aRequest.resultCacheId.
* - resultObject - the result object prepared for the remote process. See
* this.prepareObjectForRemote().
*
* @param object aRequest
* The code evaluation request object:
* - id - request ID.
* - str - string to evaluate.
* - resultCacheId - where to cache the evaluation child objects.
*/
handleEvalRequest: function JST_handleEvalRequest(aRequest)
{
let id = aRequest.id;
let input = aRequest.str;
let result, error = null;
let timestamp;
this.helperEvaluated = false;
this.helperRawOutput = false;
this._evalInput = input;
try {
timestamp = Date.now();
result = this.evalInSandbox(input);
}
catch (ex) {
error = ex;
}
delete this._evalInput;
let inspectable = !error && WebConsoleUtils.isObjectInspectable(result);
let resultString = undefined;
if (!error) {
resultString = this.helperRawOutput ? result :
WebConsoleUtils.formatResult(result);
}
let message = {
id: id,
input: input,
resultString: resultString,
timestamp: timestamp,
error: error,
errorMessage: error ? String(error) : null,
inspectable: inspectable,
helperResult: this.helperEvaluated,
helperRawOutput: this.helperRawOutput,
};
if (inspectable) {
message.childrenCacheId = aRequest.resultCacheId;
message.resultObject =
this.prepareObjectForRemote(result, message.childrenCacheId);
}
Manager.sendMessage("JSTerm:EvalResult", message);
},
/**
* Handler for the remote "JSTerm:GetEvalObject" message. This allows the
* remote Web Console instance to retrieve an object from the content process.
* The "JSTerm:EvalObject" message is sent back to the remote process:
* - id - the request ID, used to trace back to the initial request.
* - cacheId - the cache ID where the requested object is stored.
* - objectId - the ID of the object being sent.
* - object - the object representation prepared for remote inspection. See
* this.prepareObjectForRemote().
* - childrenCacheId - the cache ID where any child object of |object| are
* stored.
*
* @param object aRequest
* The message that requests the content object. Properties: cacheId,
* objectId and resultCacheId.
*
* Evaluated objects are stored in "buckets" (cache IDs). Each object
* is assigned an ID (object ID). You can request a specific object
* (objectId) from a specific cache (cacheId) and tell where the result
* should be cached (resultCacheId). The requested object can have
* further references to other objects - those references will be
* cached in the "bucket" of your choice (based on resultCacheId). If
* you do not provide any resultCacheId in the request message, then
* cacheId will be used.
*/
handleGetEvalObject: function JST_handleGetEvalObject(aRequest)
{
if (aRequest.cacheId in this._objectCache &&
aRequest.objectId in this._objectCache[aRequest.cacheId]) {
let object = this._objectCache[aRequest.cacheId][aRequest.objectId];
let resultCacheId = aRequest.resultCacheId || aRequest.cacheId;
let message = {
id: aRequest.id,
cacheId: aRequest.cacheId,
objectId: aRequest.objectId,
object: this.prepareObjectForRemote(object, resultCacheId),
childrenCacheId: resultCacheId,
};
Manager.sendMessage("JSTerm:EvalObject", message);
}
else {
Cu.reportError("JSTerm:GetEvalObject request " + aRequest.id +
": stale object.");
}
},
/**
* Handler for the remote "JSTerm:ClearObjectCache" message. This allows the
* remote Web Console instance to clear the cache of objects that it no longer
* uses.
*
* @param object aRequest
* An object that holds one property: the cacheId you want cleared.
*/
handleClearObjectCache: function JST_handleClearObjectCache(aRequest)
{
if (aRequest.cacheId in this._objectCache) {
delete this._objectCache[aRequest.cacheId];
}
},
/**
* Prepare an object to be sent to the remote Web Console instance.
*
* @param object aObject
* The object you want to send to the remote Web Console instance.
* @param number aCacheId
* Cache ID where you want object references to be stored into. The
* given object may include references to other objects - those
* references will be stored in the given cache ID so the remote
* process can later retrieve them as well.
* @return array
* An array that holds one element for each enumerable property and
* method in aObject. Each element describes the property. For details
* see WebConsoleUtils.namesAndValuesOf().
*/
prepareObjectForRemote: function JST_prepareObjectForRemote(aObject, aCacheId)
{
// Cache the properties that have inspectable values.
let propCache = this._objectCache[aCacheId] || {};
let result = WebConsoleUtils.namesAndValuesOf(aObject, propCache);
if (!(aCacheId in this._objectCache) && Object.keys(propCache).length > 0) {
this._objectCache[aCacheId] = propCache;
}
return result;
},
/**
* Handler for the "JSTerm:Autocomplete" remote message. This handler provides
* completion results for user input. The "JSterm:AutocompleteProperties"
* message is sent to the remote process:
* - id - the same as request ID.
* - input - the user input (same as in the request message).
* - matches - an array of matched properties (strings).
* - matchProp - the part that was used from the user input for finding the
* matches. For details see the JSPropertyProvider description and
* implementation.
*
*
* @param object aRequest
* The remote request object which holds two properties: an |id| and
* the user |input|.
*/
handleAutocomplete: function JST_handleAutocomplete(aRequest)
{
let result = JSPropertyProvider(this.window, aRequest.input) || {};
let message = {
id: aRequest.id,
input: aRequest.input,
matches: result.matches || [],
matchProp: result.matchProp,
};
Manager.sendMessage("JSTerm:AutocompleteProperties", message);
},
/**
* Create the JavaScript sandbox where user input is evaluated.
* @private
*/
_createSandbox: function JST__createSandbox()
{
this._sandboxLocation = this.window.location;
this.sandbox = new Cu.Sandbox(this.window, {
sandboxPrototype: this.window,
wantXrays: false,
});
this.sandbox.console = this.console;
JSTermHelper(this);
},
/**
* Evaluates a string in the sandbox.
*
* @param string aString
* String to evaluate in the sandbox.
* @return mixed
* The result of the evaluation.
*/
evalInSandbox: function JST_evalInSandbox(aString)
{
// If the user changed to a different location, we need to update the
// sandbox.
if (this._sandboxLocation !== this.window.location) {
this._createSandbox();
}
// The help function needs to be easy to guess, so we make the () optional
if (aString.trim() == "help" || aString.trim() == "?") {
aString = "help()";
}
let window = WebConsoleUtils.unwrap(this.sandbox.window);
let $ = null, $$ = null;
// We prefer to execute the page-provided implementations for the $() and
// $$() functions.
if (typeof window.$ == "function") {
$ = this.sandbox.$;
delete this.sandbox.$;
}
if (typeof window.$$ == "function") {
$$ = this.sandbox.$$;
delete this.sandbox.$$;
}
let result = Cu.evalInSandbox(aString, this.sandbox, "1.8",
"Web Console", 1);
if ($) {
this.sandbox.$ = $;
}
if ($$) {
this.sandbox.$$ = $$;
}
return result;
},
/**
* Destroy the JSTerm instance.
*/
destroy: function JST_destroy()
{
for (let name in this._messageHandlers) {
Manager.removeMessageHandler(name);
}
delete this.sandbox;
delete this._sandboxLocation;
delete this._messageHandlers;
delete this._objectCache;
},
};
///////////////////////////////////////////////////////////////////////////////
// The window.console API observer
///////////////////////////////////////////////////////////////////////////////
/**
* The window.console API observer. This allows the window.console API messages
* to be sent to the remote Web Console instance.
*/
let ConsoleAPIObserver = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
/**
* Initialize the window.console API observer.
*/
init: function CAO_init()
{
// Note that the observer is process-wide. We will filter the messages as
// needed, see CAO_observe().
Services.obs.addObserver(this, "console-api-log-event", false);
Manager.addMessageHandler("ConsoleAPI:ClearCache",
this.handleClearCache.bind(this));
},
/**
* The console API message observer. When messages are received from the
* observer service we forward them to the remote Web Console instance.
*
* @param object aMessage
* The message object receives from the observer service.
* @param string aTopic
* The message topic received from the observer service.
*/
observe: function CAO_observe(aMessage, aTopic)
{
if (!_alive || !aMessage || aTopic != "console-api-log-event") {
return;
}
let apiMessage = aMessage.wrappedJSObject;
let msgWindow =
WebConsoleUtils.getWindowByOuterId(apiMessage.ID, Manager.window);
if (!msgWindow || msgWindow.top != Manager.window) {
// Not the same window!
return;
}
let messageToChrome = {};
this._prepareApiMessageForRemote(apiMessage, messageToChrome);
Manager.sendMessage("WebConsole:ConsoleAPI", messageToChrome);
},
/**
* Prepare a message from the console APi to be sent to the remote Web Console
* instance.
*
* @param object aOriginalMessage
* The original message received from console-api-log-event.
* @param object aRemoteMessage
* The object you want to send to the remote Web Console. This object
* is updated to hold information from the original message. New
* properties added:
* - timeStamp
* Message timestamp (same as the aOriginalMessage.timeStamp property).
* - apiMessage
* An object that copies almost all the properties from
* aOriginalMessage. Arguments might be skipped if it holds references
* to objects that cannot be sent as they are to the remote Web Console
* instance.
* - argumentsToString
* Optional: the aOriginalMessage.arguments object stringified.
*
* The apiMessage.arguments property is set to hold data appropriate
* to the message level. A similar approach is used for
* argumentsToString.
*/
_prepareApiMessageForRemote:
function CAO__prepareApiMessageForRemote(aOriginalMessage, aRemoteMessage)
{
aRemoteMessage.apiMessage =
WebConsoleUtils.cloneObject(aOriginalMessage, true,
function(aKey, aValue, aObject) {
// We need to skip the arguments property from the original object.
if (aKey == "wrappedJSObject" || aObject === aOriginalMessage &&
aKey == "arguments") {
return false;
}
return true;
});
aRemoteMessage.timeStamp = aOriginalMessage.timeStamp;
switch (aOriginalMessage.level) {
case "trace":
case "time":
case "timeEnd":
case "group":
case "groupCollapsed":
aRemoteMessage.apiMessage.arguments =
WebConsoleUtils.cloneObject(aOriginalMessage.arguments, true);
break;
case "groupEnd":
aRemoteMessage.argumentsToString =
Array.map(aOriginalMessage.arguments || [],
this._formatObject.bind(this));
break;
case "log":
case "info":
case "warn":
case "error":
case "debug":
case "dir": {
aRemoteMessage.objectsCacheId = Manager.sequenceId;
aRemoteMessage.argumentsToString = [];
let mapFunction = function(aItem) {
let formattedObject = this._formatObject(aItem);
aRemoteMessage.argumentsToString.push(formattedObject);
if (WebConsoleUtils.isObjectInspectable(aItem)) {
return JSTerm.prepareObjectForRemote(aItem,
aRemoteMessage.objectsCacheId);
}
return formattedObject;
}.bind(this);
aRemoteMessage.apiMessage.arguments =
Array.map(aOriginalMessage.arguments || [], mapFunction);
break;
}
default:
Cu.reportError("Unknown Console API log level: " +
aOriginalMessage.level);
break;
}
},
/**
* Format an object's value to be displayed in the Web Console.
*
* @private
* @param object aObject
* The object you want to display.
* @return string
* The string you can display for the given object.
*/
_formatObject: function CAO__formatObject(aObject)
{
return typeof aObject == "string" ?
aObject : WebConsoleUtils.formatResult(aObject);
},
/**
* Get the cached messages for the current inner window.
*
* @see this._prepareApiMessageForRemote()
* @return array
* The array of cached messages. Each element is a Console API
* prepared to be sent to the remote Web Console instance.
*/
getCachedMessages: function CAO_getCachedMessages()
{
let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window);
let messages = gConsoleStorage.getEvents(innerWindowId);
let result = messages.map(function(aMessage) {
let remoteMessage = { _type: "ConsoleAPI" };
this._prepareApiMessageForRemote(aMessage.wrappedJSObject, remoteMessage);
return remoteMessage;
}, this);
return result;
},
/**
* Handler for the "ConsoleAPI:ClearCache" message.
*/
handleClearCache: function CAO_handleClearCache()
{
let windowId = WebConsoleUtils.getInnerWindowId(Manager.window);
gConsoleStorage.clearEvents(windowId);
},
/**
* Destroy the ConsoleAPIObserver listeners.
*/
destroy: function CAO_destroy()
{
Manager.removeMessageHandler("ConsoleAPI:ClearCache");
Services.obs.removeObserver(this, "console-api-log-event");
},
};
///////////////////////////////////////////////////////////////////////////////
// The page errors listener
///////////////////////////////////////////////////////////////////////////////
/**
* The nsIConsoleService listener. This is used to send all the page errors
* (JavaScript, CSS and more) to the remote Web Console instance.
*/
let ConsoleListener = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
/**
* Initialize the nsIConsoleService listener.
*/
init: function CL_init()
{
Services.console.registerListener(this);
},
/**
* The nsIConsoleService observer. This method takes all the script error
* messages belonging to the current window and sends them to the remote Web
* Console instance.
*
* @param nsIScriptError aScriptError
* The script error object coming from the nsIConsoleService.
*/
observe: function CL_observe(aScriptError)
{
if (!_alive || !(aScriptError instanceof Ci.nsIScriptError) ||
!aScriptError.outerWindowID) {
return;
}
if (!this.isCategoryAllowed(aScriptError.category)) {
return;
}
let errorWindow =
WebConsoleUtils.getWindowByOuterId(aScriptError.outerWindowID,
Manager.window);
if (!errorWindow || errorWindow.top != Manager.window) {
return;
}
Manager.sendMessage("WebConsole:PageError", { pageError: aScriptError });
},
/**
* Check if the given script error category is allowed to be tracked or not.
* We ignore chrome-originating errors as we only care about content.
*
* @param string aCategory
* The nsIScriptError category you want to check.
* @return boolean
* True if the category is allowed to be logged, false otherwise.
*/
isCategoryAllowed: function CL_isCategoryAllowed(aCategory)
{
switch (aCategory) {
case "XPConnect JavaScript":
case "component javascript":
case "chrome javascript":
case "chrome registration":
case "XBL":
case "XBL Prototype Handler":
case "XBL Content Sink":
case "xbl javascript":
return false;
}
return true;
},
/**
* Get the cached page errors for the current inner window.
*
* @return array
* The array of cached messages. Each element is an nsIScriptError
* with an added _type property so the remote Web Console instance can
* tell the difference between various types of cached messages.
*/
getCachedMessages: function CL_getCachedMessages()
{
let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window);
let result = [];
let errors = {};
Services.console.getMessageArray(errors, {});
(errors.value || []).forEach(function(aError) {
if (!(aError instanceof Ci.nsIScriptError) ||
aError.innerWindowID != innerWindowId ||
!this.isCategoryAllowed(aError.category)) {
return;
}
let remoteMessage = WebConsoleUtils.cloneObject(aError);
remoteMessage._type = "PageError";
result.push(remoteMessage);
}, this);
return result;
},
/**
* Remove the nsIConsoleService listener.
*/
destroy: function CL_destroy()
{
Services.console.unregisterListener(this);
},
};
///////////////////////////////////////////////////////////////////////////////
// Network logging
///////////////////////////////////////////////////////////////////////////////
// The maximum uint32 value.
const PR_UINT32_MAX = 4294967295;
// HTTP status codes.
const HTTP_MOVED_PERMANENTLY = 301;
const HTTP_FOUND = 302;
const HTTP_SEE_OTHER = 303;
const HTTP_TEMPORARY_REDIRECT = 307;
// The maximum number of bytes a NetworkResponseListener can hold.
const RESPONSE_BODY_LIMIT = 1048576; // 1 MB
/**
* The network response listener implements the nsIStreamListener and
* nsIRequestObserver interfaces. This is used within the NetworkMonitor feature
* to get the response body of the request.
*
* The code is mostly based on code listings from:
*
* http://www.softwareishard.com/blog/firebug/
* nsitraceablechannel-intercept-http-traffic/
*
* @constructor
* @param object aHttpActivity
* HttpActivity object associated with this request. Once the request is
* complete the aHttpActivity object is updated to include the response
* headers and body.
*/
function NetworkResponseListener(aHttpActivity) {
this.receivedData = "";
this.httpActivity = aHttpActivity;
this.bodySize = 0;
}
NetworkResponseListener.prototype = {
QueryInterface:
XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback,
Ci.nsIRequestObserver, Ci.nsISupports]),
/**
* This NetworkResponseListener tracks the NetworkMonitor.openResponses object
* to find the associated uncached headers.
* @private
*/
_foundOpenResponse: false,
/**
* The response will be written into the outputStream of this nsIPipe.
* Both ends of the pipe must be blocking.
*/
sink: null,
/**
* The HttpActivity object associated with this response.
*/
httpActivity: null,
/**
* Stores the received data as a string.
*/
receivedData: null,
/**
* The network response body size.
*/
bodySize: null,
/**
* The nsIRequest we are started for.
*/
request: null,
/**
* Set the async listener for the given nsIAsyncInputStream. This allows us to
* wait asynchronously for any data coming from the stream.
*
* @param nsIAsyncInputStream aStream
* The input stream from where we are waiting for data to come in.
* @param nsIInputStreamCallback aListener
* The input stream callback you want. This is an object that must have
* the onInputStreamReady() method. If the argument is null, then the
* current callback is removed.
* @return void
*/
setAsyncListener: function NRL_setAsyncListener(aStream, aListener)
{
// Asynchronously wait for the stream to be readable or closed.
aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread);
},
/**
* Stores the received data, if request/response body logging is enabled. It
* also does limit the number of stored bytes, based on the
* RESPONSE_BODY_LIMIT constant.
*
* Learn more about nsIStreamListener at:
* https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
*
* @param nsIRequest aRequest
* @param nsISupports aContext
* @param nsIInputStream aInputStream
* @param unsigned long aOffset
* @param unsigned long aCount
*/
onDataAvailable:
function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount)
{
this._findOpenResponse();
let data = NetUtil.readInputStreamToString(aInputStream, aCount);
this.bodySize += aCount;
if (!this.httpActivity.meta.discardResponseBody &&
this.receivedData.length < RESPONSE_BODY_LIMIT) {
this.receivedData += NetworkHelper.
convertToUnicode(data, aRequest.contentCharset);
}
},
/**
* See documentation at
* https://developer.mozilla.org/En/NsIRequestObserver
*
* @param nsIRequest aRequest
* @param nsISupports aContext
*/
onStartRequest: function NRL_onStartRequest(aRequest)
{
this.request = aRequest;
this._findOpenResponse();
// Asynchronously wait for the data coming from the request.
this.setAsyncListener(this.sink.inputStream, this);
},
/**
* Handle the onStopRequest by closing the sink output stream.
*
* For more documentation about nsIRequestObserver go to:
* https://developer.mozilla.org/En/NsIRequestObserver
*/
onStopRequest: function NRL_onStopRequest()
{
this._findOpenResponse();
this.sink.outputStream.close();
},
/**
* Find the open response object associated to the current request. The
* NetworkMonitor.httpResponseExaminer() method saves the response headers in
* NetworkMonitor.openResponses. This method takes the data from the open
* response object and puts it into the HTTP activity object, then sends it to
* the remote Web Console instance.
*
* @private
*/
_findOpenResponse: function NRL__findOpenResponse()
{
if (!_alive || this._foundOpenResponse) {
return;
}
let openResponse = null;
for each (let item in NetworkMonitor.openResponses) {
if (item.channel === this.httpActivity.channel) {
openResponse = item;
break;
}
}
if (!openResponse) {
return;
}
this._foundOpenResponse = true;
let logResponse = this.httpActivity.log.entries[0].response;
logResponse.headers = openResponse.headers;
logResponse.httpVersion = openResponse.httpVersion;
logResponse.status = openResponse.status;
logResponse.statusText = openResponse.statusText;
if (openResponse.cookies) {
logResponse.cookies = openResponse.cookies;
}
delete NetworkMonitor.openResponses[openResponse.id];
this.httpActivity.meta.stages.push("http-on-examine-response");
NetworkMonitor.sendActivity(this.httpActivity);
},
/**
* Clean up the response listener once the response input stream is closed.
* This is called from onStopRequest() or from onInputStreamReady() when the
* stream is closed.
* @return void
*/
onStreamClose: function NRL_onStreamClose()
{
if (!this.httpActivity) {
return;
}
// Remove our listener from the request input stream.
this.setAsyncListener(this.sink.inputStream, null);
this._findOpenResponse();
let meta = this.httpActivity.meta;
let entry = this.httpActivity.log.entries[0];
let request = entry.request;
let response = entry.response;
meta.stages.push("REQUEST_STOP");
if (!meta.discardResponseBody && this.receivedData.length) {
this._onComplete(this.receivedData);
}
else if (!meta.discardResponseBody && response.status == 304) {
// Response is cached, so we load it from cache.
let charset = this.request.contentCharset || this.httpActivity.charset;
NetworkHelper.loadFromCache(request.url, charset,
this._onComplete.bind(this));
}
else {
this._onComplete();
}
},
/**
* Handler for when the response completes. This function cleans up the
* response listener.
*
* @param string [aData]
* Optional, the received data coming from the response listener or
* from the cache.
*/
_onComplete: function NRL__onComplete(aData)
{
let response = this.httpActivity.log.entries[0].response;
try {
response.bodySize = response.status != 304 ? this.request.contentLength : 0;
}
catch (ex) {
response.bodySize = -1;
}
try {
response.content = { mimeType: this.request.contentType };
}
catch (ex) {
response.content = { mimeType: "" };
}
if (response.content.mimeType && this.request.contentCharset) {
response.content.mimeType += "; charset=" + this.request.contentCharset;
}
response.content.size = this.bodySize || (aData || "").length;
if (aData) {
response.content.text = aData;
}
this.receivedData = "";
if (_alive) {
NetworkMonitor.sendActivity(this.httpActivity);
}
this.httpActivity.channel = null;
this.httpActivity = null;
this.sink = null;
this.inputStream = null;
this.request = null;
},
/**
* The nsIInputStreamCallback for when the request input stream is ready -
* either it has more data or it is closed.
*
* @param nsIAsyncInputStream aStream
* The sink input stream from which data is coming.
* @returns void
*/
onInputStreamReady: function NRL_onInputStreamReady(aStream)
{
if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
return;
}
let available = -1;
try {
// This may throw if the stream is closed normally or due to an error.
available = aStream.available();
}
catch (ex) { }
if (available != -1) {
if (available != 0) {
// Note that passing 0 as the offset here is wrong, but the
// onDataAvailable() method does not use the offset, so it does not
// matter.
this.onDataAvailable(this.request, null, aStream, 0, available);
}
this.setAsyncListener(aStream, this);
}
else {
this.onStreamClose();
}
},
};
/**
* The network monitor uses the nsIHttpActivityDistributor to monitor network
* requests. The nsIObserverService is also used for monitoring
* http-on-examine-response notifications. All network request information is
* routed to the remote Web Console.
*/
let NetworkMonitor = {
httpTransactionCodes: {
0x5001: "REQUEST_HEADER",
0x5002: "REQUEST_BODY_SENT",
0x5003: "RESPONSE_START",
0x5004: "RESPONSE_HEADER",
0x5005: "RESPONSE_COMPLETE",
0x5006: "TRANSACTION_CLOSE",
0x804b0003: "STATUS_RESOLVING",
0x804b000b: "STATUS_RESOLVED",
0x804b0007: "STATUS_CONNECTING_TO",
0x804b0004: "STATUS_CONNECTED_TO",
0x804b0005: "STATUS_SENDING_TO",
0x804b000a: "STATUS_WAITING_FOR",
0x804b0006: "STATUS_RECEIVING_FROM"
},
harCreator: {
name: Services.appinfo.name + " - Web Console",
version: Services.appinfo.version,
},
// Network response bodies are piped through a buffer of the given size (in
// bytes).
responsePipeSegmentSize: null,
/**
* Whether to save the bodies of network requests and responses. Disabled by
* default to save memory.
*/
get saveRequestAndResponseBodies() {
return Manager.getPreference("NetworkMonitor.saveRequestAndResponseBodies");
},
openRequests: null,
openResponses: null,
progressListener: null,
/**
* The network monitor initializer.
*
* @param object aMessage
* Initialization object sent by the remote Web Console instance. This
* object can hold one property: monitorFileActivity - a boolean that
* tells if monitoring of file:// requests should be enabled as well or
* not.
*/
init: function NM_init(aMessage)
{
this.responsePipeSegmentSize = Services.prefs
.getIntPref("network.buffer.cache.size");
this.openRequests = {};
this.openResponses = {};
activityDistributor.addObserver(this);
Services.obs.addObserver(this.httpResponseExaminer,
"http-on-examine-response", false);
// Monitor file:// activity as well.
if (aMessage && aMessage.monitorFileActivity) {
ConsoleProgressListener.startMonitor(ConsoleProgressListener
.MONITOR_FILE_ACTIVITY);
}
},
/**
* Observe notifications for the http-on-examine-response topic, coming from
* the nsIObserverService.
*
* @param nsIHttpChannel aSubject
* @param string aTopic
* @returns void
*/
httpResponseExaminer: function NM_httpResponseExaminer(aSubject, aTopic)
{
// The httpResponseExaminer is used to retrieve the uncached response
// headers. The data retrieved is stored in openResponses. The
// NetworkResponseListener is responsible with updating the httpActivity
// object with the data from the new object in openResponses.
if (!_alive || aTopic != "http-on-examine-response" ||
!(aSubject instanceof Ci.nsIHttpChannel)) {
return;
}
let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
// Try to get the source window of the request.
let win = NetworkHelper.getWindowForRequest(channel);
if (!win || win.top !== Manager.window) {
return;
}
let response = {
id: Manager.sequenceId,
channel: channel,
headers: [],
cookies: [],
};
let setCookieHeader = null;
channel.visitResponseHeaders({
visitHeader: function NM__visitHeader(aName, aValue) {
let lowerName = aName.toLowerCase();
if (lowerName == "set-cookie") {
setCookieHeader = aValue;
}
response.headers.push({ name: aName, value: aValue });
}
});
if (!response.headers.length) {
return; // No need to continue.
}
if (setCookieHeader) {
response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader);
}
// Determine the HTTP version.
let httpVersionMaj = {};
let httpVersionMin = {};
channel.QueryInterface(Ci.nsIHttpChannelInternal);
channel.getResponseVersion(httpVersionMaj, httpVersionMin);
response.status = channel.responseStatus;
response.statusText = channel.responseStatusText;
response.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
httpVersionMin.value;
NetworkMonitor.openResponses[response.id] = response;
},
/**
* Begin observing HTTP traffic that originates inside the current tab.
*
* @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver
*
* @param nsIHttpChannel aChannel
* @param number aActivityType
* @param number aActivitySubtype
* @param number aTimestamp
* @param number aExtraSizeData
* @param string aExtraStringData
*/
observeActivity:
function NM_observeActivity(aChannel, aActivityType, aActivitySubtype,
aTimestamp, aExtraSizeData, aExtraStringData)
{
if (!_alive ||
aActivityType != activityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION &&
aActivityType != activityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {
return;
}
if (!(aChannel instanceof Ci.nsIHttpChannel)) {
return;
}
aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel);
if (aActivitySubtype ==
activityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) {
this._onRequestHeader(aChannel, aTimestamp, aExtraStringData);
return;
}
// Iterate over all currently ongoing requests. If aChannel can't
// be found within them, then exit this function.
let httpActivity = null;
for each (let item in this.openRequests) {
if (item.channel === aChannel) {
httpActivity = item;
break;
}
}
if (!httpActivity) {
return;
}
let transCodes = this.httpTransactionCodes;
// Store the time information for this activity subtype.
if (aActivitySubtype in transCodes) {
let stage = transCodes[aActivitySubtype];
if (stage in httpActivity.timings) {
httpActivity.timings[stage].last = aTimestamp;
}
else {
httpActivity.meta.stages.push(stage);
httpActivity.timings[stage] = {
first: aTimestamp,
last: aTimestamp,
};
}
}
switch (aActivitySubtype) {
case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
this._onRequestBodySent(httpActivity);
break;
case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
this._onResponseHeader(httpActivity, aExtraStringData);
break;
case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
this._onTransactionClose(httpActivity);
break;
default:
break;
}
},
/**
* Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the
* headers are sent to the server. This method creates the |httpActivity|
* object where we store the request and response information that is
* collected through its lifetime.
*
* @private
* @param nsIHttpChannel aChannel
* @param number aTimestamp
* @param string aExtraStringData
* @return void
*/
_onRequestHeader:
function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData)
{
// Try to get the source window of the request.
let win = NetworkHelper.getWindowForRequest(aChannel);
if (!win || win.top !== Manager.window) {
return;
}
let httpActivity = this.createActivityObject(aChannel);
httpActivity.charset = win.document.characterSet; // see NM__onRequestBodySent()
httpActivity.meta.stages.push("REQUEST_HEADER"); // activity stage (aActivitySubtype)
httpActivity.timings.REQUEST_HEADER = {
first: aTimestamp,
last: aTimestamp
};
let entry = httpActivity.log.entries[0];
entry.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString();
let request = httpActivity.log.entries[0].request;
let cookieHeader = null;
// Copy the request header data.
aChannel.visitRequestHeaders({
visitHeader: function NM__visitHeader(aName, aValue)
{
if (aName == "Cookie") {
cookieHeader = aValue;
}
request.headers.push({ name: aName, value: aValue });
}
});
if (cookieHeader) {
request.cookies = NetworkHelper.parseCookieHeader(cookieHeader);
}
// Determine the HTTP version.
let httpVersionMaj = {};
let httpVersionMin = {};
aChannel.QueryInterface(Ci.nsIHttpChannelInternal);
aChannel.getRequestVersion(httpVersionMaj, httpVersionMin);
request.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
httpVersionMin.value;
request.headersSize = aExtraStringData.length;
this._setupResponseListener(httpActivity);
this.openRequests[httpActivity.id] = httpActivity;
this.sendActivity(httpActivity);
},
/**
* Create the empty HTTP activity object. This object is used for storing all
* the request and response information.
*
* This is a HAR-like object. Conformance to the spec is not guaranteed at
* this point.
*
* TODO: Bug 708717 - Add support for network log export to HAR
*
* @see http://www.softwareishard.com/blog/har-12-spec
* @param nsIHttpChannel aChannel
* The HTTP channel for which the HTTP activity object is created.
* @return object
* The new HTTP activity object.
*/
createActivityObject: function NM_createActivityObject(aChannel)
{
return {
hudId: Manager.hudId,
id: Manager.sequenceId,
channel: aChannel,
charset: null, // see NM__onRequestHeader()
meta: { // holds metadata about the activity object
stages: [], // activity stages (aActivitySubtype)
discardRequestBody: !this.saveRequestAndResponseBodies,
discardResponseBody: !this.saveRequestAndResponseBodies,
},
timings: {}, // internal timing information, see NM_observeActivity()
log: { // HAR-like object
version: "1.2",
creator: this.harCreator,
// missing |browser| and |pages|
entries: [{ // we only track one entry at a time
connection: Manager.sequenceId, // connection ID
startedDateTime: 0, // see NM__onRequestHeader()
time: 0, // see NM__setupHarTimings()
// missing |serverIPAddress| and |cache|
request: {
method: aChannel.requestMethod,
url: aChannel.URI.spec,
httpVersion: "", // see NM__onRequestHeader()
headers: [], // see NM__onRequestHeader()
cookies: [], // see NM__onRequestHeader()
queryString: [], // never set
headersSize: -1, // see NM__onRequestHeader()
bodySize: -1, // see NM__onRequestBodySent()
postData: null, // see NM__onRequestBodySent()
},
response: {
status: 0, // see NM__onResponseHeader()
statusText: "", // see NM__onResponseHeader()
httpVersion: "", // see NM__onResponseHeader()
headers: [], // see NM_httpResponseExaminer()
cookies: [], // see NM_httpResponseExaminer()
content: null, // see NRL_onStreamClose()
redirectURL: "", // never set
headersSize: -1, // see NM__onResponseHeader()
bodySize: -1, // see NRL_onStreamClose()
},
timings: {}, // see NM__setupHarTimings()
}],
},
};
},
/**
* Setup the network response listener for the given HTTP activity. The
* NetworkResponseListener is responsible for storing the response body.
*
* @private
* @param object aHttpActivity
* The HTTP activity object we are tracking.
*/
_setupResponseListener: function NM__setupResponseListener(aHttpActivity)
{
let channel = aHttpActivity.channel;
channel.QueryInterface(Ci.nsITraceableChannel);
// The response will be written into the outputStream of this pipe.
// This allows us to buffer the data we are receiving and read it
// asynchronously.
// Both ends of the pipe must be blocking.
let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
// The streams need to be blocking because this is required by the
// stream tee.
sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null);
// Add listener for the response body.
let newListener = new NetworkResponseListener(aHttpActivity);
// Remember the input stream, so it isn't released by GC.
newListener.inputStream = sink.inputStream;
newListener.sink = sink;
let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].
createInstance(Ci.nsIStreamListenerTee);
let originalListener = channel.setNewListener(tee);
tee.init(originalListener, sink.outputStream, newListener);
},
/**
* Send an HTTP activity object to the remote Web Console instance.
* A WebConsole:NetworkActivity message is sent. The message holds two
* properties:
* - meta - the |aHttpActivity.meta| object.
* - log - the |aHttpActivity.log| object.
*
* @param object aHttpActivity
* The HTTP activity object you want to send.
*/
sendActivity: function NM_sendActivity(aHttpActivity)
{
Manager.sendMessage("WebConsole:NetworkActivity", {
meta: aHttpActivity.meta,
log: aHttpActivity.log,
});
},
/**
* Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged
* here.
*
* @private
* @param object aHttpActivity
* The HTTP activity object we are working with.
*/
_onRequestBodySent: function NM__onRequestBodySent(aHttpActivity)
{
if (aHttpActivity.meta.discardRequestBody) {
return;
}
let request = aHttpActivity.log.entries[0].request;
let sentBody = NetworkHelper.
readPostTextFromRequest(aHttpActivity.channel,
aHttpActivity.charset);
if (!sentBody && request.url == Manager.window.location.href) {
// If the request URL is the same as the current page URL, then
// we can try to get the posted text from the page directly.
// This check is necessary as otherwise the
// NetworkHelper.readPostTextFromPage()
// function is called for image requests as well but these
// are not web pages and as such don't store the posted text
// in the cache of the webpage.
sentBody = NetworkHelper.readPostTextFromPage(docShell,
aHttpActivity.charset);
}
if (!sentBody) {
return;
}
request.postData = {
mimeType: "", // never set
params: [], // never set
text: sentBody,
};
request.bodySize = sentBody.length;
this.sendActivity(aHttpActivity);
},
/**
* Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores
* information about the response headers.
*
* @private
* @param object aHttpActivity
* The HTTP activity object we are working with.
* @param string aExtraStringData
* The uncached response headers.
*/
_onResponseHeader:
function NM__onResponseHeader(aHttpActivity, aExtraStringData)
{
// aExtraStringData contains the uncached response headers. The first line
// contains the response status (e.g. HTTP/1.1 200 OK).
//
// Note: The response header is not saved here. Calling the
// channel.visitResponseHeaders() methood at this point sometimes causes an
// NS_ERROR_NOT_AVAILABLE exception.
//
// We could parse aExtraStringData to get the headers and their values, but
// that is not trivial to do in an accurate manner. Hence, we save the
// response headers in this.httpResponseExaminer().
let response = aHttpActivity.log.entries[0].response;
let headers = aExtraStringData.split(/\r\n|\n|\r/);
let statusLine = headers.shift();
let statusLineArray = statusLine.split(" ");
response.httpVersion = statusLineArray.shift();
response.status = statusLineArray.shift();
response.statusText = statusLineArray.join(" ");
response.headersSize = aExtraStringData.length;
// Discard the response body for known response statuses.
switch (parseInt(response.status)) {
case HTTP_MOVED_PERMANENTLY:
case HTTP_FOUND:
case HTTP_SEE_OTHER:
case HTTP_TEMPORARY_REDIRECT:
aHttpActivity.meta.discardResponseBody = true;
break;
}
this.sendActivity(aHttpActivity);
},
/**
* Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR
* timing information on the HTTP activity object and clears the request
* from the list of known open requests.
*
* @private
* @param object aHttpActivity
* The HTTP activity object we work with.
*/
_onTransactionClose: function NM__onTransactionClose(aHttpActivity)
{
this._setupHarTimings(aHttpActivity);
this.sendActivity(aHttpActivity);
delete this.openRequests[aHttpActivity.id];
},
/**
* Update the HTTP activity object to include timing information as in the HAR
* spec. The HTTP activity object holds the raw timing information in
* |timings| - these are timings stored for each activity notification. The
* HAR timing information is constructed based on these lower level data.
*
* @param object aHttpActivity
* The HTTP activity object we are working with.
*/
_setupHarTimings: function NM__setupHarTimings(aHttpActivity)
{
let timings = aHttpActivity.timings;
let entry = aHttpActivity.log.entries[0];
let harTimings = entry.timings;
// Not clear how we can determine "blocked" time.
harTimings.blocked = -1;
// DNS timing information is available only in when the DNS record is not
// cached.
harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ?
timings.STATUS_RESOLVED.last -
timings.STATUS_RESOLVING.first : -1;
if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) {
harTimings.connect = timings.STATUS_CONNECTED_TO.last -
timings.STATUS_CONNECTING_TO.first;
}
else if (timings.STATUS_SENDING_TO) {
harTimings.connect = timings.STATUS_SENDING_TO.first -
timings.REQUEST_HEADER.first;
}
else {
harTimings.connect = -1;
}
if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) &&
(timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) {
harTimings.send = (timings.STATUS_WAITING_FOR ||
timings.STATUS_RECEIVING_FROM).first -
(timings.STATUS_CONNECTED_TO ||
timings.STATUS_SENDING_TO).last;
}
else {
harTimings.send = -1;
}
if (timings.RESPONSE_START) {
harTimings.wait = timings.RESPONSE_START.first -
(timings.REQUEST_BODY_SENT ||
timings.STATUS_SENDING_TO).last;
}
else {
harTimings.wait = -1;
}
if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) {
harTimings.receive = timings.RESPONSE_COMPLETE.last -
timings.RESPONSE_START.first;
}
else {
harTimings.receive = -1;
}
entry.time = 0;
for (let timing in harTimings) {
let time = Math.max(Math.round(harTimings[timing] / 1000), -1);
harTimings[timing] = time;
if (time > -1) {
entry.time += time;
}
}
},
/**
* Suspend Web Console activity. This is called when all Web Consoles are
* closed.
*/
destroy: function NM_destroy()
{
Services.obs.removeObserver(this.httpResponseExaminer,
"http-on-examine-response");
activityDistributor.removeObserver(this);
ConsoleProgressListener.stopMonitor(ConsoleProgressListener
.MONITOR_FILE_ACTIVITY);
delete this.openRequests;
delete this.openResponses;
},
};
/**
* A WebProgressListener that listens for location changes.
*
* This progress listener is used to track file loads and other kinds of
* location changes.
*
* When a file:// URI is loaded a "WebConsole:FileActivity" message is sent to
* the remote Web Console instance. The message JSON holds only one property:
* uri (the file URI).
*
* When the current page location changes a "WebConsole:LocationChange" message
* is sent. See ConsoleProgressListener.sendLocation() for details.
*/
let ConsoleProgressListener = {
/**
* Constant used for startMonitor()/stopMonitor() that tells you want to
* monitor file loads.
*/
MONITOR_FILE_ACTIVITY: 1,
/**
* Constant used for startMonitor()/stopMonitor() that tells you want to
* monitor page location changes.
*/
MONITOR_LOCATION_CHANGE: 2,
/**
* Tells if you want to monitor file activity.
* @private
* @type boolean
*/
_fileActivity: false,
/**
* Tells if you want to monitor location changes.
* @private
* @type boolean
*/
_locationChange: false,
/**
* Tells if the console progress listener is initialized or not.
* @private
* @type boolean
*/
_initialized: false,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
/**
* Initialize the ConsoleProgressListener.
* @private
*/
_init: function CPL__init()
{
if (this._initialized) {
return;
}
this._initialized = true;
let webProgress = docShell.QueryInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL);
},
/**
* Start a monitor/tracker related to the current nsIWebProgressListener
* instance.
*
* @param number aMonitor
* Tells what you want to track. Available constants:
* - this.MONITOR_FILE_ACTIVITY
* Track file loads.
* - this.MONITOR_LOCATION_CHANGE
* Track location changes for the top window.
*/
startMonitor: function CPL_startMonitor(aMonitor)
{
switch (aMonitor) {
case this.MONITOR_FILE_ACTIVITY:
this._fileActivity = true;
break;
case this.MONITOR_LOCATION_CHANGE:
this._locationChange = true;
break;
default:
throw new Error("HUDService-content: unknown monitor type " +
aMonitor + " for the ConsoleProgressListener!");
}
this._init();
},
/**
* Stop a monitor.
*
* @param number aMonitor
* Tells what you want to stop tracking. See this.startMonitor() for
* the list of constants.
*/
stopMonitor: function CPL_stopMonitor(aMonitor)
{
switch (aMonitor) {
case this.MONITOR_FILE_ACTIVITY:
this._fileActivity = false;
break;
case this.MONITOR_LOCATION_CHANGE:
this._locationChange = false;
break;
default:
throw new Error("HUDService-content: unknown monitor type " +
aMonitor + " for the ConsoleProgressListener!");
}
if (!this._fileActivity && !this._locationChange) {
this.destroy();
}
},
onStateChange:
function CPL_onStateChange(aProgress, aRequest, aState, aStatus)
{
if (!_alive) {
return;
}
if (this._fileActivity) {
this._checkFileActivity(aProgress, aRequest, aState, aStatus);
}
if (this._locationChange) {
this._checkLocationChange(aProgress, aRequest, aState, aStatus);
}
},
/**
* Check if there is any file load, given the arguments of
* nsIWebProgressListener.onStateChange. If the state change tells that a file
* URI has been loaded, then the remote Web Console instance is notified.
* @private
*/
_checkFileActivity:
function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus)
{
if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
return;
}
let uri = null;
if (aRequest instanceof Ci.imgIRequest) {
let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest);
uri = imgIRequest.URI;
}
else if (aRequest instanceof Ci.nsIChannel) {
let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel);
uri = nsIChannel.URI;
}
if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) {
return;
}
Manager.sendMessage("WebConsole:FileActivity", {uri: uri.spec});
},
/**
* Check if the current window.top location is changing, given the arguments
* of nsIWebProgressListener.onStateChange. If that is the case, the remote
* Web Console instance is notified.
* @private
*/
_checkLocationChange:
function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus)
{
let isStart = aState & Ci.nsIWebProgressListener.STATE_START;
let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP;
let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
// Skip non-interesting states.
if (!isNetwork || !isWindow ||
aProgress.DOMWindow != Manager.window) {
return;
}
if (isStart && aRequest instanceof Ci.nsIChannel) {
this.sendLocation(aRequest.URI.spec, "");
}
else if (isStop) {
this.sendLocation(Manager.window.location.href,
Manager.window.document.title);
}
},
onLocationChange: function() {},
onStatusChange: function() {},
onProgressChange: function() {},
onSecurityChange: function() {},
/**
* Send the location of the current top window to the remote Web Console.
* A "WebConsole:LocationChange" message is sent. The JSON object holds two
* properties: location and title.
*
* @param string aLocation
* Current page address.
* @param string aTitle
* Current page title.
*/
sendLocation: function CPL_sendLocation(aLocation, aTitle)
{
let message = {
"location": aLocation,
"title": aTitle,
};
Manager.sendMessage("WebConsole:LocationChange", message);
},
/**
* Destroy the ConsoleProgressListener.
*/
destroy: function CPL_destroy()
{
if (!this._initialized) {
return;
}
this._initialized = false;
this._fileActivity = false;
this._locationChange = false;
let webProgress = docShell.QueryInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(this);
},
};
Manager.init();
})();