RequestPanel.js (9480B)
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 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 const { 14 connect, 15 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 16 const { 17 L10N, 18 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 19 const { 20 fetchNetworkUpdatePacket, 21 parseFormData, 22 parseJSON, 23 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 24 const { 25 sortObjectKeys, 26 } = require("resource://devtools/client/netmonitor/src/utils/sort-utils.js"); 27 const { 28 FILTER_SEARCH_DELAY, 29 } = require("resource://devtools/client/netmonitor/src/constants.js"); 30 const { 31 updateFormDataSections, 32 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 33 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 34 35 // Components 36 const PropertiesView = createFactory( 37 require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") 38 ); 39 const SearchBox = createFactory( 40 require("resource://devtools/client/shared/components/SearchBox.js") 41 ); 42 43 loader.lazyGetter(this, "SourcePreview", function () { 44 return createFactory( 45 require("resource://devtools/client/netmonitor/src/components/previews/SourcePreview.js") 46 ); 47 }); 48 49 const { div, input, label, span, h2 } = dom; 50 51 const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName"); 52 const REQUEST_EMPTY_TEXT = L10N.getStr("paramsNoPayloadText"); 53 const REQUEST_FILTER_TEXT = L10N.getStr("paramsFilterText"); 54 const REQUEST_FORM_DATA = L10N.getStr("paramsFormData"); 55 const REQUEST_POST_PAYLOAD = L10N.getStr("paramsPostPayload"); 56 const RAW_REQUEST_PAYLOAD = L10N.getStr("netmonitor.request.raw"); 57 const REQUEST_TRUNCATED = L10N.getStr("requestTruncated"); 58 59 /** 60 * Params panel component 61 * Displays the GET parameters and POST data of a request 62 */ 63 class RequestPanel extends Component { 64 static get propTypes() { 65 return { 66 connector: PropTypes.object.isRequired, 67 openLink: PropTypes.func, 68 request: PropTypes.object.isRequired, 69 updateRequest: PropTypes.func.isRequired, 70 targetSearchResult: PropTypes.object, 71 }; 72 } 73 74 constructor(props) { 75 super(props); 76 this.state = { 77 filterText: "", 78 rawRequestPayloadDisplayed: !!props.targetSearchResult, 79 }; 80 81 this.toggleRawRequestPayload = this.toggleRawRequestPayload.bind(this); 82 this.renderRawRequestPayloadBtn = 83 this.renderRawRequestPayloadBtn.bind(this); 84 } 85 86 componentDidMount() { 87 const { request, connector } = this.props; 88 fetchNetworkUpdatePacket(connector.requestData, request, [ 89 "requestPostData", 90 ]); 91 updateFormDataSections(this.props); 92 } 93 94 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 95 UNSAFE_componentWillReceiveProps(nextProps) { 96 const { request, connector } = nextProps; 97 fetchNetworkUpdatePacket(connector.requestData, request, [ 98 "requestPostData", 99 ]); 100 updateFormDataSections(nextProps); 101 102 if (nextProps.targetSearchResult !== null) { 103 this.setState({ 104 rawRequestPayloadDisplayed: !!nextProps.targetSearchResult, 105 }); 106 } 107 } 108 109 /** 110 * Update only if: 111 * 1) The rendered object has changed 112 * 2) The filter text has changed 113 * 2) The display got toggled between formatted and raw data 114 * 3) The user selected another search result target. 115 */ 116 shouldComponentUpdate(nextProps, nextState) { 117 return ( 118 this.props.request !== nextProps.request || 119 this.state.filterText !== nextState.filterText || 120 this.state.rawRequestPayloadDisplayed !== 121 nextState.rawRequestPayloadDisplayed || 122 this.props.targetSearchResult !== nextProps.targetSearchResult 123 ); 124 } 125 126 /** 127 * Mapping array to dict for TreeView usage. 128 * Since TreeView only support Object(dict) format. 129 * This function also deal with duplicate key case 130 * (for multiple selection and query params with same keys) 131 * 132 * This function is not sorting result properties since it can 133 * results in unexpected order of params. See bug 1469533 134 * 135 * @param {object[]} arr - key-value pair array or form params 136 * @returns {object} Rep compatible object 137 */ 138 getProperties(arr) { 139 return arr.reduce((map, obj) => { 140 const value = map[obj.name]; 141 if (value || value === "") { 142 if (typeof value !== "object") { 143 map[obj.name] = [value]; 144 } 145 map[obj.name].push(obj.value); 146 } else { 147 map[obj.name] = obj.value; 148 } 149 return map; 150 }, {}); 151 } 152 153 toggleRawRequestPayload() { 154 this.setState({ 155 rawRequestPayloadDisplayed: !this.state.rawRequestPayloadDisplayed, 156 }); 157 } 158 159 renderRawRequestPayloadBtn(key, checked, onChange) { 160 return [ 161 label( 162 { 163 key: `${key}RawRequestPayloadBtn`, 164 className: "raw-data-toggle", 165 onClick: event => { 166 // stop the header click event 167 event.stopPropagation(); 168 }, 169 }, 170 span({ className: "raw-data-toggle-label" }, RAW_REQUEST_PAYLOAD), 171 span( 172 { className: "raw-data-toggle-input" }, 173 input({ 174 id: `raw-${key}-checkbox`, 175 checked, 176 className: "devtools-checkbox-toggle", 177 onChange, 178 type: "checkbox", 179 }) 180 ) 181 ), 182 ]; 183 } 184 185 renderRequestPayload(component, componentProps) { 186 return component(componentProps); 187 } 188 189 render() { 190 const { request, targetSearchResult } = this.props; 191 const { filterText, rawRequestPayloadDisplayed } = this.state; 192 const { formDataSections, mimeType, requestPostData, url } = request; 193 const postData = requestPostData ? requestPostData.postData?.text : null; 194 195 if ((!formDataSections || formDataSections.length === 0) && !postData) { 196 return div({ className: "empty-notice" }, REQUEST_EMPTY_TEXT); 197 } 198 199 let component; 200 let componentProps; 201 let requestPayloadLabel = REQUEST_POST_PAYLOAD; 202 let hasFormattedDisplay = false; 203 204 let error; 205 206 // Form Data section 207 if (formDataSections && formDataSections.length) { 208 const sections = formDataSections.filter(str => /\S/.test(str)).join("&"); 209 component = PropertiesView; 210 componentProps = { 211 object: this.getProperties(parseFormData(sections)), 212 filterText, 213 targetSearchResult, 214 defaultSelectFirstNode: false, 215 url, 216 }; 217 requestPayloadLabel = REQUEST_FORM_DATA; 218 hasFormattedDisplay = true; 219 } 220 221 // Request payload section 222 const limit = Services.prefs.getIntPref( 223 "devtools.netmonitor.requestBodyLimit" 224 ); 225 226 // Check if the request post data has been truncated from the backend, 227 // in which case no parse should be attempted. 228 if (postData && limit > 0 && limit <= postData.length) { 229 error = REQUEST_TRUNCATED; 230 } 231 if (formDataSections && formDataSections.length === 0 && postData) { 232 if (!error) { 233 const jsonParsedPostData = parseJSON(postData); 234 const { json, strippedChars } = jsonParsedPostData; 235 // If XSSI characters were present in the request just display the raw 236 // data because a request should never have XSSI escape characters 237 if (strippedChars) { 238 hasFormattedDisplay = false; 239 } else if (json) { 240 component = PropertiesView; 241 componentProps = { 242 object: sortObjectKeys(json), 243 filterText, 244 targetSearchResult, 245 defaultSelectFirstNode: false, 246 url, 247 }; 248 requestPayloadLabel = JSON_SCOPE_NAME; 249 hasFormattedDisplay = true; 250 } 251 } 252 } 253 254 if ( 255 (!hasFormattedDisplay || this.state.rawRequestPayloadDisplayed) && 256 postData 257 ) { 258 component = SourcePreview; 259 componentProps = { 260 text: postData, 261 mimeType: mimeType?.replace(/;.+/, ""), 262 targetSearchResult, 263 url, 264 }; 265 requestPayloadLabel = REQUEST_POST_PAYLOAD; 266 } 267 268 return div( 269 { className: "panel-container" }, 270 error && div({ className: "request-error-header", title: error }, error), 271 div( 272 { className: "devtools-toolbar devtools-input-toolbar" }, 273 SearchBox({ 274 delay: FILTER_SEARCH_DELAY, 275 type: "filter", 276 onChange: text => this.setState({ filterText: text }), 277 placeholder: REQUEST_FILTER_TEXT, 278 }) 279 ), 280 h2({ className: "data-header", role: "heading" }, [ 281 span( 282 { 283 key: "data-label", 284 className: "data-label", 285 }, 286 requestPayloadLabel 287 ), 288 hasFormattedDisplay && 289 this.renderRawRequestPayloadBtn( 290 "request", 291 rawRequestPayloadDisplayed, 292 this.toggleRawRequestPayload 293 ), 294 ]), 295 this.renderRequestPayload(component, componentProps) 296 ); 297 } 298 } 299 300 module.exports = connect(null, dispatch => ({ 301 updateRequest: (id, data, batch) => 302 dispatch(Actions.updateRequest(id, data, batch)), 303 }))(RequestPanel);