summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/netmonitor/src/components/new-request/HTTPCustomRequestPanel.js511
1 files changed, 511 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);