summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/net
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/net')
-rw-r--r--devtools/client/webconsole/net/.eslintrc.js20
-rw-r--r--devtools/client/webconsole/net/components/cookies-tab.js75
-rw-r--r--devtools/client/webconsole/net/components/headers-tab.js79
-rw-r--r--devtools/client/webconsole/net/components/moz.build25
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.css112
-rw-r--r--devtools/client/webconsole/net/components/net-info-body.js179
-rw-r--r--devtools/client/webconsole/net/components/net-info-group-list.js47
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.css80
-rw-r--r--devtools/client/webconsole/net/components/net-info-group.js80
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.css23
-rw-r--r--devtools/client/webconsole/net/components/net-info-params.js58
-rw-r--r--devtools/client/webconsole/net/components/params-tab.js41
-rw-r--r--devtools/client/webconsole/net/components/post-tab.js279
-rw-r--r--devtools/client/webconsole/net/components/response-tab.css21
-rw-r--r--devtools/client/webconsole/net/components/response-tab.js277
-rw-r--r--devtools/client/webconsole/net/components/size-limit.css15
-rw-r--r--devtools/client/webconsole/net/components/size-limit.js62
-rw-r--r--devtools/client/webconsole/net/components/spinner.js26
-rw-r--r--devtools/client/webconsole/net/components/stacktrace-tab.js29
-rw-r--r--devtools/client/webconsole/net/data-provider.js66
-rw-r--r--devtools/client/webconsole/net/main.js98
-rw-r--r--devtools/client/webconsole/net/moz.build19
-rw-r--r--devtools/client/webconsole/net/net-request.css35
-rw-r--r--devtools/client/webconsole/net/net-request.js323
-rw-r--r--devtools/client/webconsole/net/test/mochitest/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser.ini22
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_basic.js33
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js54
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_headers.js40
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_params.js69
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_post.js88
-rw-r--r--devtools/client/webconsole/net/test/mochitest/browser_net_response.js86
-rw-r--r--devtools/client/webconsole/net/test/mochitest/head.js209
-rw-r--r--devtools/client/webconsole/net/test/mochitest/page_basic.html14
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^2
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.json^headers^1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.txt1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml1
-rw-r--r--devtools/client/webconsole/net/test/mochitest/test.xml^headers^1
-rw-r--r--devtools/client/webconsole/net/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/webconsole/net/test/unit/test_json-utils.js45
-rw-r--r--devtools/client/webconsole/net/test/unit/test_net-utils.js77
-rw-r--r--devtools/client/webconsole/net/test/unit/xpcshell.ini9
-rw-r--r--devtools/client/webconsole/net/utils/events.js21
-rw-r--r--devtools/client/webconsole/net/utils/json.js234
-rw-r--r--devtools/client/webconsole/net/utils/moz.build11
-rw-r--r--devtools/client/webconsole/net/utils/net.js134
49 files changed, 3235 insertions, 0 deletions
diff --git a/devtools/client/webconsole/net/.eslintrc.js b/devtools/client/webconsole/net/.eslintrc.js
new file mode 100644
index 000000000..e105ac6e2
--- /dev/null
+++ b/devtools/client/webconsole/net/.eslintrc.js
@@ -0,0 +1,20 @@
+"use strict";
+
+module.exports = {
+ "globals": {
+ "Locale": true,
+ "Document": true,
+ "document": true,
+ "Node": true,
+ "Element": true,
+ "MessageEvent": true,
+ "BrowserLoader": true,
+ "addEventListener": true,
+ "DOMParser": true,
+ "dispatchEvent": true,
+ "setTimeout": true
+ },
+ "rules": {
+ "no-unused-vars": ["error", {"args": "none"}],
+ }
+};
diff --git a/devtools/client/webconsole/net/components/cookies-tab.js b/devtools/client/webconsole/net/components/cookies-tab.js
new file mode 100644
index 000000000..d76414679
--- /dev/null
+++ b/devtools/client/webconsole/net/components/cookies-tab.js
@@ -0,0 +1,75 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Cookies' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * sent and received cookies.
+ */
+var CookiesTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "CookiesTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestCookies = data.request.cookies;
+ let responseCookies = data.response.cookies;
+
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestCookies || !requestCookies.length) {
+ actions.requestData("requestCookies");
+ }
+
+ if (!responseCookies || !responseCookies.length) {
+ actions.requestData("responseCookies");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+ let requestCookies = file.request.cookies;
+ let responseCookies = file.response.cookies;
+
+ // The cookie panel displays two groups of cookies:
+ // 1) Response Cookies
+ // 2) Request Cookies
+ let groups = [{
+ key: "responseCookies",
+ name: Locale.$STR("responseCookies"),
+ params: responseCookies
+ }, {
+ key: "requestCookies",
+ name: Locale.$STR("requestCookies"),
+ params: requestCookies
+ }];
+
+ return (
+ DOM.div({className: "cookiesTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = CookiesTab;
diff --git a/devtools/client/webconsole/net/components/headers-tab.js b/devtools/client/webconsole/net/components/headers-tab.js
new file mode 100644
index 000000000..2eca3fd2f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/headers-tab.js
@@ -0,0 +1,79 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Headers' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for rendering
+ * request and response HTTP headers.
+ */
+var HeadersTab = React.createClass({
+ propTypes: {
+ actions: PropTypes.shape({
+ requestData: PropTypes.func.isRequired
+ }),
+ data: PropTypes.object.isRequired,
+ },
+
+ displayName: "HeadersTab",
+
+ componentDidMount() {
+ let { actions, data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // Request headers if they are not available yet.
+ // TODO: use async action objects as soon as Redux is in place
+ if (!requestHeaders) {
+ actions.requestData("requestHeaders");
+ }
+
+ if (!responseHeaders) {
+ actions.requestData("responseHeaders");
+ }
+ },
+
+ render() {
+ let { data } = this.props;
+ let requestHeaders = data.request.headers;
+ let responseHeaders = data.response.headers;
+
+ // TODO: Another groups to implement:
+ // 1) Cached Headers
+ // 2) Headers from upload stream
+ let groups = [{
+ key: "responseHeaders",
+ name: Locale.$STR("responseHeaders"),
+ params: responseHeaders
+ }, {
+ key: "requestHeaders",
+ name: Locale.$STR("requestHeaders"),
+ params: requestHeaders
+ }];
+
+ // If response headers are not available yet, display a spinner
+ if (!responseHeaders || !responseHeaders.length) {
+ groups[0].content = Spinner();
+ }
+
+ return (
+ DOM.div({className: "headersTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({groups: groups})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = HeadersTab;
diff --git a/devtools/client/webconsole/net/components/moz.build b/devtools/client/webconsole/net/components/moz.build
new file mode 100644
index 000000000..0053de780
--- /dev/null
+++ b/devtools/client/webconsole/net/components/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'cookies-tab.js',
+ 'headers-tab.js',
+ 'net-info-body.css',
+ 'net-info-body.js',
+ 'net-info-group-list.js',
+ 'net-info-group.css',
+ 'net-info-group.js',
+ 'net-info-params.css',
+ 'net-info-params.js',
+ 'params-tab.js',
+ 'post-tab.js',
+ 'response-tab.css',
+ 'response-tab.js',
+ 'size-limit.css',
+ 'size-limit.js',
+ 'spinner.js',
+ 'stacktrace-tab.js',
+)
diff --git a/devtools/client/webconsole/net/components/net-info-body.css b/devtools/client/webconsole/net/components/net-info-body.css
new file mode 100644
index 000000000..2d0bac70e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.css
@@ -0,0 +1,112 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Network Info Body */
+
+.netInfoBody {
+ margin: 10px 0 0 0;
+ width: 100%;
+ cursor: default;
+ display: block;
+}
+
+.netInfoBody *:focus {
+ outline: 0 !important;
+}
+
+.netInfoBody .panelContent {
+ word-break: break-all;
+}
+
+/******************************************************************************/
+/* Network Info Body Tabs */
+
+.netInfoBody > .tabs {
+ background-color: transparent;
+ background-image: none;
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-navigation {
+ border-bottom-color: var(--net-border);
+ background-color: transparent;
+ text-decoration: none;
+ padding-top: 3px;
+ padding-left: 7px;
+ padding-bottom: 1px;
+ border-bottom: 1px solid var(--net-border);
+}
+
+.netInfoBody > .tabs .tabs-menu {
+ display: table;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+/* This is the trick that makes the tab bottom border invisible */
+.netInfoBody > .tabs .tabs-menu-item {
+ position: relative;
+ bottom: -2px;
+ float: left;
+}
+
+.netInfoBody > .tabs .tabs-menu-item a {
+ display: block;
+ border: 1px solid transparent;
+ text-decoration: none;
+ padding: 5px 8px 4px 8px;;
+ font-weight: bold;
+ color: var(--theme-body-color);
+ border-radius: 4px 4px 0 0;
+}
+
+.netInfoBody > .tabs .tab-panel {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-top: none;
+ padding: 10px;
+ overflow: auto;
+ height: calc(100% - 31px); /* minus the height of the tab bar */
+}
+
+.netInfoBody > .tabs .tab-panel > div,
+.netInfoBody > .tabs .tab-panel > div > div {
+ height: 100%;
+}
+
+.netInfoBody > .tabs .tabs-menu-item.is-active a,
+.netInfoBody > .tabs .tabs-menu-item.is-active a:focus,
+.netInfoBody > .tabs .tabs-menu-item.is-active:hover a {
+ background-color: var(--theme-body-background);
+ border: 1px solid transparent;
+ border-bottom-color: var(--theme-highlight-bluegrey);
+ color: var(--theme-highlight-bluegrey);
+}
+
+.netInfoBody > .tabs .tabs-menu-item:hover a {
+ border: 1px solid transparent;
+ border-bottom: 1px solid var(--net-border);
+ background-color: var(--theme-body-background);
+}
+
+
+/******************************************************************************/
+/* Themes */
+
+.theme-firebug .netInfoBody > .tabs .tab-panel {
+ border-color: var(--net-border);
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active:hover a,
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item.is-active a:focus {
+ border: 1px solid var(--net-border);
+ border-bottom-color: transparent;
+}
+
+.theme-firebug .netInfoBody > .tabs .tabs-menu-item:hover a {
+ border-bottom-color: transparent;
+}
diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js
new file mode 100644
index 000000000..c5eccd458
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-body.js
@@ -0,0 +1,179 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Tabs, TabPanel } = createFactories(require("devtools/client/shared/components/tabs/tabs"));
+
+// Network
+const HeadersTab = React.createFactory(require("./headers-tab"));
+const ResponseTab = React.createFactory(require("./response-tab"));
+const ParamsTab = React.createFactory(require("./params-tab"));
+const CookiesTab = React.createFactory(require("./cookies-tab"));
+const PostTab = React.createFactory(require("./post-tab"));
+const StackTraceTab = React.createFactory(require("./stacktrace-tab"));
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders the basic Network log info body. It's not
+ * visible by default, the user needs to expand the network log
+ * to see it.
+ *
+ * This is the set of tabs displaying details about network events:
+ * 1) Headers - request and response headers
+ * 2) Params - URL parameters
+ * 3) Response - response body
+ * 4) Cookies - request and response cookies
+ * 5) Post - posted data
+ */
+var NetInfoBody = React.createClass({
+ propTypes: {
+ tabActive: PropTypes.number.isRequired,
+ actions: PropTypes.object.isRequired,
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "NetInfoBody",
+
+ getDefaultProps() {
+ return {
+ tabActive: 0
+ };
+ },
+
+ getInitialState() {
+ return {
+ data: {
+ request: {},
+ response: {}
+ },
+ tabActive: this.props.tabActive,
+ };
+ },
+
+ onTabChanged(index) {
+ this.setState({tabActive: index});
+ },
+
+ hasCookies() {
+ let {request, response} = this.state.data;
+ return this.state.hasCookies ||
+ NetUtils.getHeaderValue(request.headers, "Cookie") ||
+ NetUtils.getHeaderValue(response.headers, "Set-Cookie");
+ },
+
+ hasStackTrace() {
+ let {cause} = this.state.data;
+ return cause && cause.stacktrace && cause.stacktrace.length > 0;
+ },
+
+ getTabPanels() {
+ let actions = this.props.actions;
+ let data = this.state.data;
+ let {request} = data;
+
+ // Flags for optional tabs. Some tabs are visible only if there
+ // are data to display.
+ let hasParams = request.queryString && request.queryString.length;
+ let hasPostData = request.bodySize > 0;
+
+ let panels = [];
+
+ // Headers tab
+ panels.push(
+ TabPanel({
+ className: "headers",
+ key: "headers",
+ title: Locale.$STR("netRequest.headers")},
+ HeadersTab({data: data, actions: actions})
+ )
+ );
+
+ // URL parameters tab
+ if (hasParams) {
+ panels.push(
+ TabPanel({
+ className: "params",
+ key: "params",
+ title: Locale.$STR("netRequest.params")},
+ ParamsTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Posted data tab
+ if (hasPostData) {
+ panels.push(
+ TabPanel({
+ className: "post",
+ key: "post",
+ title: Locale.$STR("netRequest.post")},
+ PostTab({data: data, actions: actions})
+ )
+ );
+ }
+
+ // Response tab
+ panels.push(
+ TabPanel({className: "response", key: "response",
+ title: Locale.$STR("netRequest.response")},
+ ResponseTab({data: data, actions: actions})
+ )
+ );
+
+ // Cookies tab
+ if (this.hasCookies()) {
+ panels.push(
+ TabPanel({
+ className: "cookies",
+ key: "cookies",
+ title: Locale.$STR("netRequest.cookies")},
+ CookiesTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ // Stacktrace tab
+ if (this.hasStackTrace()) {
+ panels.push(
+ TabPanel({
+ className: "stacktrace-tab",
+ key: "stacktrace",
+ title: Locale.$STR("netRequest.callstack")},
+ StackTraceTab({
+ data: data,
+ actions: actions
+ })
+ )
+ );
+ }
+
+ return panels;
+ },
+
+ render() {
+ let tabActive = this.state.tabActive;
+ let tabPanels = this.getTabPanels();
+ return (
+ Tabs({
+ tabActive: tabActive,
+ onAfterChange: this.onTabChanged},
+ tabPanels
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoBody;
diff --git a/devtools/client/webconsole/net/components/net-info-group-list.js b/devtools/client/webconsole/net/components/net-info-group-list.js
new file mode 100644
index 000000000..247a23bb7
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group-list.js
@@ -0,0 +1,47 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoGroup = React.createFactory(require("./net-info-group"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template is responsible for rendering sections/groups inside tabs.
+ * It's used e.g to display Response and Request headers as separate groups.
+ */
+var NetInfoGroupList = React.createClass({
+ propTypes: {
+ groups: PropTypes.array.isRequired,
+ },
+
+ displayName: "NetInfoGroupList",
+
+ render() {
+ let groups = this.props.groups;
+
+ // Filter out empty groups.
+ groups = groups.filter(group => {
+ return group && ((group.params && group.params.length) || group.content);
+ });
+
+ // Render groups
+ groups = groups.map(group => {
+ group.type = group.key;
+ return NetInfoGroup(group);
+ });
+
+ return (
+ DOM.div({className: "netInfoGroupList"},
+ groups
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroupList;
diff --git a/devtools/client/webconsole/net/components/net-info-group.css b/devtools/client/webconsole/net/components/net-info-group.css
new file mode 100644
index 000000000..43800019f
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.css
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Net Info Group */
+
+.netInfoBody .netInfoGroup {
+ padding-bottom: 6px;
+}
+
+/* Last group doesn't need bottom padding */
+.netInfoBody .netInfoGroup:last-child {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroup:last-child .netInfoGroupContent {
+ padding-bottom: 0;
+}
+
+.netInfoBody .netInfoGroupTitle {
+ cursor: pointer;
+ font-weight: bold;
+ -moz-user-select: none;
+ cursor: pointer;
+ padding-left: 3px;
+}
+
+.netInfoBody .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/controls.png");
+ background-size: 56px 28px;
+ background-position: 0 -14px;
+ background-repeat: no-repeat;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-position: -14px -14px;
+}
+
+/* Group content is expandable/collapsible by clicking on the title */
+.netInfoBody .netInfoGroupContent {
+ padding-top: 7px;
+ margin-top: 3px;
+ padding-bottom: 14px;
+ border-top: 1px solid var(--net-border);
+ display: none;
+}
+
+/* Toggle group visibility */
+.netInfoBody .netInfoGroup.opened .netInfoGroupContent {
+ display: block;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netInfoBody .netInfoGroup {
+ color: var(--theme-body-color);
+}
+
+.theme-dark .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ filter: invert(1);
+}
+
+/* Twisties */
+.theme-firebug .netInfoBody .netInfoGroup .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg");
+ background-position: 0 2px;
+ background-size: 11px 11px;
+ width: 15px;
+}
+
+.theme-firebug .netInfoBody .netInfoGroup.opened .netInfoGroupTwisty {
+ background-image: url("chrome://devtools/skin/images/firebug/twisty-open-firebug.svg");
+}
diff --git a/devtools/client/webconsole/net/components/net-info-group.js b/devtools/client/webconsole/net/components/net-info-group.js
new file mode 100644
index 000000000..d9794652e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-group.js
@@ -0,0 +1,80 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a group of data within a tab. For example,
+ * Headers tab has two groups 'Request Headers' and 'Response Headers'
+ * The Response tab can also have two groups 'Raw Data' and 'JSON'
+ */
+var NetInfoGroup = React.createClass({
+ propTypes: {
+ type: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ params: PropTypes.array,
+ content: PropTypes.element,
+ open: PropTypes.bool
+ },
+
+ displayName: "NetInfoGroup",
+
+ getDefaultProps() {
+ return {
+ open: true,
+ };
+ },
+
+ getInitialState() {
+ return {
+ open: this.props.open,
+ };
+ },
+
+ onToggle(event) {
+ this.setState({
+ open: !this.state.open
+ });
+ },
+
+ render() {
+ let content = this.props.content;
+
+ if (!content && this.props.params) {
+ content = NetInfoParams({
+ params: this.props.params
+ });
+ }
+
+ let open = this.state.open;
+ let className = open ? "opened" : "";
+
+ return (
+ DOM.div({className: "netInfoGroup" + " " + className + " " +
+ this.props.type},
+ DOM.span({
+ className: "netInfoGroupTwisty",
+ onClick: this.onToggle
+ }),
+ DOM.span({
+ className: "netInfoGroupTitle",
+ onClick: this.onToggle},
+ this.props.name
+ ),
+ DOM.div({className: "netInfoGroupContent"},
+ content
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoGroup;
diff --git a/devtools/client/webconsole/net/components/net-info-params.css b/devtools/client/webconsole/net/components/net-info-params.css
new file mode 100644
index 000000000..4ec7140f8
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Net Info Params */
+
+.netInfoBody .netInfoParamName {
+ padding: 0 10px 0 0;
+ font-weight: bold;
+ vertical-align: top;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.netInfoBody .netInfoParamValue {
+ width: 100%;
+ word-wrap: break-word;
+}
+
+.netInfoBody .netInfoParamValue > code {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/webconsole/net/components/net-info-params.js b/devtools/client/webconsole/net/components/net-info-params.js
new file mode 100644
index 000000000..573257b28
--- /dev/null
+++ b/devtools/client/webconsole/net/components/net-info-params.js
@@ -0,0 +1,58 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template renders list of parameters within a group.
+ * It's essentially a list of name + value pairs.
+ */
+var NetInfoParams = React.createClass({
+ displayName: "NetInfoParams",
+
+ propTypes: {
+ params: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+ })).isRequired,
+ },
+
+ render() {
+ let params = this.props.params || [];
+
+ params.sort(function (a, b) {
+ return a.name > b.name ? 1 : -1;
+ });
+
+ let rows = [];
+ params.forEach((param, index) => {
+ rows.push(
+ DOM.tr({key: index},
+ DOM.td({className: "netInfoParamName"},
+ DOM.span({title: param.name}, param.name)
+ ),
+ DOM.td({className: "netInfoParamValue"},
+ DOM.code({}, param.value)
+ )
+ )
+ );
+ });
+
+ return (
+ DOM.table({cellPadding: 0, cellSpacing: 0},
+ DOM.tbody({},
+ rows
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = NetInfoParams;
diff --git a/devtools/client/webconsole/net/components/params-tab.js b/devtools/client/webconsole/net/components/params-tab.js
new file mode 100644
index 000000000..c3fefc669
--- /dev/null
+++ b/devtools/client/webconsole/net/components/params-tab.js
@@ -0,0 +1,41 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Params' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying URL parameters (query string).
+ */
+var ParamsTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ })
+ },
+
+ displayName: "ParamsTab",
+
+ render() {
+ let data = this.props.data;
+
+ return (
+ DOM.div({className: "paramsTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoParams({params: data.request.queryString})
+ )
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = ParamsTab;
diff --git a/devtools/client/webconsole/net/components/post-tab.js b/devtools/client/webconsole/net/components/post-tab.js
new file mode 100644
index 000000000..6d06eb40b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/post-tab.js
@@ -0,0 +1,279 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const NetInfoParams = React.createFactory(require("./net-info-params"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetUtils = require("../utils/net");
+const Json = require("../utils/json");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Post' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * displaying posted data (HTTP post body).
+ */
+var PostTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "PostTab",
+
+ isJson(file) {
+ let text = file.request.postData.text;
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return Json.isJSON(value, text);
+ },
+
+ parseJson(file) {
+ let postData = file.request.postData;
+ if (!postData) {
+ return null;
+ }
+
+ let jsonString = new String(postData.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ /**
+ * Render JSON post data as an expandable tree.
+ */
+ renderJson(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isJson(file)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ parseXml(file) {
+ let text = file.request.postData.text;
+ if (isLongString(text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml({
+ mimeType: NetUtils.getHeaderValue(file.request.headers, "content-type"),
+ text: text,
+ });
+ },
+
+ isXml(file) {
+ if (isLongString(file.request.postData.text)) {
+ return false;
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ if (!value) {
+ return false;
+ }
+
+ return NetUtils.isHTML(value);
+ },
+
+ renderXml(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!this.isXml(file)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * Multipart post data are parsed and nicely rendered
+ * as an expandable tree of individual parts.
+ */
+ renderMultiPart(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return;
+ }
+
+ if (NetUtils.isMultiPartRequest(file)) {
+ // TODO: render multi part request (bug: 1247423)
+ }
+
+ return;
+ },
+
+ /**
+ * URL encoded post data are nicely rendered as a list
+ * of parameters.
+ */
+ renderUrlEncoded(file) {
+ let text = file.request.postData.text;
+ if (!text || isLongString(text)) {
+ return null;
+ }
+
+ if (!NetUtils.isURLEncodedRequest(file)) {
+ return null;
+ }
+
+ let lines = text.split("\n");
+ let params = parseURLEncodedText(lines[lines.length - 1]);
+
+ return {
+ key: "url-encoded",
+ content: NetInfoParams({params: params}),
+ name: Locale.$STR("netRequest.params")
+ };
+ },
+
+ renderRawData(file) {
+ let text = file.request.postData.text;
+
+ let group;
+
+ // The post body might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text.initial),
+ SizeLimit({
+ actions: this.props.actions,
+ data: file.request.postData,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ sanitize(text)
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+
+ if (!file.request.postData) {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("requestPostData");
+ }
+ },
+
+ render() {
+ let { actions, data: file } = this.props;
+
+ if (file.discardRequestBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.requestBodyDiscarded")
+ );
+ }
+
+ if (!file.request.postData) {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render post body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderUrlEncoded(file));
+ // TODO: render multi part request (bug: 1247423)
+ // groups.push(this.renderMultiPart(file));
+ groups.push(this.renderJson(file));
+ groups.push(this.renderXml(file));
+ groups.push(this.renderRawData(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[groups.length - 1].open = false;
+ }
+
+ return (
+ DOM.div({className: "postTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+/**
+ * Workaround for a "not well-formed" error that react
+ * reports when there's multipart data passed to render.
+ */
+function sanitize(text) {
+ text = JSON.stringify(text);
+ text = text.replace(/\\r\\n/g, "\r\n").replace(/\\"/g, "\"");
+ return text.slice(1, text.length - 1);
+}
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = PostTab;
diff --git a/devtools/client/webconsole/net/components/response-tab.css b/devtools/client/webconsole/net/components/response-tab.css
new file mode 100644
index 000000000..e1c31fca4
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Response Tab */
+
+.netInfoBody .netInfoBodiesDiscarded {
+ font-style: italic;
+ color: gray;
+}
+
+.netInfoBody .netInfoResponseContent {
+ font-family: var(--monospace-font-family);
+ word-wrap: break-word;
+}
+
+.netInfoBody .responseTabBox img {
+ max-width: 300px;
+ max-height: 300px;
+}
diff --git a/devtools/client/webconsole/net/components/response-tab.js b/devtools/client/webconsole/net/components/response-tab.js
new file mode 100644
index 000000000..78d8b2f77
--- /dev/null
+++ b/devtools/client/webconsole/net/components/response-tab.js
@@ -0,0 +1,277 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Reps
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+// Network
+const SizeLimit = React.createFactory(require("./size-limit"));
+const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
+const Spinner = React.createFactory(require("./spinner"));
+const Json = require("../utils/json");
+const NetUtils = require("../utils/net");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents 'Response' tab displayed when the user
+ * expands network log in the Console panel. It's responsible for
+ * rendering HTTP response body.
+ *
+ * In case of supported response mime-type (e.g. application/json,
+ * text/xml, etc.), the response is parsed using appropriate parser
+ * and rendered accordingly.
+ */
+var ResponseTab = React.createClass({
+ propTypes: {
+ data: PropTypes.shape({
+ request: PropTypes.object.isRequired,
+ response: PropTypes.object.isRequired
+ }),
+ actions: PropTypes.object.isRequired
+ },
+
+ displayName: "ResponseTab",
+
+ // Response Types
+
+ isJson(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return Json.isJSON(content.mimeType, content.text);
+ },
+
+ parseJson(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ let jsonString = new String(content.text);
+ return Json.parseJSONString(jsonString);
+ },
+
+ isImage(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isImage(content.mimeType);
+ },
+
+ isXml(content) {
+ if (isLongString(content.text)) {
+ return false;
+ }
+
+ return NetUtils.isHTML(content.mimeType);
+ },
+
+ parseXml(file) {
+ let content = file.response.content;
+ if (isLongString(content.text)) {
+ return null;
+ }
+
+ return NetUtils.parseXml(content);
+ },
+
+ // Rendering
+
+ renderJson(file) {
+ let content = file.response.content;
+ if (!this.isJson(content)) {
+ return null;
+ }
+
+ let json = this.parseJson(file);
+ if (!json) {
+ return null;
+ }
+
+ return {
+ key: "json",
+ content: TreeView({
+ columns: [{id: "value"}],
+ object: json,
+ mode: "tiny",
+ renderValue: props => Rep(Object.assign({}, props, {
+ cropLimit: 50,
+ })),
+ }),
+ name: Locale.$STR("jsonScopeName")
+ };
+ },
+
+ renderImage(file) {
+ let content = file.response.content;
+ if (!this.isImage(content)) {
+ return null;
+ }
+
+ let dataUri = "data:" + content.mimeType + ";base64," + content.text;
+ return {
+ key: "image",
+ content: DOM.img({src: dataUri}),
+ name: Locale.$STR("netRequest.image")
+ };
+ },
+
+ renderXml(file) {
+ let content = file.response.content;
+ if (!this.isXml(content)) {
+ return null;
+ }
+
+ let doc = this.parseXml(file);
+ if (!doc) {
+ return null;
+ }
+
+ // Proper component for rendering XML should be used (see bug 1247392)
+ return null;
+ },
+
+ /**
+ * If full response text is available, let's try to parse and
+ * present nicely according to the underlying format.
+ */
+ renderFormattedResponse(file) {
+ let content = file.response.content;
+ if (typeof content.text == "object") {
+ return null;
+ }
+
+ let group = this.renderJson(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderImage(file);
+ if (group) {
+ return group;
+ }
+
+ group = this.renderXml(file);
+ if (group) {
+ return group;
+ }
+ },
+
+ renderRawResponse(file) {
+ let group;
+ let content = file.response.content;
+
+ // The response might reached the limit, so check if we are
+ // dealing with a long string.
+ if (typeof content.text == "object") {
+ group = {
+ key: "raw-longstring",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text.initial,
+ SizeLimit({
+ actions: this.props.actions,
+ data: content,
+ message: Locale.$STR("netRequest.sizeLimitMessage"),
+ link: Locale.$STR("netRequest.sizeLimitMessageLink")
+ })
+ )
+ };
+ } else {
+ group = {
+ key: "raw",
+ name: Locale.$STR("netRequest.rawData"),
+ content: DOM.div({className: "netInfoResponseContent"},
+ content.text
+ )
+ };
+ }
+
+ return group;
+ },
+
+ componentDidMount() {
+ let { actions, data: file } = this.props;
+ let content = file.response.content;
+
+ if (!content || typeof (content.text) == "undefined") {
+ // TODO: use async action objects as soon as Redux is in place
+ actions.requestData("responseContent");
+ }
+ },
+
+ /**
+ * The response panel displays two groups:
+ *
+ * 1) Formatted response (in case of supported format, e.g. JSON, XML, etc.)
+ * 2) Raw response data (always displayed if not discarded)
+ */
+ render() {
+ let { actions, data: file } = this.props;
+
+ // If response bodies are discarded (not collected) let's just
+ // display a info message indicating what to do to collect even
+ // response bodies.
+ if (file.discardResponseBody) {
+ return DOM.span({className: "netInfoBodiesDiscarded"},
+ Locale.$STR("netRequest.responseBodyDiscarded")
+ );
+ }
+
+ // Request for the response content is done only if the response
+ // is not fetched yet - i.e. the `content.text` is undefined.
+ // Empty content.text` can also be a valid response either
+ // empty or not available yet.
+ let content = file.response.content;
+ if (!content || typeof (content.text) == "undefined") {
+ return (
+ Spinner()
+ );
+ }
+
+ // Render response body data. The right representation of the data
+ // is picked according to the content type.
+ let groups = [];
+ groups.push(this.renderFormattedResponse(file));
+ groups.push(this.renderRawResponse(file));
+
+ // Filter out empty groups.
+ groups = groups.filter(group => group);
+
+ // The raw response is collapsed by default if a nice formatted
+ // version is available.
+ if (groups.length > 1) {
+ groups[1].open = false;
+ }
+
+ return (
+ DOM.div({className: "responseTabBox"},
+ DOM.div({className: "panelContent"},
+ NetInfoGroupList({
+ groups: groups
+ })
+ )
+ )
+ );
+ }
+});
+
+// Helpers
+
+function isLongString(text) {
+ return typeof text == "object";
+}
+
+// Exports from this module
+module.exports = ResponseTab;
diff --git a/devtools/client/webconsole/net/components/size-limit.css b/devtools/client/webconsole/net/components/size-limit.css
new file mode 100644
index 000000000..a5c214d9e
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+/******************************************************************************/
+/* Response Size Limit */
+
+.netInfoBody .netInfoSizeLimit {
+ font-weight: bold;
+ padding-top: 10px;
+}
+
+.netInfoBody .netInfoSizeLimit .objectLink {
+ color: var(--theme-highlight-blue);
+}
diff --git a/devtools/client/webconsole/net/components/size-limit.js b/devtools/client/webconsole/net/components/size-limit.js
new file mode 100644
index 000000000..de8839314
--- /dev/null
+++ b/devtools/client/webconsole/net/components/size-limit.js
@@ -0,0 +1,62 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+const PropTypes = React.PropTypes;
+
+/**
+ * This template represents a size limit notification message
+ * used e.g. in the Response tab when response body exceeds
+ * size limit. The message contains a link allowing the user
+ * to fetch the rest of the data from the backend (debugger server).
+ */
+var SizeLimit = React.createClass({
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ message: PropTypes.string.isRequired,
+ link: PropTypes.string.isRequired,
+ actions: PropTypes.shape({
+ resolveString: PropTypes.func.isRequired
+ }),
+ },
+
+ displayName: "SizeLimit",
+
+ // Event Handlers
+
+ onClickLimit(event) {
+ let actions = this.props.actions;
+ let content = this.props.data;
+
+ actions.resolveString(content, "text");
+ },
+
+ // Rendering
+
+ render() {
+ let message = this.props.message;
+ let link = this.props.link;
+ let reLink = /^(.*)\{\{link\}\}(.*$)/;
+ let m = message.match(reLink);
+
+ return (
+ DOM.div({className: "netInfoSizeLimit"},
+ DOM.span({}, m[1]),
+ DOM.a({
+ className: "objectLink",
+ onClick: this.onClickLimit},
+ link
+ ),
+ DOM.span({}, m[2])
+ )
+ );
+ }
+});
+
+// Exports from this module
+module.exports = SizeLimit;
diff --git a/devtools/client/webconsole/net/components/spinner.js b/devtools/client/webconsole/net/components/spinner.js
new file mode 100644
index 000000000..fe79f7dd1
--- /dev/null
+++ b/devtools/client/webconsole/net/components/spinner.js
@@ -0,0 +1,26 @@
+/* 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";
+
+const React = require("devtools/client/shared/vendor/react");
+
+// Shortcuts
+const DOM = React.DOM;
+
+/**
+ * This template represents a throbber displayed when the UI
+ * is waiting for data coming from the backend (debugging server).
+ */
+var Spinner = React.createClass({
+ displayName: "Spinner",
+
+ render() {
+ return (
+ DOM.div({className: "devtools-throbber"})
+ );
+ }
+});
+
+// Exports from this module
+module.exports = Spinner;
diff --git a/devtools/client/webconsole/net/components/stacktrace-tab.js b/devtools/client/webconsole/net/components/stacktrace-tab.js
new file mode 100644
index 000000000..51eb7689b
--- /dev/null
+++ b/devtools/client/webconsole/net/components/stacktrace-tab.js
@@ -0,0 +1,29 @@
+/* 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";
+
+const { PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+const StackTraceTab = createClass({
+ displayName: "StackTraceTab",
+
+ propTypes: {
+ data: PropTypes.object.isRequired,
+ actions: PropTypes.shape({
+ onViewSourceInDebugger: PropTypes.func.isRequired
+ })
+ },
+
+ render() {
+ let { stacktrace } = this.props.data.cause;
+ let { actions } = this.props;
+ let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions);
+
+ return StackTrace({ stacktrace, onViewSourceInDebugger });
+ }
+});
+
+// Exports from this module
+module.exports = StackTraceTab;
diff --git a/devtools/client/webconsole/net/data-provider.js b/devtools/client/webconsole/net/data-provider.js
new file mode 100644
index 000000000..d8a70d72d
--- /dev/null
+++ b/devtools/client/webconsole/net/data-provider.js
@@ -0,0 +1,66 @@
+/* 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";
+
+const promise = require("promise");
+
+/**
+ * Map of pending requests. Used mainly by tests to wait
+ * till things are ready.
+ */
+var promises = new Map();
+
+/**
+ * This object is used to fetch network data from the backend.
+ * Communication with the chrome scope is based on message
+ * exchange.
+ */
+var DataProvider = {
+ hasPendingRequests: function () {
+ return promises.size > 0;
+ },
+
+ requestData: function (client, actor, method) {
+ let key = actor + ":" + method;
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ let deferred = promise.defer();
+ let realMethodName = "get" + method.charAt(0).toUpperCase() +
+ method.slice(1);
+
+ if (!client[realMethodName]) {
+ return null;
+ }
+
+ client[realMethodName](actor, response => {
+ promises.delete(key);
+ deferred.resolve(response);
+ });
+
+ promises.set(key, deferred.promise);
+ return deferred.promise;
+ },
+
+ resolveString: function (client, stringGrip) {
+ let key = stringGrip.actor + ":getString";
+ let p = promises.get(key);
+ if (p) {
+ return p;
+ }
+
+ p = client.getString(stringGrip).then(result => {
+ promises.delete(key);
+ return result;
+ });
+
+ promises.set(key, p);
+ return p;
+ },
+};
+
+// Exports from this module
+module.exports = DataProvider;
diff --git a/devtools/client/webconsole/net/main.js b/devtools/client/webconsole/net/main.js
new file mode 100644
index 000000000..6fdf9494d
--- /dev/null
+++ b/devtools/client/webconsole/net/main.js
@@ -0,0 +1,98 @@
+/* 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";
+
+/* global BrowserLoader */
+
+var { utils: Cu } = Components;
+
+// Initialize module loader and load all modules of the new inline
+// preview feature. The entire code-base doesn't need any extra
+// privileges and runs entirely in content scope.
+const rootUrl = "resource://devtools/client/webconsole/net/";
+const require = BrowserLoader({
+ baseURI: rootUrl,
+ window}).require;
+
+const NetRequest = require("./net-request");
+const { loadSheet } = require("sdk/stylesheet/utils");
+
+// Localization
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/netmonitor.properties");
+
+// Stylesheets
+var styleSheets = [
+ "resource://devtools/client/jsonview/css/toolbar.css",
+ "resource://devtools/client/shared/components/tree/tree-view.css",
+ "resource://devtools/client/shared/components/reps/reps.css",
+ "resource://devtools/client/webconsole/net/net-request.css",
+ "resource://devtools/client/webconsole/net/components/size-limit.css",
+ "resource://devtools/client/webconsole/net/components/net-info-body.css",
+ "resource://devtools/client/webconsole/net/components/net-info-group.css",
+ "resource://devtools/client/webconsole/net/components/net-info-params.css",
+ "resource://devtools/client/webconsole/net/components/response-tab.css"
+];
+
+// Load theme stylesheets into the Console frame. This should be
+// done automatically by UI Components as soon as we have consensus
+// on the right CSS strategy FIXME.
+// It would also be nice to include them using @import.
+styleSheets.forEach(url => {
+ loadSheet(this, url, "author");
+});
+
+// Localization API used by React components
+// accessing strings from *.properties file.
+// Example:
+// let localizedString = Locale.$STR('string-key');
+//
+// Resources:
+// http://l20n.org/
+// https://github.com/yahoo/react-intl
+this.Locale = {
+ $STR: key => {
+ try {
+ return L10N.getStr(key);
+ } catch (err) {
+ console.error(key + ": " + err);
+ }
+ }
+};
+
+// List of NetRequest instances represents the state.
+// As soon as Redux is in place it should be maintained using a reducer.
+var netRequests = new Map();
+
+/**
+ * This function handles network events received from the backend. It's
+ * executed from within the webconsole.js
+ */
+function onNetworkEvent(log) {
+ // The 'from' field is set only in case of a 'networkEventUpdate' packet.
+ // The initial 'networkEvent' packet uses 'actor'.
+ // Check if NetRequest object is already created for this event actor and
+ // if there is none make sure to create one.
+ let response = log.response;
+ let netRequest = response.from ? netRequests.get(response.from) : null;
+ if (!netRequest && !log.update) {
+ netRequest = new NetRequest(log);
+ netRequests.set(response.actor, netRequest);
+ }
+
+ if (!netRequest) {
+ return;
+ }
+
+ if (log.update) {
+ netRequest.updateBody(response);
+ }
+
+ return;
+}
+
+// Make the 'onNetworkEvent' accessible from chrome (see webconsole.js)
+this.NetRequest = {
+ onNetworkEvent: onNetworkEvent
+};
diff --git a/devtools/client/webconsole/net/moz.build b/devtools/client/webconsole/net/moz.build
new file mode 100644
index 000000000..1b9eca7fe
--- /dev/null
+++ b/devtools/client/webconsole/net/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'components',
+ 'utils'
+]
+
+DevToolsModules(
+ 'data-provider.js',
+ 'main.js',
+ 'net-request.css',
+ 'net-request.js',
+)
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini']
diff --git a/devtools/client/webconsole/net/net-request.css b/devtools/client/webconsole/net/net-request.css
new file mode 100644
index 000000000..82b6a027f
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.css
@@ -0,0 +1,35 @@
+ /* 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/. */
+
+/******************************************************************************/
+/* General */
+
+:root {
+ --net-border: #d7d7d7;
+}
+
+:root.theme-dark {
+ --net-border: #5f7387;
+}
+
+/******************************************************************************/
+/* Network log */
+
+/* No background if a Net log is opened */
+.netRequest.message.opened,
+.netRequest.message.opened:hover {
+ background: transparent !important;
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-dark .netRequest.opened:hover,
+.theme-dark .netRequest.opened {
+ background: transparent;
+}
+
+.theme-firebug .netRequest.message.opened:hover {
+ background-image: linear-gradient(rgba(214, 233, 246, 0.8), rgba(255, 255, 255, 1.6)) !important;
+}
diff --git a/devtools/client/webconsole/net/net-request.js b/devtools/client/webconsole/net/net-request.js
new file mode 100644
index 000000000..48cf66fdd
--- /dev/null
+++ b/devtools/client/webconsole/net/net-request.js
@@ -0,0 +1,323 @@
+/* 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;
diff --git a/devtools/client/webconsole/net/test/mochitest/.eslintrc.js b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..76904829d
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/webconsole/net/test/mochitest/browser.ini b/devtools/client/webconsole/net/test/mochitest/browser.ini
new file mode 100644
index 000000000..9414414c6
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ page_basic.html
+ test.json
+ test.json^headers^
+ test-cookies.json
+ test-cookies.json^headers^
+ test.txt
+ test.xml
+ test.xml^headers^
+ !/devtools/client/webconsole/test/head.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_net_basic.js]
+[browser_net_cookies.js]
+[browser_net_headers.js]
+[browser_net_params.js]
+[browser_net_post.js]
+[browser_net_response.js]
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
new file mode 100644
index 000000000..57273bec0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_basic.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * Basic test that generates XHR in the content and
+ * checks the related log in the Console panel can
+ * be expanded.
+ */
+add_task(function* () {
+ info("Test XHR Spy basic started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ ok(netInfoBody, "The network details must be available");
+
+ // There should be at least two tabs: Headers and Response
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.headers"),
+ "Headers tab must be available");
+ ok(netInfoBody.querySelector(".tabs .tabs-menu-item.response"),
+ "Response tab must be available");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
new file mode 100644
index 000000000..cfd85c2ed
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_cookies.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test-cookies.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Cookies are properly displayed.
+ */
+add_task(function* () {
+ info("Test XHR Spy cookies started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Cookies" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "cookies");
+
+ let requestCookieName = tabBody.querySelector(
+ ".netInfoGroup.requestCookies .netInfoParamName > span[title='bar']");
+
+ // Verify request cookies (name and value)
+ ok(requestCookieName, "Request Cookie name must exist");
+ is(requestCookieName.textContent, "bar",
+ "The cookie name must have proper value");
+
+ let requestCookieValue = requestCookieName.parentNode.nextSibling;
+ ok(requestCookieValue, "Request Cookie value must exist");
+ is(requestCookieValue.textContent, "foo",
+ "The cookie value must have proper value");
+
+ let responseCookieName = tabBody.querySelector(
+ ".netInfoGroup.responseCookies .netInfoParamName > span[title='test']");
+
+ // Verify response cookies (name and value)
+ ok(responseCookieName, "Response Cookie name must exist");
+ is(responseCookieName.textContent, "test",
+ "The cookie name must have proper value");
+
+ let responseCookieValue = responseCookieName.parentNode.nextSibling;
+ ok(responseCookieValue, "Response Cookie value must exist");
+ is(responseCookieValue.textContent, "abc",
+ "The cookie value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
new file mode 100644
index 000000000..4a47074ee
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_headers.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP headers are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy headers started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL
+ });
+
+ // Select "Headers" tab
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "headers");
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='Content-Type']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "Content-Type",
+ "The header name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "Header value must exist");
+ is(paramValue.textContent, "application/json; charset=utf-8",
+ "The header value must have proper value");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_params.js b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
new file mode 100644
index 000000000..d8b0e2c84
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_params.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * HTTP parameters (query string) are there.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?foo=bar"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let paramName = tabBody.querySelector(
+ ".netInfoParamName > span[title='foo']");
+
+ // Verify "Content-Type" header (name and value)
+ ok(paramName, "Header name must exist");
+ is(paramName.textContent, "foo",
+ "The param name must have proper value");
+
+ let paramValue = paramName.parentNode.nextSibling;
+ ok(paramValue, "param value must exist");
+ is(paramValue.textContent, "bar",
+ "The param value must have proper value");
+});
+
+/**
+ * Test URL parameters with the same name.
+ */
+add_task(function* () {
+ info("Test XHR Spy params started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ queryString: "?box[]=123&box[]=456"
+ });
+
+ // Check headers
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "params");
+
+ let params = tabBody.querySelectorAll(
+ ".netInfoParamName > span[title='box[]']");
+ is(params.length, 2, "Two URI parameters must exist");
+
+ let values = tabBody.querySelectorAll(
+ ".netInfoParamValue > code");
+ is(values[0].textContent, 123, "First value must match");
+ is(values[1].textContent, 456, "Second value must match");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_post.js b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
new file mode 100644
index 000000000..f6e776ef0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_post.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+
+const plainPostBody = "test-data";
+const jsonData = "{\"bar\": \"baz\"}";
+const jsonRendered = "bar\"baz\"";
+const xmlPostBody = "<xml><name>John</name></xml>";
+
+/**
+ * This test generates XHR requests in the page, expands
+ * networks details in the Console panel and checks that
+ * Post data are properly rendered.
+ */
+add_task(function* () {
+ info("Test XHR Spy post plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: plainPostBody
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(postContent.textContent, plainPostBody,
+ "Post body must be properly rendered");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: jsonData,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/json"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let postContent = tabBody.querySelector(
+ ".netInfoGroup.json.opened .netInfoGroupContent");
+ is(postContent.textContent, jsonRendered,
+ "Post body must be properly rendered");
+
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawPostContent, "Raw response group must be collapsed");
+});
+
+add_task(function* () {
+ info("Test XHR Spy post XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "POST",
+ url: JSON_XHR_URL,
+ body: xmlPostBody,
+ requestHeaders: [{
+ name: "Content-Type",
+ value: "application/xml"
+ }]
+ });
+
+ // Check post body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "post");
+ let rawPostContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ is(rawPostContent.textContent, xmlPostBody,
+ "Raw response group must not be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/browser_net_response.js b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
new file mode 100644
index 000000000..ec5543043
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/browser_net_response.js
@@ -0,0 +1,86 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PAGE_URL = URL_ROOT + "page_basic.html";
+const TEXT_XHR_URL = URL_ROOT + "test.txt";
+const JSON_XHR_URL = URL_ROOT + "test.json";
+const XML_XHR_URL = URL_ROOT + "test.xml";
+
+const textResponseBody = "this is a response";
+const jsonResponseBody = "name\"John\"";
+
+// Individual tests below generate XHR request in the page, expand
+// network details in the Console panel and checks various types
+// of response bodies.
+
+/**
+ * Validate plain text response
+ */
+add_task(function* () {
+ info("Test XHR Spy respone plain body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: TEXT_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+
+ ok(responseContent.textContent.indexOf(textResponseBody) > -1,
+ "Response body must be properly rendered");
+});
+
+/**
+ * Validate XML response
+ */
+add_task(function* () {
+ info("Test XHR Spy response XML body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: XML_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(rawResponseContent, "Raw response group must not be collapsed");
+});
+
+/**
+ * Validate JSON response
+ */
+add_task(function* () {
+ info("Test XHR Spy response JSON body started");
+
+ let {hud} = yield addTestTab(TEST_PAGE_URL);
+
+ let netInfoBody = yield executeAndInspectXhr(hud, {
+ method: "GET",
+ url: JSON_XHR_URL,
+ });
+
+ // Check response body data
+ let tabBody = yield selectNetInfoTab(hud, netInfoBody, "response");
+ let responseContent = tabBody.querySelector(
+ ".netInfoGroup.json .netInfoGroupContent");
+
+ is(responseContent.textContent, jsonResponseBody,
+ "Response body must be properly rendered");
+
+ let rawResponseContent = tabBody.querySelector(
+ ".netInfoGroup.raw.opened .netInfoGroupContent");
+ ok(!rawResponseContent, "Raw response group must be collapsed");
+});
diff --git a/devtools/client/webconsole/net/test/mochitest/head.js b/devtools/client/webconsole/net/test/mochitest/head.js
new file mode 100644
index 000000000..c01206948
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/head.js
@@ -0,0 +1,209 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
+/* import-globals-from ../../../test/head.js */
+
+"use strict";
+
+// Load Web Console head.js, it implements helper console test API
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/webconsole/test/head.js", this);
+
+const FRAME_SCRIPT_UTILS_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+
+const NET_INFO_PREF = "devtools.webconsole.filter.networkinfo";
+const NET_XHR_PREF = "devtools.webconsole.filter.netxhr";
+
+// Enable XHR logging for the test
+Services.prefs.setBoolPref(NET_INFO_PREF, true);
+Services.prefs.setBoolPref(NET_XHR_PREF, true);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(NET_INFO_PREF, true);
+ Services.prefs.clearUserPref(NET_XHR_PREF, true);
+});
+
+// Use the old webconsole since the new one doesn't yet support
+// XHR spy. See Bug 1304794.
+Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+function addTestTab(url) {
+ info("Adding a new JSON tab with URL: '" + url + "'");
+
+ return Task.spawn(function* () {
+ let tab = yield addTab(url);
+
+ // Load devtools/shared/frame-script-utils.js
+ loadCommonFrameScript(tab);
+
+ // Open the Console panel
+ let hud = yield openConsole();
+
+ return {
+ tab: tab,
+ browser: tab.linkedBrowser,
+ hud: hud
+ };
+ });
+}
+
+/**
+ *
+ * @param hud
+ * @param options
+ */
+function executeAndInspectXhr(hud, options) {
+ hud.jsterm.clearOutput();
+
+ options.queryString = options.queryString || "";
+
+ // Execute XHR in the content scope.
+ performRequestsInContent({
+ method: options.method,
+ url: options.url + options.queryString,
+ body: options.body,
+ nocache: options.nocache,
+ requestHeaders: options.requestHeaders
+ });
+
+ return Task.spawn(function* () {
+ // Wait till the appropriate Net log appears in the Console panel.
+ let rules = yield waitForMessages({
+ webconsole: hud,
+ messages: [{
+ text: options.url,
+ category: CATEGORY_NETWORK,
+ severity: SEVERITY_INFO,
+ isXhr: true,
+ }]
+ });
+
+ // The log is here, get its parent element (className: 'message').
+ let msg = [...rules[0].matched][0];
+ let body = msg.querySelector(".message-body");
+
+ // Open XHR HTTP details body and wait till the UI fetches
+ // all necessary data from the backend. All RPD requests
+ // needs to be finished before we can continue testing.
+ yield synthesizeMouseClickSoon(hud, body);
+ yield waitForBackend(msg);
+ let netInfoBody = body.querySelector(".netInfoBody");
+ ok(netInfoBody, "Net info body must exist");
+ return netInfoBody;
+ });
+}
+
+/**
+ * Wait till XHR data are fetched from the backend (i.e. there are
+ * no pending RDP requests.
+ */
+function waitForBackend(element) {
+ if (!element.hasAttribute("loading")) {
+ return;
+ }
+ return once(element, "netlog-no-pending-requests", true);
+}
+
+/**
+ * Select specific tab in XHR info body.
+ *
+ * @param netInfoBody The main XHR info body
+ * @param tabId Tab ID (possible values: 'headers', 'cookies', 'params',
+ * 'post', 'response');
+ *
+ * @returns Tab body element.
+ */
+function selectNetInfoTab(hud, netInfoBody, tabId) {
+ let tab = netInfoBody.querySelector(".tabs-menu-item." + tabId);
+ ok(tab, "Tab must exist " + tabId);
+
+ // Click to select specified tab and wait till its
+ // UI is populated with data from the backend.
+ // There must be no pending RDP requests before we can
+ // continue testing the UI.
+ return Task.spawn(function* () {
+ yield synthesizeMouseClickSoon(hud, tab);
+ let msg = getAncestorByClass(netInfoBody, "message");
+ yield waitForBackend(msg);
+ let tabBody = netInfoBody.querySelector("." + tabId + "TabBox");
+ ok(tabBody, "Tab body must exist");
+ return tabBody;
+ });
+}
+
+/**
+ * Return parent node with specified class.
+ *
+ * @param node A child element
+ * @param className Specified class name.
+ *
+ * @returns A parent element.
+ */
+function getAncestorByClass(node, className) {
+ for (let parent = node; parent; parent = parent.parentNode) {
+ if (parent.classList && parent.classList.contains(className)) {
+ return parent;
+ }
+ }
+ return null;
+}
+
+/**
+ * Synthesize asynchronous click event (with clean stack trace).
+ */
+function synthesizeMouseClickSoon(hud, element) {
+ return new Promise((resolve) => {
+ executeSoon(() => {
+ EventUtils.synthesizeMouse(element, 2, 2, {}, hud.iframeWindow);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Execute XHR in the content scope.
+ */
+function performRequestsInContent(requests) {
+ info("Performing requests in the context of the content.");
+ return executeInContent("devtools:test:xhr", requests);
+}
+
+function executeInContent(name, data = {}, objects = {},
+ expectResponse = true) {
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ }
+
+ return Promise.resolve();
+}
+
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ return new Promise((resolve) => {
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ resolve(msg.data);
+ });
+ });
+}
+
+function loadCommonFrameScript(tab) {
+ let browser = tab ? tab.linkedBrowser : gBrowser.selectedBrowser;
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+}
diff --git a/devtools/client/webconsole/net/test/mochitest/page_basic.html b/devtools/client/webconsole/net/test/mochitest/page_basic.html
new file mode 100644
index 000000000..da7158492
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/page_basic.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>XHR Spy test page</title>
+ </head>
+ <body>
+ <script type="text/javascript">
+ document.cookie = "bar=foo";
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
new file mode 100644
index 000000000..b5e739025
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json
@@ -0,0 +1 @@
+{"name":"Cookies Test"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
new file mode 100644
index 000000000..94a8c0c69
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test-cookies.json^headers^
@@ -0,0 +1,2 @@
+Content-Type: application/json; charset=utf-8
+Set-Cookie: test=abc
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json b/devtools/client/webconsole/net/test/mochitest/test.json
new file mode 100644
index 000000000..6548f8e3e
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json
@@ -0,0 +1 @@
+{"name":"John"}
diff --git a/devtools/client/webconsole/net/test/mochitest/test.json^headers^ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
new file mode 100644
index 000000000..6010bfd18
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/mochitest/test.txt b/devtools/client/webconsole/net/test/mochitest/test.txt
new file mode 100644
index 000000000..af7014e11
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.txt
@@ -0,0 +1 @@
+this is a response
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml b/devtools/client/webconsole/net/test/mochitest/test.xml
new file mode 100644
index 000000000..3749c8e5a
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml
@@ -0,0 +1 @@
+<xml><name>John</name></xml>
diff --git a/devtools/client/webconsole/net/test/mochitest/test.xml^headers^ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
new file mode 100644
index 000000000..10ecdf5f4
--- /dev/null
+++ b/devtools/client/webconsole/net/test/mochitest/test.xml^headers^
@@ -0,0 +1 @@
+Content-Type: application/xml; charset=utf-8
diff --git a/devtools/client/webconsole/net/test/unit/.eslintrc.js b/devtools/client/webconsole/net/test/unit/.eslintrc.js
new file mode 100644
index 000000000..54a9a6361
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/webconsole/net/test/unit/test_json-utils.js b/devtools/client/webconsole/net/test/unit/test_json-utils.js
new file mode 100644
index 000000000..f8ccdf3aa
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_json-utils.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { parseJSONString, isJSON } = require("devtools/client/webconsole/net/utils/json");
+
+// Test data
+const simpleJson = '{"name":"John"}';
+const jsonInFunc = 'someFunc({"name":"John"})';
+
+const json1 = "{'a': 1}";
+const json2 = " {'a': 1}";
+const json3 = "\t {'a': 1}";
+const json4 = "\n\n\t {'a': 1}";
+const json5 = "\n\n\t ";
+
+const textMimeType = "text/plain";
+const jsonMimeType = "text/javascript";
+const unknownMimeType = "text/unknown";
+
+/**
+ * Testing API provided by webconsole/net/utils/json.js
+ */
+function run_test() {
+ // parseJSONString
+ equal(parseJSONString(simpleJson).name, "John");
+ equal(parseJSONString(jsonInFunc).name, "John");
+
+ // isJSON
+ equal(isJSON(textMimeType, json1), true);
+ equal(isJSON(textMimeType, json2), true);
+ equal(isJSON(jsonMimeType, json3), true);
+ equal(isJSON(jsonMimeType, json4), true);
+
+ equal(isJSON(unknownMimeType, json1), true);
+ equal(isJSON(textMimeType, json1), true);
+
+ equal(isJSON(unknownMimeType), false);
+ equal(isJSON(unknownMimeType, json5), false);
+}
diff --git a/devtools/client/webconsole/net/test/unit/test_net-utils.js b/devtools/client/webconsole/net/test/unit/test_net-utils.js
new file mode 100644
index 000000000..512ebcbc7
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/test_net-utils.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {
+ isImage,
+ isHTML,
+ getHeaderValue,
+ isURLEncodedRequest,
+ isMultiPartRequest
+} = require("devtools/client/webconsole/net/utils/net");
+
+// Test data
+const imageMimeTypes = ["image/jpeg", "image/jpg", "image/gif",
+ "image/png", "image/bmp"];
+
+const htmlMimeTypes = ["text/html", "text/xml", "application/xml",
+ "application/rss+xml", "application/atom+xml", "application/xhtml+xml",
+ "application/mathml+xml", "application/rdf+xml"];
+
+const headers = [{name: "headerName", value: "value1"}];
+
+const har1 = {
+ request: {
+ postData: {
+ text: "content-type: application/x-www-form-urlencoded"
+ }
+ }
+};
+
+const har2 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "application/x-www-form-urlencoded"
+ }]
+ }
+};
+
+const har3 = {
+ request: {
+ headers: [{
+ name: "content-type",
+ value: "multipart/form-data"
+ }]
+ }
+};
+
+/**
+ * Testing API provided by webconsole/net/utils/net.js
+ */
+function run_test() {
+ // isImage
+ imageMimeTypes.forEach(mimeType => {
+ ok(isImage(mimeType));
+ });
+
+ // isHTML
+ htmlMimeTypes.forEach(mimeType => {
+ ok(isHTML(mimeType));
+ });
+
+ // getHeaderValue
+ equal(getHeaderValue(headers, "headerName"), "value1");
+
+ // isURLEncodedRequest
+ ok(isURLEncodedRequest(har1));
+ ok(isURLEncodedRequest(har2));
+
+ // isMultiPartRequest
+ ok(isMultiPartRequest(har3));
+}
diff --git a/devtools/client/webconsole/net/test/unit/xpcshell.ini b/devtools/client/webconsole/net/test/unit/xpcshell.ini
new file mode 100644
index 000000000..d988a2ad0
--- /dev/null
+++ b/devtools/client/webconsole/net/test/unit/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_json-utils.js]
+[test_net-utils.js]
diff --git a/devtools/client/webconsole/net/utils/events.js b/devtools/client/webconsole/net/utils/events.js
new file mode 100644
index 000000000..9f8705593
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/events.js
@@ -0,0 +1,21 @@
+/* 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";
+
+function isLeftClick(event, allowKeyModifiers) {
+ return event.button === 0 && (allowKeyModifiers || noKeyModifiers(event));
+}
+
+function noKeyModifiers(event) {
+ return !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
+}
+
+function cancelEvent(event) {
+ event.stopPropagation();
+ event.preventDefault();
+}
+
+// Exports from this module
+exports.isLeftClick = isLeftClick;
+exports.cancelEvent = cancelEvent;
diff --git a/devtools/client/webconsole/net/utils/json.js b/devtools/client/webconsole/net/utils/json.js
new file mode 100644
index 000000000..70d733f28
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/json.js
@@ -0,0 +1,234 @@
+/* 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";
+
+// List of JSON content types.
+const contentTypes = {
+ "text/plain": 1,
+ "text/javascript": 1,
+ "text/x-javascript": 1,
+ "text/json": 1,
+ "text/x-json": 1,
+ "application/json": 1,
+ "application/x-json": 1,
+ "application/javascript": 1,
+ "application/x-javascript": 1,
+ "application/json-rpc": 1
+};
+
+// Implementation
+var Json = {};
+
+/**
+ * Parsing JSON
+ */
+Json.parseJSONString = function (jsonString) {
+ if (!jsonString.length) {
+ return null;
+ }
+
+ let regex, matches;
+
+ let first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{") {
+ // This (probably) isn't pure JSON. Let's try to strip various sorts
+ // of XSSI protection/wrapping and see if that works better.
+
+ // Prototype-style secure requests
+ regex = /^\s*\/\*-secure-([\s\S]*)\*\/\s*$/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+
+ if (jsonString[0] === "\\" && jsonString[1] === "n") {
+ jsonString = jsonString.substr(2);
+ }
+
+ if (jsonString[jsonString.length - 2] === "\\" &&
+ jsonString[jsonString.length - 1] === "n") {
+ jsonString = jsonString.substr(0, jsonString.length - 2);
+ }
+ }
+
+ // Google-style (?) delimiters
+ if (jsonString.indexOf("&&&START&&&") !== -1) {
+ regex = /&&&START&&&([\s\S]*)&&&END&&&/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[1];
+ }
+ }
+
+ // while(1);, for(;;);, and )]}'
+ regex = /^\s*(\)\]\}[^\n]*\n|while\s*\(1\);|for\s*\(;;\);)([\s\S]*)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+
+ // JSONP
+ regex = /^\s*([A-Za-z0-9_$.]+\s*(?:\[.*\]|))\s*\(([\s\S]*)\)/;
+ matches = regex.exec(jsonString);
+ if (matches) {
+ jsonString = matches[2];
+ }
+ }
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ // Give up if we don't have valid start, to avoid some unnecessary overhead.
+ first = firstNonWs(jsonString);
+ if (first !== "[" && first !== "{" && isNaN(first) && first !== '"') {
+ return null;
+ }
+
+ // Remove JavaScript comments, quote non-quoted identifiers, and merge
+ // multi-line structures like |{"a": 1} \n {"b": 2}| into a single JSON
+ // object [{"a": 1}, {"b": 2}].
+ jsonString = pseudoJsonToJson(jsonString);
+
+ try {
+ return JSON.parse(jsonString);
+ } catch (err) {
+ // eslint-disable-line no-empty
+ }
+
+ return null;
+};
+
+function firstNonWs(str) {
+ for (let i = 0, len = str.length; i < len; i++) {
+ let ch = str[i];
+ if (ch !== " " && ch !== "\n" && ch !== "\t" && ch !== "\r") {
+ return ch;
+ }
+ }
+ return "";
+}
+
+function pseudoJsonToJson(json) {
+ let ret = "";
+ let at = 0, lasti = 0, lastch = "", hasMultipleParts = false;
+ for (let i = 0, len = json.length; i < len; ++i) {
+ let ch = json[i];
+ if (/\s/.test(ch)) {
+ continue;
+ }
+
+ if (ch === '"') {
+ // Consume a string.
+ ++i;
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === '"') {
+ break;
+ }
+ ++i;
+ }
+ } else if (ch === "'") {
+ // Convert an invalid string into a valid one.
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ ++i;
+
+ while (i < len) {
+ if (json[i] === "\\") {
+ ++i;
+ } else if (json[i] === "'") {
+ break;
+ }
+ ++i;
+ }
+
+ if (i < len) {
+ ret += json.slice(at, i) + "\"";
+ at = i + 1;
+ }
+ } else if ((ch === "[" || ch === "{") &&
+ (lastch === "]" || lastch === "}")) {
+ // Multiple JSON messages in one... Make it into a single array by
+ // inserting a comma and setting the "multiple parts" flag.
+ ret += json.slice(at, i) + ",";
+ hasMultipleParts = true;
+ at = i;
+ } else if (lastch === "," && (ch === "]" || ch === "}")) {
+ // Trailing commas in arrays/objects.
+ ret += json.slice(at, lasti);
+ at = i;
+ } else if (lastch === "/" && lasti === i - 1) {
+ // Some kind of comment; remove it.
+ if (ch === "/") {
+ ret += json.slice(at, i - 1);
+ at = i + json.slice(i).search(/\n|\r|$/);
+ i = at - 1;
+ } else if (ch === "*") {
+ ret += json.slice(at, i - 1);
+ at = json.indexOf("*/", i + 1) + 2;
+ if (at === 1) {
+ at = len;
+ }
+ i = at - 1;
+ }
+ ch = "\0";
+ } else if (/[a-zA-Z$_]/.test(ch) && lastch !== ":") {
+ // Non-quoted identifier. Quote it.
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ i = i + json.slice(i).search(/[^a-zA-Z0-9$_]|$/);
+ ret += json.slice(at, i) + "\"";
+ at = i;
+ }
+
+ lastch = ch;
+ lasti = i;
+ }
+
+ ret += json.slice(at);
+ if (hasMultipleParts) {
+ ret = "[" + ret + "]";
+ }
+
+ return ret;
+}
+
+Json.isJSON = function (contentType, data) {
+ // Workaround for JSON responses without proper content type
+ // Let's consider all responses starting with "{" as JSON. In the worst
+ // case there will be an exception when parsing. This means that no-JSON
+ // responses (and post data) (with "{") can be parsed unnecessarily,
+ // which represents a little overhead, but this happens only if the request
+ // is actually expanded by the user in the UI (Net & Console panels).
+ // Do a manual string search instead of checking (data.strip()[0] === "{")
+ // to improve performance/memory usage.
+ let len = data ? data.length : 0;
+ for (let i = 0; i < len; i++) {
+ let ch = data.charAt(i);
+ if (ch === "{") {
+ return true;
+ }
+
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
+ continue;
+ }
+
+ break;
+ }
+
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return !!contentTypes[contentType];
+};
+
+// Exports from this module
+module.exports = Json;
+
diff --git a/devtools/client/webconsole/net/utils/moz.build b/devtools/client/webconsole/net/utils/moz.build
new file mode 100644
index 000000000..3fdc458e3
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'events.js',
+ 'json.js',
+ 'net.js',
+)
diff --git a/devtools/client/webconsole/net/utils/net.js b/devtools/client/webconsole/net/utils/net.js
new file mode 100644
index 000000000..782ec032a
--- /dev/null
+++ b/devtools/client/webconsole/net/utils/net.js
@@ -0,0 +1,134 @@
+/* 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";
+
+const mimeCategoryMap = {
+ "text/plain": "txt",
+ "application/octet-stream": "bin",
+ "text/html": "html",
+ "text/xml": "html",
+ "application/xml": "html",
+ "application/rss+xml": "html",
+ "application/atom+xml": "html",
+ "application/xhtml+xml": "html",
+ "application/mathml+xml": "html",
+ "application/rdf+xml": "html",
+ "text/css": "css",
+ "application/x-javascript": "js",
+ "text/javascript": "js",
+ "application/javascript": "js",
+ "text/ecmascript": "js",
+ "application/ecmascript": "js",
+ "image/jpeg": "image",
+ "image/jpg": "image",
+ "image/gif": "image",
+ "image/png": "image",
+ "image/bmp": "image",
+ "application/x-shockwave-flash": "plugin",
+ "application/x-silverlight-app": "plugin",
+ "video/x-flv": "media",
+ "audio/mpeg3": "media",
+ "audio/x-mpeg-3": "media",
+ "video/mpeg": "media",
+ "video/x-mpeg": "media",
+ "video/webm": "media",
+ "video/mp4": "media",
+ "video/ogg": "media",
+ "audio/ogg": "media",
+ "application/ogg": "media",
+ "application/x-ogg": "media",
+ "application/x-midi": "media",
+ "audio/midi": "media",
+ "audio/x-mid": "media",
+ "audio/x-midi": "media",
+ "music/crescendo": "media",
+ "audio/wav": "media",
+ "audio/x-wav": "media",
+ "application/x-woff": "font",
+ "application/font-woff": "font",
+ "application/x-font-woff": "font",
+ "application/x-ttf": "font",
+ "application/x-font-ttf": "font",
+ "font/ttf": "font",
+ "font/woff": "font",
+ "application/x-otf": "font",
+ "application/x-font-otf": "font"
+};
+
+var NetUtils = {};
+
+NetUtils.isImage = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "image";
+};
+
+NetUtils.isHTML = function (contentType) {
+ if (!contentType) {
+ return false;
+ }
+
+ contentType = contentType.split(";")[0];
+ contentType = contentType.trim();
+ return mimeCategoryMap[contentType] == "html";
+};
+
+NetUtils.getHeaderValue = function (headers, name) {
+ if (!headers) {
+ return null;
+ }
+
+ name = name.toLowerCase();
+ for (let i = 0; i < headers.length; ++i) {
+ let headerName = headers[i].name.toLowerCase();
+ if (headerName == name) {
+ return headers[i].value;
+ }
+ }
+};
+
+NetUtils.parseXml = function (content) {
+ let contentType = content.mimeType.split(";")[0];
+ contentType = contentType.trim();
+
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(content.text, contentType);
+ let root = doc.documentElement;
+
+ // Error handling
+ let nsURI = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
+ if (root.namespaceURI == nsURI && root.nodeName == "parsererror") {
+ return null;
+ }
+
+ return doc;
+};
+
+NetUtils.isURLEncodedRequest = function (file) {
+ let mimeType = "application/x-www-form-urlencoded";
+
+ let postData = file.request.postData;
+ if (postData && postData.text) {
+ let text = postData.text.toLowerCase();
+ if (text.startsWith("content-type: " + mimeType)) {
+ return true;
+ }
+ }
+
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+NetUtils.isMultiPartRequest = function (file) {
+ let mimeType = "multipart/form-data";
+ let value = NetUtils.getHeaderValue(file.request.headers, "content-type");
+ return value && value.startsWith(mimeType);
+};
+
+// Exports from this module
+module.exports = NetUtils;