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