diff options
Diffstat (limited to 'devtools/client/webconsole/net')
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; |