зеркало из https://github.com/mozilla/gecko-dev.git
324 строки
9.5 KiB
JavaScript
324 строки
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;
|