From e7e699e7f7ce038606b0581d7bfc33d71ce532ca Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Tue, 29 May 2012 15:48:05 +0300 Subject: [PATCH] Bug 673148 - (async-webconsole) Part 4 - Make network logging async; r=rcampbell --- .../devtools/webconsole/HUDService-content.js | 1101 +++++++++- browser/devtools/webconsole/HUDService.jsm | 1769 +++-------------- browser/devtools/webconsole/Makefile.in | 1 + browser/devtools/webconsole/NetworkHelper.jsm | 103 +- browser/devtools/webconsole/NetworkPanel.jsm | 668 +++++++ browser/devtools/webconsole/PropertyPanel.jsm | 9 +- .../devtools/webconsole/WebConsoleUtils.jsm | 4 +- .../browser_webconsole_bug_595223_file_uri.js | 37 +- ..._webconsole_bug_599725_response_headers.js | 42 +- .../browser_webconsole_bug_600183_charset.js | 29 +- ...bconsole_bug_602572_log_bodies_checkbox.js | 16 +- ...browser_webconsole_bug_603750_websocket.js | 13 +- ...wser_webconsole_bug_618311_close_panels.js | 2 +- ..._webconsole_bug_618311_private_browsing.js | 2 +- ...le_bug_630733_response_redirect_headers.js | 52 +- .../test/browser_webconsole_bug_632817.js | 59 +- .../test/browser_webconsole_netlogging.js | 95 +- .../test/browser_webconsole_network_panel.js | 162 +- 18 files changed, 2379 insertions(+), 1785 deletions(-) create mode 100644 browser/devtools/webconsole/NetworkPanel.jsm diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js index d38d0b3123bf..803a3fafea19 100644 --- a/browser/devtools/webconsole/HUDService-content.js +++ b/browser/devtools/webconsole/HUDService-content.js @@ -17,6 +17,8 @@ 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; @@ -24,8 +26,12 @@ 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. /** @@ -37,9 +43,11 @@ let Manager = { hudId: null, _sequence: 0, _messageListeners: ["WebConsole:Init", "WebConsole:EnableFeature", - "WebConsole:DisableFeature", "WebConsole:Destroy"], + "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. @@ -102,6 +110,12 @@ let Manager = { 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; @@ -142,6 +156,8 @@ let Manager = { * 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. @@ -152,11 +168,17 @@ let Manager = { * 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]); @@ -237,9 +259,11 @@ let Manager = { * 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. * * @param string aFeature - * One of the supported features: JSTerm, ConsoleAPI. + * 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. @@ -260,6 +284,9 @@ let Manager = { case "PageError": ConsoleListener.init(aMessage); break; + case "NetworkMonitor": + NetworkMonitor.init(aMessage); + break; default: Cu.reportError("Web Console content: unknown feature " + aFeature); break; @@ -294,12 +321,81 @@ let Manager = { case "PageError": ConsoleListener.destroy(); break; + case "NetworkMonitor": + NetworkMonitor.destroy(); + 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. * @@ -388,12 +484,20 @@ let Manager = { this.hudId = null; this._messageHandlers = null; - Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = null; + + Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = NetworkMonitor = + NetworkResponseListener = ConsoleProgressListener = null; + Cc = Ci = Cu = XPCOMUtils = Services = gConsoleStorage = - WebConsoleUtils = l10n = null; + WebConsoleUtils = l10n = JSPropertyProvider = NetworkHelper = + NetUtil = activityDistributor = null; }, }; +/////////////////////////////////////////////////////////////////////////////// +// JavaScript Terminal +/////////////////////////////////////////////////////////////////////////////// + /** * JSTerm helper functions. * @@ -922,6 +1026,10 @@ let JSTerm = { }, }; +/////////////////////////////////////////////////////////////////////////////// +// 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. @@ -1111,6 +1219,10 @@ let ConsoleAPIObserver = { }, }; +/////////////////////////////////////////////////////////////////////////////// +// 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. @@ -1202,5 +1314,986 @@ let ConsoleListener = { }, }; +/////////////////////////////////////////////////////////////////////////////// +// 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 (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 = ""; + + 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) { + let webProgress = docShell.QueryInterface(Ci.nsIWebProgress); + this.progressListener = new ConsoleProgressListener(); + webProgress.addProgressListener(this.progressListener, + Ci.nsIWebProgress.NOTIFY_STATE_ALL); + } + }, + + /** + * 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 (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.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); + + if (this.progressListener) { + let webProgress = docShell.QueryInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(this.progressListener); + delete this.progressListener; + } + + delete this.openRequests; + delete this.openResponses; + }, +}; + +/** + * A WebProgressListener that listens for location changes. This progress + * listener is used to track file loads. 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). + * + * @constructor + */ +function ConsoleProgressListener() { } + +ConsoleProgressListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + onStateChange: function CPL_onStateChange(aProgress, aRequest, aState, + aStatus) + { + if (!_alive || !(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}); + }, + + onLocationChange: function() {}, + onStatusChange: function() {}, + onProgressChange: function() {}, + onSecurityChange: function() {}, +}; + Manager.init(); })(); diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 2837d6b514a7..405e67c7b1bd 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -12,18 +12,9 @@ const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/NetworkHelper.jsm"); var EXPORTED_SYMBOLS = ["HUDService", "ConsoleUtils"]; -XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor", - "@mozilla.org/network/http-activity-distributor;1", - "nsIHttpActivityDistributor"); - -XPCOMUtils.defineLazyServiceGetter(this, "mimeService", - "@mozilla.org/mime;1", - "nsIMIMEService"); - XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); @@ -34,46 +25,20 @@ XPCOMUtils.defineLazyGetter(this, "gcli", function () { return obj.gcli; }); -XPCOMUtils.defineLazyGetter(this, "CssRuleView", function() { - let tmp = {}; - Cu.import("resource:///modules/devtools/CssRuleView.jsm", tmp); - return tmp.CssRuleView; -}); - -XPCOMUtils.defineLazyGetter(this, "NetUtil", function () { - var obj = {}; - Cu.import("resource://gre/modules/NetUtil.jsm", obj); - return obj.NetUtil; -}); - XPCOMUtils.defineLazyGetter(this, "template", function () { var obj = {}; Cu.import("resource:///modules/devtools/Templater.jsm", obj); return obj.template; }); -XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () { - let obj = {}; - Cu.import("resource:///modules/PropertyPanel.jsm", obj); - return obj.PropertyPanel; -}); +XPCOMUtils.defineLazyModuleGetter(this, "PropertyPanel", + "resource:///modules/PropertyPanel.jsm"); -XPCOMUtils.defineLazyGetter(this, "PropertyTreeView", function () { - let obj = {}; - Cu.import("resource:///modules/PropertyPanel.jsm", obj); - return obj.PropertyTreeView; -}); +XPCOMUtils.defineLazyModuleGetter(this, "PropertyTreeView", + "resource:///modules/PropertyPanel.jsm"); -XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () { - var obj = {}; - try { - Cu.import("resource:///modules/AutocompletePopup.jsm", obj); - } - catch (err) { - Cu.reportError(err); - } - return obj.AutocompletePopup; -}); +XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", + "resource:///modules/AutocompletePopup.jsm"); XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () { var obj = {}; @@ -86,11 +51,11 @@ XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () { return obj.ScratchpadManager; }); -XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () { - let obj = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", obj); - return obj.WebConsoleUtils; -}); +XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel", + "resource:///modules/NetworkPanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", + "resource:///modules/WebConsoleUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "l10n", function() { return WebConsoleUtils.l10n; @@ -161,14 +126,8 @@ const LEVELS = { // The lowest HTTP response code (inclusive) that is considered an error. const MIN_HTTP_ERROR_CODE = 400; -// The highest HTTP response code (exclusive) that is considered an error. -const MAX_HTTP_ERROR_CODE = 600; - -// HTTP status codes. -const HTTP_MOVED_PERMANENTLY = 301; -const HTTP_FOUND = 302; -const HTTP_SEE_OTHER = 303; -const HTTP_TEMPORARY_REDIRECT = 307; +// The highest HTTP response code (inclusive) that is considered an error. +const MAX_HTTP_ERROR_CODE = 599; // The HTML namespace. const HTML_NS = "http://www.w3.org/1999/xhtml"; @@ -217,12 +176,6 @@ const ANIMATE_IN = 1; const HISTORY_BACK = -1; const HISTORY_FORWARD = 1; -// The maximum number of bytes a Network ResponseListener can hold. -const RESPONSE_BODY_LIMIT = 1024*1024; // 1 MB - -// The maximum uint32 value. -const PR_UINT32_MAX = 4294967295; - // Minimum console height, in pixels. const MINIMUM_CONSOLE_HEIGHT = 150; @@ -233,9 +186,6 @@ const MINIMUM_PAGE_HEIGHT = 50; // The default console height, as a ratio from the content window inner height. const DEFAULT_CONSOLE_HEIGHT = 0.33; -// Constant used when checking the typeof objects. -const TYPEOF_FUNCTION = "function"; - // This script is inserted into the content process. const CONTENT_SCRIPT_URL = "chrome://browser/content/devtools/HUDService-content.js"; @@ -252,278 +202,6 @@ const GROUP_INDENT = 12; // The pref prefix for webconsole filters const PREFS_PREFIX = "devtools.webconsole.filter."; -/** - * Implements the nsIStreamListener and nsIRequestObserver interface. Used - * within the HS_httpObserverFactory function to get the response body of - * requests. - * - * The code is mostly based on code listings from: - * - * http://www.softwareishard.com/blog/firebug/ - * nsitraceablechannel-intercept-http-traffic/ - * - * @param object aHttpActivity - * HttpActivity object associated with this request (see - * HS_httpObserverFactory). As the response is done, the response header, - * body and status is stored on aHttpActivity. - */ -function ResponseListener(aHttpActivity) { - this.receivedData = ""; - this.httpActivity = aHttpActivity; -} - -ResponseListener.prototype = -{ - /** - * 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 nsIRequest we are started for. - */ - request: null, - - /** - * Sets the httpActivity object's response header if it isn't set already. - * - * @param nsIRequest aRequest - */ - setResponseHeader: function RL_setResponseHeader(aRequest) - { - let httpActivity = this.httpActivity; - // Check if the header isn't set yet. - if (!httpActivity.response.header) { - if (aRequest instanceof Ci.nsIHttpChannel) { - httpActivity.response.header = {}; - try { - aRequest.visitResponseHeaders({ - visitHeader: function(aName, aValue) { - httpActivity.response.header[aName] = aValue; - } - }); - } - // Accessing the response header can throw an NS_ERROR_NOT_AVAILABLE - // exception. Catch it and stop it to make it not show up in the. - // This can happen if the response is not finished yet and the user - // reloades the page. - catch (ex) { - delete httpActivity.response.header; - } - } - } - }, - - /** - * 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. - * - * @returns void - */ - setAsyncListener: function RL_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 RL_onDataAvailable(aRequest, aContext, aInputStream, - aOffset, aCount) - { - this.setResponseHeader(aRequest); - - let data = NetUtil.readInputStreamToString(aInputStream, aCount); - - if (!this.httpActivity.response.bodyDiscarded && - 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 RL_onStartRequest(aRequest, aContext) - { - this.request = aRequest; - - // Always discard the response body if logging is not enabled in the Web - // Console. - this.httpActivity.response.bodyDiscarded = - !HUDService.saveRequestAndResponseBodies; - - // Check response status and discard the body for redirects. - if (!this.httpActivity.response.bodyDiscarded && - this.httpActivity.channel instanceof Ci.nsIHttpChannel) { - switch (this.httpActivity.channel.responseStatus) { - case HTTP_MOVED_PERMANENTLY: - case HTTP_FOUND: - case HTTP_SEE_OTHER: - case HTTP_TEMPORARY_REDIRECT: - this.httpActivity.response.bodyDiscarded = true; - break; - } - } - - // Asynchronously wait for the data coming from the request. - this.setAsyncListener(this.sink.inputStream, this); - }, - - /** - * Handle the onStopRequest by storing the response header is stored on the - * httpActivity object. The sink output stream is also closed. - * - * For more documentation about nsIRequestObserver go to: - * https://developer.mozilla.org/En/NsIRequestObserver - * - * @param nsIRequest aRequest - * The request we are observing. - * @param nsISupports aContext - * @param nsresult aStatusCode - */ - onStopRequest: function RL_onStopRequest(aRequest, aContext, aStatusCode) - { - // Retrieve the response headers, as they are, from the server. - let response = null; - for each (let item in HUDService.openResponseHeaders) { - if (item.channel === this.httpActivity.channel) { - response = item; - break; - } - } - - if (response) { - this.httpActivity.response.header = response.headers; - delete HUDService.openResponseHeaders[response.id]; - } - else { - this.setResponseHeader(aRequest); - } - - this.sink.outputStream.close(); - }, - - /** - * 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. - * - * @returns void - */ - onStreamClose: function RL_onStreamClose() - { - if (!this.httpActivity) { - return; - } - - // Remove our listener from the request input stream. - this.setAsyncListener(this.sink.inputStream, null); - - if (!this.httpActivity.response.bodyDiscarded && - HUDService.saveRequestAndResponseBodies) { - this.httpActivity.response.body = this.receivedData; - } - - if (HUDService.lastFinishedRequestCallback) { - HUDService.lastFinishedRequestCallback(this.httpActivity); - } - - // Call update on all panels. - this.httpActivity.panels.forEach(function(weakRef) { - let panel = weakRef.get(); - if (panel) { - panel.update(); - } - }); - this.httpActivity.response.isDone = true; - this.httpActivity = null; - this.receivedData = ""; - this.request = null; - this.sink = null; - this.inputStream = 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 RL_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(); - } - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIStreamListener, - Ci.nsIInputStreamCallback, - Ci.nsIRequestObserver, - Ci.nsISupports, - ]) -} - /////////////////////////////////////////////////////////////////////////// //// Helper for creating the network panel. @@ -551,637 +229,6 @@ function createElement(aDocument, aTag, aAttributes) return node; } -/** - * Creates a new DOMNode and appends it to aParent. - * - * @param nsIDOMNode aParent - * A parent node to append the created element. - * @param string aTag - * Name of the tag for the DOMNode. - * @param object aAttributes - * Attributes set on the created DOMNode. - * - * @returns nsIDOMNode - */ -function createAndAppendElement(aParent, aTag, aAttributes) -{ - let node = createElement(aParent.ownerDocument, aTag, aAttributes); - aParent.appendChild(node); - return node; -} - -/////////////////////////////////////////////////////////////////////////// -//// NetworkPanel - -/** - * Creates a new NetworkPanel. - * - * @param nsIDOMNode aParent - * Parent node to append the created panel to. - * @param object aHttpActivity - * HttpActivity to display in the panel. - */ -function NetworkPanel(aParent, aHttpActivity) -{ - let doc = aParent.ownerDocument; - this.httpActivity = aHttpActivity; - - // Create the underlaying panel - this.panel = createElement(doc, "panel", { - label: l10n.getStr("NetworkPanel.label"), - titlebar: "normal", - noautofocus: "true", - noautohide: "true", - close: "true" - }); - - // Create the iframe that displays the NetworkPanel XHTML. - this.iframe = createAndAppendElement(this.panel, "iframe", { - src: "chrome://browser/content/NetworkPanel.xhtml", - type: "content", - flex: "1" - }); - - let self = this; - - // Destroy the panel when it's closed. - this.panel.addEventListener("popuphidden", function onPopupHide() { - self.panel.removeEventListener("popuphidden", onPopupHide, false); - self.panel.parentNode.removeChild(self.panel); - self.panel = null; - self.iframe = null; - self.document = null; - self.httpActivity = null; - - if (self.linkNode) { - self.linkNode._panelOpen = false; - self.linkNode = null; - } - }, false); - - // Set the document object and update the content once the panel is loaded. - this.panel.addEventListener("load", function onLoad() { - self.panel.removeEventListener("load", onLoad, true); - self.document = self.iframe.contentWindow.document; - self.update(); - }, true); - - // Create the footer. - let footer = createElement(doc, "hbox", { align: "end" }); - createAndAppendElement(footer, "spacer", { flex: 1 }); - - createAndAppendElement(footer, "resizer", { dir: "bottomend" }); - this.panel.appendChild(footer); - - aParent.appendChild(this.panel); -} - -NetworkPanel.prototype = -{ - /** - * Callback is called once the NetworkPanel is processed completly. Used by - * unit tests. - */ - isDoneCallback: null, - - /** - * The current state of the output. - */ - _state: 0, - - /** - * State variables. - */ - _INIT: 0, - _DISPLAYED_REQUEST_HEADER: 1, - _DISPLAYED_REQUEST_BODY: 2, - _DISPLAYED_RESPONSE_HEADER: 3, - _TRANSITION_CLOSED: 4, - - _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/, - - /** - * Small helper function that is nearly equal to HUDService.getFormatStr - * except that it prefixes aName with "NetworkPanel.". - * - * @param string aName - * The name of an i10n string to format. This string is prefixed with - * "NetworkPanel." before calling the HUDService.getFormatStr function. - * @param array aArray - * Values used as placeholder for the i10n string. - * @returns string - * The i10n formated string. - */ - _format: function NP_format(aName, aArray) - { - return l10n.getFormatStr("NetworkPanel." + aName, aArray); - }, - - /** - * Returns the content type of the response body. This is based on the - * response.header["Content-Type"] info. If this value is not available, then - * the content type is tried to be estimated by the url file ending. - * - * @returns string or null - * Content type or null if no content type could be figured out. - */ - get _contentType() - { - let response = this.httpActivity.response; - let contentTypeValue = null; - - if (response.header && response.header["Content-Type"]) { - let types = response.header["Content-Type"].split(/,|;/); - for (let i = 0; i < types.length; i++) { - let type = NetworkHelper.mimeCategoryMap[types[i]]; - if (type) { - return types[i]; - } - } - } - - // Try to get the content type from the request file extension. - let uri = NetUtil.newURI(this.httpActivity.url); - let mimeType = null; - if ((uri instanceof Ci.nsIURL) && uri.fileExtension) { - try { - mimeType = mimeService.getTypeFromExtension(uri.fileExtension); - } catch(e) { - // Added to prevent failures on OS X 64. No Flash? - Cu.reportError(e); - // Return empty string to pass unittests. - return ""; - } - } - return mimeType; - }, - - /** - * - * @returns boolean - * True if the response is an image, false otherwise. - */ - get _responseIsImage() - { - return NetworkHelper.mimeCategoryMap[this._contentType] == "image"; - }, - - /** - * - * @returns boolean - * True if the response body contains text, false otherwise. - */ - get _isResponseBodyTextData() - { - let contentType = this._contentType; - - if (!contentType) - return false; - - if (contentType.indexOf("text/") == 0) { - return true; - } - - switch (NetworkHelper.mimeCategoryMap[contentType]) { - case "txt": - case "js": - case "json": - case "css": - case "html": - case "svg": - case "xml": - return true; - - default: - return false; - } - }, - - /** - * - * @returns boolean - * Returns true if the server responded that the request is already - * in the browser's cache, false otherwise. - */ - get _isResponseCached() - { - return this.httpActivity.response.status.indexOf("304") != -1; - }, - - /** - * - * @returns boolean - * Returns true if the posted body contains form data. - */ - get _isRequestBodyFormData() - { - let requestBody = this.httpActivity.request.body; - return this._fromDataRegExp.test(requestBody); - }, - - /** - * Appends the node with id=aId by the text aValue. - * - * @param string aId - * @param string aValue - * @returns void - */ - _appendTextNode: function NP_appendTextNode(aId, aValue) - { - let textNode = this.document.createTextNode(aValue); - this.document.getElementById(aId).appendChild(textNode); - }, - - /** - * Generates some HTML to display the key-value pair of the aList data. The - * generated HTML is added to node with id=aParentId. - * - * @param string aParentId - * Id of the parent node to append the list to. - * @oaram object aList - * Object that holds the key-value information to display in aParentId. - * @param boolean aIgnoreCookie - * If true, the key-value named "Cookie" is not added to the list. - * @returns void - */ - _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie) - { - let parent = this.document.getElementById(aParentId); - let doc = this.document; - - let sortedList = {}; - Object.keys(aList).sort().forEach(function(aKey) { - sortedList[aKey] = aList[aKey]; - }); - - for (let key in sortedList) { - if (aIgnoreCookie && key == "Cookie") { - continue; - } - - /** - * The following code creates the HTML: - * - * ${line}: - * ${aList[line]} - * - * and adds it to parent. - */ - let row = doc.createElement("tr"); - let textNode = doc.createTextNode(key + ":"); - let th = doc.createElement("th"); - th.setAttribute("scope", "row"); - th.setAttribute("class", "property-name"); - th.appendChild(textNode); - row.appendChild(th); - - textNode = doc.createTextNode(sortedList[key]); - let td = doc.createElement("td"); - td.setAttribute("class", "property-value"); - td.appendChild(textNode); - row.appendChild(td); - - parent.appendChild(row); - } - }, - - /** - * Displays the node with id=aId. - * - * @param string aId - * @returns void - */ - _displayNode: function NP_displayNode(aId) - { - this.document.getElementById(aId).style.display = "block"; - }, - - /** - * Sets the request URL, request method, the timing information when the - * request started and the request header content on the NetworkPanel. - * If the request header contains cookie data, a list of sent cookies is - * generated and a special sent cookie section is displayed + the cookie list - * added to it. - * - * @returns void - */ - _displayRequestHeader: function NP_displayRequestHeader() - { - let timing = this.httpActivity.timing; - let request = this.httpActivity.request; - - this._appendTextNode("headUrl", this.httpActivity.url); - this._appendTextNode("headMethod", this.httpActivity.method); - - this._appendTextNode("requestHeadersInfo", - l10n.timestampString(timing.REQUEST_HEADER/1000)); - - this._appendList("requestHeadersContent", request.header, true); - - if ("Cookie" in request.header) { - this._displayNode("requestCookie"); - - let cookies = request.header.Cookie.split(";"); - let cookieList = {}; - let cookieListSorted = {}; - cookies.forEach(function(cookie) { - let name, value; - [name, value] = cookie.trim().split("="); - cookieList[name] = value; - }); - this._appendList("requestCookieContent", cookieList); - } - }, - - /** - * Displays the request body section of the NetworkPanel and set the request - * body content on the NetworkPanel. - * - * @returns void - */ - _displayRequestBody: function NP_displayRequestBody() { - this._displayNode("requestBody"); - this._appendTextNode("requestBodyContent", this.httpActivity.request.body); - }, - - /* - * Displays the `sent form data` section. Parses the request header for the - * submitted form data displays it inside of the `sent form data` section. - * - * @returns void - */ - _displayRequestForm: function NP_processRequestForm() { - let requestBodyLines = this.httpActivity.request.body.split("\n"); - let formData = requestBodyLines[requestBodyLines.length - 1]. - replace(/\+/g, " ").split("&"); - - function unescapeText(aText) - { - try { - return decodeURIComponent(aText); - } - catch (ex) { - return decodeURIComponent(unescape(aText)); - } - } - - let formDataObj = {}; - for (let i = 0; i < formData.length; i++) { - let data = formData[i]; - let idx = data.indexOf("="); - let key = data.substring(0, idx); - let value = data.substring(idx + 1); - formDataObj[unescapeText(key)] = unescapeText(value); - } - - this._appendList("requestFormDataContent", formDataObj); - this._displayNode("requestFormData"); - }, - - /** - * Displays the response section of the NetworkPanel, sets the response status, - * the duration between the start of the request and the receiving of the - * response header as well as the response header content on the the NetworkPanel. - * - * @returns void - */ - _displayResponseHeader: function NP_displayResponseHeader() - { - let timing = this.httpActivity.timing; - let response = this.httpActivity.response; - - this._appendTextNode("headStatus", response.status); - - let deltaDuration = - Math.round((timing.RESPONSE_HEADER - timing.REQUEST_HEADER) / 1000); - this._appendTextNode("responseHeadersInfo", - this._format("durationMS", [deltaDuration])); - - this._displayNode("responseContainer"); - this._appendList("responseHeadersContent", response.header); - }, - - /** - * Displays the respones image section, sets the source of the image displayed - * in the image response section to the request URL and the duration between - * the receiving of the response header and the end of the request. Once the - * image is loaded, the size of the requested image is set. - * - * @returns void - */ - _displayResponseImage: function NP_displayResponseImage() - { - let self = this; - let timing = this.httpActivity.timing; - let response = this.httpActivity.response; - let cached = ""; - - if (this._isResponseCached) { - cached = "Cached"; - } - - let imageNode = this.document.getElementById("responseImage" + cached +"Node"); - imageNode.setAttribute("src", this.httpActivity.url); - - // This function is called to set the imageInfo. - function setImageInfo() { - let deltaDuration = - Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); - self._appendTextNode("responseImage" + cached + "Info", - self._format("imageSizeDeltaDurationMS", [ - imageNode.width, imageNode.height, deltaDuration - ] - )); - } - - // Check if the image is already loaded. - if (imageNode.width != 0) { - setImageInfo(); - } - else { - // Image is not loaded yet therefore add a load event. - imageNode.addEventListener("load", function imageNodeLoad() { - imageNode.removeEventListener("load", imageNodeLoad, false); - setImageInfo(); - }, false); - } - - this._displayNode("responseImage" + cached); - }, - - /** - * Displays the response body section, sets the the duration between - * the receiving of the response header and the end of the request as well as - * the content of the response body on the NetworkPanel. - * - * @param [optional] string aCachedContent - * Cached content for this request. If this argument is set, the - * responseBodyCached section is displayed. - * @returns void - */ - _displayResponseBody: function NP_displayResponseBody(aCachedContent) - { - let timing = this.httpActivity.timing; - let response = this.httpActivity.response; - let cached = ""; - if (aCachedContent) { - cached = "Cached"; - } - - let deltaDuration = - Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); - this._appendTextNode("responseBody" + cached + "Info", - this._format("durationMS", [deltaDuration])); - - this._displayNode("responseBody" + cached); - this._appendTextNode("responseBody" + cached + "Content", - aCachedContent || response.body); - }, - - /** - * Displays the `Unknown Content-Type hint` and sets the duration between the - * receiving of the response header on the NetworkPanel. - * - * @returns void - */ - _displayResponseBodyUnknownType: function NP_displayResponseBodyUnknownType() - { - let timing = this.httpActivity.timing; - - this._displayNode("responseBodyUnknownType"); - let deltaDuration = - Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); - this._appendTextNode("responseBodyUnknownTypeInfo", - this._format("durationMS", [deltaDuration])); - - this._appendTextNode("responseBodyUnknownTypeContent", - this._format("responseBodyUnableToDisplay.content", [this._contentType])); - }, - - /** - * Displays the `no response body` section and sets the the duration between - * the receiving of the response header and the end of the request. - * - * @returns void - */ - _displayNoResponseBody: function NP_displayNoResponseBody() - { - let timing = this.httpActivity.timing; - - this._displayNode("responseNoBody"); - let deltaDuration = - Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000); - this._appendTextNode("responseNoBodyInfo", - this._format("durationMS", [deltaDuration])); - }, - - /* - * Calls the isDoneCallback function if one is specified. - */ - _callIsDone: function() { - if (this.isDoneCallback) { - this.isDoneCallback(); - } - }, - - /** - * Updates the content of the NetworkPanel's iframe. - * - * @returns void - */ - update: function NP_update() - { - /** - * After the iframe's contentWindow is ready, the document object is set. - * If the document object isn't set yet, then the page is loaded and nothing - * can be updated. - */ - if (!this.document) { - return; - } - - let timing = this.httpActivity.timing; - let request = this.httpActivity.request; - let response = this.httpActivity.response; - - switch (this._state) { - case this._INIT: - this._displayRequestHeader(); - this._state = this._DISPLAYED_REQUEST_HEADER; - // FALL THROUGH - - case this._DISPLAYED_REQUEST_HEADER: - // Process the request body if there is one. - if (!request.bodyDiscarded && request.body) { - // Check if we send some form data. If so, display the form data special. - if (this._isRequestBodyFormData) { - this._displayRequestForm(); - } - else { - this._displayRequestBody(); - } - this._state = this._DISPLAYED_REQUEST_BODY; - } - // FALL THROUGH - - case this._DISPLAYED_REQUEST_BODY: - // There is always a response header. Therefore we can skip here if - // we don't have a response header yet and don't have to try updating - // anything else in the NetworkPanel. - if (!response.header) { - break - } - this._displayResponseHeader(); - this._state = this._DISPLAYED_RESPONSE_HEADER; - // FALL THROUGH - - case this._DISPLAYED_RESPONSE_HEADER: - // Check if the transition is done. - if (timing.TRANSACTION_CLOSE && response.isDone) { - if (response.bodyDiscarded) { - this._callIsDone(); - } - else if (this._responseIsImage) { - this._displayResponseImage(); - this._callIsDone(); - } - else if (!this._isResponseBodyTextData) { - this._displayResponseBodyUnknownType(); - this._callIsDone(); - } - else if (response.body) { - this._displayResponseBody(); - this._callIsDone(); - } - else if (this._isResponseCached) { - let self = this; - NetworkHelper.loadFromCache(this.httpActivity.url, - this.httpActivity.charset, - function(aContent) { - // If some content could be loaded from the cache, then display - // the body. - if (aContent) { - self._displayResponseBody(aContent); - self._callIsDone(); - } - // Otherwise, show the "There is no response body" hint. - else { - self._displayNoResponseBody(); - self._callIsDone(); - } - }); - } - else { - this._displayNoResponseBody(); - this._callIsDone(); - } - this._state = this._TRANSITION_CLOSED; - } - break; - } - } -} - /////////////////////////////////////////////////////////////////////////// //// Private utility functions for the HUD service @@ -1271,11 +318,6 @@ function HUD_SERVICE() // Remembers the last console height, in pixels. this.lastConsoleHeight = Services.prefs.getIntPref("devtools.hud.height"); - // Network response bodies are piped through a buffer of the given size (in - // bytes). - this.responsePipeSegmentSize = - Services.prefs.getIntPref("network.buffer.cache.size"); - /** * Collection of HUDIds that map to the tabs/windows/contexts * that a HeadsUpDisplay can be activated for. @@ -1296,16 +338,6 @@ function HUD_SERVICE() * Keeps a reference for each HeadsUpDisplay that is created */ this.hudReferences = {}; - - /** - * Requests that haven't finished yet. - */ - this.openRequests = {}; - - /** - * Response headers for requests that haven't finished yet. - */ - this.openResponseHeaders = {}; }; HUD_SERVICE.prototype = @@ -1325,12 +357,6 @@ HUD_SERVICE.prototype = */ sequencer: null, - /** - * Whether to save the bodies of network requests and responses. Disabled by - * default to save memory. - */ - saveRequestAndResponseBodies: false, - /** * Tell the HUDService that a HeadsUpDisplay can be activated * for the window or context that has 'aContextDOMId' node id @@ -1450,10 +476,6 @@ HUD_SERVICE.prototype = this.storeHeight(hudId); } - let hud = this.hudReferences[hudId]; - browser.webProgress.removeProgressListener(hud.progressListener); - delete hud.progressListener; - this.unregisterDisplay(hudId); window.focus(); @@ -1727,6 +749,8 @@ HUD_SERVICE.prototype = hud.gcliterm.clearOutput(); } + let document = hud.chromeDocument; + hud.destroy(); // Make sure that the console panel does not try to call @@ -1758,14 +782,14 @@ HUD_SERVICE.prototype = this.unregisterActiveContext(aHUDId); - let popupset = hud.chromeDocument.getElementById("mainPopupSet"); + let popupset = document.getElementById("mainPopupSet"); let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]"); for (let i = 0; i < panels.length; i++) { panels[i].hidePopup(); } if (Object.keys(this.hudReferences).length == 0) { - let autocompletePopup = hud.chromeDocument. + let autocompletePopup = document. getElementById("webConsole_autocompletePopup"); if (autocompletePopup) { autocompletePopup.parentNode.removeChild(autocompletePopup); @@ -1787,9 +811,6 @@ HUD_SERVICE.prototype = return; } - // begin observing HTTP traffic - this.startHTTPObservation(); - WebConsoleObserver.init(); }, @@ -1801,17 +822,7 @@ HUD_SERVICE.prototype = */ suspend: function HS_suspend() { - activityDistributor.removeObserver(this.httpObserver); - delete this.httpObserver; - - Services.obs.removeObserver(this.httpResponseExaminer, - "http-on-examine-response"); - - this.openRequests = {}; - this.openResponseHeaders = {}; - delete this.defaultFilterPrefs; - delete this.lastFinishedRequestCallback; WebConsoleObserver.uninit(); @@ -1945,8 +956,11 @@ HUD_SERVICE.prototype = applicationHooks: null, /** - * Assign a function to this property to listen for finished httpRequests. - * Used by unit tests. + * Assign a function to this property to listen for every request that + * completes. Used by unit tests. The callback takes one argument: the HTTP + * activity object as received from the remote Web Console. + * + * @type function */ lastFinishedRequestCallback: null, @@ -1954,11 +968,12 @@ HUD_SERVICE.prototype = * Opens a NetworkPanel. * * @param nsIDOMNode aNode - * DOMNode to display the panel next to. + * The message node you want the panel to be anchored to. * @param object aHttpActivity - * httpActivity object. The data of this object is displayed in the - * NetworkPanel. - * @returns NetworkPanel + * The HTTP activity object that holds network request and response + * information. This object is given to the NetworkPanel constructor. + * @return object + * The new NetworkPanel instance. */ openNetworkPanel: function HS_openNetworkPanel(aNode, aHttpActivity) { @@ -1966,421 +981,25 @@ HUD_SERVICE.prototype = let parent = doc.getElementById("mainPopupSet"); let netPanel = new NetworkPanel(parent, aHttpActivity); netPanel.linkNode = aNode; + aNode._netPanel = netPanel; let panel = netPanel.panel; panel.openPopup(aNode, "after_pointer", 0, 0, false, false); panel.sizeTo(450, 500); panel.setAttribute("hudId", aHttpActivity.hudId); - aHttpActivity.panels.push(Cu.getWeakReference(netPanel)); + + panel.addEventListener("popuphiding", function HS_netPanel_onHide() { + panel.removeEventListener("popuphiding", HS_netPanel_onHide); + + aNode._panelOpen = false; + aNode._netPanel = null; + }); + + aNode._panelOpen = true; + return netPanel; }, - /** - * Begin observing HTTP traffic that we care about, - * namely traffic that originates inside any context that a Heads Up Display - * is active for. - */ - startHTTPObservation: function HS_httpObserverFactory() - { - // creates an observer for http traffic - var self = this; - var httpObserver = { - observeActivity : - function HS_SHO_observeActivity(aChannel, - aActivityType, - aActivitySubtype, - aTimestamp, - aExtraSizeData, - aExtraStringData) - { - if (aActivityType == - activityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION || - aActivityType == - activityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { - - aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); - - let transCodes = this.httpTransactionCodes; - let hudId; - - if (aActivitySubtype == - activityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER ) { - // Try to get the source window of the request. - let win = NetworkHelper.getWindowForRequest(aChannel); - if (!win) { - return; - } - - // Try to get the hudId that is associated to the window. - hudId = self.getHudIdByWindow(win.top); - if (!hudId) { - return; - } - - // The httpActivity object will hold all information concerning - // this request and later response. - - let httpActivity = { - id: self.sequenceId(), - hudId: hudId, - url: aChannel.URI.spec, - method: aChannel.requestMethod, - channel: aChannel, - charset: win.document.characterSet, - - panels: [], - request: { - header: { } - }, - response: { - header: null - }, - timing: { - "REQUEST_HEADER": aTimestamp - } - }; - - // Add a new output entry. - let loggedNode = self.logNetActivity(httpActivity); - - // In some cases loggedNode can be undefined (e.g. if an image was - // requested). Don't continue in such a case. - if (!loggedNode) { - return; - } - - aChannel.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, HUDService.responsePipeSegmentSize, - PR_UINT32_MAX, null); - - // Add listener for the response body. - let newListener = new ResponseListener(httpActivity); - - // 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 = aChannel.setNewListener(tee); - - tee.init(originalListener, sink.outputStream, newListener); - - // Copy the request header data. - aChannel.visitRequestHeaders({ - visitHeader: function(aName, aValue) { - httpActivity.request.header[aName] = aValue; - } - }); - - // Store the loggedNode and the httpActivity object for later reuse. - let linkNode = loggedNode.querySelector(".webconsole-msg-link"); - - httpActivity.messageObject = { - messageNode: loggedNode, - linkNode: linkNode - }; - self.openRequests[httpActivity.id] = httpActivity; - - // Make the network span clickable. - linkNode.setAttribute("aria-haspopup", "true"); - linkNode.addEventListener("mousedown", function(aEvent) { - this._startX = aEvent.clientX; - this._startY = aEvent.clientY; - }, false); - - linkNode.addEventListener("click", function(aEvent) { - if (aEvent.detail != 1 || aEvent.button != 0 || - (this._startX != aEvent.clientX && - this._startY != aEvent.clientY)) { - return; - } - - if (!this._panelOpen) { - self.openNetworkPanel(this, httpActivity); - this._panelOpen = true; - } - }, false); - } - else { - // 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 self.openRequests) { - if (item.channel !== aChannel) { - continue; - } - httpActivity = item; - break; - } - - if (!httpActivity) { - return; - } - - hudId = httpActivity.hudId; - let msgObject = httpActivity.messageObject; - - let updatePanel = false; - let data; - // Store the time information for this activity subtype. - httpActivity.timing[transCodes[aActivitySubtype]] = aTimestamp; - - switch (aActivitySubtype) { - case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: { - if (!self.saveRequestAndResponseBodies) { - httpActivity.request.bodyDiscarded = true; - break; - } - - let gBrowser = msgObject.messageNode.ownerDocument. - defaultView.gBrowser; - let HUD = HUDService.hudReferences[hudId]; - let browser = gBrowser. - getBrowserForDocument(HUD.contentDocument); - - let sentBody = NetworkHelper. - readPostTextFromRequest(aChannel, browser); - if (!sentBody) { - // 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. - if (httpActivity.url == browser.contentWindow.location.href) { - sentBody = NetworkHelper.readPostTextFromPage(browser); - } - if (!sentBody) { - sentBody = ""; - } - } - httpActivity.request.body = sentBody; - break; - } - - case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: { - // aExtraStringData contains the response header. The first line - // contains the response status (e.g. HTTP/1.1 200 OK). - // - // Note: The response header is not saved here. Calling the - // aChannel.visitResponseHeaders at this point sometimes - // causes an NS_ERROR_NOT_AVAILABLE exception. Therefore, - // the response header and response body is stored on the - // httpActivity object within the RL_onStopRequest function. - httpActivity.response.status = - aExtraStringData.split(/\r\n|\n|\r/)[0]; - - // Add the response status. - let linkNode = msgObject.linkNode; - let statusNode = linkNode. - querySelector(".webconsole-msg-status"); - let statusText = "[" + httpActivity.response.status + "]"; - statusNode.setAttribute("value", statusText); - - let clipboardTextPieces = - [ httpActivity.method, httpActivity.url, statusText ]; - msgObject.messageNode.clipboardText = - clipboardTextPieces.join(" "); - - let status = parseInt(httpActivity.response.status. - replace(/^HTTP\/\d\.\d (\d+).+$/, "$1")); - - if (status >= MIN_HTTP_ERROR_CODE && - status < MAX_HTTP_ERROR_CODE) { - ConsoleUtils.setMessageType(msgObject.messageNode, - CATEGORY_NETWORK, - SEVERITY_ERROR); - } - - break; - } - - case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: { - let timing = httpActivity.timing; - let requestDuration = - Math.round((timing.RESPONSE_COMPLETE - - timing.REQUEST_HEADER) / 1000); - - // Add the request duration. - let linkNode = msgObject.linkNode; - let statusNode = linkNode. - querySelector(".webconsole-msg-status"); - - let statusText = httpActivity.response.status; - let timeText = l10n.getFormatStr("NetworkPanel.durationMS", - [ requestDuration ]); - let fullStatusText = "[" + statusText + " " + timeText + "]"; - statusNode.setAttribute("value", fullStatusText); - - let clipboardTextPieces = - [ httpActivity.method, httpActivity.url, fullStatusText ]; - msgObject.messageNode.clipboardText = - clipboardTextPieces.join(" "); - - delete httpActivity.messageObject; - delete self.openRequests[httpActivity.id]; - updatePanel = true; - break; - } - } - - if (updatePanel) { - httpActivity.panels.forEach(function(weakRef) { - let panel = weakRef.get(); - if (panel) { - panel.update(); - } - }); - } - } - } - }, - - 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" - } - }; - - this.httpObserver = httpObserver; - - activityDistributor.addObserver(httpObserver); - - // This is used to find the correct HTTP response headers. - Services.obs.addObserver(this.httpResponseExaminer, - "http-on-examine-response", false); - }, - - /** - * Observe notifications for the http-on-examine-response topic, coming from - * the nsIObserver service. - * - * @param string aTopic - * @param nsIHttpChannel aSubject - * @returns void - */ - httpResponseExaminer: function HS_httpResponseExaminer(aSubject, aTopic) - { - if (aTopic != "http-on-examine-response" || - !(aSubject instanceof Ci.nsIHttpChannel)) { - return; - } - - let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); - let win = NetworkHelper.getWindowForRequest(channel); - if (!win) { - return; - } - let hudId = HUDService.getHudIdByWindow(win); - if (!hudId) { - return; - } - - let response = { - id: HUDService.sequenceId(), - hudId: hudId, - channel: channel, - headers: {}, - }; - - try { - channel.visitResponseHeaders({ - visitHeader: function(aName, aValue) { - response.headers[aName] = aValue; - } - }); - } - catch (ex) { - delete response.headers; - } - - if (response.headers) { - HUDService.openResponseHeaders[response.id] = response; - } - }, - - /** - * Logs network activity. - * - * @param object aActivityObject - * The activity to log. - * @returns void - */ - logNetActivity: function HS_logNetActivity(aActivityObject) - { - let hudId = aActivityObject.hudId; - let outputNode = this.hudReferences[hudId].outputNode; - - let chromeDocument = outputNode.ownerDocument; - let msgNode = chromeDocument.createElementNS(XUL_NS, "hbox"); - - let methodNode = chromeDocument.createElementNS(XUL_NS, "label"); - methodNode.setAttribute("value", aActivityObject.method); - methodNode.classList.add("webconsole-msg-body-piece"); - msgNode.appendChild(methodNode); - - let linkNode = chromeDocument.createElementNS(XUL_NS, "hbox"); - linkNode.setAttribute("flex", "1"); - linkNode.classList.add("webconsole-msg-body-piece"); - linkNode.classList.add("webconsole-msg-link"); - msgNode.appendChild(linkNode); - - let urlNode = chromeDocument.createElementNS(XUL_NS, "label"); - urlNode.setAttribute("crop", "center"); - urlNode.setAttribute("flex", "1"); - urlNode.setAttribute("title", aActivityObject.url); - urlNode.setAttribute("value", aActivityObject.url); - urlNode.classList.add("hud-clickable"); - urlNode.classList.add("webconsole-msg-body-piece"); - urlNode.classList.add("webconsole-msg-url"); - linkNode.appendChild(urlNode); - - let statusNode = chromeDocument.createElementNS(XUL_NS, "label"); - statusNode.setAttribute("value", ""); - statusNode.classList.add("hud-clickable"); - statusNode.classList.add("webconsole-msg-body-piece"); - statusNode.classList.add("webconsole-msg-status"); - linkNode.appendChild(statusNode); - - let clipboardText = aActivityObject.method + " " + aActivityObject.url; - - let messageNode = ConsoleUtils.createMessageNode(chromeDocument, - CATEGORY_NETWORK, - SEVERITY_LOG, - msgNode, - hudId, - null, - null, - clipboardText); - - ConsoleUtils.outputMessageNode(messageNode, aActivityObject.hudId); - return messageNode; - }, - /** * Creates a generator that always returns a unique number for use in the * indexes @@ -2521,11 +1140,6 @@ HUD_SERVICE.prototype = HUDService.registerHUDReference(hud); let windowId = WebConsoleUtils.getOuterWindowId(aContentWindow.top); this.windowIds[windowId] = hudId; - - hud.progressListener = new ConsoleProgressListener(hudId); - - _browser.webProgress.addProgressListener(hud.progressListener, - Ci.nsIWebProgress.NOTIFY_STATE_ALL); } else { hud = this.hudReferences[hudId]; @@ -2845,7 +1459,8 @@ HeadsUpDisplay.prototype = { _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI", "WebConsole:CachedMessages", "WebConsole:PageError", "JSTerm:EvalResult", "JSTerm:AutocompleteProperties", "JSTerm:ClearOutput", - "JSTerm:InspectObject"], + "JSTerm:InspectObject", "WebConsole:NetworkActivity", + "WebConsole:FileActivity"], consolePanel: null, @@ -2854,6 +1469,34 @@ HeadsUpDisplay.prototype = { */ groupDepth: 0, + _saveRequestAndResponseBodies: false, + + /** + * Tells whether to save the bodies of network requests and responses. + * Disabled by default to save memory. + * @type boolean + */ + get saveRequestAndResponseBodies() this._saveRequestAndResponseBodies, + + /** + * Setter for saving of network request and response bodies. + * + * @param boolean aValue + * The new value you want to set. + */ + set saveRequestAndResponseBodies(aValue) { + this._saveRequestAndResponseBodies = aValue; + + let message = { + preferences: { + "NetworkMonitor.saveRequestAndResponseBodies": + this._saveRequestAndResponseBodies, + }, + }; + + this.sendMessageToContent("WebConsole:SetPreferences", message); + }, + get mainPopupSet() { return this.chromeDocument.getElementById("mainPopupSet"); @@ -3524,9 +2167,8 @@ HeadsUpDisplay.prototype = { let id = this.hudId + "-output-contextmenu"; menuPopup.setAttribute("id", id); menuPopup.addEventListener("popupshowing", function() { - saveBodiesItem.setAttribute("checked", - HUDService.saveRequestAndResponseBodies); - }, true); + saveBodiesItem.setAttribute("checked", this.saveRequestAndResponseBodies); + }.bind(this), true); let saveBodiesItem = this.makeXULNode("menuitem"); saveBodiesItem.setAttribute("label", l10n.getStr("saveBodies.label")); @@ -3535,6 +2177,7 @@ HeadsUpDisplay.prototype = { saveBodiesItem.setAttribute("type", "checkbox"); saveBodiesItem.setAttribute("buttonType", "saveBodies"); saveBodiesItem.setAttribute("oncommand", "HUDConsoleUI.command(this);"); + saveBodiesItem.setAttribute("hudId", this.hudId); menuPopup.appendChild(saveBodiesItem); menuPopup.appendChild(this.makeXULNode("menuseparator")); @@ -3927,6 +2570,106 @@ HeadsUpDisplay.prototype = { ConsoleUtils.outputMessageNode(node, this.hudId); }, + /** + * Log network activity. + * + * @param object aHttpActivity + * The HTTP activity to log. + */ + logNetActivity: function HUD_logNetActivity(aHttpActivity) + { + let entry = aHttpActivity.log.entries[0]; + let request = entry.request; + + let msgNode = this.chromeDocument.createElementNS(XUL_NS, "hbox"); + + let methodNode = this.chromeDocument.createElementNS(XUL_NS, "label"); + methodNode.setAttribute("value", request.method); + methodNode.classList.add("webconsole-msg-body-piece"); + msgNode.appendChild(methodNode); + + let linkNode = this.chromeDocument.createElementNS(XUL_NS, "hbox"); + linkNode.setAttribute("flex", "1"); + linkNode.classList.add("webconsole-msg-body-piece"); + linkNode.classList.add("webconsole-msg-link"); + msgNode.appendChild(linkNode); + + let urlNode = this.chromeDocument.createElementNS(XUL_NS, "label"); + urlNode.setAttribute("crop", "center"); + urlNode.setAttribute("flex", "1"); + urlNode.setAttribute("title", request.url); + urlNode.setAttribute("value", request.url); + urlNode.classList.add("hud-clickable"); + urlNode.classList.add("webconsole-msg-body-piece"); + urlNode.classList.add("webconsole-msg-url"); + linkNode.appendChild(urlNode); + + let statusNode = this.chromeDocument.createElementNS(XUL_NS, "label"); + statusNode.setAttribute("value", ""); + statusNode.classList.add("hud-clickable"); + statusNode.classList.add("webconsole-msg-body-piece"); + statusNode.classList.add("webconsole-msg-status"); + linkNode.appendChild(statusNode); + + let clipboardText = request.method + " " + request.url; + + let messageNode = ConsoleUtils.createMessageNode(this.chromeDocument, + CATEGORY_NETWORK, + SEVERITY_LOG, + msgNode, + this.hudId, + null, + null, + clipboardText); + + messageNode.setAttribute("connectionId", entry.connection); + + messageNode._httpActivity = aHttpActivity; + + this.makeOutputMessageLink(messageNode, function HUD_net_message_link() { + if (!messageNode._panelOpen) { + HUDService.openNetworkPanel(messageNode, messageNode._httpActivity); + } + }.bind(this)); + + ConsoleUtils.outputMessageNode(messageNode, this.hudId); + }, + + /** + * Log file activity. + * + * @param string aFileURI + * The file URI that was loaded. + */ + logFileActivity: function HUD_logFileActivity(aFileURI) + { + let chromeDocument = this.chromeDocument; + + let urlNode = chromeDocument.createElementNS(XUL_NS, "label"); + urlNode.setAttribute("crop", "center"); + urlNode.setAttribute("flex", "1"); + urlNode.setAttribute("title", aFileURI); + urlNode.setAttribute("value", aFileURI); + urlNode.classList.add("hud-clickable"); + urlNode.classList.add("webconsole-msg-url"); + + let outputNode = ConsoleUtils.createMessageNode(chromeDocument, + CATEGORY_NETWORK, + SEVERITY_LOG, + urlNode, + this.hudId, + null, + null, + aFileURI); + + this.makeOutputMessageLink(outputNode, function HUD__onFileClick() { + let viewSourceUtils = chromeDocument.defaultView.gViewSourceUtils; + viewSourceUtils.viewSource(aFileURI, null, chromeDocument); + }); + + ConsoleUtils.outputMessageNode(outputNode, this.hudId); + }, + ERRORS: { HUD_BOX_DOES_NOT_EXIST: "Heads Up Display does not exist", TAB_ID_REQUIRED: "Tab DOM ID is required", @@ -3949,9 +2692,13 @@ HeadsUpDisplay.prototype = { }, this); let message = { - hudId: this.hudId, - features: ["ConsoleAPI", "JSTerm", "PageError"], + features: ["ConsoleAPI", "JSTerm", "PageError", "NetworkMonitor"], cachedMessages: ["ConsoleAPI", "PageError"], + NetworkMonitor: { monitorFileActivity: true }, + preferences: { + "NetworkMonitor.saveRequestAndResponseBodies": + this.saveRequestAndResponseBodies, + }, }; this.sendMessageToContent("WebConsole:Init", message); }, @@ -3993,6 +2740,12 @@ HeadsUpDisplay.prototype = { this._displayCachedConsoleMessages(aMessage.json.messages); this._onInitComplete(); break; + case "WebConsole:NetworkActivity": + this.handleNetworkActivity(aMessage.json); + break; + case "WebConsole:FileActivity": + this.logFileActivity(aMessage.json.uri); + break; } }, @@ -4069,6 +2822,75 @@ HeadsUpDisplay.prototype = { this.messageManager.sendAsyncMessage(aName, aMessage); }, + /** + * Handle the "WebConsole:NetworkActivity" message coming from the remote Web + * Console. + * + * @param object aMessage + * The HTTP activity object. This object needs to hold two properties: + * - meta - some metadata about the request log: + * - stages - the stages the network request went through. + * - discardRequestBody and discardResponseBody - booleans that tell + * if the network request/response body was discarded or not. + * - log - the request and response information. This is a HAR-like + * object. See HUDService-content.js + * NetworkMonitor.createActivityObject(). + */ + handleNetworkActivity: function HUD_handleNetworkActivity(aMessage) + { + let stage = aMessage.meta.stages[aMessage.meta.stages.length - 1]; + + if (stage == "REQUEST_HEADER") { + this.logNetActivity(aMessage); + return; + } + + let entry = aMessage.log.entries[0]; + let request = entry.request; + let response = entry.response; + + let messageNode = this.outputNode. + querySelector("richlistitem[connectionId=" + entry.connection + "]"); + if (!messageNode) { + return; + } + messageNode._httpActivity = aMessage; + + if (stage == "TRANSACTION_CLOSE" || stage == "RESPONSE_HEADER") { + let status = [response.httpVersion, response.status, response.statusText]; + if (stage == "TRANSACTION_CLOSE") { + status.push(l10n.getFormatStr("NetworkPanel.durationMS", [entry.time])); + } + let statusText = "[" + status.join(" ") + "]"; + + let linkNode = messageNode.querySelector(".webconsole-msg-link"); + let statusNode = linkNode.querySelector(".webconsole-msg-status"); + statusNode.setAttribute("value", statusText); + + messageNode.clipboardText = [request.method, request.url, statusText] + .join(" "); + + if (stage == "RESPONSE_HEADER" && + response.status >= MIN_HTTP_ERROR_CODE && + response.status <= MAX_HTTP_ERROR_CODE) { + ConsoleUtils.setMessageType(messageNode, CATEGORY_NETWORK, + SEVERITY_ERROR); + } + } + + if (messageNode._netPanel) { + messageNode._netPanel.update(); + } + + // For unit tests we pass the HTTP activity object to the test callback, + // once requests complete. + if (HUDService.lastFinishedRequestCallback && + aMessage.meta.stages.indexOf("REQUEST_STOP") > -1 && + aMessage.meta.stages.indexOf("TRANSACTION_CLOSE") > -1) { + HUDService.lastFinishedRequestCallback(aMessage); + } + }, + /** * Make a link given an output element. * @@ -4082,7 +2904,7 @@ HeadsUpDisplay.prototype = { { let linkNode; if (aNode.category === CATEGORY_NETWORK) { - linkNode = aNode.querySelector(".webconsole-msg-link"); + linkNode = aNode.querySelector(".webconsole-msg-link, .webconsole-msg-url"); } else { linkNode = aNode.querySelector(".webconsole-msg-body"); @@ -4129,6 +2951,7 @@ HeadsUpDisplay.prototype = { delete this.asyncRequests; delete this.messageManager; delete this.browser; + delete this.chromeDocument; this.positionMenuitems.above.removeEventListener("command", this._positionConsoleAbove, false); @@ -5919,7 +4742,7 @@ HeadsUpDisplayUICommands = { } case "saveBodies": { let checked = aButton.getAttribute("checked") === "true"; - HUDService.saveRequestAndResponseBodies = checked; + HUDService.hudReferences[hudId].saveRequestAndResponseBodies = checked; break; } } @@ -6046,88 +4869,6 @@ CommandController.prototype = { } }; -/** - * A WebProgressListener that listens for location changes, to update HUDService - * state information on page navigation. - * - * @constructor - * @param string aHudId - * The HeadsUpDisplay ID. - */ -function ConsoleProgressListener(aHudId) -{ - this.hudId = aHudId; -} - -ConsoleProgressListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]), - - onStateChange: function CPL_onStateChange(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; - } - - let outputNode = HUDService.hudReferences[this.hudId].outputNode; - - let chromeDocument = outputNode.ownerDocument; - let msgNode = chromeDocument.createElementNS(HTML_NS, "html:span"); - - // Create the clickable URL part of the message. - let linkNode = chromeDocument.createElementNS(HTML_NS, "html:span"); - linkNode.appendChild(chromeDocument.createTextNode(uri.spec)); - linkNode.classList.add("hud-clickable"); - linkNode.classList.add("webconsole-msg-url"); - - linkNode.addEventListener("mousedown", function(aEvent) { - this._startX = aEvent.clientX; - this._startY = aEvent.clientY; - }, false); - - linkNode.addEventListener("click", function(aEvent) { - if (aEvent.detail == 1 && aEvent.button == 0 && - this._startX == aEvent.clientX && this._startY == aEvent.clientY) { - let viewSourceUtils = chromeDocument.defaultView.gViewSourceUtils; - viewSourceUtils.viewSource(uri.spec, null, chromeDocument); - } - }, false); - - msgNode.appendChild(linkNode); - - let messageNode = ConsoleUtils.createMessageNode(chromeDocument, - CATEGORY_NETWORK, - SEVERITY_LOG, - msgNode, - this.hudId, - null, - null, - uri.spec); - - ConsoleUtils.outputMessageNode(messageNode, this.hudId); - }, - - onLocationChange: function() {}, - onStatusChange: function() {}, - onProgressChange: function() {}, - onSecurityChange: function() {}, -}; - /////////////////////////////////////////////////////////////////////////// // appName /////////////////////////////////////////////////////////////////////////// diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in index a1a6359cb9b1..4aa67e2eda2b 100644 --- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -13,6 +13,7 @@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ PropertyPanel.jsm \ NetworkHelper.jsm \ + NetworkPanel.jsm \ AutocompletePopup.jsm \ WebConsoleUtils.jsm \ $(NULL) diff --git a/browser/devtools/webconsole/NetworkHelper.jsm b/browser/devtools/webconsole/NetworkHelper.jsm index 44cdba2d1b43..4db6b03b5717 100644 --- a/browser/devtools/webconsole/NetworkHelper.jsm +++ b/browser/devtools/webconsole/NetworkHelper.jsm @@ -49,6 +49,7 @@ * Austin Andrews * Christoph Dorn * Steven Roussey (AppCenter Inc, Network54) + * Mihai Sucan (Mozilla Corp.) */ const Cc = Components.classes; @@ -68,7 +69,7 @@ var EXPORTED_SYMBOLS = ["NetworkHelper"]; /** * Helper object for networking stuff. * - * All of the following functions have been taken from the Firebug source. They + * Most of the following functions have been taken from the Firebug source. They * have been modified to match the Firefox coding rules. */ @@ -128,12 +129,13 @@ var NetworkHelper = * Reads the posted text from aRequest. * * @param nsIHttpChannel aRequest - * @param nsIDOMNode aBrowser + * @param string aCharset + * The content document charset, used when reading the POSTed data. * @returns string or null * Returns the posted string if it was possible to read from aRequest * otherwise null. */ - readPostTextFromRequest: function NH_readPostTextFromRequest(aRequest, aBrowser) + readPostTextFromRequest: function NH_readPostTextFromRequest(aRequest, aCharset) { if (aRequest instanceof Ci.nsIUploadChannel) { let iStream = aRequest.uploadStream; @@ -150,8 +152,7 @@ var NetworkHelper = } // Read data from the stream. - let charset = aBrowser.contentWindow.document.characterSet; - let text = this.readAndConvertFromStream(iStream, charset); + let text = this.readAndConvertFromStream(iStream, aCharset); // Seek locks the file, so seek to the beginning only if necko hasn't // read it yet, since necko doesn't seek to 0 before reading (at lest @@ -167,14 +168,15 @@ var NetworkHelper = /** * Reads the posted text from the page's cache. * - * @param nsIDOMNode aBrowser + * @param nsIDocShell aDocShell + * @param string aCharset * @returns string or null - * Returns the posted string if it was possible to read from aBrowser - * otherwise null. + * Returns the posted string if it was possible to read from + * aDocShell otherwise null. */ - readPostTextFromPage: function NH_readPostTextFromPage(aBrowser) + readPostTextFromPage: function NH_readPostTextFromPage(aDocShell, aCharset) { - let webNav = aBrowser.webNavigation; + let webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation); if (webNav instanceof Ci.nsIWebPageDescriptor) { let descriptor = webNav.currentDescriptor; @@ -182,8 +184,7 @@ var NetworkHelper = descriptor instanceof Ci.nsISeekableStream) { descriptor.seek(NS_SEEK_SET, 0); - let charset = browser.contentWindow.document.characterSet; - return this.readAndConvertFromStream(descriptor, charset); + return this.readAndConvertFromStream(descriptor, aCharset); } } return null; @@ -266,6 +267,81 @@ var NetworkHelper = }); }, + /** + * Parse a raw Cookie header value. + * + * @param string aHeader + * The raw Cookie header value. + * @return array + * Array holding an object for each cookie. Each object holds the + * following properties: name and value. + */ + parseCookieHeader: function NH_parseCookieHeader(aHeader) + { + let cookies = aHeader.split(";"); + let result = []; + + cookies.forEach(function(aCookie) { + let [name, value] = aCookie.split("="); + result.push({name: unescape(name.trim()), + value: unescape(value.trim())}); + }); + + return result; + }, + + /** + * Parse a raw Set-Cookie header value. + * + * @param string aHeader + * The raw Set-Cookie header value. + * @return array + * Array holding an object for each cookie. Each object holds the + * following properties: name, value, secure (boolean), httpOnly + * (boolean), path, domain and expires (ISO date string). + */ + parseSetCookieHeader: function NH_parseSetCookieHeader(aHeader) + { + let rawCookies = aHeader.split(/\r\n|\n|\r/); + let cookies = []; + + rawCookies.forEach(function(aCookie) { + let name = unescape(aCookie.substr(0, aCookie.indexOf("=")).trim()); + let parts = aCookie.substr(aCookie.indexOf("=") + 1).split(";"); + let value = unescape(parts.shift().trim()); + + let cookie = {name: name, value: value}; + + parts.forEach(function(aPart) { + let part = aPart.trim(); + if (part.toLowerCase() == "secure") { + cookie.secure = true; + } + else if (part.toLowerCase() == "httponly") { + cookie.httpOnly = true; + } + else if (part.indexOf("=") > -1) { + let pair = part.split("="); + pair[0] = pair[0].toLowerCase(); + if (pair[0] == "path" || pair[0] == "domain") { + cookie[pair[0]] = pair[1]; + } + else if (pair[0] == "expires") { + try { + pair[1] = pair[1].replace(/-/g, ' '); + cookie.expires = new Date(pair[1]).toISOString(); + } + catch (ex) { } + } + } + }); + + cookies.push(cookie); + }); + + return cookies; + }, + // This is a list of all the mime category maps jviereck could find in the // firebug code base. mimeCategoryMap: { @@ -333,6 +409,7 @@ var NetworkHelper = "audio/x-wav": "media", "text/json": "json", "application/x-json": "json", - "application/json-rpc": "json" + "application/json-rpc": "json", + "application/x-web-app-manifest+json": "json", } } diff --git a/browser/devtools/webconsole/NetworkPanel.jsm b/browser/devtools/webconsole/NetworkPanel.jsm new file mode 100644 index 000000000000..b3a6bcd893b9 --- /dev/null +++ b/browser/devtools/webconsole/NetworkPanel.jsm @@ -0,0 +1,668 @@ +/* -*- 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"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", + "nsIMIMEService"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", + "resource:///modules/NetworkHelper.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", + "resource:///modules/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "l10n", function() { + return WebConsoleUtils.l10n; +}); + +var EXPORTED_SYMBOLS = ["NetworkPanel"]; + +/** + * Creates a new NetworkPanel. + * + * @param nsIDOMNode aParent + * Parent node to append the created panel to. + * @param object aHttpActivity + * HttpActivity to display in the panel. + */ +function NetworkPanel(aParent, aHttpActivity) +{ + let doc = aParent.ownerDocument; + this.httpActivity = aHttpActivity; + + // Create the underlaying panel + this.panel = createElement(doc, "panel", { + label: l10n.getStr("NetworkPanel.label"), + titlebar: "normal", + noautofocus: "true", + noautohide: "true", + close: "true" + }); + + // Create the iframe that displays the NetworkPanel XHTML. + this.iframe = createAndAppendElement(this.panel, "iframe", { + src: "chrome://browser/content/NetworkPanel.xhtml", + type: "content", + flex: "1" + }); + + let self = this; + + // Destroy the panel when it's closed. + this.panel.addEventListener("popuphidden", function onPopupHide() { + self.panel.removeEventListener("popuphidden", onPopupHide, false); + self.panel.parentNode.removeChild(self.panel); + self.panel = null; + self.iframe = null; + self.document = null; + self.httpActivity = null; + + if (self.linkNode) { + self.linkNode._panelOpen = false; + self.linkNode = null; + } + }, false); + + // Set the document object and update the content once the panel is loaded. + this.panel.addEventListener("load", function onLoad() { + self.panel.removeEventListener("load", onLoad, true); + self.document = self.iframe.contentWindow.document; + self.update(); + }, true); + + // Create the footer. + let footer = createElement(doc, "hbox", { align: "end" }); + createAndAppendElement(footer, "spacer", { flex: 1 }); + + createAndAppendElement(footer, "resizer", { dir: "bottomend" }); + this.panel.appendChild(footer); + + aParent.appendChild(this.panel); +} + +NetworkPanel.prototype = +{ + /** + * Callback is called once the NetworkPanel is processed completely. Used by + * unit tests. + */ + isDoneCallback: null, + + /** + * The current state of the output. + */ + _state: 0, + + /** + * State variables. + */ + _INIT: 0, + _DISPLAYED_REQUEST_HEADER: 1, + _DISPLAYED_REQUEST_BODY: 2, + _DISPLAYED_RESPONSE_HEADER: 3, + _TRANSITION_CLOSED: 4, + + _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/, + + _contentType: null, + + /** + * Small helper function that is nearly equal to l10n.getFormatStr + * except that it prefixes aName with "NetworkPanel.". + * + * @param string aName + * The name of an i10n string to format. This string is prefixed with + * "NetworkPanel." before calling the HUDService.getFormatStr function. + * @param array aArray + * Values used as placeholder for the i10n string. + * @returns string + * The i10n formated string. + */ + _format: function NP_format(aName, aArray) + { + return l10n.getFormatStr("NetworkPanel." + aName, aArray); + }, + + /** + * Returns the content type of the response body. This is based on the + * response.content.mimeType property. If this value is not available, then + * the content type is guessed by the file extension of the request URL. + * + * @return string + * Content type or empty string if no content type could be figured + * out. + */ + get contentType() + { + if (this._contentType) { + return this._contentType; + } + + let entry = this.httpActivity.log.entries[0]; + let request = entry.request; + let response = entry.response; + + let contentType = ""; + let types = response.content ? + (response.content.mimeType || "").split(/,|;/) : []; + for (let i = 0; i < types.length; i++) { + if (types[i] in NetworkHelper.mimeCategoryMap) { + contentType = types[i]; + break; + } + } + + if (contentType) { + this._contentType = contentType; + return contentType; + } + + // Try to get the content type from the request file extension. + let uri = NetUtil.newURI(request.url); + if ((uri instanceof Ci.nsIURL) && uri.fileExtension) { + try { + contentType = mimeService.getTypeFromExtension(uri.fileExtension); + } + catch(ex) { + // Added to prevent failures on OS X 64. No Flash? + Cu.reportError(ex); + } + } + + this._contentType = contentType; + return contentType; + }, + + /** + * + * @returns boolean + * True if the response is an image, false otherwise. + */ + get _responseIsImage() + { + return this.contentType && + NetworkHelper.mimeCategoryMap[this.contentType] == "image"; + }, + + /** + * + * @returns boolean + * True if the response body contains text, false otherwise. + */ + get _isResponseBodyTextData() + { + let contentType = this.contentType; + + if (!contentType) + return false; + + if (contentType.indexOf("text/") == 0) { + return true; + } + + switch (NetworkHelper.mimeCategoryMap[contentType]) { + case "txt": + case "js": + case "json": + case "css": + case "html": + case "svg": + case "xml": + return true; + + default: + return false; + } + }, + + /** + * Tells if the server response is cached. + * + * @returns boolean + * Returns true if the server responded that the request is already + * in the browser's cache, false otherwise. + */ + get _isResponseCached() + { + return this.httpActivity.log.entries[0].response.status == 304; + }, + + /** + * Tells if the request body includes form data. + * + * @returns boolean + * Returns true if the posted body contains form data. + */ + get _isRequestBodyFormData() + { + let requestBody = this.httpActivity.log.entries[0].request.postData.text; + return this._fromDataRegExp.test(requestBody); + }, + + /** + * Appends the node with id=aId by the text aValue. + * + * @param string aId + * @param string aValue + * @returns void + */ + _appendTextNode: function NP_appendTextNode(aId, aValue) + { + let textNode = this.document.createTextNode(aValue); + this.document.getElementById(aId).appendChild(textNode); + }, + + /** + * Generates some HTML to display the key-value pair of the aList data. The + * generated HTML is added to node with id=aParentId. + * + * @param string aParentId + * Id of the parent node to append the list to. + * @oaram array aList + * Array that holds the objects you want to display. Each object must + * have two properties: name and value. + * @param boolean aIgnoreCookie + * If true, the key-value named "Cookie" is not added to the list. + * @returns void + */ + _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie) + { + let parent = this.document.getElementById(aParentId); + let doc = this.document; + + aList.sort(function(a, b) { + return a.name.toLowerCase() < b.name.toLowerCase(); + }); + + aList.forEach(function(aItem) { + let name = aItem.name; + let value = aItem.value; + if (aIgnoreCookie && name == "Cookie") { + return; + } + + /** + * The following code creates the HTML: + * + * ${line}: + * ${aList[line]} + * + * and adds it to parent. + */ + let row = doc.createElement("tr"); + let textNode = doc.createTextNode(name + ":"); + let th = doc.createElement("th"); + th.setAttribute("scope", "row"); + th.setAttribute("class", "property-name"); + th.appendChild(textNode); + row.appendChild(th); + + textNode = doc.createTextNode(value); + let td = doc.createElement("td"); + td.setAttribute("class", "property-value"); + td.appendChild(textNode); + row.appendChild(td); + + parent.appendChild(row); + }); + }, + + /** + * Displays the node with id=aId. + * + * @param string aId + * @returns void + */ + _displayNode: function NP_displayNode(aId) + { + this.document.getElementById(aId).style.display = "block"; + }, + + /** + * Sets the request URL, request method, the timing information when the + * request started and the request header content on the NetworkPanel. + * If the request header contains cookie data, a list of sent cookies is + * generated and a special sent cookie section is displayed + the cookie list + * added to it. + * + * @returns void + */ + _displayRequestHeader: function NP__displayRequestHeader() + { + let entry = this.httpActivity.log.entries[0]; + let request = entry.request; + let requestTime = new Date(entry.startedDateTime); + + this._appendTextNode("headUrl", request.url); + this._appendTextNode("headMethod", request.method); + this._appendTextNode("requestHeadersInfo", + l10n.timestampString(requestTime)); + + this._appendList("requestHeadersContent", request.headers, true); + + if (request.cookies.length > 0) { + this._displayNode("requestCookie"); + this._appendList("requestCookieContent", request.cookies); + } + }, + + /** + * Displays the request body section of the NetworkPanel and set the request + * body content on the NetworkPanel. + * + * @returns void + */ + _displayRequestBody: function NP__displayRequestBody() { + let postData = this.httpActivity.log.entries[0].request.postData; + this._displayNode("requestBody"); + this._appendTextNode("requestBodyContent", postData.text); + }, + + /* + * Displays the `sent form data` section. Parses the request header for the + * submitted form data displays it inside of the `sent form data` section. + * + * @returns void + */ + _displayRequestForm: function NP__processRequestForm() { + let postData = this.httpActivity.log.entries[0].request.postData.text; + let requestBodyLines = postData.split("\n"); + let formData = requestBodyLines[requestBodyLines.length - 1]. + replace(/\+/g, " ").split("&"); + + function unescapeText(aText) + { + try { + return decodeURIComponent(aText); + } + catch (ex) { + return decodeURIComponent(unescape(aText)); + } + } + + let formDataArray = []; + for (let i = 0; i < formData.length; i++) { + let data = formData[i]; + let idx = data.indexOf("="); + let key = data.substring(0, idx); + let value = data.substring(idx + 1); + formDataArray.push({ + name: unescapeText(key), + value: unescapeText(value) + }); + } + + this._appendList("requestFormDataContent", formDataArray); + this._displayNode("requestFormData"); + }, + + /** + * Displays the response section of the NetworkPanel, sets the response status, + * the duration between the start of the request and the receiving of the + * response header as well as the response header content on the the NetworkPanel. + * + * @returns void + */ + _displayResponseHeader: function NP__displayResponseHeader() + { + let entry = this.httpActivity.log.entries[0]; + let timing = entry.timings; + let response = entry.response; + + this._appendTextNode("headStatus", + [response.httpVersion, response.status, + response.statusText].join(" ")); + + // Calculate how much time it took from the request start, until the + // response started to be received. + let deltaDuration = 0; + ["dns", "connect", "send", "wait"].forEach(function (aValue) { + let ms = timing[aValue]; + if (ms > -1) { + deltaDuration += ms; + } + }); + + this._appendTextNode("responseHeadersInfo", + this._format("durationMS", [deltaDuration])); + + this._displayNode("responseContainer"); + this._appendList("responseHeadersContent", response.headers); + }, + + /** + * Displays the respones image section, sets the source of the image displayed + * in the image response section to the request URL and the duration between + * the receiving of the response header and the end of the request. Once the + * image is loaded, the size of the requested image is set. + * + * @returns void + */ + _displayResponseImage: function NP__displayResponseImage() + { + let self = this; + let entry = this.httpActivity.log.entries[0]; + let timing = entry.timings; + let request = entry.request; + let cached = ""; + + if (this._isResponseCached) { + cached = "Cached"; + } + + let imageNode = this.document.getElementById("responseImage" + cached +"Node"); + imageNode.setAttribute("src", request.url); + + // This function is called to set the imageInfo. + function setImageInfo() { + self._appendTextNode("responseImage" + cached + "Info", + self._format("imageSizeDeltaDurationMS", + [ imageNode.width, imageNode.height, timing.receive ] + ) + ); + } + + // Check if the image is already loaded. + if (imageNode.width != 0) { + setImageInfo(); + } + else { + // Image is not loaded yet therefore add a load event. + imageNode.addEventListener("load", function imageNodeLoad() { + imageNode.removeEventListener("load", imageNodeLoad, false); + setImageInfo(); + }, false); + } + + this._displayNode("responseImage" + cached); + }, + + /** + * Displays the response body section, sets the the duration between + * the receiving of the response header and the end of the request as well as + * the content of the response body on the NetworkPanel. + * + * @returns void + */ + _displayResponseBody: function NP__displayResponseBody() + { + let entry = this.httpActivity.log.entries[0]; + let timing = entry.timings; + let response = entry.response; + let cached = this._isResponseCached ? "Cached" : ""; + + this._appendTextNode("responseBody" + cached + "Info", + this._format("durationMS", [timing.receive])); + + this._displayNode("responseBody" + cached); + this._appendTextNode("responseBody" + cached + "Content", + response.content.text); + }, + + /** + * Displays the `Unknown Content-Type hint` and sets the duration between the + * receiving of the response header on the NetworkPanel. + * + * @returns void + */ + _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType() + { + let timing = this.httpActivity.log.entries[0].timings; + + this._displayNode("responseBodyUnknownType"); + this._appendTextNode("responseBodyUnknownTypeInfo", + this._format("durationMS", [timing.receive])); + + this._appendTextNode("responseBodyUnknownTypeContent", + this._format("responseBodyUnableToDisplay.content", [this.contentType])); + }, + + /** + * Displays the `no response body` section and sets the the duration between + * the receiving of the response header and the end of the request. + * + * @returns void + */ + _displayNoResponseBody: function NP_displayNoResponseBody() + { + let timing = this.httpActivity.log.entries[0].timings; + + this._displayNode("responseNoBody"); + this._appendTextNode("responseNoBodyInfo", + this._format("durationMS", [timing.receive])); + }, + + /** + * Updates the content of the NetworkPanel's iframe. + * + * @returns void + */ + update: function NP_update() + { + // After the iframe's contentWindow is ready, the document object is set. + // If the document object is not available yet nothing needs to be updated. + if (!this.document) { + return; + } + + let stages = this.httpActivity.meta.stages; + let entry = this.httpActivity.log.entries[0]; + let timing = entry.timings; + let request = entry.request; + let response = entry.response; + + switch (this._state) { + case this._INIT: + this._displayRequestHeader(); + this._state = this._DISPLAYED_REQUEST_HEADER; + // FALL THROUGH + + case this._DISPLAYED_REQUEST_HEADER: + // Process the request body if there is one. + if (!this.httpActivity.meta.discardRequestBody && request.postData) { + // Check if we send some form data. If so, display the form data special. + if (this._isRequestBodyFormData) { + this._displayRequestForm(); + } + else { + this._displayRequestBody(); + } + this._state = this._DISPLAYED_REQUEST_BODY; + } + // FALL THROUGH + + case this._DISPLAYED_REQUEST_BODY: + // There is always a response header. Therefore we can skip here if + // we don't have a response header yet and don't have to try updating + // anything else in the NetworkPanel. + if (!response.headers.length || !Object.keys(timing).length) { + break; + } + this._displayResponseHeader(); + this._state = this._DISPLAYED_RESPONSE_HEADER; + // FALL THROUGH + + case this._DISPLAYED_RESPONSE_HEADER: + if (stages.indexOf("REQUEST_STOP") == -1 || + stages.indexOf("TRANSACTION_CLOSE") == -1) { + break; + } + + this._state = this._TRANSITION_CLOSED; + if (this.httpActivity.meta.discardResponseBody) { + break; + } + + if (!response.content || !response.content.text) { + this._displayNoResponseBody(); + } + else if (this._responseIsImage) { + this._displayResponseImage(); + } + else if (!this._isResponseBodyTextData) { + this._displayResponseBodyUnknownType(); + } + else if (response.content.text) { + this._displayResponseBody(); + } + + break; + } + } +} + +/** + * Creates a DOMNode and sets all the attributes of aAttributes on the created + * element. + * + * @param nsIDOMDocument aDocument + * Document to create the new DOMNode. + * @param string aTag + * Name of the tag for the DOMNode. + * @param object aAttributes + * Attributes set on the created DOMNode. + * + * @returns nsIDOMNode + */ +function createElement(aDocument, aTag, aAttributes) +{ + let node = aDocument.createElement(aTag); + if (aAttributes) { + for (let attr in aAttributes) { + node.setAttribute(attr, aAttributes[attr]); + } + } + return node; +} + +/** + * Creates a new DOMNode and appends it to aParent. + * + * @param nsIDOMNode aParent + * A parent node to append the created element. + * @param string aTag + * Name of the tag for the DOMNode. + * @param object aAttributes + * Attributes set on the created DOMNode. + * + * @returns nsIDOMNode + */ +function createAndAppendElement(aParent, aTag, aAttributes) +{ + let node = createElement(aParent.ownerDocument, aTag, aAttributes); + aParent.appendChild(node); + return node; +} diff --git a/browser/devtools/webconsole/PropertyPanel.jsm b/browser/devtools/webconsole/PropertyPanel.jsm index bac2dd34034c..2403ef8de5a3 100644 --- a/browser/devtools/webconsole/PropertyPanel.jsm +++ b/browser/devtools/webconsole/PropertyPanel.jsm @@ -11,17 +11,12 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () { - let obj = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", obj); - return obj.WebConsoleUtils; -}); +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", + "resource:///modules/WebConsoleUtils.jsm"); var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; - /////////////////////////////////////////////////////////////////////////// //// PropertyTreeView. diff --git a/browser/devtools/webconsole/WebConsoleUtils.jsm b/browser/devtools/webconsole/WebConsoleUtils.jsm index 21979cf89c72..d2bd321b7928 100644 --- a/browser/devtools/webconsole/WebConsoleUtils.jsm +++ b/browser/devtools/webconsole/WebConsoleUtils.jsm @@ -11,7 +11,9 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider"]; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js index 1f8f7a4507a2..14b84e20e3a4 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js @@ -5,31 +5,17 @@ const TEST_FILE = "test-network.html"; -function tabLoad(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - hud = HUDService.hudReferences[hudId]; - - browser.addEventListener("load", tabReload, true); - - content.location.reload(); -} - function tabReload(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.removeEventListener(aEvent.type, tabReload, true); - let textContent = hud.outputNode.textContent; - isnot(textContent.indexOf("test-network.html"), -1, - "found test-network.html"); - isnot(textContent.indexOf("test-image.png"), -1, "found test-image.png"); - isnot(textContent.indexOf("testscript.js"), -1, "found testscript.js"); - isnot(textContent.indexOf("running network console logging tests"), -1, + outputNode = hud.outputNode; + findLogEntry("test-network.html"); + findLogEntry("test-image.png"); + findLogEntry("testscript.js"); + isnot(outputNode.textContent.indexOf("running network console logging tests"), -1, "found the console.log() message from testscript.js"); - finishTest(); + executeSoon(finishTest); } function test() { @@ -42,5 +28,12 @@ function test() { let uri = Services.io.newFileURI(dir); addTab(uri.spec); - browser.addEventListener("load", tabLoad, true); + browser.addEventListener("load", function tabLoad() { + browser.removeEventListener("load", tabLoad, true); + openConsole(null, function(aHud) { + hud = aHud; + browser.addEventListener("load", tabReload, true); + content.location.reload(); + }); + }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js index c840f2035a9c..00529773baa9 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js @@ -10,25 +10,27 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs"; -let lastFinishedRequest = null; - -function requestDoneCallback(aHttpRequest) -{ - lastFinishedRequest = aHttpRequest; -} - -function performTest() +function performTest(lastFinishedRequest) { ok(lastFinishedRequest, "page load was logged"); - let headers = lastFinishedRequest.response.header; - ok(headers, "we have the response headers"); - ok(!headers["Content-Type"], "we do not have the Content-Type header"); - ok(headers["Content-Length"] != 60, "Content-Length != 60"); + function readHeader(aName) + { + for (let header of headers) { + if (header.name == aName) { + return header.value; + } + } + return null; + } + + let headers = lastFinishedRequest.log.entries[0].response.headers; + ok(headers, "we have the response headers"); + ok(!readHeader("Content-Type"), "we do not have the Content-Type header"); + isnot(readHeader("Content-Length"), 60, "Content-Length != 60"); - lastFinishedRequest = null; HUDService.lastFinishedRequestCallback = null; - finishTest(); + executeSoon(finishTest); } function test() @@ -37,15 +39,15 @@ function test() let initialLoad = true; - browser.addEventListener("load", function () { + browser.addEventListener("load", function onLoad() { if (initialLoad) { - openConsole(); - HUDService.lastFinishedRequestCallback = requestDoneCallback; - content.location.reload(); + openConsole(null, function() { + HUDService.lastFinishedRequestCallback = performTest; + content.location.reload(); + }); initialLoad = false; } else { - browser.removeEventListener("load", arguments.callee, true); - performTest(); + browser.removeEventListener("load", onLoad, true); } }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js index 1fc1f1993c5c..9e9eed8fe44e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js @@ -10,28 +10,19 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html"; -let lastFinishedRequest = null; - -function requestDoneCallback(aHttpRequest) -{ - lastFinishedRequest = aHttpRequest; -} - -function performTest() +function performTest(lastFinishedRequest) { ok(lastFinishedRequest, "charset test page was loaded and logged"); - let body = lastFinishedRequest.response.body; + let body = lastFinishedRequest.log.entries[0].response.content.text; ok(body, "we have the response body"); let chars = "\u7684\u95ee\u5019!"; // 的问候! isnot(body.indexOf("

