Newer
Older

Claudia
committed
/* 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");

Claudia
committed
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");

Claudia
committed
const {

Claudia
committed
getClickedRequest,
} = require("resource://devtools/client/netmonitor/src/selectors/index.js");

Claudia
committed
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")
);

Nicolas Chevobbe
committed
const { button, div, footer, label, textarea, select, option } = dom;

Claudia
committed
const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.newRequestHeaders");

Claudia
committed
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");

Claudia
committed
const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
const CUSTOM_CLEAR = L10N.getStr("netmonitor.custom.clear");

Claudia
committed
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",
];

Hubert Boma Manilla
committed
// 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",

Daniel Q
committed
"PATCH",
];

Claudia
committed
/*
* 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 {

Claudia
committed
connector: PropTypes.object.isRequired,

Claudia
committed
request: PropTypes.object,
sendCustomRequest: PropTypes.func.isRequired,
};
}

Claudia
committed
constructor(props) {
super(props);

Hubert Boma Manilla
committed
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);

Hubert Boma Manilla
committed
this.onUpdateQueryParams = this.onUpdateQueryParams.bind(this);
}
async componentDidMount() {
let { connector, request } = this.props;

Pier Angelo Vendrame
committed
const persistedCustomRequest = await asyncStorage.getItem(
"devtools.netmonitor.customRequest"
);
request = request || persistedCustomRequest;

Hubert Boma Manilla
committed
if (!request) {
this.setState({ _isStateDataReady: true });
return;
}

Hubert Boma Manilla
committed
// 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;
}

Claudia
committed

Hubert Boma Manilla
committed
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;
});

Hubert Boma Manilla
committed
if (request.requestPostDataAvailable && !request.postBody) {
const requestData = await connector.requestData(
request.id,
"requestPostData"
);

Hubert Boma Manilla
committed
request.postBody = requestData.postData.text;
}

Hubert Boma Manilla
committed
this.setState({
method: request.method,
url: request.url,
urlQueryParams: this.createQueryParamsListFromURL(request.url),
headers,
postBody: request.postBody,
_isStateDataReady: true,
});
}

Claudia
committed
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();
}
}

Hubert Boma Manilla
committed
componentWillUnmount() {

Pier Angelo Vendrame
committed
asyncStorage.setItem("devtools.netmonitor.customRequest", this.state);
}
handleChangeURL(event) {
const { value } = event.target;

Hubert Boma Manilla
committed
this.setState({
url: value,
urlQueryParams: this.createQueryParamsListFromURL(value),
});
}

Claudia
committed
handleInputChange(event) {
const { name, value } = event.target;

Hubert Boma Manilla
committed
const newState = {

Claudia
committed
[name]: value,

Hubert Boma Manilla
committed
};
// 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;
});
}

Hubert Boma Manilla
committed
this.setState(newState);

Claudia
committed
}
updateInputMapItem(stateName, event) {
const { name, value } = event.target;
const [prop, index] = name.split("-");
const updatedList = [...this.state[stateName]];
updatedList[Number(index)][prop] = value;

Claudia
committed

Hubert Boma Manilla
committed
this.setState({
[stateName]: updatedList,

Claudia
committed
});
}
addInputMapItem(stateName, name, value) {

Hubert Boma Manilla
committed
this.setState({
[stateName]: [
...this.state[stateName],
{ name, value, checked: true, disabled: false },
],
});
}
deleteInputMapItem(stateName, index) {

Hubert Boma Manilla
committed
this.setState({
[stateName]: this.state[stateName].filter((_, i) => i !== index),
});
}

Claudia
committed
checkInputMapItem(stateName, index, checked) {

Hubert Boma Manilla
committed
this.setState({

Claudia
committed
[stateName]: this.state[stateName].map((item, i) => {
if (index === i) {
return {
...item,

Claudia
committed
};
}
return item;
}),
});
}
onUpdateQueryParams() {
const { urlQueryParams, url } = this.state;
let queryString = "";
for (const { name, value, checked } of urlQueryParams) {
if (checked) {

Claudia
committed
queryString += `${encodeURIComponent(name)}=${encodeURIComponent(
value
)}&`;
}
}
let finalURL = url.split("?")[0];

Mark Banner
committed
if (queryString.length) {
finalURL += `?${queryString.substring(0, queryString.length - 1)}`;
}

Hubert Boma Manilla
committed
this.setState({
url: finalURL,
});
}

Claudia
committed
createQueryParamsListFromURL(url = "") {
const parsedQuery = parseQueryString(getUrlQuery(url) || url.split("?")[1]);
const queryArray = parsedQuery || [];
return queryArray.map(({ name, value }) => {
return {
checked: true,
name,
value,
};
});
}
handleClear() {

Hubert Boma Manilla
committed
this.setState({
method: HTTP_METHODS[0],

Claudia
committed
url: "",
urlQueryParams: [],
headers: [],
postBody: "",

Claudia
committed
});
}

Claudia
committed
render() {

Claudia
committed
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(

Claudia
committed
{
className: "http-custom-method-value",
id: "http-custom-method-value",
name: "method",
onChange: this.handleInputChange,
onBlur: this.handleInputChange,

Hubert Boma Manilla
committed
value: this.state.method,

Claudia
committed
},
HTTP_METHODS.map(item =>
option(
{
value: item,
key: item,
},
item
)
)

Claudia
committed
),

Claudia
committed
div(
{
className: "auto-growing-textarea",

Hubert Boma Manilla
committed
"data-replicated-value": this.state.url,
title: this.state.url,
},

Claudia
committed
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,

Hubert Boma Manilla
committed
value: this.state.url,

Claudia
committed
rows: 1,
})
)

Claudia
committed
),
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
),

Claudia
committed
// This is the input map for the Url Parameters Component
InputMap({

Hubert Boma Manilla
committed
list: this.state.urlQueryParams,

Claudia
committed
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
);
},
})
),

Claudia
committed
div(
{
id: "http-custom-headers",
className: "tabpanel-summary-container http-custom-section",
},
label(
{
className: "http-custom-request-label",
htmlFor: "custom-headers-value",
},
CUSTOM_HEADERS
),

Claudia
committed
// This is the input map for the Headers Component
InputMap({
ref: this.headersListRef,

Hubert Boma Manilla
committed
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);
},

Claudia
committed
})
),
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,

Claudia
committed
onChange: this.handleInputChange,

Claudia
committed
rows: 6,

Hubert Boma Manilla
committed
value: this.state.postBody,

Claudia
committed
wrap: "off",
})

Nicolas Chevobbe
committed
)
),
footer(
{ className: "http-custom-request-button-container" },
button(
{
className: "devtools-button",
id: "http-custom-request-clear-button",
onClick: this.handleClear,
},
CUSTOM_CLEAR
),

Nicolas Chevobbe
committed
button(
{
className: "devtools-button",
id: "http-custom-request-send-button",

Hubert Boma Manilla
committed
disabled:
!this.state._isStateDataReady ||
!this.state.url ||
!this.state.method,

Nicolas Chevobbe
committed
onClick: () => {

Hubert Boma Manilla
committed
const newRequest = {
method: this.state.method,
url: this.state.url,

Nicolas Chevobbe
committed
cause: this.props.request?.cause,

Hubert Boma Manilla
committed
urlQueryParams: this.state.urlQueryParams.map(

Nicolas Chevobbe
committed
({ checked, ...params }) => params
),

Hubert Boma Manilla
committed
requestHeaders: {

Hubert Boma Manilla
committed
headers: this.state.headers

Hubert Boma Manilla
committed
.filter(({ checked }) => checked)
.map(({ checked, ...headersValues }) => headersValues),
},

Nicolas Chevobbe
committed
};

Hubert Boma Manilla
committed
if (this.state.postBody) {
newRequest.requestPostData = {

Nicolas Chevobbe
committed
postData: {

Hubert Boma Manilla
committed
text: this.state.postBody,

Nicolas Chevobbe
committed
},
};
}

Hubert Boma Manilla
committed
this.props.sendCustomRequest(newRequest);

Nicolas Chevobbe
committed
},
},
CUSTOM_SEND

Claudia
committed
)
)
);
}
}
module.exports = connect(

Claudia
committed
state => ({ request: getClickedRequest(state) }),

Claudia
committed
(dispatch, props) => ({

Claudia
committed
sendCustomRequest: request =>

Alexandre Poirot
committed
dispatch(Actions.sendHTTPCustomRequest(request)),

Claudia
committed
})
)(HTTPCustomRequestPanel);