CustomRequestPanel.js (11601B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const { 13 connect, 14 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 15 const { 16 L10N, 17 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 18 const { 19 fetchNetworkUpdatePacket, 20 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 21 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 22 const { 23 getSelectedRequest, 24 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 25 const { 26 getUrlQuery, 27 parseQueryString, 28 writeHeaderText, 29 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 30 31 const { button, div, input, label, textarea } = dom; 32 33 const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel"); 34 const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers"); 35 const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest"); 36 const CUSTOM_NEW_REQUEST_METHOD_LABEL = L10N.getStr( 37 "netmonitor.custom.newRequestMethodLabel" 38 ); 39 const CUSTOM_NEW_REQUEST_URL_LABEL = L10N.getStr( 40 "netmonitor.custom.newRequestUrlLabel" 41 ); 42 const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData"); 43 const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query"); 44 const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send"); 45 46 /* 47 * Custom request panel component 48 * A network request editor which simply provide edit and resend interface 49 * for network development. 50 */ 51 class CustomRequestPanel extends Component { 52 static get propTypes() { 53 return { 54 connector: PropTypes.object.isRequired, 55 removeSelectedCustomRequest: PropTypes.func.isRequired, 56 request: PropTypes.object.isRequired, 57 sendCustomRequest: PropTypes.func.isRequired, 58 updateRequest: PropTypes.func.isRequired, 59 }; 60 } 61 62 componentDidMount() { 63 const { request, connector } = this.props; 64 this.initialRequestMethod = request.method; 65 fetchNetworkUpdatePacket(connector.requestData, request, [ 66 "requestHeaders", 67 "responseHeaders", 68 "requestPostData", 69 ]); 70 } 71 72 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 73 UNSAFE_componentWillReceiveProps(nextProps) { 74 const { request, connector } = nextProps; 75 fetchNetworkUpdatePacket(connector.requestData, request, [ 76 "requestHeaders", 77 "responseHeaders", 78 "requestPostData", 79 ]); 80 } 81 82 /** 83 * Parse a text representation of a name[divider]value list with 84 * the given name regex and divider character. 85 * 86 * @param {string} text - Text of list 87 * @return {Array} array of headers info {name, value} 88 */ 89 parseRequestText(text, namereg, divider) { 90 const regex = new RegExp(`(${namereg})\\${divider}\\s*(\\S.*)`); 91 const pairs = []; 92 93 for (const line of text.split("\n")) { 94 const matches = regex.exec(line); 95 if (matches) { 96 const [, name, value] = matches; 97 pairs.push({ name, value }); 98 } 99 } 100 return pairs; 101 } 102 103 /** 104 * Update Custom Request Fields 105 * 106 * @param {object} evt click event 107 * @param {object} request current request 108 * @param {updateRequest} updateRequest action 109 */ 110 updateCustomRequestFields(evt, request, updateRequest) { 111 const val = evt.target.value; 112 let data; 113 114 switch (evt.target.id) { 115 case "custom-headers-value": 116 data = { 117 requestHeaders: { 118 customHeadersValue: val || "", 119 // Parse text representation of multiple HTTP headers 120 headers: this.parseRequestText(val, "\\S+?", ":"), 121 }, 122 }; 123 break; 124 case "custom-method-value": 125 // If val is empty when leaving the "method" field, set the method to 126 // its original value 127 data = 128 evt.type === "blur" && val === "" 129 ? { method: this.initialRequestMethod } 130 : { method: val.trim() }; 131 break; 132 case "custom-postdata-value": { 133 // Update "content-length" header value to reflect change 134 // in post data field. 135 const { requestHeaders } = request; 136 const newHeaders = requestHeaders.headers.map(header => { 137 if (header.name.toLowerCase() == "content-length") { 138 return { 139 name: header.name, 140 value: val.length, 141 }; 142 } 143 return header; 144 }); 145 146 data = { 147 requestPostData: { 148 postData: { text: val }, 149 }, 150 requestHeaders: { 151 headers: newHeaders, 152 }, 153 }; 154 break; 155 } 156 case "custom-query-value": { 157 let customQueryValue = val || ""; 158 // Parse readable text list of a query string 159 const queryArray = customQueryValue 160 ? this.parseRequestText(customQueryValue, ".+?", "=") 161 : []; 162 // Write out a list of query params into a query string 163 const queryString = queryArray 164 .map(({ name, value }) => name + "=" + value) 165 .join("&"); 166 const url = queryString 167 ? [request.url.split("?")[0], queryString].join("?") 168 : request.url.split("?")[0]; 169 // Remove temp customQueryValue while query string is parsable 170 if ( 171 customQueryValue === "" || 172 queryArray.length === customQueryValue.split("\n").length 173 ) { 174 customQueryValue = null; 175 } 176 data = { 177 customQueryValue, 178 url, 179 }; 180 break; 181 } 182 case "custom-url-value": 183 data = { 184 customQueryValue: null, 185 url: val, 186 }; 187 break; 188 default: 189 break; 190 } 191 if (data) { 192 // All updateRequest batch mode should be disabled to make UI editing in sync 193 updateRequest(request.id, data, false); 194 } 195 } 196 197 render() { 198 const { 199 removeSelectedCustomRequest, 200 request = {}, 201 sendCustomRequest, 202 updateRequest, 203 } = this.props; 204 const { method, customQueryValue, requestHeaders, requestPostData, url } = 205 request; 206 207 let headers = ""; 208 if (requestHeaders) { 209 headers = requestHeaders.customHeadersValue 210 ? requestHeaders.customHeadersValue 211 : writeHeaderText(requestHeaders.headers).trim(); 212 } 213 const queryArray = url ? parseQueryString(getUrlQuery(url)) : []; 214 let params = customQueryValue; 215 if (!params) { 216 params = queryArray 217 ? queryArray.map(({ name, value }) => name + "=" + value).join("\n") 218 : ""; 219 } 220 const postData = requestPostData?.postData.text 221 ? requestPostData.postData.text 222 : ""; 223 224 return div( 225 { className: "custom-request-panel" }, 226 div( 227 { className: "custom-request-label custom-header" }, 228 CUSTOM_NEW_REQUEST 229 ), 230 div( 231 { className: "custom-request-panel-content" }, 232 div( 233 { className: "tabpanel-summary-container custom-request" }, 234 div( 235 { className: "custom-request-button-container" }, 236 button( 237 { 238 className: "devtools-button", 239 id: "custom-request-close-button", 240 onClick: removeSelectedCustomRequest, 241 }, 242 CUSTOM_CANCEL 243 ), 244 button( 245 { 246 className: "devtools-button", 247 id: "custom-request-send-button", 248 onClick: sendCustomRequest, 249 }, 250 CUSTOM_SEND 251 ) 252 ) 253 ), 254 div( 255 { 256 className: "tabpanel-summary-container custom-method-and-url", 257 id: "custom-method-and-url", 258 }, 259 label( 260 { 261 className: "custom-method-value-label custom-request-label", 262 htmlFor: "custom-method-value", 263 }, 264 CUSTOM_NEW_REQUEST_METHOD_LABEL 265 ), 266 input({ 267 className: "custom-method-value", 268 id: "custom-method-value", 269 onChange: evt => 270 this.updateCustomRequestFields(evt, request, updateRequest), 271 onBlur: evt => 272 this.updateCustomRequestFields(evt, request, updateRequest), 273 value: method, 274 }), 275 label( 276 { 277 className: "custom-url-value-label custom-request-label", 278 htmlFor: "custom-url-value", 279 }, 280 CUSTOM_NEW_REQUEST_URL_LABEL 281 ), 282 input({ 283 className: "custom-url-value", 284 id: "custom-url-value", 285 onChange: evt => 286 this.updateCustomRequestFields(evt, request, updateRequest), 287 value: url || "http://", 288 }) 289 ), 290 // Hide query field when there is no params 291 params 292 ? div( 293 { 294 className: "tabpanel-summary-container custom-section", 295 id: "custom-query", 296 }, 297 label( 298 { 299 className: "custom-request-label", 300 htmlFor: "custom-query-value", 301 }, 302 CUSTOM_QUERY 303 ), 304 textarea({ 305 className: "tabpanel-summary-input", 306 id: "custom-query-value", 307 onChange: evt => 308 this.updateCustomRequestFields(evt, request, updateRequest), 309 rows: 4, 310 value: params, 311 wrap: "off", 312 }) 313 ) 314 : null, 315 div( 316 { 317 id: "custom-headers", 318 className: "tabpanel-summary-container custom-section", 319 }, 320 label( 321 { 322 className: "custom-request-label", 323 htmlFor: "custom-headers-value", 324 }, 325 CUSTOM_HEADERS 326 ), 327 textarea({ 328 className: "tabpanel-summary-input", 329 id: "custom-headers-value", 330 onChange: evt => 331 this.updateCustomRequestFields(evt, request, updateRequest), 332 rows: 8, 333 value: headers, 334 wrap: "off", 335 }) 336 ), 337 div( 338 { 339 id: "custom-postdata", 340 className: "tabpanel-summary-container custom-section", 341 }, 342 label( 343 { 344 className: "custom-request-label", 345 htmlFor: "custom-postdata-value", 346 }, 347 CUSTOM_POSTDATA 348 ), 349 textarea({ 350 className: "tabpanel-summary-input", 351 id: "custom-postdata-value", 352 onChange: evt => 353 this.updateCustomRequestFields(evt, request, updateRequest), 354 rows: 6, 355 value: postData, 356 wrap: "off", 357 }) 358 ) 359 ) 360 ); 361 } 362 } 363 364 module.exports = connect( 365 state => ({ request: getSelectedRequest(state) }), 366 dispatch => ({ 367 removeSelectedCustomRequest: () => 368 dispatch(Actions.removeSelectedCustomRequest()), 369 sendCustomRequest: () => dispatch(Actions.sendCustomRequest()), 370 updateRequest: (id, data, batch) => 371 dispatch(Actions.updateRequest(id, data, batch)), 372 }) 373 )(CustomRequestPanel);