diff options
Diffstat (limited to 'devtools/client/webconsole/net/net-request.js')
-rw-r--r-- | devtools/client/webconsole/net/net-request.js | 323 |
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; |