tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);