diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/netmonitor/src/components/request-details/RequestPanel.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/src/components/request-details/RequestPanel.js')
-rw-r--r-- | devtools/client/netmonitor/src/components/request-details/RequestPanel.js | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/request-details/RequestPanel.js b/devtools/client/netmonitor/src/components/request-details/RequestPanel.js new file mode 100644 index 0000000000..5306161d2e --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-details/RequestPanel.js @@ -0,0 +1,301 @@ +/* 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 { + 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); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + 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 + * results in unexpected order of params. See bug 1469533 + * + * @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); |