tor-browser

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

HeadersPanel.js (26354B)


      1 /* eslint-disable complexity */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 "use strict";
      7 
      8 const {
      9  Component,
     10  createFactory,
     11 } = require("resource://devtools/client/shared/vendor/react.mjs");
     12 const {
     13  connect,
     14 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     15 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
     16 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     17 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     18 const {
     19  getFormattedIPAndPort,
     20  getFormattedSize,
     21  getRequestPriorityAsText,
     22 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");
     23 const {
     24  L10N,
     25 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
     26 const {
     27  getHeadersURL,
     28  getTrackingProtectionURL,
     29  getHTTPStatusCodeURL,
     30 } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js");
     31 const {
     32  fetchNetworkUpdatePacket,
     33  writeHeaderText,
     34  getRequestHeadersRawText,
     35 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
     36 const {
     37  HeadersProvider,
     38  HeaderList,
     39 } = require("resource://devtools/client/netmonitor/src/utils/headers-provider.js");
     40 const {
     41  FILTER_SEARCH_DELAY,
     42 } = require("resource://devtools/client/netmonitor/src/constants.js");
     43 // Components
     44 const PropertiesView = createFactory(
     45  require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js")
     46 );
     47 const SearchBox = createFactory(
     48  require("resource://devtools/client/shared/components/SearchBox.js")
     49 );
     50 const Accordion = createFactory(
     51  require("resource://devtools/client/shared/components/Accordion.js")
     52 );
     53 const UrlPreview = createFactory(
     54  require("resource://devtools/client/netmonitor/src/components/previews/UrlPreview.js")
     55 );
     56 const HeadersPanelContextMenu = require("resource://devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js");
     57 const StatusCode = createFactory(
     58  require("resource://devtools/client/netmonitor/src/components/StatusCode.js")
     59 );
     60 
     61 loader.lazyGetter(this, "MDNLink", function () {
     62  return createFactory(
     63    require("resource://devtools/client/shared/components/MdnLink.js")
     64  );
     65 });
     66 loader.lazyGetter(this, "Rep", function () {
     67  return ChromeUtils.importESModule(
     68    "resource://devtools/client/shared/components/reps/index.mjs"
     69  ).REPS.Rep;
     70 });
     71 loader.lazyGetter(this, "MODE", function () {
     72  return ChromeUtils.importESModule(
     73    "resource://devtools/client/shared/components/reps/index.mjs"
     74  ).MODE;
     75 });
     76 loader.lazyGetter(this, "TreeRow", function () {
     77  return createFactory(
     78    ChromeUtils.importESModule(
     79      "resource://devtools/client/shared/components/tree/TreeRow.mjs",
     80      { global: "current" }
     81    ).default
     82  );
     83 });
     84 loader.lazyRequireGetter(
     85  this,
     86  "showMenu",
     87  "resource://devtools/client/shared/components/menu/utils.js",
     88  true
     89 );
     90 loader.lazyRequireGetter(
     91  this,
     92  "openContentLink",
     93  "resource://devtools/client/shared/link.js",
     94  true
     95 );
     96 
     97 loader.lazyGetter(this, "HEADERS_PROXY_STATUS", function () {
     98  return L10N.getStr("netmonitor.headers.proxyStatus");
     99 });
    100 
    101 loader.lazyGetter(this, "HEADERS_PROXY_VERSION", function () {
    102  return L10N.getStr("netmonitor.headers.proxyVersion");
    103 });
    104 
    105 const { div, input, label, span, textarea, tr, td, button } = dom;
    106 
    107 const RESEND = L10N.getStr("netmonitor.context.resend.label");
    108 const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
    109 const RAW_HEADERS = L10N.getStr("netmonitor.headers.raw");
    110 const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
    111 const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
    112 const HEADERS_STATUS = L10N.getStr("netmonitor.headers.status");
    113 const HEADERS_EARLYHINT_STATUS = L10N.getStr(
    114  "netmonitor.headers.earlyHintsStatus"
    115 );
    116 const HEADERS_VERSION = L10N.getStr("netmonitor.headers.version");
    117 const HEADERS_TRANSFERRED = L10N.getStr("netmonitor.toolbar.transferred");
    118 const SUMMARY_STATUS_LEARN_MORE = L10N.getStr("netmonitor.summary.learnMore");
    119 const SUMMARY_ETP_LEARN_MORE = L10N.getStr(
    120  "netmonitor.enhancedTrackingProtection.learnMore"
    121 );
    122 const HEADERS_REFERRER = L10N.getStr("netmonitor.headers.referrerPolicy");
    123 const HEADERS_CONTENT_BLOCKING = L10N.getStr(
    124  "netmonitor.headers.contentBlocking"
    125 );
    126 const HEADERS_ETP = L10N.getStr(
    127  "netmonitor.trackingResource.enhancedTrackingProtection"
    128 );
    129 const HEADERS_PRIORITY = L10N.getStr("netmonitor.headers.requestPriority");
    130 const HEADERS_DNS = L10N.getStr("netmonitor.headers.dns");
    131 
    132 // Order is as displayed
    133 const HEADERS_CONFIG = {
    134  earlyHintsResponseHeaders: {
    135    // Key for fetching the data from the backend
    136    fetchKey: "earlyHintsResponseHeaders",
    137    title: L10N.getStr("earlyHintsResponseHeaders"),
    138    // Gets the content to be displayed when switched to the raw headers view
    139    rawHeaderValue: ({ headerData }) => {
    140      const preHeaderText = headerData.rawHeaders.split("\r\n")[0];
    141      return writeHeaderText(headerData.headers, preHeaderText).trim();
    142    },
    143    // Gets the full display text to be displayed in the header title bar(before the raw toggle button)
    144    displayTitle: ({ headerData }) => {
    145      const title = HEADERS_CONFIG.earlyHintsResponseHeaders.title;
    146      if (headerData.headersSize) {
    147        return `${title} (${getFormattedSize(headerData.headersSize, 3)})`;
    148      }
    149      const rawHeaderValue =
    150        HEADERS_CONFIG.earlyHintsResponseHeaders.rawHeaderValue({ headerData });
    151      return `${title} (${getFormattedSize(rawHeaderValue.length, 3)})`;
    152    },
    153  },
    154  responseHeaders: {
    155    // Key for fetching the data from the backend
    156    fetchKey: "responseHeaders",
    157    title: L10N.getStr("responseHeaders"),
    158    // Gets the content to be displayed when switched to the raw headers view
    159    rawHeaderValue: ({ status, statusText, httpVersion, headerData }) => {
    160      const preHeaderText = `${httpVersion} ${status} ${statusText}`;
    161      return writeHeaderText(headerData.headers, preHeaderText).trim();
    162    },
    163    // Gets the full display text to be displayed in the header title bar(before the raw toggle button)
    164    displayTitle: ({ status, statusText, httpVersion, headerData }) => {
    165      const title = HEADERS_CONFIG.responseHeaders.title;
    166      if (headerData.headersSize) {
    167        return `${title} (${getFormattedSize(headerData.headersSize, 3)})`;
    168      }
    169      const rawHeaderValue = HEADERS_CONFIG.responseHeaders.rawHeaderValue({
    170        httpVersion,
    171        status,
    172        statusText,
    173        headerData,
    174      });
    175      return `${title} (${getFormattedSize(rawHeaderValue.length, 3)})`;
    176    },
    177  },
    178  requestHeaders: {
    179    // See comment above
    180    fetchKey: "requestHeaders",
    181    title: L10N.getStr("requestHeaders"),
    182    // See comment above
    183    rawHeaderValue: ({ method, httpVersion, headerData, urlDetails }) => {
    184      return getRequestHeadersRawText(
    185        method,
    186        httpVersion,
    187        headerData,
    188        urlDetails
    189      );
    190    },
    191    // See comment above
    192    displayTitle: ({ method, httpVersion, headerData, urlDetails }) => {
    193      const title = HEADERS_CONFIG.requestHeaders.title;
    194      if (headerData.headersSize) {
    195        return `${title} (${getFormattedSize(headerData.headersSize, 3)})`;
    196      }
    197      const rawHeaderValue = HEADERS_CONFIG.requestHeaders.rawHeaderValue({
    198        method,
    199        httpVersion,
    200        headerData,
    201        urlDetails,
    202      });
    203      return `${title} (${getFormattedSize(rawHeaderValue.length, 3)})`;
    204    },
    205  },
    206  requestHeadersFromUploadStream: {
    207    // See comment above
    208    fetchKey: "requestPostData",
    209    title: L10N.getStr("requestHeadersFromUpload"),
    210    // See comment above
    211    rawHeaderValue: ({ headerData, preHeaderText = "" }) => {
    212      return writeHeaderText(headerData.headers, preHeaderText).trim();
    213    },
    214    // See comment above
    215    displayTitle: ({ method, httpVersion, headerData, urlDetails }) => {
    216      const title = HEADERS_CONFIG.requestHeadersFromUploadStream.title;
    217      if (headerData.headersSize) {
    218        return `${title} (${getFormattedSize(headerData.headersSize, 3)})`;
    219      }
    220      let preHeaderText = "";
    221      const hostHeader = headerData.headers.find(ele => ele.name === "Host");
    222      if (hostHeader) {
    223        preHeaderText = `${method} ${
    224          urlDetails.url.split(hostHeader.value)[1]
    225        } ${httpVersion}`;
    226      }
    227 
    228      const rawHeaderValue =
    229        HEADERS_CONFIG.requestHeadersFromUploadStream.rawHeaderValue({
    230          headerData,
    231          preHeaderText,
    232        });
    233      return `${title} (${getFormattedSize(rawHeaderValue.length, 3)})`;
    234    },
    235  },
    236 };
    237 
    238 const HEADERS_TO_FETCH = Object.values(HEADERS_CONFIG).map(
    239  headers => headers.fetchKey
    240 );
    241 /**
    242 * Headers panel component
    243 * Lists basic information about the request
    244 *
    245 * In http/2 all response headers are in small case.
    246 * See: https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_details/index.html#response-headers
    247 * RFC: https://tools.ietf.org/html/rfc7540#section-8.1.2
    248 */
    249 class HeadersPanel extends Component {
    250  static get propTypes() {
    251    return {
    252      connector: PropTypes.object.isRequired,
    253      cloneSelectedRequest: PropTypes.func.isRequired,
    254      member: PropTypes.object,
    255      request: PropTypes.object.isRequired,
    256      renderValue: PropTypes.func,
    257      openLink: PropTypes.func,
    258      targetSearchResult: PropTypes.object,
    259      openRequestBlockingAndAddUrl: PropTypes.func.isRequired,
    260      openHTTPCustomRequestTab: PropTypes.func.isRequired,
    261      cloneRequest: PropTypes.func,
    262      sendCustomRequest: PropTypes.func,
    263      shouldExpandPreview: PropTypes.bool,
    264      setHeadersUrlPreviewExpanded: PropTypes.func,
    265    };
    266  }
    267 
    268  constructor(props) {
    269    super(props);
    270 
    271    this.state = {
    272      openedRawHeaders: new Set(),
    273      lastToggledRawHeader: "",
    274      filterText: null,
    275    };
    276 
    277    this.getProperties = this.getProperties.bind(this);
    278    this.getTargetHeaderPath = this.getTargetHeaderPath.bind(this);
    279    this.toggleRawHeader = this.toggleRawHeader.bind(this);
    280    this.renderSummary = this.renderSummary.bind(this);
    281    this.renderRow = this.renderRow.bind(this);
    282    this.renderValue = this.renderValue.bind(this);
    283    this.renderRawHeadersBtn = this.renderRawHeadersBtn.bind(this);
    284    this.onShowResendMenu = this.onShowResendMenu.bind(this);
    285    this.onShowHeadersContextMenu = this.onShowHeadersContextMenu.bind(this);
    286  }
    287 
    288  componentDidMount() {
    289    const { request, connector } = this.props;
    290    fetchNetworkUpdatePacket(connector.requestData, request, HEADERS_TO_FETCH);
    291  }
    292 
    293  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    294  UNSAFE_componentWillReceiveProps(nextProps) {
    295    const { request, connector } = nextProps;
    296    fetchNetworkUpdatePacket(connector.requestData, request, HEADERS_TO_FETCH);
    297  }
    298 
    299  // The title to be display in the heading
    300  getHeadersDisplayTitle(headerKey) {
    301    const headerData = this.props.request[headerKey];
    302    if (!headerData?.headers.length) {
    303      return "";
    304    }
    305 
    306    return HEADERS_CONFIG[headerKey].displayTitle({
    307      ...this.props.request,
    308      headerData,
    309    });
    310  }
    311 
    312  getProperties(headerKey) {
    313    let propertiesResult;
    314    const headerData = this.props.request[headerKey];
    315    if (headerData?.headers.length) {
    316      propertiesResult = {
    317        [headerKey]: this.state.openedRawHeaders.has(headerKey)
    318          ? { RAW_HEADERS_ID: headerData.rawHeaders }
    319          : new HeaderList(headerData.headers),
    320      };
    321    }
    322    return propertiesResult;
    323  }
    324  // Toggles the raw headers view on / off
    325  toggleRawHeader(headerKey) {
    326    const newOpenedRawHeaders = new Set([...this.state.openedRawHeaders]);
    327    if (newOpenedRawHeaders.has(headerKey)) {
    328      newOpenedRawHeaders.delete(headerKey);
    329    } else {
    330      newOpenedRawHeaders.add(headerKey);
    331    }
    332    this.setState({
    333      openedRawHeaders: newOpenedRawHeaders,
    334      lastToggledRawHeader: headerKey,
    335    });
    336  }
    337 
    338  /**
    339   * Renders the top part of the headers detail panel - Summary.
    340   */
    341  renderSummary(summaryLabel, value, summaryClass = "") {
    342    return div(
    343      {
    344        key: summaryLabel,
    345        className: "tabpanel-summary-container headers-summary " + summaryClass,
    346      },
    347      span(
    348        { className: "tabpanel-summary-label headers-summary-label" },
    349        summaryLabel
    350      ),
    351      span({ className: "tabpanel-summary-value" }, value)
    352    );
    353  }
    354 
    355  /**
    356   * Get path for target header if it's set. It's used to select
    357   * the header automatically within the tree of headers.
    358   * Note that the target header is set by the Search panel.
    359   */
    360  getTargetHeaderPath(searchResult) {
    361    if (!searchResult || !(searchResult.type in HEADERS_CONFIG)) {
    362      return null;
    363    }
    364    // Using `HeaderList` ensures that we'll get the same
    365    // header index as it's used in the tree.
    366    const headerData = this.props.request[searchResult.type];
    367    return (
    368      "/" +
    369      searchResult.type +
    370      "/" +
    371      new HeaderList(headerData.headers).headers.findIndex(
    372        header => header.name == searchResult.label
    373      )
    374    );
    375  }
    376 
    377  /**
    378   * Custom rendering method passed to PropertiesView. It's responsible
    379   * for rendering <textarea> element with raw headers data.
    380   */
    381  renderRow(props) {
    382    const { level, path } = props.member;
    383 
    384    if (path.includes("RAW_HEADERS_ID")) {
    385      const headerKey = path.split("/")[1];
    386 
    387      const value = HEADERS_CONFIG[headerKey].rawHeaderValue({
    388        ...this.props.request,
    389        headerData: this.props.request[headerKey],
    390      });
    391 
    392      let rows;
    393      if (value) {
    394        const match = value.match(/\n/g);
    395        rows = match !== null ? match.length : 0;
    396        // Need to add 1 for the horizontal scrollbar
    397        // not to cover the last row of raw data
    398        rows = rows + 1;
    399      }
    400 
    401      return tr(
    402        {
    403          key: path,
    404          role: "treeitem",
    405          className: "raw-headers-container",
    406          onClick: event => event.stopPropagation(),
    407        },
    408        td(
    409          {
    410            colSpan: 2,
    411          },
    412          textarea({
    413            className: "raw-headers",
    414            rows,
    415            value,
    416            readOnly: true,
    417          })
    418        )
    419      );
    420    }
    421 
    422    if (level !== 1) {
    423      return null;
    424    }
    425 
    426    return TreeRow(props);
    427  }
    428 
    429  renderRawHeadersBtn(headerKey) {
    430    return [
    431      label(
    432        {
    433          key: `${headerKey}RawHeadersBtn`,
    434          className: "raw-headers-toggle",
    435          onClick: event => event.stopPropagation(),
    436          onKeyDown: event => event.stopPropagation(),
    437        },
    438        span({ className: "headers-summary-label" }, RAW_HEADERS),
    439        span(
    440          { className: "raw-headers-toggle-input" },
    441          input({
    442            id: `raw-${headerKey}-checkbox`,
    443            checked: this.state.openedRawHeaders.has(headerKey),
    444            className: "devtools-checkbox-toggle",
    445            onChange: () => this.toggleRawHeader(headerKey),
    446            type: "checkbox",
    447          })
    448        )
    449      ),
    450    ];
    451  }
    452 
    453  renderValue(props) {
    454    const { member, value } = props;
    455 
    456    if (typeof value !== "string") {
    457      return null;
    458    }
    459 
    460    const headerDocURL = getHeadersURL(member.name);
    461 
    462    return div(
    463      { className: "treeValueCellDivider" },
    464      Rep(
    465        Object.assign(props, {
    466          // FIXME: A workaround for the issue in StringRep
    467          // Force StringRep to crop the text everytime
    468          member: Object.assign({}, member, { open: false }),
    469          mode: MODE.TINY,
    470          noGrip: true,
    471          openLink: openContentLink,
    472        })
    473      ),
    474      headerDocURL ? MDNLink({ url: headerDocURL }) : null
    475    );
    476  }
    477 
    478  getShouldOpen(headerKey, filterText, targetSearchResult) {
    479    return (item, opened) => {
    480      // If closed, open panel for these reasons
    481      //  1.The raw header is switched on or off
    482      //  2.The filter text is set
    483      //  3.The search text is set
    484      if (
    485        (!opened && this.state.lastToggledRawHeader === headerKey) ||
    486        (!opened && filterText) ||
    487        (!opened && targetSearchResult)
    488      ) {
    489        return true;
    490      }
    491      return !!opened;
    492    };
    493  }
    494 
    495  onShowResendMenu(event) {
    496    const {
    497      request: { id },
    498      cloneSelectedRequest,
    499      cloneRequest,
    500      sendCustomRequest,
    501    } = this.props;
    502    const menuItems = [
    503      {
    504        label: RESEND,
    505        type: "button",
    506        click: () => {
    507          cloneRequest(id);
    508          sendCustomRequest();
    509        },
    510      },
    511      {
    512        label: EDIT_AND_RESEND,
    513        type: "button",
    514        click: evt => {
    515          cloneSelectedRequest(evt);
    516        },
    517      },
    518    ];
    519 
    520    showMenu(menuItems, { button: event.target });
    521  }
    522 
    523  onShowHeadersContextMenu(event) {
    524    if (!this.contextMenu) {
    525      this.contextMenu = new HeadersPanelContextMenu();
    526    }
    527    this.contextMenu.open(event, window.getSelection());
    528  }
    529 
    530  render() {
    531    const {
    532      targetSearchResult,
    533      request: {
    534        fromCache,
    535        fromServiceWorker,
    536        httpVersion,
    537        method,
    538        remoteAddress,
    539        remotePort,
    540        status,
    541        statusText,
    542        urlDetails,
    543        referrerPolicy,
    544        priority,
    545        isThirdPartyTrackingResource,
    546        contentSize,
    547        transferredSize,
    548        isResolvedByTRR,
    549        proxyHttpVersion,
    550        proxyStatus,
    551        proxyStatusText,
    552        earlyHintsStatus,
    553      },
    554      openRequestBlockingAndAddUrl,
    555      openHTTPCustomRequestTab,
    556      shouldExpandPreview,
    557      setHeadersUrlPreviewExpanded,
    558    } = this.props;
    559 
    560    const headersDataExists = Object.keys(HEADERS_CONFIG).some(
    561      headerKey => this.props.request[headerKey]?.headers.length
    562    );
    563 
    564    if (!headersDataExists) {
    565      return div({ className: "empty-notice" }, HEADERS_EMPTY_TEXT);
    566    }
    567 
    568    const items = [];
    569 
    570    for (const headerKey in HEADERS_CONFIG) {
    571      if (this.props.request[headerKey]?.headers.length) {
    572        const { filterText } = this.state;
    573        items.push({
    574          component: PropertiesView,
    575          componentProps: {
    576            object: this.getProperties(headerKey),
    577            filterText,
    578            targetSearchResult,
    579            renderRow: this.renderRow,
    580            renderValue: this.renderValue,
    581            provider: HeadersProvider,
    582            selectPath: this.getTargetHeaderPath,
    583            defaultSelectFirstNode: false,
    584            enableInput: false,
    585            useQuotes: false,
    586          },
    587          header: this.getHeadersDisplayTitle(headerKey),
    588          buttons: this.renderRawHeadersBtn(headerKey),
    589          id: headerKey,
    590          opened: true,
    591          shouldOpen: this.getShouldOpen(
    592            headerKey,
    593            filterText,
    594            targetSearchResult
    595          ),
    596        });
    597      }
    598    }
    599 
    600    const sizeText = L10N.getFormatStrWithNumbers(
    601      "netmonitor.headers.sizeDetails",
    602      getFormattedSize(transferredSize),
    603      getFormattedSize(contentSize)
    604    );
    605 
    606    const summarySize = this.renderSummary(HEADERS_TRANSFERRED, sizeText);
    607 
    608    let summaryEarlyStatus;
    609    if (earlyHintsStatus) {
    610      summaryEarlyStatus = div(
    611        {
    612          key: "headers-summary",
    613          className:
    614            "tabpanel-summary-container headers-summary headers-earlyhint-status",
    615        },
    616        span(
    617          {
    618            className: "tabpanel-summary-label headers-summary-label",
    619          },
    620          HEADERS_EARLYHINT_STATUS
    621        ),
    622        span(
    623          {
    624            className: "tabpanel-summary-value status",
    625            "data-code": earlyHintsStatus,
    626          },
    627          StatusCode({
    628            item: {
    629              fromCache,
    630              fromServiceWorker,
    631              status: earlyHintsStatus,
    632              statusText: "",
    633            },
    634          }),
    635          MDNLink({
    636            url: getHTTPStatusCodeURL(earlyHintsStatus),
    637            title: SUMMARY_STATUS_LEARN_MORE,
    638          })
    639        )
    640      );
    641    }
    642    let summaryStatus;
    643    if (status) {
    644      summaryStatus = div(
    645        {
    646          key: "headers-summary",
    647          className: "tabpanel-summary-container headers-summary",
    648        },
    649        span(
    650          {
    651            className: "tabpanel-summary-label headers-summary-label",
    652          },
    653          HEADERS_STATUS
    654        ),
    655        span(
    656          {
    657            className: "tabpanel-summary-value status",
    658            "data-code": status,
    659          },
    660          StatusCode({
    661            item: { fromCache, fromServiceWorker, status, statusText },
    662          }),
    663          statusText,
    664          MDNLink({
    665            url: getHTTPStatusCodeURL(status),
    666            title: SUMMARY_STATUS_LEARN_MORE,
    667          })
    668        )
    669      );
    670    }
    671 
    672    let summaryProxyStatus;
    673    if (proxyStatus) {
    674      summaryProxyStatus = div(
    675        {
    676          key: "headers-summary ",
    677          className:
    678            "tabpanel-summary-container headers-summary headers-proxy-status",
    679        },
    680        span(
    681          {
    682            className: "tabpanel-summary-label headers-summary-label",
    683          },
    684          HEADERS_PROXY_STATUS
    685        ),
    686        span(
    687          {
    688            className: "tabpanel-summary-value status",
    689            "data-code": proxyStatus,
    690          },
    691          StatusCode({
    692            item: {
    693              fromCache,
    694              fromServiceWorker,
    695              status: proxyStatus,
    696              statusText: proxyStatusText,
    697            },
    698          }),
    699          proxyStatusText,
    700          MDNLink({
    701            url: getHTTPStatusCodeURL(proxyStatus),
    702            title: SUMMARY_STATUS_LEARN_MORE,
    703          })
    704        )
    705      );
    706    }
    707 
    708    let trackingProtectionStatus;
    709    let trackingProtectionDetails = "";
    710    if (isThirdPartyTrackingResource) {
    711      const trackingProtectionDocURL = getTrackingProtectionURL();
    712 
    713      trackingProtectionStatus = this.renderSummary(
    714        HEADERS_CONTENT_BLOCKING,
    715        div(null, span({ className: "tracking-resource" }), HEADERS_ETP)
    716      );
    717      trackingProtectionDetails = this.renderSummary(
    718        "",
    719        div(
    720          {
    721            key: "tracking-protection",
    722            className: "tracking-protection",
    723          },
    724          L10N.getStr("netmonitor.trackingResource.tooltip"),
    725          trackingProtectionDocURL
    726            ? MDNLink({
    727                url: trackingProtectionDocURL,
    728                title: SUMMARY_ETP_LEARN_MORE,
    729              })
    730            : span({ className: "headers-summary learn-more-link" })
    731        )
    732      );
    733    }
    734 
    735    const summaryVersion = httpVersion
    736      ? this.renderSummary(HEADERS_VERSION, httpVersion)
    737      : null;
    738 
    739    const summaryProxyHttpVersion = proxyHttpVersion
    740      ? this.renderSummary(
    741          HEADERS_PROXY_VERSION,
    742          proxyHttpVersion,
    743          "headers-proxy-version"
    744        )
    745      : null;
    746 
    747    const summaryReferrerPolicy = referrerPolicy
    748      ? this.renderSummary(HEADERS_REFERRER, referrerPolicy)
    749      : null;
    750 
    751    const summaryPriority = priority
    752      ? this.renderSummary(HEADERS_PRIORITY, getRequestPriorityAsText(priority))
    753      : null;
    754 
    755    const summaryDNS = this.renderSummary(
    756      HEADERS_DNS,
    757      L10N.getStr(
    758        isResolvedByTRR
    759          ? "netmonitor.headers.dns.overHttps"
    760          : "netmonitor.headers.dns.basic"
    761      )
    762    );
    763 
    764    const summaryItems = [
    765      summaryEarlyStatus,
    766      summaryStatus,
    767      summaryProxyStatus,
    768      summaryVersion,
    769      summaryProxyHttpVersion,
    770      summarySize,
    771      summaryReferrerPolicy,
    772      summaryPriority,
    773      summaryDNS,
    774      trackingProtectionStatus,
    775      trackingProtectionDetails,
    776    ].filter(summaryItem => summaryItem !== null);
    777 
    778    const newEditAndResendPref = Services.prefs.getBoolPref(
    779      "devtools.netmonitor.features.newEditAndResend"
    780    );
    781 
    782    return div(
    783      { className: "headers-panel-container" },
    784      div(
    785        { className: "devtools-toolbar devtools-input-toolbar" },
    786        SearchBox({
    787          delay: FILTER_SEARCH_DELAY,
    788          type: "filter",
    789          onChange: text => this.setState({ filterText: text }),
    790          placeholder: HEADERS_FILTER_TEXT,
    791        }),
    792        span({ className: "devtools-separator" }),
    793        button(
    794          {
    795            id: "block-button",
    796            className: "devtools-button",
    797            title: L10N.getStr("netmonitor.context.blockURL"),
    798            onClick: () => openRequestBlockingAndAddUrl(urlDetails.url),
    799          },
    800          L10N.getStr("netmonitor.headers.toolbar.block")
    801        ),
    802        span({ className: "devtools-separator" }),
    803        button(
    804          {
    805            id: "edit-resend-button",
    806            className: !newEditAndResendPref
    807              ? "devtools-button devtools-dropdown-button"
    808              : "devtools-button",
    809            title: RESEND,
    810            onClick: !newEditAndResendPref
    811              ? this.onShowResendMenu
    812              : () => {
    813                  openHTTPCustomRequestTab();
    814                },
    815          },
    816          span({ className: "title" }, RESEND)
    817        )
    818      ),
    819      div(
    820        { className: "panel-container" },
    821        div(
    822          { className: "headers-overview" },
    823          UrlPreview({
    824            url: urlDetails.url,
    825            method,
    826            address: remoteAddress
    827              ? getFormattedIPAndPort(remoteAddress, remotePort)
    828              : null,
    829            shouldExpandPreview,
    830            onTogglePreview: expanded => setHeadersUrlPreviewExpanded(expanded),
    831            proxyStatus,
    832          }),
    833          div(
    834            {
    835              className: "summary",
    836              onContextMenu: this.onShowHeadersContextMenu,
    837            },
    838            summaryItems
    839          )
    840        ),
    841        Accordion({ items })
    842      )
    843    );
    844  }
    845 }
    846 
    847 module.exports = connect(
    848  state => ({
    849    shouldExpandPreview: state.ui.shouldExpandHeadersUrlPreview,
    850  }),
    851  dispatch => ({
    852    setHeadersUrlPreviewExpanded: expanded =>
    853      dispatch(Actions.setHeadersUrlPreviewExpanded(expanded)),
    854    openRequestBlockingAndAddUrl: url =>
    855      dispatch(Actions.openRequestBlockingAndAddUrl(url)),
    856    openHTTPCustomRequestTab: () =>
    857      dispatch(Actions.openHTTPCustomRequest(true)),
    858    cloneRequest: id => dispatch(Actions.cloneRequest(id)),
    859    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
    860  })
    861 )(HeadersPanel);