Bug 895747 - DevTools Net Panel: cannot inspect contents of some POST messages, r=rcampbell
authorVictor Porof <vporof@mozilla.com>
Thu, 13 Feb 2014 15:15:42 -0500
changeset 168674 88b2c27cb819b96f00be4a367a8dcca902b86433
parent 168673 0fa443dcea165aeb27104cb037d69dc7a37d13b3
child 168675 a99fda3b6d2b5015b4db47494dd2ec787fb5d523
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersrcampbell
bugs895747
milestone30.0a1
Bug 895747 - DevTools Net Panel: cannot inspect contents of some POST messages, r=rcampbell
browser/devtools/netmonitor/netmonitor-view.js
browser/devtools/netmonitor/test/browser.ini
browser/devtools/netmonitor/test/browser_net_complex-params.js
browser/devtools/netmonitor/test/browser_net_post-data-02.js
browser/devtools/netmonitor/test/head.js
browser/devtools/netmonitor/test/html_params-test-page.html
browser/devtools/netmonitor/test/html_post-raw-test-page.html
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -50,18 +50,17 @@ const DEFAULT_EDITOR_CONFIG = {
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   lazyEmptyDelay: 10, // ms
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
   preventDescriptorModifiers: true,
-  eval: () => {},
-  switch: () => {}
+  eval: () => {}
 };
 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px
 
 /**
  * Object defining the network monitor view components.
  */
 let NetMonitorView = {
   /**
@@ -2096,42 +2095,55 @@ NetworkDetailsView.prototype = {
    *        The "requestPostData" message received from the server.
    * @return object
    *        A promise that is resolved when the request post params are set.
    */
   _setRequestPostParams: function(aHeadersResponse, aPostDataResponse) {
     if (!aHeadersResponse || !aPostDataResponse) {
       return promise.resolve();
     }
-    return gNetwork.getString(aPostDataResponse.postData.text).then(aString => {
-      // Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
-      let cType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
-      let cString = cType ? cType.value : "";
-      if (cString.contains("x-www-form-urlencoded") ||
-          aString.contains("x-www-form-urlencoded")) {
-        let formDataGroups = aString.split(/\r\n|\n|\r/);
-        for (let group of formDataGroups) {
-          this._addParams(this._paramsFormData, group);
+    return gNetwork.getString(aPostDataResponse.postData.text).then(aPostData => {
+      let contentTypeHeader = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
+      let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
+
+      return gNetwork.getString(contentTypeLongString).then(aContentType => {
+        let urlencoded = "x-www-form-urlencoded";
+
+        // Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
+        if (aContentType.contains(urlencoded)) {
+          let formDataGroups = aPostData.split(/\r\n|\r|\n/);
+          for (let group of formDataGroups) {
+            this._addParams(this._paramsFormData, group);
+          }
         }
-      }
-      // Handle actual forms ("multipart/form-data" content type).
-      else {
-        // This is really awkward, but hey, it works. Let's show an empty
-        // scope in the params view and place the source editor containing
-        // the raw post data directly underneath.
-        $("#request-params-box").removeAttribute("flex");
-        let paramsScope = this._params.addScope(this._paramsPostPayload);
-        paramsScope.expanded = true;
-        paramsScope.locked = true;
+        // Handle actual forms ("multipart/form-data" content type).
+        else {
+          // This is really awkward, but hey, it works. Let's show an empty
+          // scope in the params view and place the source editor containing
+          // the raw post data directly underneath.
+          $("#request-params-box").removeAttribute("flex");
+          let paramsScope = this._params.addScope(this._paramsPostPayload);
+          paramsScope.expanded = true;
+          paramsScope.locked = true;
 
-        $("#request-post-data-textarea-box").hidden = false;
-        return NetMonitorView.editor("#request-post-data-textarea").then(aEditor => {
-          aEditor.setText(aString);
-        });
-      }
+          $("#request-post-data-textarea-box").hidden = false;
+          return NetMonitorView.editor("#request-post-data-textarea").then(aEditor => {
+            // Most POST bodies are usually JSON, so they can be neatly
+            // syntax highlighted as JS. Otheriwse, fall back to plain text.
+            try {
+              JSON.parse(aPostData);
+              aEditor.setMode(Editor.modes.js);
+            } catch (e) {
+              aEditor.setMode(Editor.modes.text);
+            } finally {
+              aEditor.setText(aPostData);
+            }
+          });
+        }
+      });
     }).then(() => window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED));
   },
 
   /**
    * Populates the params container in this view with the specified data.
    *
    * @param string aName
    *        The type of params to populate (get or post).
@@ -2142,18 +2154,18 @@ NetworkDetailsView.prototype = {
     let paramsArray = parseQueryString(aQueryString);
     if (!paramsArray) {
       return;
     }
     let paramsScope = this._params.addScope(aName);
     paramsScope.expanded = true;
 
     for (let param of paramsArray) {
-      let headerVar = paramsScope.addItem(param.name, {}, true);
-      headerVar.setGrip(param.value);
+      let paramVar = paramsScope.addItem(param.name, {}, true);
+      paramVar.setGrip(param.value);
     }
   },
 
   /**
    * Sets the network response body shown in this view.
    *
    * @param string aUrl
    *        The request's url.
@@ -2586,24 +2598,26 @@ nsIURL.store = new Map();
  *
  * @param string aQueryString
  *        The query part of a url
  * @return array
  *         Array of query params {name, value}
  */
 function parseQueryString(aQueryString) {
   // Make sure there's at least one param available.
-  if (!aQueryString || !aQueryString.contains("=")) {
+  // Be careful here, params don't necessarily need to have values, so
+  // no need to verify the existence of a "=".
+  if (!aQueryString) {
     return;
   }
   // Turn the params string into an array containing { name: value } tuples.
   let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map(e =>
     let (param = e.split("=")) {
-      name: NetworkHelper.convertToUnicode(unescape(param[0])),
-      value: NetworkHelper.convertToUnicode(unescape(param[1]))
+      name: param[0] ? NetworkHelper.convertToUnicode(unescape(param[0])) : "",
+      value: param[1] ? NetworkHelper.convertToUnicode(unescape(param[1])) : ""
     });
   return paramsArray;
 }
 
 /**
  * Parse text representation of HTTP headers.
  *
  * @param string aText
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -8,16 +8,17 @@ support-files =
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-custom-mime-test-page.html
   html_json-long-test-page.html
   html_json-malformed-test-page.html
   html_json-text-mime-test-page.html
   html_jsonp-test-page.html
   html_navigate-test-page.html
+  html_params-test-page.html
   html_post-data-test-page.html
   html_post-raw-test-page.html
   html_simple-test-page.html
   html_sorting-test-page.html
   html_statistics-test-page.html
   html_status-codes-test-page.html
   sjs_content-type-test-server.sjs
   sjs_simple-test-server.sjs
@@ -30,19 +31,20 @@ support-files =
 [browser_net_accessibility-02.js]
 [browser_net_autoscroll.js]
 [browser_net_charts-01.js]
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
 [browser_net_charts-05.js]
 [browser_net_clear.js]
+[browser_net_complex-params.js]
 [browser_net_content-type.js]
+[browser_net_copy_image_as_data_uri.js]
 [browser_net_copy_url.js]
-[browser_net_copy_image_as_data_uri.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
 [browser_net_json-long.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_complex-params.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests whether complex request params and payload sent via POST are
+ * displayed correctly.
+ */
+
+function test() {
+  initNetMonitor(PARAMS_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { document, L10N, EVENTS, Editor, NetMonitorView } = aMonitor.panelWin;
+    let { RequestsMenu, NetworkDetails } = NetMonitorView;
+
+    RequestsMenu.lazyUpdate = false;
+    NetworkDetails._params.lazyEmpty = false;
+
+    Task.spawn(function () {
+      yield waitForNetworkEvents(aMonitor, 0, 6);
+
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.getElementById("details-pane-toggle"));
+      EventUtils.sendMouseEvent({ type: "mousedown" },
+        document.querySelectorAll("#details-pane tab")[2]);
+
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '""', '{ "foo": "bar" }', '""');
+
+      RequestsMenu.selectedIndex = 1;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '"b"', '{ "foo": "bar" }', '""');
+
+      RequestsMenu.selectedIndex = 2;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab1('a', '"b"', 'foo', '"bar"');
+
+      RequestsMenu.selectedIndex = 3;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '""', '{ "foo": "bar" }', "js");
+
+      RequestsMenu.selectedIndex = 4;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '"b"', '{ "foo": "bar" }', "js");
+
+      RequestsMenu.selectedIndex = 5;
+      yield waitFor(aMonitor.panelWin, EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+      yield testParamsTab2('a', '"b"', '?foo=bar', "text");
+
+      yield teardown(aMonitor);
+      finish();
+    });
+
+    function testParamsTab1(
+      aQueryStringParamName, aQueryStringParamValue, aFormDataParamName, aFormDataParamValue)
+    {
+      let tab = document.querySelectorAll("#details-pane tab")[2];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+        "The number of param scopes displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 2,
+        "The number of param values displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+        "The empty notice should not be displayed in this tabpanel.");
+
+      is(tabpanel.querySelector("#request-params-box")
+        .hasAttribute("hidden"), false,
+        "The request params box should not be hidden.");
+      is(tabpanel.querySelector("#request-post-data-textarea-box")
+        .hasAttribute("hidden"), true,
+        "The request post data textarea box should be hidden.");
+
+      let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+      let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+      is(paramsScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsQueryString"),
+        "The params scope doesn't have the correct title.");
+      is(formDataScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsFormData"),
+        "The form data scope doesn't have the correct title.");
+
+      is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aQueryStringParamName,
+        "The first query string param name was incorrect.");
+      is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aQueryStringParamValue,
+        "The first query string param value was incorrect.");
+
+      is(formDataScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aFormDataParamName,
+        "The first form data param name was incorrect.");
+      is(formDataScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aFormDataParamValue,
+        "The first form data param value was incorrect.");
+    }
+
+    function testParamsTab2(
+      aQueryStringParamName, aQueryStringParamValue, aRequestPayload, aEditorMode)
+    {
+      let tab = document.querySelectorAll("#details-pane tab")[2];
+      let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
+
+      is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
+        "The number of param scopes displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
+        "The number of param values displayed in this tabpanel is incorrect.");
+      is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+        "The empty notice should not be displayed in this tabpanel.");
+
+      is(tabpanel.querySelector("#request-params-box")
+        .hasAttribute("hidden"), false,
+        "The request params box should not be hidden.");
+      is(tabpanel.querySelector("#request-post-data-textarea-box")
+        .hasAttribute("hidden"), false,
+        "The request post data textarea box should not be hidden.");
+
+      let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+      let payloadScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+
+      is(paramsScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsQueryString"),
+        "The params scope doesn't have the correct title.");
+      is(payloadScope.querySelector(".name").getAttribute("value"),
+        L10N.getStr("paramsPostPayload"),
+        "The request payload scope doesn't have the correct title.");
+
+      is(paramsScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
+        aQueryStringParamName,
+        "The first query string param name was incorrect.");
+      is(paramsScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
+        aQueryStringParamValue,
+        "The first query string param value was incorrect.");
+
+      return NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
+        is(aEditor.getText(), aRequestPayload,
+          "The text shown in the source editor is incorrect.");
+        is(aEditor.getMode(), Editor.modes[aEditorMode],
+          "The mode active in the source editor is incorrect.");
+
+        teardown(aMonitor).then(finish);
+      });
+    }
+
+    aDebuggee.performRequests();
+  });
+}
--- a/browser/devtools/netmonitor/test/browser_net_post-data-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-02.js
@@ -50,15 +50,16 @@ function test() {
         is(postScope.querySelectorAll(".variables-view-variable .name")[0].getAttribute("value"),
           "foo", "The first query param name was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .value")[0].getAttribute("value"),
           "\"bar\"", "The first query param value was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .name")[1].getAttribute("value"),
           "baz", "The second query param name was incorrect.");
         is(postScope.querySelectorAll(".variables-view-variable .value")[1].getAttribute("value"),
           "\"123\"", "The second query param value was incorrect.");
