/* -*- 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://gre/modules/devtools/NetworkHelper.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let l10n = new WebConsoleUtils.l10n(STRINGS_URI); this.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. */ this.NetworkPanel = 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.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.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); } 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() { 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.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; 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 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); }, /** * 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 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 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); 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.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() { // 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 || !this.document.getElementById("headUrl")) { 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) { // 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: 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(); } } } /** * 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; }