michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper")); michael@0: loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); michael@0: loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm"); michael@0: loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); michael@0: loader.lazyImporter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: loader.lazyServiceGetter(this, "gActivityDistributor", michael@0: "@mozilla.org/network/http-activity-distributor;1", michael@0: "nsIHttpActivityDistributor"); michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // Network logging michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: // The maximum uint32 value. michael@0: const PR_UINT32_MAX = 4294967295; michael@0: michael@0: // HTTP status codes. michael@0: const HTTP_MOVED_PERMANENTLY = 301; michael@0: const HTTP_FOUND = 302; michael@0: const HTTP_SEE_OTHER = 303; michael@0: const HTTP_TEMPORARY_REDIRECT = 307; michael@0: michael@0: // The maximum number of bytes a NetworkResponseListener can hold. michael@0: const RESPONSE_BODY_LIMIT = 1048576; // 1 MB michael@0: michael@0: /** michael@0: * The network response listener implements the nsIStreamListener and michael@0: * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature michael@0: * to get the response body of the request. michael@0: * michael@0: * The code is mostly based on code listings from: michael@0: * michael@0: * http://www.softwareishard.com/blog/firebug/ michael@0: * nsitraceablechannel-intercept-http-traffic/ michael@0: * michael@0: * @constructor michael@0: * @param object aOwner michael@0: * The response listener owner. This object needs to hold the michael@0: * |openResponses| object. michael@0: * @param object aHttpActivity michael@0: * HttpActivity object associated with this request. See NetworkMonitor michael@0: * for more information. michael@0: */ michael@0: function NetworkResponseListener(aOwner, aHttpActivity) michael@0: { michael@0: this.owner = aOwner; michael@0: this.receivedData = ""; michael@0: this.httpActivity = aHttpActivity; michael@0: this.bodySize = 0; michael@0: } michael@0: exports.NetworkResponseListener = NetworkResponseListener; michael@0: michael@0: NetworkResponseListener.prototype = { michael@0: QueryInterface: michael@0: XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, michael@0: Ci.nsIRequestObserver, Ci.nsISupports]), michael@0: michael@0: /** michael@0: * This NetworkResponseListener tracks the NetworkMonitor.openResponses object michael@0: * to find the associated uncached headers. michael@0: * @private michael@0: */ michael@0: _foundOpenResponse: false, michael@0: michael@0: /** michael@0: * The response listener owner. michael@0: */ michael@0: owner: null, michael@0: michael@0: /** michael@0: * The response will be written into the outputStream of this nsIPipe. michael@0: * Both ends of the pipe must be blocking. michael@0: */ michael@0: sink: null, michael@0: michael@0: /** michael@0: * The HttpActivity object associated with this response. michael@0: */ michael@0: httpActivity: null, michael@0: michael@0: /** michael@0: * Stores the received data as a string. michael@0: */ michael@0: receivedData: null, michael@0: michael@0: /** michael@0: * The network response body size. michael@0: */ michael@0: bodySize: null, michael@0: michael@0: /** michael@0: * The nsIRequest we are started for. michael@0: */ michael@0: request: null, michael@0: michael@0: /** michael@0: * Set the async listener for the given nsIAsyncInputStream. This allows us to michael@0: * wait asynchronously for any data coming from the stream. michael@0: * michael@0: * @param nsIAsyncInputStream aStream michael@0: * The input stream from where we are waiting for data to come in. michael@0: * @param nsIInputStreamCallback aListener michael@0: * The input stream callback you want. This is an object that must have michael@0: * the onInputStreamReady() method. If the argument is null, then the michael@0: * current callback is removed. michael@0: * @return void michael@0: */ michael@0: setAsyncListener: function NRL_setAsyncListener(aStream, aListener) michael@0: { michael@0: // Asynchronously wait for the stream to be readable or closed. michael@0: aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread); michael@0: }, michael@0: michael@0: /** michael@0: * Stores the received data, if request/response body logging is enabled. It michael@0: * also does limit the number of stored bytes, based on the michael@0: * RESPONSE_BODY_LIMIT constant. michael@0: * michael@0: * Learn more about nsIStreamListener at: michael@0: * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener michael@0: * michael@0: * @param nsIRequest aRequest michael@0: * @param nsISupports aContext michael@0: * @param nsIInputStream aInputStream michael@0: * @param unsigned long aOffset michael@0: * @param unsigned long aCount michael@0: */ michael@0: onDataAvailable: michael@0: function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) michael@0: { michael@0: this._findOpenResponse(); michael@0: let data = NetUtil.readInputStreamToString(aInputStream, aCount); michael@0: michael@0: this.bodySize += aCount; michael@0: michael@0: if (!this.httpActivity.discardResponseBody && michael@0: this.receivedData.length < RESPONSE_BODY_LIMIT) { michael@0: this.receivedData += NetworkHelper. michael@0: convertToUnicode(data, aRequest.contentCharset); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See documentation at michael@0: * https://developer.mozilla.org/En/NsIRequestObserver michael@0: * michael@0: * @param nsIRequest aRequest michael@0: * @param nsISupports aContext michael@0: */ michael@0: onStartRequest: function NRL_onStartRequest(aRequest) michael@0: { michael@0: this.request = aRequest; michael@0: this._findOpenResponse(); michael@0: // Asynchronously wait for the data coming from the request. michael@0: this.setAsyncListener(this.sink.inputStream, this); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the onStopRequest by closing the sink output stream. michael@0: * michael@0: * For more documentation about nsIRequestObserver go to: michael@0: * https://developer.mozilla.org/En/NsIRequestObserver michael@0: */ michael@0: onStopRequest: function NRL_onStopRequest() michael@0: { michael@0: this._findOpenResponse(); michael@0: this.sink.outputStream.close(); michael@0: }, michael@0: michael@0: /** michael@0: * Find the open response object associated to the current request. The michael@0: * NetworkMonitor._httpResponseExaminer() method saves the response headers in michael@0: * NetworkMonitor.openResponses. This method takes the data from the open michael@0: * response object and puts it into the HTTP activity object, then sends it to michael@0: * the remote Web Console instance. michael@0: * michael@0: * @private michael@0: */ michael@0: _findOpenResponse: function NRL__findOpenResponse() michael@0: { michael@0: if (!this.owner || this._foundOpenResponse) { michael@0: return; michael@0: } michael@0: michael@0: let openResponse = null; michael@0: michael@0: for each (let item in this.owner.openResponses) { michael@0: if (item.channel === this.httpActivity.channel) { michael@0: openResponse = item; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!openResponse) { michael@0: return; michael@0: } michael@0: this._foundOpenResponse = true; michael@0: michael@0: delete this.owner.openResponses[openResponse.id]; michael@0: michael@0: this.httpActivity.owner.addResponseHeaders(openResponse.headers); michael@0: this.httpActivity.owner.addResponseCookies(openResponse.cookies); michael@0: }, michael@0: michael@0: /** michael@0: * Clean up the response listener once the response input stream is closed. michael@0: * This is called from onStopRequest() or from onInputStreamReady() when the michael@0: * stream is closed. michael@0: * @return void michael@0: */ michael@0: onStreamClose: function NRL_onStreamClose() michael@0: { michael@0: if (!this.httpActivity) { michael@0: return; michael@0: } michael@0: // Remove our listener from the request input stream. michael@0: this.setAsyncListener(this.sink.inputStream, null); michael@0: michael@0: this._findOpenResponse(); michael@0: michael@0: if (!this.httpActivity.discardResponseBody && this.receivedData.length) { michael@0: this._onComplete(this.receivedData); michael@0: } michael@0: else if (!this.httpActivity.discardResponseBody && michael@0: this.httpActivity.responseStatus == 304) { michael@0: // Response is cached, so we load it from cache. michael@0: let charset = this.request.contentCharset || this.httpActivity.charset; michael@0: NetworkHelper.loadFromCache(this.httpActivity.url, charset, michael@0: this._onComplete.bind(this)); michael@0: } michael@0: else { michael@0: this._onComplete(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for when the response completes. This function cleans up the michael@0: * response listener. michael@0: * michael@0: * @param string [aData] michael@0: * Optional, the received data coming from the response listener or michael@0: * from the cache. michael@0: */ michael@0: _onComplete: function NRL__onComplete(aData) michael@0: { michael@0: let response = { michael@0: mimeType: "", michael@0: text: aData || "", michael@0: }; michael@0: michael@0: response.size = response.text.length; michael@0: michael@0: try { michael@0: response.mimeType = this.request.contentType; michael@0: } michael@0: catch (ex) { } michael@0: michael@0: if (!response.mimeType || !NetworkHelper.isTextMimeType(response.mimeType)) { michael@0: response.encoding = "base64"; michael@0: response.text = btoa(response.text); michael@0: } michael@0: michael@0: if (response.mimeType && this.request.contentCharset) { michael@0: response.mimeType += "; charset=" + this.request.contentCharset; michael@0: } michael@0: michael@0: this.receivedData = ""; michael@0: michael@0: this.httpActivity.owner. michael@0: addResponseContent(response, this.httpActivity.discardResponseBody); michael@0: michael@0: this.httpActivity.channel = null; michael@0: this.httpActivity.owner = null; michael@0: this.httpActivity = null; michael@0: this.sink = null; michael@0: this.inputStream = null; michael@0: this.request = null; michael@0: this.owner = null; michael@0: }, michael@0: michael@0: /** michael@0: * The nsIInputStreamCallback for when the request input stream is ready - michael@0: * either it has more data or it is closed. michael@0: * michael@0: * @param nsIAsyncInputStream aStream michael@0: * The sink input stream from which data is coming. michael@0: * @returns void michael@0: */ michael@0: onInputStreamReady: function NRL_onInputStreamReady(aStream) michael@0: { michael@0: if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) { michael@0: return; michael@0: } michael@0: michael@0: let available = -1; michael@0: try { michael@0: // This may throw if the stream is closed normally or due to an error. michael@0: available = aStream.available(); michael@0: } michael@0: catch (ex) { } michael@0: michael@0: if (available != -1) { michael@0: if (available != 0) { michael@0: // Note that passing 0 as the offset here is wrong, but the michael@0: // onDataAvailable() method does not use the offset, so it does not michael@0: // matter. michael@0: this.onDataAvailable(this.request, null, aStream, 0, available); michael@0: } michael@0: this.setAsyncListener(aStream, this); michael@0: } michael@0: else { michael@0: this.onStreamClose(); michael@0: } michael@0: }, michael@0: }; // NetworkResponseListener.prototype michael@0: michael@0: michael@0: /** michael@0: * The network monitor uses the nsIHttpActivityDistributor to monitor network michael@0: * requests. The nsIObserverService is also used for monitoring michael@0: * http-on-examine-response notifications. All network request information is michael@0: * routed to the remote Web Console. michael@0: * michael@0: * @constructor michael@0: * @param object aFilters michael@0: * Object with the filters to use for network requests: michael@0: * - window (nsIDOMWindow): filter network requests by the associated michael@0: * window object. michael@0: * - appId (number): filter requests by the appId. michael@0: * - topFrame (nsIDOMElement): filter requests by their topFrameElement. michael@0: * Filters are optional. If any of these filters match the request is michael@0: * logged (OR is applied). If no filter is provided then all requests are michael@0: * logged. michael@0: * @param object aOwner michael@0: * The network monitor owner. This object needs to hold: michael@0: * - onNetworkEvent(aRequestInfo, aChannel, aNetworkMonitor). michael@0: * This method is invoked once for every new network request and it is michael@0: * given the following arguments: the initial network request michael@0: * information, and the channel. The third argument is the NetworkMonitor michael@0: * instance. michael@0: * onNetworkEvent() must return an object which holds several add*() michael@0: * methods which are used to add further network request/response michael@0: * information. michael@0: */ michael@0: function NetworkMonitor(aFilters, aOwner) michael@0: { michael@0: if (aFilters) { michael@0: this.window = aFilters.window; michael@0: this.appId = aFilters.appId; michael@0: this.topFrame = aFilters.topFrame; michael@0: } michael@0: if (!this.window && !this.appId && !this.topFrame) { michael@0: this._logEverything = true; michael@0: } michael@0: this.owner = aOwner; michael@0: this.openRequests = {}; michael@0: this.openResponses = {}; michael@0: this._httpResponseExaminer = michael@0: DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this); michael@0: } michael@0: exports.NetworkMonitor = NetworkMonitor; michael@0: michael@0: NetworkMonitor.prototype = { michael@0: _logEverything: false, michael@0: window: null, michael@0: appId: null, michael@0: topFrame: null, michael@0: michael@0: httpTransactionCodes: { michael@0: 0x5001: "REQUEST_HEADER", michael@0: 0x5002: "REQUEST_BODY_SENT", michael@0: 0x5003: "RESPONSE_START", michael@0: 0x5004: "RESPONSE_HEADER", michael@0: 0x5005: "RESPONSE_COMPLETE", michael@0: 0x5006: "TRANSACTION_CLOSE", michael@0: michael@0: 0x804b0003: "STATUS_RESOLVING", michael@0: 0x804b000b: "STATUS_RESOLVED", michael@0: 0x804b0007: "STATUS_CONNECTING_TO", michael@0: 0x804b0004: "STATUS_CONNECTED_TO", michael@0: 0x804b0005: "STATUS_SENDING_TO", michael@0: 0x804b000a: "STATUS_WAITING_FOR", michael@0: 0x804b0006: "STATUS_RECEIVING_FROM" michael@0: }, michael@0: michael@0: // Network response bodies are piped through a buffer of the given size (in michael@0: // bytes). michael@0: responsePipeSegmentSize: null, michael@0: michael@0: owner: null, michael@0: michael@0: /** michael@0: * Whether to save the bodies of network requests and responses. Disabled by michael@0: * default to save memory. michael@0: * @type boolean michael@0: */ michael@0: saveRequestAndResponseBodies: false, michael@0: michael@0: /** michael@0: * Object that holds the HTTP activity objects for ongoing requests. michael@0: */ michael@0: openRequests: null, michael@0: michael@0: /** michael@0: * Object that holds response headers coming from this._httpResponseExaminer. michael@0: */ michael@0: openResponses: null, michael@0: michael@0: /** michael@0: * The network monitor initializer. michael@0: */ michael@0: init: function NM_init() michael@0: { michael@0: this.responsePipeSegmentSize = Services.prefs michael@0: .getIntPref("network.buffer.cache.size"); michael@0: michael@0: gActivityDistributor.addObserver(this); michael@0: michael@0: if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { michael@0: Services.obs.addObserver(this._httpResponseExaminer, michael@0: "http-on-examine-response", false); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Observe notifications for the http-on-examine-response topic, coming from michael@0: * the nsIObserverService. michael@0: * michael@0: * @private michael@0: * @param nsIHttpChannel aSubject michael@0: * @param string aTopic michael@0: * @returns void michael@0: */ michael@0: _httpResponseExaminer: function NM__httpResponseExaminer(aSubject, aTopic) michael@0: { michael@0: // The httpResponseExaminer is used to retrieve the uncached response michael@0: // headers. The data retrieved is stored in openResponses. The michael@0: // NetworkResponseListener is responsible with updating the httpActivity michael@0: // object with the data from the new object in openResponses. michael@0: michael@0: if (!this.owner || aTopic != "http-on-examine-response" || michael@0: !(aSubject instanceof Ci.nsIHttpChannel)) { michael@0: return; michael@0: } michael@0: michael@0: let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); michael@0: michael@0: if (!this._matchRequest(channel)) { michael@0: return; michael@0: } michael@0: michael@0: let response = { michael@0: id: gSequenceId(), michael@0: channel: channel, michael@0: headers: [], michael@0: cookies: [], michael@0: }; michael@0: michael@0: let setCookieHeader = null; michael@0: michael@0: channel.visitResponseHeaders({ michael@0: visitHeader: function NM__visitHeader(aName, aValue) { michael@0: let lowerName = aName.toLowerCase(); michael@0: if (lowerName == "set-cookie") { michael@0: setCookieHeader = aValue; michael@0: } michael@0: response.headers.push({ name: aName, value: aValue }); michael@0: } michael@0: }); michael@0: michael@0: if (!response.headers.length) { michael@0: return; // No need to continue. michael@0: } michael@0: michael@0: if (setCookieHeader) { michael@0: response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader); michael@0: } michael@0: michael@0: // Determine the HTTP version. michael@0: let httpVersionMaj = {}; michael@0: let httpVersionMin = {}; michael@0: michael@0: channel.QueryInterface(Ci.nsIHttpChannelInternal); michael@0: channel.getResponseVersion(httpVersionMaj, httpVersionMin); michael@0: michael@0: response.status = channel.responseStatus; michael@0: response.statusText = channel.responseStatusText; michael@0: response.httpVersion = "HTTP/" + httpVersionMaj.value + "." + michael@0: httpVersionMin.value; michael@0: michael@0: this.openResponses[response.id] = response; michael@0: }, michael@0: michael@0: /** michael@0: * Begin observing HTTP traffic that originates inside the current tab. michael@0: * michael@0: * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver michael@0: * michael@0: * @param nsIHttpChannel aChannel michael@0: * @param number aActivityType michael@0: * @param number aActivitySubtype michael@0: * @param number aTimestamp michael@0: * @param number aExtraSizeData michael@0: * @param string aExtraStringData michael@0: */ michael@0: observeActivity: DevToolsUtils.makeInfallible(function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, aExtraStringData) michael@0: { michael@0: if (!this.owner || michael@0: aActivityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && michael@0: aActivityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { michael@0: return; michael@0: } michael@0: michael@0: if (!(aChannel instanceof Ci.nsIHttpChannel)) { michael@0: return; michael@0: } michael@0: michael@0: aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); michael@0: michael@0: if (aActivitySubtype == michael@0: gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { michael@0: this._onRequestHeader(aChannel, aTimestamp, aExtraStringData); michael@0: return; michael@0: } michael@0: michael@0: // Iterate over all currently ongoing requests. If aChannel can't michael@0: // be found within them, then exit this function. michael@0: let httpActivity = null; michael@0: for each (let item in this.openRequests) { michael@0: if (item.channel === aChannel) { michael@0: httpActivity = item; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!httpActivity) { michael@0: return; michael@0: } michael@0: michael@0: let transCodes = this.httpTransactionCodes; michael@0: michael@0: // Store the time information for this activity subtype. michael@0: if (aActivitySubtype in transCodes) { michael@0: let stage = transCodes[aActivitySubtype]; michael@0: if (stage in httpActivity.timings) { michael@0: httpActivity.timings[stage].last = aTimestamp; michael@0: } michael@0: else { michael@0: httpActivity.timings[stage] = { michael@0: first: aTimestamp, michael@0: last: aTimestamp, michael@0: }; michael@0: } michael@0: } michael@0: michael@0: switch (aActivitySubtype) { michael@0: case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: michael@0: this._onRequestBodySent(httpActivity); michael@0: break; michael@0: case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: michael@0: this._onResponseHeader(httpActivity, aExtraStringData); michael@0: break; michael@0: case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: michael@0: this._onTransactionClose(httpActivity); michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Check if a given network request should be logged by this network monitor michael@0: * instance based on the current filters. michael@0: * michael@0: * @private michael@0: * @param nsIHttpChannel aChannel michael@0: * Request to check. michael@0: * @return boolean michael@0: * True if the network request should be logged, false otherwise. michael@0: */ michael@0: _matchRequest: function NM__matchRequest(aChannel) michael@0: { michael@0: if (this._logEverything) { michael@0: return true; michael@0: } michael@0: michael@0: if (this.window) { michael@0: let win = NetworkHelper.getWindowForRequest(aChannel); michael@0: if (win && win.top === this.window) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: if (this.topFrame) { michael@0: let topFrame = NetworkHelper.getTopFrameForRequest(aChannel); michael@0: if (topFrame && topFrame === this.topFrame) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: if (this.appId) { michael@0: let appId = NetworkHelper.getAppIdForRequest(aChannel); michael@0: if (appId && appId == this.appId) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the michael@0: * headers are sent to the server. This method creates the |httpActivity| michael@0: * object where we store the request and response information that is michael@0: * collected through its lifetime. michael@0: * michael@0: * @private michael@0: * @param nsIHttpChannel aChannel michael@0: * @param number aTimestamp michael@0: * @param string aExtraStringData michael@0: * @return void michael@0: */ michael@0: _onRequestHeader: michael@0: function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData) michael@0: { michael@0: if (!this._matchRequest(aChannel)) { michael@0: return; michael@0: } michael@0: michael@0: let win = NetworkHelper.getWindowForRequest(aChannel); michael@0: let httpActivity = this.createActivityObject(aChannel); michael@0: michael@0: // see NM__onRequestBodySent() michael@0: httpActivity.charset = win ? win.document.characterSet : null; michael@0: httpActivity.private = win ? PrivateBrowsingUtils.isWindowPrivate(win) : false; michael@0: michael@0: httpActivity.timings.REQUEST_HEADER = { michael@0: first: aTimestamp, michael@0: last: aTimestamp michael@0: }; michael@0: michael@0: let httpVersionMaj = {}; michael@0: let httpVersionMin = {}; michael@0: let event = {}; michael@0: event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString(); michael@0: event.headersSize = aExtraStringData.length; michael@0: event.method = aChannel.requestMethod; michael@0: event.url = aChannel.URI.spec; michael@0: event.private = httpActivity.private; michael@0: michael@0: // Determine if this is an XHR request. michael@0: try { michael@0: let callbacks = aChannel.notificationCallbacks; michael@0: let xhrRequest = callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null; michael@0: httpActivity.isXHR = event.isXHR = !!xhrRequest; michael@0: } catch (e) { michael@0: httpActivity.isXHR = event.isXHR = false; michael@0: } michael@0: michael@0: // Determine the HTTP version. michael@0: aChannel.QueryInterface(Ci.nsIHttpChannelInternal); michael@0: aChannel.getRequestVersion(httpVersionMaj, httpVersionMin); michael@0: michael@0: event.httpVersion = "HTTP/" + httpVersionMaj.value + "." + michael@0: httpVersionMin.value; michael@0: michael@0: event.discardRequestBody = !this.saveRequestAndResponseBodies; michael@0: event.discardResponseBody = !this.saveRequestAndResponseBodies; michael@0: michael@0: let headers = []; michael@0: let cookies = []; michael@0: let cookieHeader = null; michael@0: michael@0: // Copy the request header data. michael@0: aChannel.visitRequestHeaders({ michael@0: visitHeader: function NM__visitHeader(aName, aValue) michael@0: { michael@0: if (aName == "Cookie") { michael@0: cookieHeader = aValue; michael@0: } michael@0: headers.push({ name: aName, value: aValue }); michael@0: } michael@0: }); michael@0: michael@0: if (cookieHeader) { michael@0: cookies = NetworkHelper.parseCookieHeader(cookieHeader); michael@0: } michael@0: michael@0: httpActivity.owner = this.owner.onNetworkEvent(event, aChannel, this); michael@0: michael@0: this._setupResponseListener(httpActivity); michael@0: michael@0: this.openRequests[httpActivity.id] = httpActivity; michael@0: michael@0: httpActivity.owner.addRequestHeaders(headers); michael@0: httpActivity.owner.addRequestCookies(cookies); michael@0: }, michael@0: michael@0: /** michael@0: * Create the empty HTTP activity object. This object is used for storing all michael@0: * the request and response information. michael@0: * michael@0: * This is a HAR-like object. Conformance to the spec is not guaranteed at michael@0: * this point. michael@0: * michael@0: * TODO: Bug 708717 - Add support for network log export to HAR michael@0: * michael@0: * @see http://www.softwareishard.com/blog/har-12-spec michael@0: * @param nsIHttpChannel aChannel michael@0: * The HTTP channel for which the HTTP activity object is created. michael@0: * @return object michael@0: * The new HTTP activity object. michael@0: */ michael@0: createActivityObject: function NM_createActivityObject(aChannel) michael@0: { michael@0: return { michael@0: id: gSequenceId(), michael@0: channel: aChannel, michael@0: charset: null, // see NM__onRequestHeader() michael@0: url: aChannel.URI.spec, michael@0: discardRequestBody: !this.saveRequestAndResponseBodies, michael@0: discardResponseBody: !this.saveRequestAndResponseBodies, michael@0: timings: {}, // internal timing information, see NM_observeActivity() michael@0: responseStatus: null, // see NM__onResponseHeader() michael@0: owner: null, // the activity owner which is notified when changes happen michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Setup the network response listener for the given HTTP activity. The michael@0: * NetworkResponseListener is responsible for storing the response body. michael@0: * michael@0: * @private michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object we are tracking. michael@0: */ michael@0: _setupResponseListener: function NM__setupResponseListener(aHttpActivity) michael@0: { michael@0: let channel = aHttpActivity.channel; michael@0: channel.QueryInterface(Ci.nsITraceableChannel); michael@0: michael@0: // The response will be written into the outputStream of this pipe. michael@0: // This allows us to buffer the data we are receiving and read it michael@0: // asynchronously. michael@0: // Both ends of the pipe must be blocking. michael@0: let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); michael@0: michael@0: // The streams need to be blocking because this is required by the michael@0: // stream tee. michael@0: sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null); michael@0: michael@0: // Add listener for the response body. michael@0: let newListener = new NetworkResponseListener(this, aHttpActivity); michael@0: michael@0: // Remember the input stream, so it isn't released by GC. michael@0: newListener.inputStream = sink.inputStream; michael@0: newListener.sink = sink; michael@0: michael@0: let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]. michael@0: createInstance(Ci.nsIStreamListenerTee); michael@0: michael@0: let originalListener = channel.setNewListener(tee); michael@0: michael@0: tee.init(originalListener, sink.outputStream, newListener); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged michael@0: * here. michael@0: * michael@0: * @private michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object we are working with. michael@0: */ michael@0: _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity) michael@0: { michael@0: if (aHttpActivity.discardRequestBody) { michael@0: return; michael@0: } michael@0: michael@0: let sentBody = NetworkHelper. michael@0: readPostTextFromRequest(aHttpActivity.channel, michael@0: aHttpActivity.charset); michael@0: michael@0: if (!sentBody && this.window && michael@0: aHttpActivity.url == this.window.location.href) { michael@0: // If the request URL is the same as the current page URL, then michael@0: // we can try to get the posted text from the page directly. michael@0: // This check is necessary as otherwise the michael@0: // NetworkHelper.readPostTextFromPageViaWebNav() michael@0: // function is called for image requests as well but these michael@0: // are not web pages and as such don't store the posted text michael@0: // in the cache of the webpage. michael@0: let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor). michael@0: getInterface(Ci.nsIWebNavigation); michael@0: sentBody = NetworkHelper. michael@0: readPostTextFromPageViaWebNav(webNav, aHttpActivity.charset); michael@0: } michael@0: michael@0: if (sentBody) { michael@0: aHttpActivity.owner.addRequestPostData({ text: sentBody }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores michael@0: * information about the response headers. michael@0: * michael@0: * @private michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object we are working with. michael@0: * @param string aExtraStringData michael@0: * The uncached response headers. michael@0: */ michael@0: _onResponseHeader: michael@0: function NM__onResponseHeader(aHttpActivity, aExtraStringData) michael@0: { michael@0: // aExtraStringData contains the uncached response headers. The first line michael@0: // contains the response status (e.g. HTTP/1.1 200 OK). michael@0: // michael@0: // Note: The response header is not saved here. Calling the michael@0: // channel.visitResponseHeaders() methood at this point sometimes causes an michael@0: // NS_ERROR_NOT_AVAILABLE exception. michael@0: // michael@0: // We could parse aExtraStringData to get the headers and their values, but michael@0: // that is not trivial to do in an accurate manner. Hence, we save the michael@0: // response headers in this._httpResponseExaminer(). michael@0: michael@0: let headers = aExtraStringData.split(/\r\n|\n|\r/); michael@0: let statusLine = headers.shift(); michael@0: let statusLineArray = statusLine.split(" "); michael@0: michael@0: let response = {}; michael@0: response.httpVersion = statusLineArray.shift(); michael@0: response.status = statusLineArray.shift(); michael@0: response.statusText = statusLineArray.join(" "); michael@0: response.headersSize = aExtraStringData.length; michael@0: michael@0: aHttpActivity.responseStatus = response.status; michael@0: michael@0: // Discard the response body for known response statuses. michael@0: switch (parseInt(response.status)) { michael@0: case HTTP_MOVED_PERMANENTLY: michael@0: case HTTP_FOUND: michael@0: case HTTP_SEE_OTHER: michael@0: case HTTP_TEMPORARY_REDIRECT: michael@0: aHttpActivity.discardResponseBody = true; michael@0: break; michael@0: } michael@0: michael@0: response.discardResponseBody = aHttpActivity.discardResponseBody; michael@0: michael@0: aHttpActivity.owner.addResponseStart(response); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR michael@0: * timing information on the HTTP activity object and clears the request michael@0: * from the list of known open requests. michael@0: * michael@0: * @private michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object we work with. michael@0: */ michael@0: _onTransactionClose: function NM__onTransactionClose(aHttpActivity) michael@0: { michael@0: let result = this._setupHarTimings(aHttpActivity); michael@0: aHttpActivity.owner.addEventTimings(result.total, result.timings); michael@0: delete this.openRequests[aHttpActivity.id]; michael@0: }, michael@0: michael@0: /** michael@0: * Update the HTTP activity object to include timing information as in the HAR michael@0: * spec. The HTTP activity object holds the raw timing information in michael@0: * |timings| - these are timings stored for each activity notification. The michael@0: * HAR timing information is constructed based on these lower level data. michael@0: * michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object we are working with. michael@0: * @return object michael@0: * This object holds two properties: michael@0: * - total - the total time for all of the request and response. michael@0: * - timings - the HAR timings object. michael@0: */ michael@0: _setupHarTimings: function NM__setupHarTimings(aHttpActivity) michael@0: { michael@0: let timings = aHttpActivity.timings; michael@0: let harTimings = {}; michael@0: michael@0: // Not clear how we can determine "blocked" time. michael@0: harTimings.blocked = -1; michael@0: michael@0: // DNS timing information is available only in when the DNS record is not michael@0: // cached. michael@0: harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ? michael@0: timings.STATUS_RESOLVED.last - michael@0: timings.STATUS_RESOLVING.first : -1; michael@0: michael@0: if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { michael@0: harTimings.connect = timings.STATUS_CONNECTED_TO.last - michael@0: timings.STATUS_CONNECTING_TO.first; michael@0: } michael@0: else if (timings.STATUS_SENDING_TO) { michael@0: harTimings.connect = timings.STATUS_SENDING_TO.first - michael@0: timings.REQUEST_HEADER.first; michael@0: } michael@0: else { michael@0: harTimings.connect = -1; michael@0: } michael@0: michael@0: if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && michael@0: (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { michael@0: harTimings.send = (timings.STATUS_WAITING_FOR || michael@0: timings.STATUS_RECEIVING_FROM).first - michael@0: (timings.STATUS_CONNECTED_TO || michael@0: timings.STATUS_SENDING_TO).last; michael@0: } michael@0: else { michael@0: harTimings.send = -1; michael@0: } michael@0: michael@0: if (timings.RESPONSE_START) { michael@0: harTimings.wait = timings.RESPONSE_START.first - michael@0: (timings.REQUEST_BODY_SENT || michael@0: timings.STATUS_SENDING_TO).last; michael@0: } michael@0: else { michael@0: harTimings.wait = -1; michael@0: } michael@0: michael@0: if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { michael@0: harTimings.receive = timings.RESPONSE_COMPLETE.last - michael@0: timings.RESPONSE_START.first; michael@0: } michael@0: else { michael@0: harTimings.receive = -1; michael@0: } michael@0: michael@0: let totalTime = 0; michael@0: for (let timing in harTimings) { michael@0: let time = Math.max(Math.round(harTimings[timing] / 1000), -1); michael@0: harTimings[timing] = time; michael@0: if (time > -1) { michael@0: totalTime += time; michael@0: } michael@0: } michael@0: michael@0: return { michael@0: total: totalTime, michael@0: timings: harTimings, michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Suspend Web Console activity. This is called when all Web Consoles are michael@0: * closed. michael@0: */ michael@0: destroy: function NM_destroy() michael@0: { michael@0: if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { michael@0: Services.obs.removeObserver(this._httpResponseExaminer, michael@0: "http-on-examine-response"); michael@0: } michael@0: michael@0: gActivityDistributor.removeObserver(this); michael@0: michael@0: this.openRequests = {}; michael@0: this.openResponses = {}; michael@0: this.owner = null; michael@0: this.window = null; michael@0: this.topFrame = null; michael@0: }, michael@0: }; // NetworkMonitor.prototype michael@0: michael@0: michael@0: /** michael@0: * The NetworkMonitorChild is used to proxy all of the network activity of the michael@0: * child app process from the main process. The child WebConsoleActor creates an michael@0: * instance of this object. michael@0: * michael@0: * Network requests for apps happen in the main process. As such, michael@0: * a NetworkMonitor instance is used by the WebappsActor in the main process to michael@0: * log the network requests for this child process. michael@0: * michael@0: * The main process creates NetworkEventActorProxy instances per request. These michael@0: * send the data to this object using the nsIMessageManager. Here we proxy the michael@0: * data to the WebConsoleActor or to a NetworkEventActor. michael@0: * michael@0: * @constructor michael@0: * @param number appId michael@0: * The web appId of the child process. michael@0: * @param nsIMessageManager messageManager michael@0: * The nsIMessageManager to use to communicate with the parent process. michael@0: * @param string connID michael@0: * The connection ID to use for send messages to the parent process. michael@0: * @param object owner michael@0: * The WebConsoleActor that is listening for the network requests. michael@0: */ michael@0: function NetworkMonitorChild(appId, messageManager, connID, owner) { michael@0: this.appId = appId; michael@0: this.connID = connID; michael@0: this.owner = owner; michael@0: this._messageManager = messageManager; michael@0: this._onNewEvent = this._onNewEvent.bind(this); michael@0: this._onUpdateEvent = this._onUpdateEvent.bind(this); michael@0: this._netEvents = new Map(); michael@0: } michael@0: exports.NetworkMonitorChild = NetworkMonitorChild; michael@0: michael@0: NetworkMonitorChild.prototype = { michael@0: appId: null, michael@0: owner: null, michael@0: _netEvents: null, michael@0: _saveRequestAndResponseBodies: false, michael@0: michael@0: get saveRequestAndResponseBodies() { michael@0: return this._saveRequestAndResponseBodies; michael@0: }, michael@0: michael@0: set saveRequestAndResponseBodies(val) { michael@0: this._saveRequestAndResponseBodies = val; michael@0: michael@0: this._messageManager.sendAsyncMessage("debug:netmonitor:" + this.connID, { michael@0: appId: this.appId, michael@0: action: "setPreferences", michael@0: preferences: { michael@0: saveRequestAndResponseBodies: this._saveRequestAndResponseBodies, michael@0: }, michael@0: }); michael@0: }, michael@0: michael@0: init: function() { michael@0: let mm = this._messageManager; michael@0: mm.addMessageListener("debug:netmonitor:" + this.connID + ":newEvent", michael@0: this._onNewEvent); michael@0: mm.addMessageListener("debug:netmonitor:" + this.connID + ":updateEvent", michael@0: this._onUpdateEvent); michael@0: mm.sendAsyncMessage("debug:netmonitor:" + this.connID, { michael@0: appId: this.appId, michael@0: action: "start", michael@0: }); michael@0: }, michael@0: michael@0: _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) { michael@0: let {id, event} = msg.data; michael@0: let actor = this.owner.onNetworkEvent(event); michael@0: this._netEvents.set(id, Cu.getWeakReference(actor)); michael@0: }), michael@0: michael@0: _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) { michael@0: let {id, method, args} = msg.data; michael@0: let weakActor = this._netEvents.get(id); michael@0: let actor = weakActor ? weakActor.get() : null; michael@0: if (!actor) { michael@0: Cu.reportError("Received debug:netmonitor:updateEvent for unknown event ID: " + id); michael@0: return; michael@0: } michael@0: if (!(method in actor)) { michael@0: Cu.reportError("Received debug:netmonitor:updateEvent unsupported method: " + method); michael@0: return; michael@0: } michael@0: actor[method].apply(actor, args); michael@0: }), michael@0: michael@0: destroy: function() { michael@0: let mm = this._messageManager; michael@0: mm.removeMessageListener("debug:netmonitor:" + this.connID + ":newEvent", michael@0: this._onNewEvent); michael@0: mm.removeMessageListener("debug:netmonitor:" + this.connID + ":updateEvent", michael@0: this._onUpdateEvent); michael@0: mm.sendAsyncMessage("debug:netmonitor:" + this.connID, { michael@0: action: "disconnect", michael@0: }); michael@0: this._netEvents.clear(); michael@0: this._messageManager = null; michael@0: this.owner = null; michael@0: }, michael@0: }; // NetworkMonitorChild.prototype michael@0: michael@0: /** michael@0: * The NetworkEventActorProxy is used to send network request information from michael@0: * the main process to the child app process. One proxy is used per request. michael@0: * Similarly, one NetworkEventActor in the child app process is used per michael@0: * request. The client receives all network logs from the child actors. michael@0: * michael@0: * The child process has a NetworkMonitorChild instance that is listening for michael@0: * all network logging from the main process. The net monitor shim is used to michael@0: * proxy the data to the WebConsoleActor instance of the child process. michael@0: * michael@0: * @constructor michael@0: * @param nsIMessageManager messageManager michael@0: * The message manager for the child app process. This is used for michael@0: * communication with the NetworkMonitorChild instance of the process. michael@0: * @param string connID michael@0: * The connection ID to use to send messages to the child process. michael@0: */ michael@0: function NetworkEventActorProxy(messageManager, connID) { michael@0: this.id = gSequenceId(); michael@0: this.connID = connID; michael@0: this.messageManager = messageManager; michael@0: } michael@0: exports.NetworkEventActorProxy = NetworkEventActorProxy; michael@0: michael@0: NetworkEventActorProxy.methodFactory = function(method) { michael@0: return DevToolsUtils.makeInfallible(function() { michael@0: let args = Array.slice(arguments); michael@0: let mm = this.messageManager; michael@0: mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":updateEvent", { michael@0: id: this.id, michael@0: method: method, michael@0: args: args, michael@0: }); michael@0: }, "NetworkEventActorProxy." + method); michael@0: }; michael@0: michael@0: NetworkEventActorProxy.prototype = { michael@0: /** michael@0: * Initialize the network event. This method sends the network request event michael@0: * to the content process. michael@0: * michael@0: * @param object event michael@0: * Object describing the network request. michael@0: * @return object michael@0: * This object. michael@0: */ michael@0: init: DevToolsUtils.makeInfallible(function(event) michael@0: { michael@0: let mm = this.messageManager; michael@0: mm.sendAsyncMessage("debug:netmonitor:" + this.connID + ":newEvent", { michael@0: id: this.id, michael@0: event: event, michael@0: }); michael@0: return this; michael@0: }), michael@0: }; michael@0: michael@0: (function() { michael@0: // Listeners for new network event data coming from the NetworkMonitor. michael@0: let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData", michael@0: "addResponseStart", "addResponseHeaders", "addResponseCookies", michael@0: "addResponseContent", "addEventTimings"]; michael@0: let factory = NetworkEventActorProxy.methodFactory; michael@0: for (let method of methods) { michael@0: NetworkEventActorProxy.prototype[method] = factory(method); michael@0: } michael@0: })(); michael@0: michael@0: michael@0: /** michael@0: * The NetworkMonitor manager used by the Webapps actor in the main process. michael@0: * This object uses the message manager to listen for requests from the child michael@0: * process to start/stop the network monitor. michael@0: * michael@0: * @constructor michael@0: * @param nsIDOMElement frame michael@0: * The browser frame to work with (mozbrowser). michael@0: * @param string id michael@0: * Instance identifier to use for messages. michael@0: */ michael@0: function NetworkMonitorManager(frame, id) michael@0: { michael@0: this.id = id; michael@0: let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; michael@0: this.messageManager = mm; michael@0: this.frame = frame; michael@0: this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this); michael@0: this.onNetworkEvent = this.onNetworkEvent.bind(this); michael@0: michael@0: mm.addMessageListener("debug:netmonitor:" + id, this.onNetMonitorMessage); michael@0: } michael@0: exports.NetworkMonitorManager = NetworkMonitorManager; michael@0: michael@0: NetworkMonitorManager.prototype = { michael@0: netMonitor: null, michael@0: frame: null, michael@0: messageManager: null, michael@0: michael@0: /** michael@0: * Handler for "debug:monitor" messages received through the message manager michael@0: * from the content process. michael@0: * michael@0: * @param object msg michael@0: * Message from the content. michael@0: */ michael@0: onNetMonitorMessage: DevToolsUtils.makeInfallible(function _onNetMonitorMessage(msg) { michael@0: let { action, appId } = msg.json; michael@0: // Pipe network monitor data from parent to child via the message manager. michael@0: switch (action) { michael@0: case "start": michael@0: if (!this.netMonitor) { michael@0: this.netMonitor = new NetworkMonitor({ michael@0: topFrame: this.frame, michael@0: appId: appId, michael@0: }, this); michael@0: this.netMonitor.init(); michael@0: } michael@0: break; michael@0: michael@0: case "setPreferences": { michael@0: let {preferences} = msg.json; michael@0: for (let key of Object.keys(preferences)) { michael@0: if (key == "saveRequestAndResponseBodies" && this.netMonitor) { michael@0: this.netMonitor.saveRequestAndResponseBodies = preferences[key]; michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "stop": michael@0: if (this.netMonitor) { michael@0: this.netMonitor.destroy(); michael@0: this.netMonitor = null; michael@0: } michael@0: break; michael@0: michael@0: case "disconnect": michael@0: this.destroy(); michael@0: break; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Handler for new network requests. This method is invoked by the current michael@0: * NetworkMonitor instance. michael@0: * michael@0: * @param object event michael@0: * Object describing the network request. michael@0: * @return object michael@0: * A NetworkEventActorProxy instance which is notified when further michael@0: * data about the request is available. michael@0: */ michael@0: onNetworkEvent: DevToolsUtils.makeInfallible(function _onNetworkEvent(event) { michael@0: return new NetworkEventActorProxy(this.messageManager, this.id).init(event); michael@0: }), michael@0: michael@0: destroy: function() michael@0: { michael@0: if (this.messageManager) { michael@0: this.messageManager.removeMessageListener("debug:netmonitor:" + this.id, michael@0: this.onNetMonitorMessage); michael@0: } michael@0: this.messageManager = null; michael@0: this.filters = null; michael@0: michael@0: if (this.netMonitor) { michael@0: this.netMonitor.destroy(); michael@0: this.netMonitor = null; michael@0: } michael@0: }, michael@0: }; // NetworkMonitorManager.prototype michael@0: michael@0: michael@0: /** michael@0: * A WebProgressListener that listens for location changes. michael@0: * michael@0: * This progress listener is used to track file loads and other kinds of michael@0: * location changes. michael@0: * michael@0: * @constructor michael@0: * @param object aWindow michael@0: * The window for which we need to track location changes. michael@0: * @param object aOwner michael@0: * The listener owner which needs to implement two methods: michael@0: * - onFileActivity(aFileURI) michael@0: * - onLocationChange(aState, aTabURI, aPageTitle) michael@0: */ michael@0: function ConsoleProgressListener(aWindow, aOwner) michael@0: { michael@0: this.window = aWindow; michael@0: this.owner = aOwner; michael@0: } michael@0: exports.ConsoleProgressListener = ConsoleProgressListener; michael@0: michael@0: ConsoleProgressListener.prototype = { michael@0: /** michael@0: * Constant used for startMonitor()/stopMonitor() that tells you want to michael@0: * monitor file loads. michael@0: */ michael@0: MONITOR_FILE_ACTIVITY: 1, michael@0: michael@0: /** michael@0: * Constant used for startMonitor()/stopMonitor() that tells you want to michael@0: * monitor page location changes. michael@0: */ michael@0: MONITOR_LOCATION_CHANGE: 2, michael@0: michael@0: /** michael@0: * Tells if you want to monitor file activity. michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _fileActivity: false, michael@0: michael@0: /** michael@0: * Tells if you want to monitor location changes. michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _locationChange: false, michael@0: michael@0: /** michael@0: * Tells if the console progress listener is initialized or not. michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _initialized: false, michael@0: michael@0: _webProgress: null, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: /** michael@0: * Initialize the ConsoleProgressListener. michael@0: * @private michael@0: */ michael@0: _init: function CPL__init() michael@0: { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: this._webProgress = this.window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIWebProgress); michael@0: this._webProgress.addProgressListener(this, michael@0: Ci.nsIWebProgress.NOTIFY_STATE_ALL); michael@0: michael@0: this._initialized = true; michael@0: }, michael@0: michael@0: /** michael@0: * Start a monitor/tracker related to the current nsIWebProgressListener michael@0: * instance. michael@0: * michael@0: * @param number aMonitor michael@0: * Tells what you want to track. Available constants: michael@0: * - this.MONITOR_FILE_ACTIVITY michael@0: * Track file loads. michael@0: * - this.MONITOR_LOCATION_CHANGE michael@0: * Track location changes for the top window. michael@0: */ michael@0: startMonitor: function CPL_startMonitor(aMonitor) michael@0: { michael@0: switch (aMonitor) { michael@0: case this.MONITOR_FILE_ACTIVITY: michael@0: this._fileActivity = true; michael@0: break; michael@0: case this.MONITOR_LOCATION_CHANGE: michael@0: this._locationChange = true; michael@0: break; michael@0: default: michael@0: throw new Error("ConsoleProgressListener: unknown monitor type " + michael@0: aMonitor + "!"); michael@0: } michael@0: this._init(); michael@0: }, michael@0: michael@0: /** michael@0: * Stop a monitor. michael@0: * michael@0: * @param number aMonitor michael@0: * Tells what you want to stop tracking. See this.startMonitor() for michael@0: * the list of constants. michael@0: */ michael@0: stopMonitor: function CPL_stopMonitor(aMonitor) michael@0: { michael@0: switch (aMonitor) { michael@0: case this.MONITOR_FILE_ACTIVITY: michael@0: this._fileActivity = false; michael@0: break; michael@0: case this.MONITOR_LOCATION_CHANGE: michael@0: this._locationChange = false; michael@0: break; michael@0: default: michael@0: throw new Error("ConsoleProgressListener: unknown monitor type " + michael@0: aMonitor + "!"); michael@0: } michael@0: michael@0: if (!this._fileActivity && !this._locationChange) { michael@0: this.destroy(); michael@0: } michael@0: }, michael@0: michael@0: onStateChange: michael@0: function CPL_onStateChange(aProgress, aRequest, aState, aStatus) michael@0: { michael@0: if (!this.owner) { michael@0: return; michael@0: } michael@0: michael@0: if (this._fileActivity) { michael@0: this._checkFileActivity(aProgress, aRequest, aState, aStatus); michael@0: } michael@0: michael@0: if (this._locationChange) { michael@0: this._checkLocationChange(aProgress, aRequest, aState, aStatus); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Check if there is any file load, given the arguments of michael@0: * nsIWebProgressListener.onStateChange. If the state change tells that a file michael@0: * URI has been loaded, then the remote Web Console instance is notified. michael@0: * @private michael@0: */ michael@0: _checkFileActivity: michael@0: function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus) michael@0: { michael@0: if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { michael@0: return; michael@0: } michael@0: michael@0: let uri = null; michael@0: if (aRequest instanceof Ci.imgIRequest) { michael@0: let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest); michael@0: uri = imgIRequest.URI; michael@0: } michael@0: else if (aRequest instanceof Ci.nsIChannel) { michael@0: let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel); michael@0: uri = nsIChannel.URI; michael@0: } michael@0: michael@0: if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) { michael@0: return; michael@0: } michael@0: michael@0: this.owner.onFileActivity(uri.spec); michael@0: }, michael@0: michael@0: /** michael@0: * Check if the current window.top location is changing, given the arguments michael@0: * of nsIWebProgressListener.onStateChange. If that is the case, the remote michael@0: * Web Console instance is notified. michael@0: * @private michael@0: */ michael@0: _checkLocationChange: michael@0: function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus) michael@0: { michael@0: let isStart = aState & Ci.nsIWebProgressListener.STATE_START; michael@0: let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP; michael@0: let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK; michael@0: let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW; michael@0: michael@0: // Skip non-interesting states. michael@0: if (!isNetwork || !isWindow || aProgress.DOMWindow != this.window) { michael@0: return; michael@0: } michael@0: michael@0: if (isStart && aRequest instanceof Ci.nsIChannel) { michael@0: this.owner.onLocationChange("start", aRequest.URI.spec, ""); michael@0: } michael@0: else if (isStop) { michael@0: this.owner.onLocationChange("stop", this.window.location.href, michael@0: this.window.document.title); michael@0: } michael@0: }, michael@0: michael@0: onLocationChange: function() {}, michael@0: onStatusChange: function() {}, michael@0: onProgressChange: function() {}, michael@0: onSecurityChange: function() {}, michael@0: michael@0: /** michael@0: * Destroy the ConsoleProgressListener. michael@0: */ michael@0: destroy: function CPL_destroy() michael@0: { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: this._initialized = false; michael@0: this._fileActivity = false; michael@0: this._locationChange = false; michael@0: michael@0: try { michael@0: this._webProgress.removeProgressListener(this); michael@0: } michael@0: catch (ex) { michael@0: // This can throw during browser shutdown. michael@0: } michael@0: michael@0: this._webProgress = null; michael@0: this.window = null; michael@0: this.owner = null; michael@0: }, michael@0: }; // ConsoleProgressListener.prototype michael@0: michael@0: function gSequenceId() { return gSequenceId.n++; } michael@0: gSequenceId.n = 1;