diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/netmonitor/src/components/new-request | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/src/components/new-request')
3 files changed, 730 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js b/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js new file mode 100644 index 0000000000..826f0317ba --- /dev/null +++ b/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js @@ -0,0 +1,511 @@ +/* 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 asyncStorage = require("resource://devtools/shared/async-storage.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.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 Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); +const { + getClickedRequest, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); +const { + getUrlQuery, + parseQueryString, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const InputMap = createFactory( + require("resource://devtools/client/netmonitor/src/components/new-request/InputMap.js") +); +const { button, div, footer, label, textarea, select, option } = dom; + +const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.newRequestHeaders"); +const CUSTOM_NEW_REQUEST_URL_LABEL = L10N.getStr( + "netmonitor.custom.newRequestUrlLabel" +); +const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postBody"); +const CUSTOM_POSTDATA_PLACEHOLDER = L10N.getStr( + "netmonitor.custom.postBody.placeholder" +); +const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.urlParameters"); +const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send"); +const CUSTOM_CLEAR = L10N.getStr("netmonitor.custom.clear"); + +const FIREFOX_DEFAULT_HEADERS = [ + "Accept-Charset", + "Accept-Encoding", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Connection", + "Content-Length", + "Cookie", + "Cookie2", + "Date", + "DNT", + "Expect", + "Feature-Policy", + "Host", + "Keep-Alive", + "Origin", + "Proxy-", + "Sec-", + "Referer", + "TE", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Via", +]; +// This does not include the CONNECT method as it is restricted and special. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1769572#c2 for details +const HTTP_METHODS = [ + "GET", + "HEAD", + "POST", + "DELETE", + "PUT", + "OPTIONS", + "TRACE", + "PATCH", +]; + +/* + * HTTP Custom request panel component + * A network request panel which enables creating and sending new requests + * or selecting, editing and re-sending current requests. + */ +class HTTPCustomRequestPanel extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + request: PropTypes.object, + sendCustomRequest: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + method: HTTP_METHODS[0], + url: "", + urlQueryParams: [], + headers: [], + postBody: "", + // Flag to know the data from either the request or the async storage has + // been loaded in componentDidMount + _isStateDataReady: false, + }; + + this.handleInputChange = this.handleInputChange.bind(this); + this.handleChangeURL = this.handleChangeURL.bind(this); + this.updateInputMapItem = this.updateInputMapItem.bind(this); + this.addInputMapItem = this.addInputMapItem.bind(this); + this.deleteInputMapItem = this.deleteInputMapItem.bind(this); + this.checkInputMapItem = this.checkInputMapItem.bind(this); + this.handleClear = this.handleClear.bind(this); + this.createQueryParamsListFromURL = + this.createQueryParamsListFromURL.bind(this); + this.onUpdateQueryParams = this.onUpdateQueryParams.bind(this); + } + + async componentDidMount() { + let { connector, request } = this.props; + const persistedCustomRequest = await asyncStorage.getItem( + "devtools.netmonitor.customRequest" + ); + request = request || persistedCustomRequest; + + if (!request) { + this.setState({ _isStateDataReady: true }); + return; + } + + // We need this part because in the asyncStorage we are saving the request in one format + // and from the edit and resend it comes in a different form with different properties, + // so we need this to nomalize the request. + if (request.requestHeaders) { + request.headers = request.requestHeaders.headers; + } + + if (request.requestPostData?.postData?.text) { + request.postBody = request.requestPostData.postData.text; + } + + const headers = request.headers + .map(({ name, value }) => { + return { + name, + value, + checked: true, + disabled: FIREFOX_DEFAULT_HEADERS.some(i => name.startsWith(i)), + }; + }) + .sort((a, b) => { + if (a.disabled && !b.disabled) { + return -1; + } + if (!a.disabled && b.disabled) { + return 1; + } + return 0; + }); + + if (request.requestPostDataAvailable && !request.postBody) { + const requestData = await connector.requestData( + request.id, + "requestPostData" + ); + request.postBody = requestData.postData.text; + } + + this.setState({ + method: request.method, + url: request.url, + urlQueryParams: this.createQueryParamsListFromURL(request.url), + headers, + postBody: request.postBody, + _isStateDataReady: true, + }); + } + + componentDidUpdate(prevProps, prevState) { + // This is when the query params change in the url params input map + if ( + prevState.urlQueryParams !== this.state.urlQueryParams && + prevState.url === this.state.url + ) { + this.onUpdateQueryParams(); + } + } + + componentWillUnmount() { + asyncStorage.setItem("devtools.netmonitor.customRequest", this.state); + } + + handleChangeURL(event) { + const { value } = event.target; + + this.setState({ + url: value, + urlQueryParams: this.createQueryParamsListFromURL(value), + }); + } + + handleInputChange(event) { + const { name, value } = event.target; + const newState = { + [name]: value, + }; + + // If the message body changes lets make sure we + // keep the content-length up to date. + if (name == "postBody") { + newState.headers = this.state.headers.map(header => { + if (header.name == "Content-Length") { + header.value = value.length; + } + return header; + }); + } + + this.setState(newState); + } + + updateInputMapItem(stateName, event) { + const { name, value } = event.target; + const [prop, index] = name.split("-"); + const updatedList = [...this.state[stateName]]; + updatedList[Number(index)][prop] = value; + + this.setState({ + [stateName]: updatedList, + }); + } + + addInputMapItem(stateName, name, value) { + this.setState({ + [stateName]: [ + ...this.state[stateName], + { name, value, checked: true, disabled: false }, + ], + }); + } + + deleteInputMapItem(stateName, index) { + this.setState({ + [stateName]: this.state[stateName].filter((_, i) => i !== index), + }); + } + + checkInputMapItem(stateName, index, checked) { + this.setState({ + [stateName]: this.state[stateName].map((item, i) => { + if (index === i) { + return { + ...item, + checked, + }; + } + return item; + }), + }); + } + + onUpdateQueryParams() { + const { urlQueryParams, url } = this.state; + let queryString = ""; + for (const { name, value, checked } of urlQueryParams) { + if (checked) { + queryString += `${encodeURIComponent(name)}=${encodeURIComponent( + value + )}&`; + } + } + + let finalURL = url.split("?")[0]; + + if (queryString.length) { + finalURL += `?${queryString.substring(0, queryString.length - 1)}`; + } + this.setState({ + url: finalURL, + }); + } + + createQueryParamsListFromURL(url = "") { + const parsedQuery = parseQueryString(getUrlQuery(url) || url.split("?")[1]); + const queryArray = parsedQuery || []; + return queryArray.map(({ name, value }) => { + return { + checked: true, + name, + value, + }; + }); + } + + handleClear() { + this.setState({ + method: HTTP_METHODS[0], + url: "", + urlQueryParams: [], + headers: [], + postBody: "", + }); + } + + render() { + return div( + { className: "http-custom-request-panel" }, + div( + { className: "http-custom-request-panel-content" }, + div( + { + className: "tabpanel-summary-container http-custom-method-and-url", + id: "http-custom-method-and-url", + }, + select( + { + className: "http-custom-method-value", + id: "http-custom-method-value", + name: "method", + onChange: this.handleInputChange, + onBlur: this.handleInputChange, + value: this.state.method, + }, + + HTTP_METHODS.map(item => + option( + { + value: item, + key: item, + }, + item + ) + ) + ), + div( + { + className: "auto-growing-textarea", + "data-replicated-value": this.state.url, + title: this.state.url, + }, + textarea({ + className: "http-custom-url-value", + id: "http-custom-url-value", + name: "url", + placeholder: CUSTOM_NEW_REQUEST_URL_LABEL, + onChange: event => { + this.handleChangeURL(event); + }, + onBlur: this.handleTextareaChange, + value: this.state.url, + rows: 1, + }) + ) + ), + div( + { + className: "tabpanel-summary-container http-custom-section", + id: "http-custom-query", + }, + label( + { + className: "http-custom-request-label", + htmlFor: "http-custom-query-value", + }, + CUSTOM_QUERY + ), + // This is the input map for the Url Parameters Component + InputMap({ + list: this.state.urlQueryParams, + onUpdate: event => { + this.updateInputMapItem( + "urlQueryParams", + event, + this.onUpdateQueryParams + ); + }, + onAdd: (name, value) => + this.addInputMapItem( + "urlQueryParams", + name, + value, + this.onUpdateQueryParams + ), + onDelete: index => + this.deleteInputMapItem( + "urlQueryParams", + index, + this.onUpdateQueryParams + ), + onChecked: (index, checked) => { + this.checkInputMapItem( + "urlQueryParams", + index, + checked, + this.onUpdateQueryParams + ); + }, + }) + ), + div( + { + id: "http-custom-headers", + className: "tabpanel-summary-container http-custom-section", + }, + label( + { + className: "http-custom-request-label", + htmlFor: "custom-headers-value", + }, + CUSTOM_HEADERS + ), + // This is the input map for the Headers Component + InputMap({ + ref: this.headersListRef, + list: this.state.headers, + onUpdate: event => { + this.updateInputMapItem("headers", event); + }, + onAdd: (name, value) => + this.addInputMapItem("headers", name, value), + onDelete: index => this.deleteInputMapItem("headers", index), + onChecked: (index, checked) => { + this.checkInputMapItem("headers", index, checked); + }, + }) + ), + div( + { + id: "http-custom-postdata", + className: "tabpanel-summary-container http-custom-section", + }, + label( + { + className: "http-custom-request-label", + htmlFor: "http-custom-postdata-value", + }, + CUSTOM_POSTDATA + ), + textarea({ + className: "tabpanel-summary-input", + id: "http-custom-postdata-value", + name: "postBody", + placeholder: CUSTOM_POSTDATA_PLACEHOLDER, + onChange: this.handleInputChange, + rows: 6, + value: this.state.postBody, + wrap: "off", + }) + ) + ), + footer( + { className: "http-custom-request-button-container" }, + button( + { + className: "devtools-button", + id: "http-custom-request-clear-button", + onClick: this.handleClear, + }, + CUSTOM_CLEAR + ), + button( + { + className: "devtools-button", + id: "http-custom-request-send-button", + disabled: + !this.state._isStateDataReady || + !this.state.url || + !this.state.method, + onClick: () => { + const newRequest = { + method: this.state.method, + url: this.state.url, + cause: this.props.request?.cause, + urlQueryParams: this.state.urlQueryParams.map( + ({ checked, ...params }) => params + ), + requestHeaders: { + headers: this.state.headers + .filter(({ checked }) => checked) + .map(({ checked, ...headersValues }) => headersValues), + }, + }; + + if (this.state.postBody) { + newRequest.requestPostData = { + postData: { + text: this.state.postBody, + }, + }; + } + this.props.sendCustomRequest(newRequest); + }, + }, + CUSTOM_SEND + ) + ) + ); + } +} + +module.exports = connect( + state => ({ request: getClickedRequest(state) }), + (dispatch, props) => ({ + sendCustomRequest: request => + dispatch(Actions.sendHTTPCustomRequest(request)), + }) +)(HTTPCustomRequestPanel); diff --git a/devtools/client/netmonitor/src/components/new-request/InputMap.js b/devtools/client/netmonitor/src/components/new-request/InputMap.js new file mode 100644 index 0000000000..6e9eff2845 --- /dev/null +++ b/devtools/client/netmonitor/src/components/new-request/InputMap.js @@ -0,0 +1,211 @@ +/* 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, + createRef, +} = require("resource://devtools/client/shared/vendor/react.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { div, input, textarea, button } = dom; + +const CUSTOM_NEW_REQUEST_INPUT_NAME = L10N.getStr( + "netmonitor.custom.placeholder.name" +); + +const CUSTOM_NEW_REQUEST_INPUT_VALUE = L10N.getStr( + "netmonitor.custom.placeholder.value" +); + +const REMOVE_ITEM = L10N.getStr("netmonitor.custom.removeItem"); + +/** + * Editable name and value list component with optional form to add new items + **/ +class InputMap extends Component { + static get propTypes() { + return { + list: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + disabled: PropTypes.bool, + }) + ).isRequired, + onUpdate: PropTypes.func, + onAdd: PropTypes.func, + onDelete: PropTypes.func, + onChange: PropTypes.func, + onChecked: PropTypes.func, + }; + } + + constructor(props) { + super(props); + + this.listRef = createRef(); + + this.state = { + name: "", + value: "", + }; + } + + render() { + const { list, onUpdate, onAdd, onDelete, onChecked } = this.props; + const { name, value } = this.state; + + // Adds a new item with name and value when the user starts typing on the form + const onKeyDown = event => { + const { target } = event; + onAdd(name, value); + this.setState({ name: "", value: "" }, () => { + // Get next to last child on the list, + // because that was the item that was just added and + // we need to focous on it, so the user can keep editing it. + const targetParentNode = + this.listRef.current.childNodes?.[ + this.listRef.current.childElementCount - 2 + ]; + targetParentNode?.querySelector(`.${target.className}`).focus(); + }); + }; + + return div( + { + ref: this.listRef, + className: "http-custom-input-and-map-form", + }, + list.map((item, index) => { + return div( + { + className: "tabpanel-summary-container http-custom-input", + id: `http-custom-${item.name.toLowerCase()}`, + key: index, + }, + input({ + className: "tabpanel-summary-input-checkbox", + name: `checked-${index}`, + type: "checkbox", + onChange: event => { + onChecked(index, event.target.checked); + }, + checked: item.checked, + disabled: !!item.disabled, + wrap: "off", + }), + div( + { className: "tabpanel-summary-input-name" }, + div( + { + className: "auto-growing-textarea", + "data-replicated-value": item.name, + title: item.name, + }, + textarea({ + className: "http-custom-input-name", + name: `name-${index}`, + value: item.name, + disabled: !!item.disabled, + onChange: event => { + onUpdate(event); + }, + rows: 1, + }) + ) + ), + div( + { className: "tabpanel-summary-input-value" }, + div( + { + className: "auto-growing-textarea", + "data-replicated-value": item.value, + title: item.value, + }, + textarea({ + className: "http-custom-input-value", + name: `value-${index}`, + placeholder: "value", + disabled: !!item.disabled, + onChange: event => { + onUpdate(event); + }, + value: item.value, + rows: 1, + }) + ) + ), + !item.disabled && + onDelete && + button({ + className: "http-custom-delete-button", + title: REMOVE_ITEM, + "aria-label": REMOVE_ITEM, + onClick: () => onDelete(index), + }) + ); + }), + onAdd && + div( + { + className: "map-add-new-inputs", + }, + input({ + className: "tabpanel-summary-input-checkbox", + onChange: () => {}, + checked: true, + type: "checkbox", + }), + div( + { className: "tabpanel-summary-input-name" }, + div( + { + className: "auto-growing-textarea", + "data-replicated-value": name, + title: value, + }, + textarea({ + className: "http-custom-input-name", + type: "text", + ref: "addInputName", + checked: true, + value: name, + rows: 1, + placeholder: CUSTOM_NEW_REQUEST_INPUT_NAME, + onChange: e => this.setState({ name: e.target.value }), + onKeyDown, + }) + ) + ), + div( + { className: "tabpanel-summary-input-value" }, + div( + { + className: "auto-growing-textarea", + "data-replicated-value": value, + title: value, + }, + textarea({ + className: "http-custom-input-value", + type: "text", + ref: "addInputValue", + value, + onChange: e => this.setState({ value: e.target.value }), + rows: 1, + placeholder: CUSTOM_NEW_REQUEST_INPUT_VALUE, + onKeyDown, + }) + ) + ) + ) + ); + } +} + +module.exports = InputMap; diff --git a/devtools/client/netmonitor/src/components/new-request/moz.build b/devtools/client/netmonitor/src/components/new-request/moz.build new file mode 100644 index 0000000000..7e47799a22 --- /dev/null +++ b/devtools/client/netmonitor/src/components/new-request/moz.build @@ -0,0 +1,8 @@ +# 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( + "HTTPCustomRequestPanel.js", + "InputMap.js", +) |