" + chars + "

"), -1, "found the chinese simplified string"); - lastFinishedRequest = null; - HUDService.saveRequestAndResponseBodies = false; HUDService.lastFinishedRequestCallback = null; - finishTest(); + executeSoon(finishTest); } function test() @@ -40,20 +31,18 @@ function test() let initialLoad = true; - browser.addEventListener("load", function () { + browser.addEventListener("load", function onLoad() { if (initialLoad) { - waitForFocus(function() { - openConsole(); + openConsole(null, function(hud) { - HUDService.saveRequestAndResponseBodies = true; - HUDService.lastFinishedRequestCallback = requestDoneCallback; + hud.saveRequestAndResponseBodies = true; + HUDService.lastFinishedRequestCallback = performTest; content.location = TEST_URI; - }, content); + }); initialLoad = false; } else { - browser.removeEventListener("load", arguments.callee, true); - performTest(); + browser.removeEventListener("load", onLoad, true); } }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js index 2cec9e1b1083..0dc39c5694e5 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js @@ -57,10 +57,10 @@ function onpopupshown2(aEvent) isnot(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is not checked"); - ok(!HUDService.saveRequestAndResponseBodies, "bodies are not logged"); + ok(!huds[1].saveRequestAndResponseBodies, "bodies are not logged"); // Enable body logging. - HUDService.saveRequestAndResponseBodies = true; + huds[1].saveRequestAndResponseBodies = true; menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) { menupopups[1].removeEventListener(aEvent.type, _onhidden, false); @@ -103,11 +103,12 @@ function onpopupshown1(aEvent) { menupopups[0].removeEventListener(aEvent.type, onpopupshown1, false); - // The menuitem checkbox must be in sync with the other tabs. - is(menuitems[0].getAttribute("checked"), "true", "menuitems[0] is checked"); + // The menuitem checkbox must not be in sync with the other tabs. + isnot(menuitems[0].getAttribute("checked"), "true", + "menuitems[0] is not checked"); - // Disable body logging. - HUDService.saveRequestAndResponseBodies = false; + // Enable body logging for tab 1 as well. + huds[0].saveRequestAndResponseBodies = true; // Close the menu, and switch back to tab 2. menupopups[0].addEventListener("popuphidden", function _onhidden(aEvent) { @@ -127,8 +128,7 @@ function onpopupshown2c(aEvent) { menupopups[1].removeEventListener(aEvent.type, onpopupshown2c, false); - isnot(menuitems[1].getAttribute("checked"), "true", - "menuitems[1] is not checked"); + is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked"); menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) { menupopups[1].removeEventListener(aEvent.type, _onhidden, false); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js index ae0c180cbf3e..b2fe6c8ad3c7 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js @@ -41,14 +41,11 @@ let TestObserver = { function tabLoad(aEvent) { browser.removeEventListener(aEvent.type, tabLoad, true); - openConsole(); - - let hudId = HUDService.getHudIdByWindow(content); - hud = HUDService.hudReferences[hudId]; - - Services.console.registerListener(TestObserver); - - content.location = TEST_URI; + openConsole(null, function(aHud) { + hud = aHud; + Services.console.registerListener(TestObserver); + content.location = TEST_URI; + }); } function performTest() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js index dafdcbde3147..ea09d117d139 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js @@ -92,8 +92,8 @@ function tabLoaded() { successFn: function() { let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); - EventUtils.synthesizeMouse(networkLink, 2, 2, {}); EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + EventUtils.synthesizeMouse(networkLink, 2, 2, {}); }, failureFn: finishTest, }); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js index 8ee7f93ff102..44235994d943 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js @@ -118,8 +118,8 @@ function tabLoaded() { successFn: function() { let jstermMessage = HUD.outputNode.querySelector(".webconsole-msg-output"); - EventUtils.synthesizeMouse(networkLink, 2, 2, {}); EventUtils.synthesizeMouse(jstermMessage, 2, 2, {}); + EventUtils.synthesizeMouse(networkLink, 2, 2, {}); }, failureFn: finishTest, }); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js index 085919440ea1..20a3cfb3b553 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js @@ -13,57 +13,63 @@ let lastFinishedRequests = {}; function requestDoneCallback(aHttpRequest) { - let status = aHttpRequest.response.status. - replace(/^HTTP\/\d\.\d (\d+).+$/, "$1"); + let status = aHttpRequest.log.entries[0].response.status; lastFinishedRequests[status] = aHttpRequest; } function performTest(aEvent) { - HUDService.saveRequestAndResponseBodies = false; HUDService.lastFinishedRequestCallback = null; ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently"); ok("404" in lastFinishedRequests, "request 2: 404 Not found"); - let headers0 = lastFinishedRequests["301"].response.header; - is(headers0["Content-Type"], "text/html", + function readHeader(aName) + { + for (let header of headers) { + if (header.name == aName) { + return header.value; + } + } + return null; + } + + let headers = lastFinishedRequests["301"].log.entries[0].response.headers; + is(readHeader("Content-Type"), "text/html", "we do have the Content-Type header"); - is(headers0["Content-Length"], 71, "Content-Length is correct"); - is(headers0["Location"], "/redirect-from-bug-630733", + is(readHeader("Content-Length"), 71, "Content-Length is correct"); + is(readHeader("Location"), "/redirect-from-bug-630733", "Content-Length is correct"); - is(headers0["x-foobar-bug630733"], "bazbaz", + is(readHeader("x-foobar-bug630733"), "bazbaz", "X-Foobar-bug630733 is correct"); - let body = lastFinishedRequests["301"].response.body; - ok(!body, "body discarded for request 1"); + let body = lastFinishedRequests["301"].log.entries[0].response.content; + ok(!body.text, "body discarded for request 1"); - let headers1 = lastFinishedRequests["404"].response.header; - ok(!headers1["Location"], "no Location header"); - ok(!headers1["x-foobar-bug630733"], "no X-Foobar-bug630733 header"); + headers = lastFinishedRequests["404"].log.entries[0].response.headers; + ok(!readHeader("Location"), "no Location header"); + ok(!readHeader("x-foobar-bug630733"), "no X-Foobar-bug630733 header"); - body = lastFinishedRequests["404"].response.body; + body = lastFinishedRequests["404"].log.entries[0].response.content.text; isnot(body.indexOf("404"), -1, "body is correct for request 2"); lastFinishedRequests = null; - finishTest(); + executeSoon(finishTest); } function test() { addTab("data:text/html;charset=utf-8,