+
         teardown(aMonitor).then(finish);
       });
     });
 
     aDebuggee.performRequests();
   });
 }
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -17,16 +17,17 @@ const EXAMPLE_URL = "http://example.com/
 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
 const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
+const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
 const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
 const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/html_params-test-page.html
@@ -0,0 +1,54 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Network Monitor test page</title>
+  </head>
+
+  <body>
+    <p>Request params type test</p>
+
+    <script type="text/javascript">
+      function post(aAddress, aQuery, aContentType, aPostBody) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("POST", aAddress + aQuery, true);
+        xhr.setRequestHeader("content-type", aContentType);
+        xhr.send(aPostBody);
+      }
+
+      function performRequests() {
+        var urlencoded = "application/x-www-form-urlencoded";
+
+        setTimeout(function() {
+          post("baz", "?a", urlencoded, '{ "foo": "bar" }');
+
+          setTimeout(function() {
+            post("baz", "?a=b", urlencoded, '{ "foo": "bar" }');
+
+            setTimeout(function() {
+              post("baz", "?a=b", urlencoded, '?foo=bar');
+
+              setTimeout(function() {
+                post("baz", "?a", undefined, '{ "foo": "bar" }');
+
+                setTimeout(function() {
+                  post("baz", "?a=b", undefined, '{ "foo": "bar" }');
+
+                  setTimeout(function() {
+                    post("baz", "?a=b", undefined, '?foo=bar');
+
+                    // Done.
+                  }, 10);
+                }, 10);
+              }, 10);
+            }, 10);
+          }, 10);
+        }, 10);
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/netmonitor/test/html_post-raw-test-page.html
+++ b/browser/devtools/netmonitor/test/html_post-raw-test-page.html
@@ -10,27 +10,28 @@
 
   <body>
     <p>POST raw test</p>
 
     <script type="text/javascript">
       function post(aAddress, aMessage, aCallback) {
         var xhr = new XMLHttpRequest();
         xhr.open("POST", aAddress, true);
+        xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
 
         xhr.onreadystatechange = function() {
           if (this.readyState == this.DONE) {
             aCallback();
           }
         };
         xhr.send(aMessage);
       }
 
       function performRequests() {
-        var rawData = "Content-Type: application/x-www-form-urlencoded\r\n\r\nfoo=bar&baz=123";
+        var rawData = "foo=bar&baz=123";
         post("sjs_simple-test-server.sjs", rawData, function() {
           // Done.
         });
       }
     </script>
   </body>
 
 </html>