Mypal/devtools/client/webconsole/net/net-request.js
2019-03-11 13:26:37 +03:00

324 lines
9.5 KiB
JavaScript

/* 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;