diff options
Diffstat (limited to 'toolkit/devtools/webconsole/network-panel.js')
-rw-r--r-- | toolkit/devtools/webconsole/network-panel.js | 836 |
1 files changed, 836 insertions, 0 deletions
diff --git a/toolkit/devtools/webconsole/network-panel.js b/toolkit/devtools/webconsole/network-panel.js new file mode 100644 index 000000000..626241b1d --- /dev/null +++ b/toolkit/devtools/webconsole/network-panel.js @@ -0,0 +1,836 @@ +/* -*- js-indent-level: 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, Ci, Cu} = require("chrome"); + +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper")); +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService"); + +let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; + +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let l10n = new WebConsoleUtils.l10n(STRINGS_URI); + + +/** + * Creates a new NetworkPanel. + * + * @constructor + * @param nsIDOMNode aParent + * Parent node to append the created panel to. + * @param object aHttpActivity + * HttpActivity to display in the panel. + * @param object aWebConsoleFrame + * The parent WebConsoleFrame object that owns this network panel + * instance. + */ +function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame) +{ + let doc = aParent.ownerDocument; + this.httpActivity = aHttpActivity; + this.webconsole = aWebConsoleFrame; + this._longStringClick = this._longStringClick.bind(this); + this._responseBodyFetch = this._responseBodyFetch.bind(this); + this._requestBodyFetch = this._requestBodyFetch.bind(this); + + // 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/devtools/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.httpActivity = null; + self.webconsole = 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.iframe.addEventListener("load", function onLoad() { + if (!self.iframe) { + return; + } + + self.iframe.removeEventListener("load", onLoad, true); + self.update(); + }, true); + + this.panel.addEventListener("popupshown", function onPopupShown() { + self.panel.removeEventListener("popupshown", onPopupShown, true); + 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); +} +exports.NetworkPanel = NetworkPanel; + +NetworkPanel.prototype = +{ + /** + * 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, + + /** + * Function callback invoked whenever the panel content is updated. This is + * used only by tests. + * + * @private + * @type function + */ + _onUpdate: null, + + get document() { + return this.iframe && this.iframe.contentWindow ? + this.iframe.contentWindow.document : 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 request = this.httpActivity.request; + let response = this.httpActivity.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() + { + return this.contentType ? + NetworkHelper.isTextMimeType(this.contentType) : 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.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.request.postData.text; + if (typeof requestBody == "object" && requestBody.type == "longString") { + requestBody = requestBody.initial; + } + return this._fromDataRegExp.test(requestBody); + }, + + /** + * Appends the node with id=aId by the text aValue. + * + * @private + * @param string aId + * @param string aValue + * @return nsIDOMElement + * The DOM element with id=aId. + */ + _appendTextNode: function NP__appendTextNode(aId, aValue) + { + let textNode = this.document.createTextNode(aValue); + let elem = this.document.getElementById(aId); + elem.appendChild(textNode); + return elem; + }, + + /** + * 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((aItem) => { + let name = aItem.name; + if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) { + return; + } + + let value = aItem.value; + let longString = null; + if (typeof value == "object" && value.type == "longString") { + value = value.initial; + longString = true; + } + + /** + * The following code creates the HTML: + * <tr> + * <th scope="row" class="property-name">${line}:</th> + * <td class="property-value">${aList[line]}</td> + * </tr> + * 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); + + if (longString) { + let a = doc.createElement("a"); + a.href = "#"; + a.className = "longStringEllipsis"; + a.addEventListener("mousedown", this._longStringClick.bind(this, aItem)); + a.textContent = l10n.getStr("longStringEllipsis"); + td.appendChild(a); + } + + row.appendChild(td); + + parent.appendChild(row); + }); + }, + + /** + * The click event handler for the ellipsis which allows the user to retrieve + * the full header value. + * + * @private + * @param object aHeader + * The header object with the |name| and |value| properties. + * @param nsIDOMEvent aEvent + * The DOM click event object. + */ + _longStringClick: function NP__longStringClick(aHeader, aEvent) + { + aEvent.preventDefault(); + + let longString = this.webconsole.webConsoleClient.longString(aHeader.value); + + longString.substring(longString.initial.length, longString.length, + function NP__onLongStringSubstring(aResponse) + { + if (aResponse.error) { + Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error); + return; + } + + aHeader.value = aHeader.value.initial + aResponse.substring; + + let textNode = aEvent.target.previousSibling; + textNode.textContent += aResponse.substring; + textNode.parentNode.removeChild(aEvent.target); + }); + }, + + /** + * Displays the node with id=aId. + * + * @private + * @param string aId + * @return nsIDOMElement + * The element with id=aId. + */ + _displayNode: function NP__displayNode(aId) + { + let elem = this.document.getElementById(aId); + elem.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 request = this.httpActivity.request; + let requestTime = new Date(this.httpActivity.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.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.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 timing = this.httpActivity.timings; + let response = this.httpActivity.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, true); + + if (response.cookies.length > 0) { + this._displayNode("responseCookie"); + this._appendList("responseCookieContent", response.cookies); + } + }, + + /** + * 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.timings; + let request = this.httpActivity.request; + let response = this.httpActivity.response; + let cached = ""; + + if (this._isResponseCached) { + cached = "Cached"; + } + + let imageNode = this.document.getElementById("responseImage" + + cached + "Node"); + + let text = response.content.text; + if (typeof text == "object" && text.type == "longString") { + this._showResponseBodyFetchLink(); + } + else { + imageNode.setAttribute("src", + "data:" + this.contentType + ";base64," + text); + } + + // 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 timing = this.httpActivity.timings; + let response = this.httpActivity.response; + let cached = this._isResponseCached ? "Cached" : ""; + + this._appendTextNode("responseBody" + cached + "Info", + this._format("durationMS", [timing.receive])); + + this._displayNode("responseBody" + cached); + + let text = response.content.text; + if (typeof text == "object") { + text = text.initial; + this._showResponseBodyFetchLink(); + } + + this._appendTextNode("responseBody" + cached + "Content", text); + }, + + /** + * Show the "fetch response body" link. + * @private + */ + _showResponseBodyFetchLink: function NP__showResponseBodyFetchLink() + { + let content = this.httpActivity.response.content; + + let elem = this._appendTextNode("responseBodyFetchLink", + this._format("fetchRemainingResponseContentLink", + [content.text.length - content.text.initial.length])); + + elem.style.display = "block"; + elem.addEventListener("mousedown", this._responseBodyFetch); + }, + + /** + * Click event handler for the link that allows users to fetch the remaining + * response body. + * + * @private + * @param nsIDOMEvent aEvent + */ + _responseBodyFetch: function NP__responseBodyFetch(aEvent) + { + aEvent.target.style.display = "none"; + aEvent.target.removeEventListener("mousedown", this._responseBodyFetch); + + let content = this.httpActivity.response.content; + let longString = this.webconsole.webConsoleClient.longString(content.text); + longString.substring(longString.initial.length, longString.length, + (aResponse) => + { + if (aResponse.error) { + Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error); + return; + } + + content.text = content.text.initial + aResponse.substring; + let cached = this._isResponseCached ? "Cached" : ""; + + if (this._responseIsImage) { + let imageNode = this.document.getElementById("responseImage" + + cached + "Node"); + imageNode.src = + "data:" + this.contentType + ";base64," + content.text; + } + else { + this._appendTextNode("responseBody" + cached + "Content", + aResponse.substring); + } + }); + }, + + /** + * 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.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.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() + { + if (!this.document || this.document.readyState != "complete") { + return; + } + + let updates = this.httpActivity.updates; + let timing = this.httpActivity.timings; + 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 (!this.httpActivity.discardRequestBody && request.postData.text) { + this._updateRequestBody(); + this._state = this._DISPLAYED_REQUEST_BODY; + } + // FALL THROUGH + + case this._DISPLAYED_REQUEST_BODY: + 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 (updates.indexOf("responseContent") == -1 || + updates.indexOf("eventTimings") == -1) { + break; + } + + this._state = this._TRANSITION_CLOSED; + if (this.httpActivity.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; + } + + if (this._onUpdate) { + this._onUpdate(); + } + }, + + /** + * Update the panel to hold the current information we have about the request + * body. + * @private + */ + _updateRequestBody: function NP__updateRequestBody() + { + let postData = this.httpActivity.request.postData; + if (typeof postData.text == "object" && postData.text.type == "longString") { + let elem = this._appendTextNode("requestBodyFetchLink", + this._format("fetchRemainingRequestContentLink", + [postData.text.length - postData.text.initial.length])); + + elem.style.display = "block"; + elem.addEventListener("mousedown", this._requestBodyFetch); + return; + } + + // Check if we send some form data. If so, display the form data special. + if (this._isRequestBodyFormData) { + this._displayRequestForm(); + } + else { + this._displayRequestBody(); + } + }, + + /** + * Click event handler for the link that allows users to fetch the remaining + * request body. + * + * @private + * @param nsIDOMEvent aEvent + */ + _requestBodyFetch: function NP__requestBodyFetch(aEvent) + { + aEvent.target.style.display = "none"; + aEvent.target.removeEventListener("mousedown", this._responseBodyFetch); + + let postData = this.httpActivity.request.postData; + let longString = this.webconsole.webConsoleClient.longString(postData.text); + longString.substring(longString.initial.length, longString.length, + (aResponse) => + { + if (aResponse.error) { + Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error); + return; + } + + postData.text = postData.text.initial + aResponse.substring; + this._updateRequestBody(); + }); + }, +}; + +/** + * 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; +} |