Web Console test for bug 630733"); - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.addEventListener("load", function onLoad1(aEvent) { + browser.removeEventListener(aEvent.type, onLoad1, true); - executeSoon(function() { - openConsole(); - - HUDService.saveRequestAndResponseBodies = true; + openConsole(null, function(hud) { + hud.saveRequestAndResponseBodies = true; HUDService.lastFinishedRequestCallback = requestDoneCallback; - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.addEventListener("load", function onLoad2(aEvent) { + browser.removeEventListener(aEvent.type, onLoad2, true); executeSoon(performTest); }, true); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js index a0736a22a099..69ec1f45d008 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js @@ -18,38 +18,36 @@ function test() { addTab("data:text/html;charset=utf-8,Web Console network logging tests"); - browser.addEventListener("load", function() { - browser.removeEventListener("load", arguments.callee, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); - openConsole(); + openConsole(null, function(aHud) { + hud = aHud; - hud = HUDService.getHudByWindow(content); - ok(hud, "Web Console is now open"); + HUDService.lastFinishedRequestCallback = function(aRequest) { + lastRequest = aRequest.log.entries[0]; + if (requestCallback) { + requestCallback(); + } + }; - HUDService.lastFinishedRequestCallback = function(aRequest) { - lastRequest = aRequest; - if (requestCallback) { - requestCallback(); - } - }; - - executeSoon(testPageLoad); + executeSoon(testPageLoad); + }); }, true); } function testPageLoad() { - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - + requestCallback = function() { // Check if page load was logged correctly. ok(lastRequest, "Page load was logged"); - is(lastRequest.url, TEST_NETWORK_REQUEST_URI, + is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI, "Logged network entry is page load"); - is(lastRequest.method, "GET", "Method is correct"); + is(lastRequest.request.method, "GET", "Method is correct"); lastRequest = null; + requestCallback = null; executeSoon(testPageLoadBody); - }, true); + }; content.location = TEST_NETWORK_REQUEST_URI; } @@ -57,12 +55,12 @@ function testPageLoad() function testPageLoadBody() { // Turn off logging of request bodies and check again. - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + requestCallback = function() { ok(lastRequest, "Page load was logged again"); lastRequest = null; + requestCallback = null; executeSoon(testXhrGet); - }, true); + }; content.location.reload(); } @@ -71,7 +69,7 @@ function testXhrGet() { requestCallback = function() { ok(lastRequest, "testXhrGet() was logged"); - is(lastRequest.method, "GET", "Method is correct"); + is(lastRequest.request.method, "GET", "Method is correct"); lastRequest = null; requestCallback = null; executeSoon(testXhrPost); @@ -85,7 +83,7 @@ function testXhrPost() { requestCallback = function() { ok(lastRequest, "testXhrPost() was logged"); - is(lastRequest.method, "POST", "Method is correct"); + is(lastRequest.request.method, "POST", "Method is correct"); lastRequest = null; requestCallback = null; executeSoon(testFormSubmission); @@ -99,12 +97,11 @@ function testFormSubmission() { // Start the form submission test. As the form is submitted, the page is // loaded again. Bind to the load event to catch when this is done. - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + requestCallback = function() { ok(lastRequest, "testFormSubmission() was logged"); - is(lastRequest.method, "POST", "Method is correct"); + is(lastRequest.request.method, "POST", "Method is correct"); executeSoon(testLiveFilteringOnSearchStrings); - }, true); + }; let form = content.document.querySelector("form"); ok(form, "we have the HTML form"); @@ -112,9 +109,6 @@ function testFormSubmission() } function testLiveFilteringOnSearchStrings() { - browser.removeEventListener("DOMContentLoaded", - testLiveFilteringOnSearchStrings, false); - setStringFilter("http"); isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " + "search string is set to \"http\""); @@ -146,6 +140,9 @@ function testLiveFilteringOnSearchStrings() { is(countMessageNodes(), 0, "the log nodes are hidden when searching for " + "the string \"foo\"bar'baz\"boo'\""); + HUDService.lastFinishedRequestCallback = null; + lastRequest = null; + requestCallback = null; finishTest(); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js index a4e043553541..074c8d1cb921 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js +++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js @@ -21,47 +21,47 @@ const TEST_DATA_JSON_CONTENT = let lastRequest = null; let requestCallback = null; +let lastActivity = null; function test() { addTab("data:text/html;charset=utf-8,Web Console network logging tests"); - browser.addEventListener("load", function() { - browser.removeEventListener("load", arguments.callee, true); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); - openConsole(); + openConsole(null, function(aHud) { + hud = aHud; - hud = HUDService.getHudByWindow(content); - ok(hud, "Web Console is now open"); + HUDService.lastFinishedRequestCallback = function(aRequest) { + lastRequest = aRequest.log.entries[0]; + lastActivity = aRequest; + if (requestCallback) { + requestCallback(); + } + }; - HUDService.lastFinishedRequestCallback = function(aRequest) { - lastRequest = aRequest; - if (requestCallback) { - requestCallback(); - } - }; - - executeSoon(testPageLoad); + executeSoon(testPageLoad); + }); }, true); } function testPageLoad() { - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - + requestCallback = function() { // Check if page load was logged correctly. ok(lastRequest, "Page load was logged"); - is(lastRequest.url, TEST_NETWORK_REQUEST_URI, + + is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI, "Logged network entry is page load"); - is(lastRequest.method, "GET", "Method is correct"); - ok(!("body" in lastRequest.request), "No request body was stored"); - ok(!("body" in lastRequest.response), "No response body was stored"); - ok(!lastRequest.response.listener, "No response listener is stored"); + is(lastRequest.request.method, "GET", "Method is correct"); + ok(!lastRequest.request.postData, "No request body was stored"); + ok(!lastRequest.response.content.text, "No response body was stored"); lastRequest = null; + requestCallback = null; executeSoon(testPageLoadBody); - }, true); + }; content.location = TEST_NETWORK_REQUEST_URI; } @@ -69,17 +69,16 @@ function testPageLoad() function testPageLoadBody() { // Turn on logging of request bodies and check again. - HUDService.saveRequestAndResponseBodies = true; - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - + hud.saveRequestAndResponseBodies = true; + requestCallback = function() { ok(lastRequest, "Page load was logged again"); - is(lastRequest.response.body.indexOf(""), 0, + is(lastRequest.response.content.text.indexOf(""), 0, "Response body's beginning is okay"); lastRequest = null; + requestCallback = null; executeSoon(testXhrGet); - }, true); + }; content.location.reload(); } @@ -88,9 +87,9 @@ function testXhrGet() { requestCallback = function() { ok(lastRequest, "testXhrGet() was logged"); - is(lastRequest.method, "GET", "Method is correct"); - is(lastRequest.request.body, null, "No request body was sent"); - is(lastRequest.response.body, TEST_DATA_JSON_CONTENT, + is(lastRequest.request.method, "GET", "Method is correct"); + ok(!lastRequest.request.postData, "No request body was sent"); + is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT, "Response is correct"); lastRequest = null; @@ -106,10 +105,10 @@ function testXhrPost() { requestCallback = function() { ok(lastRequest, "testXhrPost() was logged"); - is(lastRequest.method, "POST", "Method is correct"); - is(lastRequest.request.body, "Hello world!", + is(lastRequest.request.method, "POST", "Method is correct"); + is(lastRequest.request.postData.text, "Hello world!", "Request body was logged"); - is(lastRequest.response.body, TEST_DATA_JSON_CONTENT, + is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT, "Response is correct"); lastRequest = null; @@ -125,23 +124,21 @@ function testFormSubmission() { // Start the form submission test. As the form is submitted, the page is // loaded again. Bind to the load event to catch when this is done. - browser.addEventListener("load", function(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); - + requestCallback = function() { ok(lastRequest, "testFormSubmission() was logged"); - is(lastRequest.method, "POST", "Method is correct"); - isnot(lastRequest.request.body. + is(lastRequest.request.method, "POST", "Method is correct"); + isnot(lastRequest.request.postData.text. indexOf("Content-Type: application/x-www-form-urlencoded"), -1, "Content-Type is correct"); - isnot(lastRequest.request.body. + isnot(lastRequest.request.postData.text. indexOf("Content-Length: 20"), -1, "Content-length is correct"); - isnot(lastRequest.request.body. + isnot(lastRequest.request.postData.text. indexOf("name=foo+bar&age=144"), -1, "Form data is correct"); - ok(lastRequest.response.body.indexOf("") == 0, + is(lastRequest.response.content.text.indexOf(""), 0, "Response body's beginning is okay"); executeSoon(testNetworkPanel); - }, true); + }; let form = content.document.querySelector("form"); ok(form, "we have the HTML form"); @@ -152,19 +149,19 @@ function testNetworkPanel() { // Open the NetworkPanel. The functionality of the NetworkPanel is tested // within separate test files. - let networkPanel = HUDService.openNetworkPanel(hud.filterBox, lastRequest); - is(networkPanel, lastRequest.panels[0].get(), - "Network panel stored on lastRequest object"); + let networkPanel = HUDService.openNetworkPanel(hud.filterBox, lastActivity); + is(networkPanel, hud.filterBox._netPanel, + "Network panel stored on anchor node"); - networkPanel.panel.addEventListener("load", function(aEvent) { - networkPanel.panel.removeEventListener(aEvent.type, arguments.callee, - true); + networkPanel.panel.addEventListener("load", function onLoad(aEvent) { + networkPanel.panel.removeEventListener(aEvent.type, onLoad, true); ok(true, "NetworkPanel was opened"); // All tests are done. Shutdown. networkPanel.panel.hidePopup(); lastRequest = null; + lastActivity = null; HUDService.lastFinishedRequestCallback = null; executeSoon(finishTest); }, true); diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js index 30ea2a937785..3d17d961d00e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js +++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js @@ -68,25 +68,39 @@ function testGen() { let l10n = tempScope.WebConsoleUtils.l10n; tempScope = null; - var httpActivity = { - url: "http://www.testpage.com", - method: "GET", - - panels: [], - request: { - header: { - foo: "bar" - } + let httpActivity = { + meta: { + stages: [], + discardRequestBody: true, + discardResponseBody: true, + }, + log: { + entries: [{ + startedDateTime: (new Date()).toISOString(), + request: { + url: "http://www.testpage.com", + method: "GET", + cookies: [], + headers: [ + { name: "foo", value: "bar" }, + ], + }, + response: { + headers: [], + content: {}, + }, + timings: {}, + }], }, - response: { }, - timing: { - "REQUEST_HEADER": 0 - } }; + let entry = httpActivity.log.entries[0]; + let networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); - is (networkPanel, httpActivity.panels[0].get(), "Network panel stored on httpActivity object"); + is(filterBox._netPanel, networkPanel, + "Network panel stored on the anchor object"); + networkPanel.panel.addEventListener("load", function onLoad() { networkPanel.panel.removeEventListener("load", onLoad, true); testDriver.next(); @@ -94,6 +108,8 @@ function testGen() { yield; + info("test 1"); + checkIsVisible(networkPanel, { requestCookie: false, requestFormData: false, @@ -110,8 +126,11 @@ function testGen() { checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar"); // Test request body. - httpActivity.request.body = "hello world"; + info("test 2: request body"); + httpActivity.meta.discardRequestBody = false; + entry.request.postData = { text: "hello world" }; networkPanel.update(); + checkIsVisible(networkPanel, { requestBody: true, requestFormData: false, @@ -125,13 +144,19 @@ function testGen() { checkNodeContent(networkPanel, "requestBodyContent", "hello world"); // Test response header. - httpActivity.timing.RESPONSE_HEADER = 1000; - httpActivity.response.status = "999 earthquake win"; - httpActivity.response.header = { - "Content-Type": "text/html", - leaveHouses: "true" - } + info("test 3: response header"); + entry.timings.wait = 10; + entry.response.httpVersion = "HTTP/3.14"; + entry.response.status = 999; + entry.response.statusText = "earthquake win"; + entry.response.content.mimeType = "text/html"; + entry.response.headers.push( + { name: "Content-Type", value: "text/html" }, + { name: "leaveHouses", value: "true" } + ); + networkPanel.update(); + checkIsVisible(networkPanel, { requestBody: true, requestFormData: false, @@ -143,13 +168,14 @@ function testGen() { responseImageCached: false }); - checkNodeContent(networkPanel, "header", "999 earthquake win"); + checkNodeContent(networkPanel, "header", "HTTP/3.14 999 earthquake win"); checkNodeKeyValue(networkPanel, "responseHeadersContent", "leaveHouses", "true"); - checkNodeContent(networkPanel, "responseHeadersInfo", "1ms"); + checkNodeContent(networkPanel, "responseHeadersInfo", "10ms"); - httpActivity.timing.RESPONSE_COMPLETE = 2500; - // This is necessary to show that the request is done. - httpActivity.timing.TRANSACTION_CLOSE = 2500; + info("test 4"); + + httpActivity.meta.discardResponseBody = false; + entry.timings.receive = 2; networkPanel.update(); checkIsVisible(networkPanel, { @@ -163,7 +189,9 @@ function testGen() { responseImageCached: false }); - httpActivity.response.isDone = true; + info("test 5"); + + httpActivity.meta.stages.push("REQUEST_STOP", "TRANSACTION_CLOSE"); networkPanel.update(); checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms"); @@ -180,11 +208,17 @@ function testGen() { networkPanel.panel.hidePopup(); // Second run: Test for cookies and response body. - httpActivity.request.header.Cookie = "foo=bar; hello=world"; - httpActivity.response.body = "get out here"; + info("test 6: cookies and response body"); + entry.request.cookies.push( + { name: "foo", value: "bar" }, + { name: "hello", value: "world" } + ); + entry.response.content.text = "get out here"; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); - is (networkPanel, httpActivity.panels[1].get(), "Network panel stored on httpActivity object"); + is(filterBox._netPanel, networkPanel, + "Network panel stored on httpActivity object"); + networkPanel.panel.addEventListener("load", function onLoad() { networkPanel.panel.removeEventListener("load", onLoad, true); testDriver.next(); @@ -192,7 +226,6 @@ function testGen() { yield; - checkIsVisible(networkPanel, { requestBody: true, requestFormData: false, @@ -212,8 +245,10 @@ function testGen() { networkPanel.panel.hidePopup(); // Check image request. - httpActivity.response.header["Content-Type"] = "image/png"; - httpActivity.url = TEST_IMG; + info("test 7: image request"); + entry.response.headers[1].value = "image/png"; + entry.response.content.mimeType = "image/png"; + entry.request.url = TEST_IMG; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); networkPanel.panel.addEventListener("load", function onLoad() { @@ -259,7 +294,10 @@ function testGen() { } // Check cached image request. - httpActivity.response.status = "HTTP/1.1 304 Not Modified"; + info("test 8: cached image request"); + entry.response.httpVersion = "HTTP/1.1"; + entry.response.status = 304; + entry.response.statusText = "Not Modified"; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); networkPanel.panel.addEventListener("load", function onLoad() { @@ -286,11 +324,12 @@ function testGen() { networkPanel.panel.hidePopup(); // Test sent form data. - httpActivity.request.body = [ - "Content-Type: application/x-www-form-urlencoded\n" + - "Content-Length: 59\n" + + info("test 9: sent form data"); + entry.request.postData.text = [ + "Content-Type: application/x-www-form-urlencoded", + "Content-Length: 59", "name=rob&age=20" - ].join(""); + ].join("\n"); networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); networkPanel.panel.addEventListener("load", function onLoad() { @@ -316,7 +355,8 @@ function testGen() { networkPanel.panel.hidePopup(); // Test no space after Content-Type: - httpActivity.request.body = "Content-Type:application/x-www-form-urlencoded\n"; + info("test 10: no space after Content-Type header in post data"); + entry.request.postData.text = "Content-Type:application/x-www-form-urlencoded\n"; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); networkPanel.panel.addEventListener("load", function onLoad() { @@ -341,25 +381,18 @@ function testGen() { // Test cached data. - // Load a Latin-1 encoded page. - browser.addEventListener("load", function onLoad () { - browser.removeEventListener("load", onLoad, true); - httpActivity.charset = content.document.characterSet; - testDriver.next(); - }, true); - browser.contentWindow.wrappedJSObject.document.location = TEST_ENCODING_ISO_8859_1; + info("test 11: cached data"); - yield; - - httpActivity.url = TEST_ENCODING_ISO_8859_1; - httpActivity.response.header["Content-Type"] = "application/json"; - httpActivity.response.body = ""; + entry.request.url = TEST_ENCODING_ISO_8859_1; + entry.response.headers[1].value = "application/json"; + entry.response.content.mimeType = "application/json"; + entry.response.content.text = "my cached data is here!"; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); - networkPanel.isDoneCallback = function NP_doneCallback() { - networkPanel.isDoneCallback = null; + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); testDriver.next(); - } + }, true); yield; @@ -375,21 +408,22 @@ function testGen() { responseImageCached: false }); - checkNodeContent(networkPanel, "responseBodyCachedContent", "\u00fc\u00f6\u00E4"); + checkNodeContent(networkPanel, "responseBodyCachedContent", + "my cached data is here!"); + networkPanel.panel.hidePopup(); // Test a response with a content type that can't be displayed in the // NetworkPanel. - httpActivity.response.header["Content-Type"] = "application/x-shockwave-flash"; + info("test 12: unknown content type"); + entry.response.headers[1].value = "application/x-shockwave-flash"; + entry.response.content.mimeType = "application/x-shockwave-flash"; networkPanel = HUDService.openNetworkPanel(filterBox, httpActivity); - networkPanel.isDoneCallback = function NP_doneCallback() { - networkPanel.isDoneCallback = null; - try { - testDriver.next(); - } catch (e if e instanceof StopIteration) { - } - } + networkPanel.panel.addEventListener("load", function onLoad() { + networkPanel.panel.removeEventListener("load", onLoad, true); + testDriver.next(); + }, true); yield; @@ -453,4 +487,6 @@ function testGen() { // All done! testDriver = null; executeSoon(finishTest); + + yield; }