Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"use strict";
const {
Component,
createFactory,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const {
connect,
} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
const {
L10N,
} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
const {
fetchNetworkUpdatePacket,
parseFormData,
parseJSON,
} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
const {
sortObjectKeys,
} = require("resource://devtools/client/netmonitor/src/utils/sort-utils.js");
const {
FILTER_SEARCH_DELAY,
} = require("resource://devtools/client/netmonitor/src/constants.js");
const {
updateFormDataSections,
} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
// Components
const PropertiesView = createFactory(
require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js")
);
const SearchBox = createFactory(
require("resource://devtools/client/shared/components/SearchBox.js")
);
loader.lazyGetter(this, "SourcePreview", function () {
return createFactory(
require("resource://devtools/client/netmonitor/src/components/previews/SourcePreview.js")
);
});
const { div, input, label, span, h2 } = dom;
const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
const REQUEST_EMPTY_TEXT = L10N.getStr("paramsNoPayloadText");
const REQUEST_FILTER_TEXT = L10N.getStr("paramsFilterText");
const REQUEST_FORM_DATA = L10N.getStr("paramsFormData");
const REQUEST_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
const RAW_REQUEST_PAYLOAD = L10N.getStr("netmonitor.request.raw");
const REQUEST_TRUNCATED = L10N.getStr("requestTruncated");
/**
* Params panel component
* Displays the GET parameters and POST data of a request
*/
class RequestPanel extends Component {
static get propTypes() {
return {
connector: PropTypes.object.isRequired,
openLink: PropTypes.func,
request: PropTypes.object.isRequired,
updateRequest: PropTypes.func.isRequired,
targetSearchResult: PropTypes.object,
};
}
constructor(props) {
super(props);
this.state = {
filterText: "",
rawRequestPayloadDisplayed: !!props.targetSearchResult,
};
this.toggleRawRequestPayload = this.toggleRawRequestPayload.bind(this);
this.renderRawRequestPayloadBtn =
this.renderRawRequestPayloadBtn.bind(this);
}
componentDidMount() {
const { request, connector } = this.props;
fetchNetworkUpdatePacket(connector.requestData, request, [
"requestPostData",
]);
updateFormDataSections(this.props);
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { request, connector } = nextProps;
fetchNetworkUpdatePacket(connector.requestData, request, [
"requestPostData",
]);
updateFormDataSections(nextProps);
if (nextProps.targetSearchResult !== null) {
this.setState({
rawRequestPayloadDisplayed: !!nextProps.targetSearchResult,
});
}
}
/**
* Update only if:
* 1) The rendered object has changed
* 2) The filter text has changed
* 2) The display got toggled between formatted and raw data
* 3) The user selected another search result target.
*/
shouldComponentUpdate(nextProps, nextState) {
return (
this.props.request !== nextProps.request ||
this.state.filterText !== nextState.filterText ||
this.state.rawRequestPayloadDisplayed !==
nextState.rawRequestPayloadDisplayed ||
this.props.targetSearchResult !== nextProps.targetSearchResult
);
}
/**
* Mapping array to dict for TreeView usage.
* Since TreeView only support Object(dict) format.
* This function also deal with duplicate key case
* (for multiple selection and query params with same keys)
*
* This function is not sorting result properties since it can
*
* @param {Object[]} arr - key-value pair array or form params
* @returns {Object} Rep compatible object
*/
getProperties(arr) {
return arr.reduce((map, obj) => {
const value = map[obj.name];
if (value || value === "") {
if (typeof value !== "object") {
map[obj.name] = [value];
}
map[obj.name].push(obj.value);
} else {
map[obj.name] = obj.value;
}
return map;
}, {});
}
toggleRawRequestPayload() {
this.setState({
rawRequestPayloadDisplayed: !this.state.rawRequestPayloadDisplayed,
});
}
renderRawRequestPayloadBtn(key, checked, onChange) {
return [
label(
{
key: `${key}RawRequestPayloadBtn`,
className: "raw-data-toggle",
htmlFor: `raw-${key}-checkbox`,
onClick: event => {
// stop the header click event
event.stopPropagation();
},
},
span({ className: "raw-data-toggle-label" }, RAW_REQUEST_PAYLOAD),
span(
{ className: "raw-data-toggle-input" },
input({
id: `raw-${key}-checkbox`,
checked,
className: "devtools-checkbox-toggle",
onChange,
type: "checkbox",
})
)
),
];
}
renderRequestPayload(component, componentProps) {
return component(componentProps);
}
render() {
const { request, targetSearchResult } = this.props;
const { filterText, rawRequestPayloadDisplayed } = this.state;
const { formDataSections, mimeType, requestPostData } = request;
const postData = requestPostData ? requestPostData.postData?.text : null;
if ((!formDataSections || formDataSections.length === 0) && !postData) {
return div({ className: "empty-notice" }, REQUEST_EMPTY_TEXT);
}
let component;
let componentProps;
let requestPayloadLabel = REQUEST_POST_PAYLOAD;
let hasFormattedDisplay = false;
let error;
// Form Data section
if (formDataSections && formDataSections.length) {
const sections = formDataSections.filter(str => /\S/.test(str)).join("&");
component = PropertiesView;
componentProps = {
object: this.getProperties(parseFormData(sections)),
filterText,
targetSearchResult,
defaultSelectFirstNode: false,
};
requestPayloadLabel = REQUEST_FORM_DATA;
hasFormattedDisplay = true;
}
// Request payload section
const limit = Services.prefs.getIntPref(
"devtools.netmonitor.requestBodyLimit"
);
// Check if the request post data has been truncated from the backend,
// in which case no parse should be attempted.
if (postData && limit <= postData.length) {
error = REQUEST_TRUNCATED;
}
if (formDataSections && formDataSections.length === 0 && postData) {
if (!error) {
const jsonParsedPostData = parseJSON(postData);
const { json, strippedChars } = jsonParsedPostData;
// If XSSI characters were present in the request just display the raw
// data because a request should never have XSSI escape characters
if (strippedChars) {
hasFormattedDisplay = false;
} else if (json) {
component = PropertiesView;
componentProps = {
object: sortObjectKeys(json),
filterText,
targetSearchResult,
defaultSelectFirstNode: false,
};
requestPayloadLabel = JSON_SCOPE_NAME;
hasFormattedDisplay = true;
}
}
}
if (
(!hasFormattedDisplay || this.state.rawRequestPayloadDisplayed) &&
postData
) {
component = SourcePreview;
componentProps = {
text: postData,
mode: mimeType?.replace(/;.+/, ""),
targetSearchResult,
};
requestPayloadLabel = REQUEST_POST_PAYLOAD;
}
return div(
{ className: "panel-container" },
error && div({ className: "request-error-header", title: error }, error),
div(
{ className: "devtools-toolbar devtools-input-toolbar" },
SearchBox({
delay: FILTER_SEARCH_DELAY,
type: "filter",
onChange: text => this.setState({ filterText: text }),
placeholder: REQUEST_FILTER_TEXT,
})
),
h2({ className: "data-header", role: "heading" }, [
span(
{
key: "data-label",
className: "data-label",
},
requestPayloadLabel
),
hasFormattedDisplay &&
this.renderRawRequestPayloadBtn(
"request",
rawRequestPayloadDisplayed,
this.toggleRawRequestPayload
),
]),
this.renderRequestPayload(component, componentProps)
);
}
}
module.exports = connect(null, dispatch => ({
updateRequest: (id, data, batch) =>
dispatch(Actions.updateRequest(id, data, batch)),
}))(RequestPanel);