summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/net/net-request.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/net/net-request.js')
-rw-r--r--devtools/client/webconsole/net/net-request.js323
1 files changed, 323 insertions, 0 deletions
diff --git a/devtools/client/webconsole/net/net-request.js b/devtools/client/webconsole/net/net-request.js
new file mode 100644
index 000000000..48cf66fdd
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.js
@@ -0,0 +1,323 @@
+/* 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";
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+// Reps
+const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils");
+
+// Network
+const { cancelEvent, isLeftClick } = require("./utils/events");
+const NetInfoBody = React.createFactory(require("./components/net-info-body"));
+const DataProvider = require("./data-provider");
+
+// Constants
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * This object represents a network log in the Console panel (and in the
+ * Network panel in the future).
+ * It's associated with an existing log and so, also with an existing
+ * element in the DOM.
+ *
+ * The object neither render no request for more data by default. It only
+ * reqisters a click listener to the associated log entry (a network event)
+ * and changes the class attribute of the log entry, so a twisty icon
+ * appears to indicates that there are more details displayed if the
+ * log entry is expanded.
+ *
+ * When the user expands the log, data are requested from the backend
+ * and rendered directly within the Console iframe.
+ */
+function NetRequest(log) {
+ this.initialize(log);
+}
+
+NetRequest.prototype = {
+ initialize: function (log) {
+ this.client = log.consoleFrame.webConsoleClient;
+ this.owner = log.consoleFrame.owner;
+
+ // 'this.file' field is following HAR spec.
+ // http://www.softwareishard.com/blog/har-12-spec/
+ this.file = log.response;
+ this.parentNode = log.node;
+ this.file.request.queryString = parseURLParams(this.file.request.url);
+ this.hasCookies = false;
+
+ // Map of fetched responses (to avoid unnecessary RDP round trip).
+ this.cachedResponses = new Map();
+
+ let doc = this.parentNode.ownerDocument;
+ let twisty = doc.createElementNS(XHTML_NS, "a");
+ twisty.className = "theme-twisty";
+ twisty.href = "#";
+
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+ this.parentNode.insertBefore(twisty, messageBody);
+ this.parentNode.setAttribute("collapsible", true);
+
+ this.parentNode.classList.add("netRequest");
+
+ // Register a click listener.
+ this.addClickListener();
+ },
+
+ addClickListener: function () {
+ // Add an event listener to toggle the expanded state when clicked.
+ // The event bubbling is canceled if the user clicks on the log
+ // itself (not on the expanded body), so opening of the default
+ // modal dialog is avoided.
+ this.parentNode.addEventListener("click", (event) => {
+ if (!isLeftClick(event)) {
+ return;
+ }
+
+ // Clicking on the toggle button or the method expands/collapses
+ // the body with HTTP details.
+ let classList = event.originalTarget.classList;
+ if (!(classList.contains("theme-twisty") ||
+ classList.contains("method"))) {
+ return;
+ }
+
+ // Alright, the user is clicking fine, let's open HTTP details!
+ this.onToggleBody(event);
+
+ // Avoid the default modal dialog
+ cancelEvent(event);
+ }, true);
+ },
+
+ onToggleBody: function (event) {
+ let target = event.currentTarget;
+ let logRow = target.closest(".netRequest");
+ logRow.classList.toggle("opened");
+
+ let twisty = this.parentNode.querySelector(".theme-twisty");
+ if (logRow.classList.contains("opened")) {
+ twisty.setAttribute("open", true);
+ } else {
+ twisty.removeAttribute("open");
+ }
+
+ let isOpen = logRow.classList.contains("opened");
+ if (isOpen) {
+ this.renderBody();
+ } else {
+ this.closeBody();
+ }
+ },
+
+ updateCookies: function(method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ if (response.cookies > 0 &&
+ ["requestCookies", "responseCookies"].includes(method)) {
+ this.hasCookies = true;
+ this.refresh();
+ }
+ },
+
+ /**
+ * Executed when 'networkEventUpdate' is received from the backend.
+ */
+ updateBody: function (response) {
+ // 'networkEventUpdate' event indicates that there are new data
+ // available on the backend. The following logic checks the response
+ // cache and if this data has been already requested before they
+ // need to be updated now (re-requested).
+ let method = response.updateType;
+ this.updateCookies(method, response);
+ if (this.cachedResponses.get(method)) {
+ this.cachedResponses.delete(method);
+ this.requestData(method);
+ }
+ },
+
+ /**
+ * Close network inline preview body.
+ */
+ closeBody: function () {
+ this.netInfoBodyBox.parentNode.removeChild(this.netInfoBodyBox);
+ },
+
+ /**
+ * Render network inline preview body.
+ */
+ renderBody: function () {
+ let messageBody = this.parentNode.querySelector(".message-body-wrapper");
+
+ // Create box for all markup rendered by ReactJS. Since we are
+ // rendering within webconsole.xul (i.e. XUL document) we need
+ // to explicitly specify XHTML namespace.
+ let doc = messageBody.ownerDocument;
+ this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div");
+ this.netInfoBodyBox.classList.add("netInfoBody");
+ messageBody.appendChild(this.netInfoBodyBox);
+
+ // As soon as Redux is in place state and actions will come from
+ // separate modules.
+ let body = NetInfoBody({
+ actions: this
+ });
+
+ // Render net info body!
+ this.body = ReactDOM.render(body, this.netInfoBodyBox);
+
+ this.refresh();
+ },
+
+ /**
+ * Render top level ReactJS component.
+ */
+ refresh: function () {
+ if (!this.netInfoBodyBox) {
+ return;
+ }
+
+ // TODO: As soon as Redux is in place there will be reducer
+ // computing a new state.
+ let newState = Object.assign({}, this.body.state, {
+ data: this.file,
+ hasCookies: this.hasCookies
+ });
+
+ this.body.setState(newState);
+ },
+
+ // Communication with the backend
+
+ requestData: function (method) {
+ // If the response has already been received bail out.
+ let response = this.cachedResponses.get(method);
+ if (response) {
+ return;
+ }
+
+ // Set an attribute indicating that this net log is waiting for
+ // data coming from the backend. Intended mainly for tests.
+ this.parentNode.setAttribute("loading", "true");
+
+ let actor = this.file.actor;
+ DataProvider.requestData(this.client, actor, method).then(args => {
+ this.cachedResponses.set(method, args);
+ this.onRequestData(method, args);
+
+ if (!DataProvider.hasPendingRequests()) {
+ this.parentNode.removeAttribute("loading");
+
+ // Fire an event indicating that all pending requests for
+ // data from the backend has finished. Intended for tests.
+ // Do it asynchronously so, it's done after all handlers
+ // for the current promise are executed.
+ setTimeout(() => {
+ let event = document.createEvent("Event");
+ event.initEvent("netlog-no-pending-requests", true, true);
+ this.parentNode.dispatchEvent(event);
+ });
+ }
+ });
+ },
+
+ onRequestData: function (method, response) {
+ // TODO: This code will be part of a reducer.
+ let result;
+ switch (method) {
+ case "requestHeaders":
+ result = this.onRequestHeaders(response);
+ break;
+ case "responseHeaders":
+ result = this.onResponseHeaders(response);
+ break;
+ case "requestCookies":
+ result = this.onRequestCookies(response);
+ break;
+ case "responseCookies":
+ result = this.onResponseCookies(response);
+ break;
+ case "responseContent":
+ result = this.onResponseContent(response);
+ break;
+ case "requestPostData":
+ result = this.onRequestPostData(response);
+ break;
+ }
+
+ result.then(() => {
+ this.refresh();
+ });
+ },
+
+ onRequestHeaders: function (response) {
+ this.file.request.headers = response.headers;
+
+ return this.resolveHeaders(this.file.request.headers);
+ },
+
+ onResponseHeaders: function (response) {
+ this.file.response.headers = response.headers;
+
+ return this.resolveHeaders(this.file.response.headers);
+ },
+
+ onResponseContent: function (response) {
+ let content = response.content;
+
+ for (let p in content) {
+ this.file.response.content[p] = content[p];
+ }
+
+ return Promise.resolve();
+ },
+
+ onRequestPostData: function (response) {
+ this.file.request.postData = response.postData;
+ return Promise.resolve();
+ },
+
+ onRequestCookies: function (response) {
+ this.file.request.cookies = response.cookies;
+ return this.resolveHeaders(this.file.request.cookies);
+ },
+
+ onResponseCookies: function (response) {
+ this.file.response.cookies = response.cookies;
+ return this.resolveHeaders(this.file.response.cookies);
+ },
+
+ onViewSourceInDebugger: function (frame) {
+ this.owner.viewSourceInDebugger(frame.source, frame.line);
+ },
+
+ resolveHeaders: function (headers) {
+ let promises = [];
+
+ for (let header of headers) {
+ if (typeof header.value == "object") {
+ promises.push(this.resolveString(header.value).then(value => {
+ header.value = value;
+ }));
+ }
+ }
+
+ return Promise.all(promises);
+ },
+
+ resolveString: function (object, propName) {
+ let stringGrip = object[propName];
+ if (typeof stringGrip == "object") {
+ DataProvider.resolveString(this.client, stringGrip).then(args => {
+ object[propName] = args;
+ this.refresh();
+ });
+ }
+ }
+};
+
+// Exports from this module
+module.exports = NetRequest;