/* -*- 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:
*
* ${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);
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;
}