Split RequestHeadersView into multiple components. [3/3]

This stack of CLs extracts multiple separate components from the
`RequestHeadersView`. The existing logic and UI stay almost unchanged.

Part 3/3:
ResponseHeaderSection: contains the logic for rendering response
headers; renders multiple HeaderSectionRows; additional logic for displaying and editing overridden headers will be added to this
component in follow-up CLs.

Bug: 1297533
Change-Id: I965bd150356de9a7791bb17850c1789f81fbfd71
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3870305
Commit-Queue: Wolfgang Beyer <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
diff --git a/front_end/panels/network/components/ResponseHeaderSection.ts b/front_end/panels/network/components/ResponseHeaderSection.ts
new file mode 100644
index 0000000..e2dd874
--- /dev/null
+++ b/front_end/panels/network/components/ResponseHeaderSection.ts
@@ -0,0 +1,267 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
+import * as LitHtml from '../../../ui/lit-html/lit-html.js';
+
+import type * as SDK from '../../../core/sdk/sdk.js';
+import * as Protocol from '../../../generated/protocol.js';
+import * as i18n from '../../../core/i18n/i18n.js';
+import * as NetworkForward from '../../../panels/network/forward/forward.js';
+import * as Host from '../../../core/host/host.js';
+import * as IssuesManager from '../../../models/issues_manager/issues_manager.js';
+import {type HeaderDescriptor, HeaderSectionRow, type HeaderSectionRowData} from './HeaderSectionRow.js';
+
+import responseHeaderSectionStyles from './ResponseHeaderSection.css.js';
+
+const {render, html} = LitHtml;
+
+const UIStrings = {
+  /**
+  *@description Explanation text for which cross-origin policy to set.
+  */
+  chooseThisOptionIfTheResourceAnd:
+      'Choose this option if the resource and the document are served from the same site.',
+  /**
+  *@description Explanation text for which cross-origin policy to set.
+  */
+  onlyChooseThisOptionIfAn:
+      'Only choose this option if an arbitrary website including this resource does not impose a security risk.',
+  /**
+  *@description Message in the Headers View of the Network panel when a cross-origin opener policy blocked loading a sandbox iframe.
+  */
+  thisDocumentWasBlockedFrom:
+      'This document was blocked from loading in an `iframe` with a `sandbox` attribute because this document specified a cross-origin opener policy.',
+  /**
+  *@description Message in the Headers View of the Network panel when a cross-origin embedder policy header needs to be set.
+  */
+  toEmbedThisFrameInYourDocument:
+      'To embed this frame in your document, the response needs to enable the cross-origin embedder policy by specifying the following response header:',
+  /**
+  *@description Message in the Headers View of the Network panel when a cross-origin resource policy header needs to be set.
+  */
+  toUseThisResourceFromADifferent:
+      'To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers:',
+  /**
+  *@description Message in the Headers View of the Network panel when the cross-origin resource policy header is too strict.
+  */
+  toUseThisResourceFromADifferentOrigin:
+      'To use this resource from a different origin, the server may relax the cross-origin resource policy response header:',
+  /**
+  *@description Message in the Headers View of the Network panel when the cross-origin resource policy header is too strict.
+  */
+  toUseThisResourceFromADifferentSite:
+      'To use this resource from a different site, the server may relax the cross-origin resource policy response header:',
+};
+
+const str_ = i18n.i18n.registerUIStrings('panels/network/components/ResponseHeaderSection.ts', UIStrings);
+const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
+
+export interface ResponseHeaderSectionData {
+  request: SDK.NetworkRequest.NetworkRequest;
+  toReveal?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string};
+}
+
+export class ResponseHeaderSection extends HTMLElement {
+  static readonly litTagName = LitHtml.literal`devtools-response-header-section`;
+  readonly #shadow = this.attachShadow({mode: 'open'});
+  #request?: Readonly<SDK.NetworkRequest.NetworkRequest>;
+  #headers: HeaderDescriptor[] = [];
+
+  connectedCallback(): void {
+    this.#shadow.adoptedStyleSheets = [responseHeaderSectionStyles];
+  }
+
+  set data(data: ResponseHeaderSectionData) {
+    this.#request = data.request;
+
+    const headersWithIssues = [];
+    if (this.#request.wasBlocked()) {
+      const headerWithIssues =
+          BlockedReasonDetails.get((this.#request.blockedReason() as Protocol.Network.BlockedReason));
+      if (headerWithIssues) {
+        if (IssuesManager.RelatedIssue.hasIssueOfCategory(
+                this.#request, IssuesManager.Issue.IssueCategory.CrossOriginEmbedderPolicy)) {
+          const followLink = (): void => {
+            Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.LearnMoreLinkCOEP);
+            if (this.#request) {
+              void IssuesManager.RelatedIssue.reveal(
+                  this.#request, IssuesManager.Issue.IssueCategory.CrossOriginEmbedderPolicy);
+            }
+          };
+          if (headerWithIssues.blockedDetails) {
+            headerWithIssues.blockedDetails.reveal = followLink;
+          }
+        }
+        headersWithIssues.push(headerWithIssues);
+      }
+    }
+
+    function mergeHeadersWithIssues(
+        headers: SDK.NetworkRequest.NameValue[], headersWithIssues: HeaderDescriptor[]): HeaderDescriptor[] {
+      let i = 0, j = 0;
+      const result: HeaderDescriptor[] = [];
+      while (i < headers.length && j < headersWithIssues.length) {
+        if (headers[i].name < headersWithIssues[j].name) {
+          result.push({...headers[i++], headerNotSet: false});
+        } else if (headers[i].name > headersWithIssues[j].name) {
+          result.push({...headersWithIssues[j++], headerNotSet: true});
+        } else {
+          result.push({...headersWithIssues[j++], ...headers[i++], headerNotSet: false});
+        }
+      }
+      while (i < headers.length) {
+        result.push({...headers[i++], headerNotSet: false});
+      }
+      while (j < headersWithIssues.length) {
+        result.push({...headersWithIssues[j++], headerNotSet: true});
+      }
+      return result;
+    }
+
+    this.#headers = mergeHeadersWithIssues(this.#request.sortedResponseHeaders.slice(), headersWithIssues);
+
+    const blockedResponseCookies = this.#request.blockedResponseCookies();
+    const blockedCookieLineToReasons = new Map<string, Protocol.Network.SetCookieBlockedReason[]>(
+        blockedResponseCookies?.map(c => [c.cookieLine, c.blockedReasons]));
+    for (const header of this.#headers) {
+      if (header.name.toLowerCase() === 'set-cookie' && header.value) {
+        const matchingBlockedReasons = blockedCookieLineToReasons.get(header.value.toString());
+        if (matchingBlockedReasons) {
+          header.setCookieBlockedReasons = matchingBlockedReasons;
+        }
+      }
+    }
+
+    if (data.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.Response) {
+      this.#headers.filter(header => header.name.toUpperCase() === data.toReveal?.header?.toUpperCase())
+          .forEach(header => {
+            header.highlight = true;
+          });
+    }
+
+    this.#render();
+  }
+
+  #render(): void {
+    if (!this.#request) {
+      return;
+    }
+
+    // Disabled until https://crbug.com/1079231 is fixed.
+    // clang-format off
+    render(html`
+      ${this.#headers.map(header => html`
+        <${HeaderSectionRow.litTagName} .data=${{
+          header: header,
+        } as HeaderSectionRowData}></${HeaderSectionRow.litTagName}>
+      `)}
+    `, this.#shadow, {host: this});
+    // clang-format on
+  }
+}
+
+ComponentHelpers.CustomElements.defineComponent('devtools-response-header-section', ResponseHeaderSection);
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'devtools-response-header-section': ResponseHeaderSection;
+  }
+}
+
+const BlockedReasonDetails = new Map<Protocol.Network.BlockedReason, HeaderDescriptor>([
+  [
+    Protocol.Network.BlockedReason.CoepFrameResourceNeedsCoepHeader,
+    {
+      name: 'cross-origin-embedder-policy',
+      value: null,
+      headerValueIncorrect: null,
+      blockedDetails: {
+        explanation: i18nLazyString(UIStrings.toEmbedThisFrameInYourDocument),
+        examples: [{codeSnippet: 'Cross-Origin-Embedder-Policy: require-corp', comment: undefined}],
+        link: {url: 'https://web.dev/coop-coep/'},
+      },
+      headerNotSet: null,
+    },
+  ],
+  [
+    Protocol.Network.BlockedReason.CorpNotSameOriginAfterDefaultedToSameOriginByCoep,
+    {
+      name: 'cross-origin-resource-policy',
+      value: null,
+      headerValueIncorrect: null,
+      blockedDetails: {
+        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferent),
+        examples: [
+          {
+            codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
+            comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
+          },
+          {
+            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+          },
+        ],
+        link: {url: 'https://web.dev/coop-coep/'},
+      },
+      headerNotSet: null,
+    },
+  ],
+  [
+    Protocol.Network.BlockedReason.CoopSandboxedIframeCannotNavigateToCoopPage,
+    {
+      name: 'cross-origin-opener-policy',
+      value: null,
+      headerValueIncorrect: false,
+      blockedDetails: {
+        explanation: i18nLazyString(UIStrings.thisDocumentWasBlockedFrom),
+        examples: [],
+        link: {url: 'https://web.dev/coop-coep/'},
+      },
+      headerNotSet: null,
+    },
+  ],
+  [
+    Protocol.Network.BlockedReason.CorpNotSameSite,
+    {
+      name: 'cross-origin-resource-policy',
+      value: null,
+      headerValueIncorrect: true,
+      blockedDetails: {
+        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentSite),
+        examples: [
+          {
+            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+          },
+        ],
+        link: null,
+      },
+      headerNotSet: null,
+    },
+  ],
+  [
+    Protocol.Network.BlockedReason.CorpNotSameOrigin,
+    {
+      name: 'cross-origin-resource-policy',
+      value: null,
+      headerValueIncorrect: true,
+      blockedDetails: {
+        explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentOrigin),
+        examples: [
+          {
+            codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
+            comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
+          },
+          {
+            codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+            comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+          },
+        ],
+        link: null,
+      },
+      headerNotSet: null,
+    },
+  ],
+]);