net-request.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. // React
  6. const React = require("devtools/client/shared/vendor/react");
  7. const ReactDOM = require("devtools/client/shared/vendor/react-dom");
  8. // Reps
  9. const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils");
  10. // Network
  11. const { cancelEvent, isLeftClick } = require("./utils/events");
  12. const NetInfoBody = React.createFactory(require("./components/net-info-body"));
  13. const DataProvider = require("./data-provider");
  14. // Constants
  15. const XHTML_NS = "http://www.w3.org/1999/xhtml";
  16. /**
  17. * This object represents a network log in the Console panel (and in the
  18. * Network panel in the future).
  19. * It's associated with an existing log and so, also with an existing
  20. * element in the DOM.
  21. *
  22. * The object neither render no request for more data by default. It only
  23. * reqisters a click listener to the associated log entry (a network event)
  24. * and changes the class attribute of the log entry, so a twisty icon
  25. * appears to indicates that there are more details displayed if the
  26. * log entry is expanded.
  27. *
  28. * When the user expands the log, data are requested from the backend
  29. * and rendered directly within the Console iframe.
  30. */
  31. function NetRequest(log) {
  32. this.initialize(log);
  33. }
  34. NetRequest.prototype = {
  35. initialize: function (log) {
  36. this.client = log.consoleFrame.webConsoleClient;
  37. this.owner = log.consoleFrame.owner;
  38. // 'this.file' field is following HAR spec.
  39. // http://www.softwareishard.com/blog/har-12-spec/
  40. this.file = log.response;
  41. this.parentNode = log.node;
  42. this.file.request.queryString = parseURLParams(this.file.request.url);
  43. this.hasCookies = false;
  44. // Map of fetched responses (to avoid unnecessary RDP round trip).
  45. this.cachedResponses = new Map();
  46. let doc = this.parentNode.ownerDocument;
  47. let twisty = doc.createElementNS(XHTML_NS, "a");
  48. twisty.className = "theme-twisty";
  49. twisty.href = "#";
  50. let messageBody = this.parentNode.querySelector(".message-body-wrapper");
  51. this.parentNode.insertBefore(twisty, messageBody);
  52. this.parentNode.setAttribute("collapsible", true);
  53. this.parentNode.classList.add("netRequest");
  54. // Register a click listener.
  55. this.addClickListener();
  56. },
  57. addClickListener: function () {
  58. // Add an event listener to toggle the expanded state when clicked.
  59. // The event bubbling is canceled if the user clicks on the log
  60. // itself (not on the expanded body), so opening of the default
  61. // modal dialog is avoided.
  62. this.parentNode.addEventListener("click", (event) => {
  63. if (!isLeftClick(event)) {
  64. return;
  65. }
  66. // Clicking on the toggle button or the method expands/collapses
  67. // the body with HTTP details.
  68. let classList = event.originalTarget.classList;
  69. if (!(classList.contains("theme-twisty") ||
  70. classList.contains("method"))) {
  71. return;
  72. }
  73. // Alright, the user is clicking fine, let's open HTTP details!
  74. this.onToggleBody(event);
  75. // Avoid the default modal dialog
  76. cancelEvent(event);
  77. }, true);
  78. },
  79. onToggleBody: function (event) {
  80. let target = event.currentTarget;
  81. let logRow = target.closest(".netRequest");
  82. logRow.classList.toggle("opened");
  83. let twisty = this.parentNode.querySelector(".theme-twisty");
  84. if (logRow.classList.contains("opened")) {
  85. twisty.setAttribute("open", true);
  86. } else {
  87. twisty.removeAttribute("open");
  88. }
  89. let isOpen = logRow.classList.contains("opened");
  90. if (isOpen) {
  91. this.renderBody();
  92. } else {
  93. this.closeBody();
  94. }
  95. },
  96. updateCookies: function(method, response) {
  97. // TODO: This code will be part of a reducer.
  98. let result;
  99. if (response.cookies > 0 &&
  100. ["requestCookies", "responseCookies"].includes(method)) {
  101. this.hasCookies = true;
  102. this.refresh();
  103. }
  104. },
  105. /**
  106. * Executed when 'networkEventUpdate' is received from the backend.
  107. */
  108. updateBody: function (response) {
  109. // 'networkEventUpdate' event indicates that there are new data
  110. // available on the backend. The following logic checks the response
  111. // cache and if this data has been already requested before they
  112. // need to be updated now (re-requested).
  113. let method = response.updateType;
  114. this.updateCookies(method, response);
  115. if (this.cachedResponses.get(method)) {
  116. this.cachedResponses.delete(method);
  117. this.requestData(method);
  118. }
  119. },
  120. /**
  121. * Close network inline preview body.
  122. */
  123. closeBody: function () {
  124. this.netInfoBodyBox.parentNode.removeChild(this.netInfoBodyBox);
  125. },
  126. /**
  127. * Render network inline preview body.
  128. */
  129. renderBody: function () {
  130. let messageBody = this.parentNode.querySelector(".message-body-wrapper");
  131. // Create box for all markup rendered by ReactJS. Since we are
  132. // rendering within webconsole.xul (i.e. XUL document) we need
  133. // to explicitly specify XHTML namespace.
  134. let doc = messageBody.ownerDocument;
  135. this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div");
  136. this.netInfoBodyBox.classList.add("netInfoBody");
  137. messageBody.appendChild(this.netInfoBodyBox);
  138. // As soon as Redux is in place state and actions will come from
  139. // separate modules.
  140. let body = NetInfoBody({
  141. actions: this
  142. });
  143. // Render net info body!
  144. this.body = ReactDOM.render(body, this.netInfoBodyBox);
  145. this.refresh();
  146. },
  147. /**
  148. * Render top level ReactJS component.
  149. */
  150. refresh: function () {
  151. if (!this.netInfoBodyBox) {
  152. return;
  153. }
  154. // TODO: As soon as Redux is in place there will be reducer
  155. // computing a new state.
  156. let newState = Object.assign({}, this.body.state, {
  157. data: this.file,
  158. hasCookies: this.hasCookies
  159. });
  160. this.body.setState(newState);
  161. },
  162. // Communication with the backend
  163. requestData: function (method) {
  164. // If the response has already been received bail out.
  165. let response = this.cachedResponses.get(method);
  166. if (response) {
  167. return;
  168. }
  169. // Set an attribute indicating that this net log is waiting for
  170. // data coming from the backend. Intended mainly for tests.
  171. this.parentNode.setAttribute("loading", "true");
  172. let actor = this.file.actor;
  173. DataProvider.requestData(this.client, actor, method).then(args => {
  174. this.cachedResponses.set(method, args);
  175. this.onRequestData(method, args);
  176. if (!DataProvider.hasPendingRequests()) {
  177. this.parentNode.removeAttribute("loading");
  178. // Fire an event indicating that all pending requests for
  179. // data from the backend has finished. Intended for tests.
  180. // Do it asynchronously so, it's done after all handlers
  181. // for the current promise are executed.
  182. setTimeout(() => {
  183. let event = document.createEvent("Event");
  184. event.initEvent("netlog-no-pending-requests", true, true);
  185. this.parentNode.dispatchEvent(event);
  186. });
  187. }
  188. });
  189. },
  190. onRequestData: function (method, response) {
  191. // TODO: This code will be part of a reducer.
  192. let result;
  193. switch (method) {
  194. case "requestHeaders":
  195. result = this.onRequestHeaders(response);
  196. break;
  197. case "responseHeaders":
  198. result = this.onResponseHeaders(response);
  199. break;
  200. case "requestCookies":
  201. result = this.onRequestCookies(response);
  202. break;
  203. case "responseCookies":
  204. result = this.onResponseCookies(response);
  205. break;
  206. case "responseContent":
  207. result = this.onResponseContent(response);
  208. break;
  209. case "requestPostData":
  210. result = this.onRequestPostData(response);
  211. break;
  212. }
  213. result.then(() => {
  214. this.refresh();
  215. });
  216. },
  217. onRequestHeaders: function (response) {
  218. this.file.request.headers = response.headers;
  219. return this.resolveHeaders(this.file.request.headers);
  220. },
  221. onResponseHeaders: function (response) {
  222. this.file.response.headers = response.headers;
  223. return this.resolveHeaders(this.file.response.headers);
  224. },
  225. onResponseContent: function (response) {
  226. let content = response.content;
  227. for (let p in content) {
  228. this.file.response.content[p] = content[p];
  229. }
  230. return Promise.resolve();
  231. },
  232. onRequestPostData: function (response) {
  233. this.file.request.postData = response.postData;
  234. return Promise.resolve();
  235. },
  236. onRequestCookies: function (response) {
  237. this.file.request.cookies = response.cookies;
  238. return this.resolveHeaders(this.file.request.cookies);
  239. },
  240. onResponseCookies: function (response) {
  241. this.file.response.cookies = response.cookies;
  242. return this.resolveHeaders(this.file.response.cookies);
  243. },
  244. onViewSourceInDebugger: function (frame) {
  245. this.owner.viewSourceInDebugger(frame.source, frame.line);
  246. },
  247. resolveHeaders: function (headers) {
  248. let promises = [];
  249. for (let header of headers) {
  250. if (typeof header.value == "object") {
  251. promises.push(this.resolveString(header.value).then(value => {
  252. header.value = value;
  253. }));
  254. }
  255. }
  256. return Promise.all(promises);
  257. },
  258. resolveString: function (object, propName) {
  259. let stringGrip = object[propName];
  260. if (typeof stringGrip == "object") {
  261. DataProvider.resolveString(this.client, stringGrip).then(args => {
  262. object[propName] = args;
  263. this.refresh();
  264. });
  265. }
  266. }
  267. };
  268. // Exports from this module
  269. module.exports = NetRequest;