/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env mozilla/remote-page */
/* eslint-disable import/no-unassigned-import */

import { NetErrorCard } from "chrome://global/content/net-error-card.mjs";
import {
  gIsCertError,
  gErrorCode,
  gHasSts,
  searchParams,
  getHostName,
  getSubjectAltNames,
  getFailedCertificatesAsPEMString,
  recordSecurityUITelemetry,
  getCSSClass,
} from "chrome://global/content/aboutNetErrorHelpers.mjs";

const formatter = new Intl.DateTimeFormat();

const HOST_NAME = getHostName();

const FELT_PRIVACY_REFRESH = RPMGetBoolPref(
  "security.certerrors.felt-privacy-v1",
  false
);

// Used to check if we have a specific localized message for an error.
const KNOWN_ERROR_TITLE_IDS = new Set([
  // Error titles:
  "connectionFailure-title",
  "deniedPortAccess-title",
  "dnsNotFound-title",
  "dns-not-found-trr-only-title2",
  "fileNotFound-title",
  "fileAccessDenied-title",
  "generic-title",
  "captivePortal-title",
  "malformedURI-title",
  "netInterrupt-title",
  "notCached-title",
  "netOffline-title",
  "contentEncodingError-title",
  "unsafeContentType-title",
  "netReset-title",
  "netTimeout-title",
  "serverError-title",
  "unknownProtocolFound-title",
  "proxyConnectFailure-title",
  "proxyResolveFailure-title",
  "redirectLoop-title",
  "unknownSocketType-title",
  "nssFailure2-title",
  "csp-xfo-error-title",
  "corruptedContentError-title",
  "sslv3Used-title",
  "inadequateSecurityError-title",
  "blockedByPolicy-title",
  "clockSkewError-title",
  "networkProtocolError-title",
  "nssBadCert-title",
  "nssBadCert-sts-title",
  "certerror-mitm-title",
]);

/* The error message IDs from nsserror.ftl get processed into
 * aboutNetErrorCodes.js which is loaded before we are: */
/* global KNOWN_ERROR_MESSAGE_IDS */
const ERROR_MESSAGES_FTL = "toolkit/neterror/nsserrors.ftl";

// If the location of the favicon changes, FAVICON_CERTERRORPAGE_URL and/or
// FAVICON_ERRORPAGE_URL in toolkit/components/places/nsFaviconService.idl
// should also be updated.
document.getElementById("favicon").href =
  gIsCertError || gErrorCode == "nssFailure2"
    ? "chrome://global/skin/icons/warning.svg"
    : "chrome://global/skin/icons/info.svg";

function getDescription() {
  return searchParams.get("d");
}

function isCaptive() {
  return searchParams.get("captive") == "true";
}

/**
 * We don't actually know what the MitM is called (since we don't
 * maintain a list), so we'll try and display the common name of the
 * root issuer to the user. In the worst case they are as clueless as
 * before, in the best case this gives them an actionable hint.
 * This may be revised in the future.
 */
function getMitmName(failedCertInfo) {
  return failedCertInfo.issuerCommonName;
}

function retryThis(buttonEl) {
  RPMSendAsyncMessage("Browser:EnableOnlineMode");
  buttonEl.disabled = true;
}

function showPrefChangeContainer() {
  const panel = document.getElementById("prefChangeContainer");
  panel.hidden = false;
  document.getElementById("netErrorButtonContainer").hidden = true;
  document
    .getElementById("prefResetButton")
    .addEventListener("click", function resetPreferences() {
      RPMSendAsyncMessage("Browser:ResetSSLPreferences");
    });
  setFocus("#prefResetButton", "beforeend");
}

function toggleCertErrorDebugInfoVisibility(shouldShow) {
  let debugInfo = document.getElementById("certificateErrorDebugInformation");
  let copyButton = document.getElementById("copyToClipboardTop");

  if (shouldShow === undefined) {
    shouldShow = debugInfo.hidden;
  }
  debugInfo.hidden = !shouldShow;
  if (shouldShow) {
    copyButton.scrollIntoView({ block: "start", behavior: "smooth" });
    copyButton.focus();
  }
}

function setupAdvancedButton() {
  // Get the hostname and add it to the panel
  var panel = document.getElementById("badCertAdvancedPanel");

  // Register click handler for the weakCryptoAdvancedPanel
  document
    .getElementById("advancedButton")
    .addEventListener("click", togglePanelVisibility);

  function togglePanelVisibility() {
    if (panel.hidden) {
      // Reveal
      revealAdvancedPanelSlowlyAsync();
    } else {
      // Hide
      panel.hidden = true;
    }
  }

  if (getCSSClass() == "expertBadCert") {
    revealAdvancedPanelSlowlyAsync();
  }
}

async function revealAdvancedPanelSlowlyAsync() {
  const badCertAdvancedPanel = document.getElementById("badCertAdvancedPanel");
  const exceptionDialogButton = document.getElementById(
    "exceptionDialogButton"
  );

  // Toggling the advanced panel must ensure that the debugging
  // information panel is hidden as well, since it's opened by the
  // error code link in the advanced panel.
  toggleCertErrorDebugInfoVisibility(false);

  // Reveal, but disabled (and grayed-out) for 3.0s.
  badCertAdvancedPanel.hidden = false;
  exceptionDialogButton.disabled = true;

  // -

  if (exceptionDialogButton.resetReveal) {
    exceptionDialogButton.resetReveal(); // Reset if previous is pending.
  }
  let wasReset = false;
  exceptionDialogButton.resetReveal = () => {
    wasReset = true;
  };

  // Wait for 10 frames to ensure that the warning text is rendered
  // and gets all the way to the screen for the user to read it.
  // This is only ~0.160s at 60Hz, so it's not too much extra time that we're
  // taking to ensure that we're caught up with rendering, on top of the
  // (by default) whole second(s) we're going to wait based on the
  // security.dialog_enable_delay pref.
  // The catching-up to rendering is the important part, not the
  // N-frame-delay here.
  for (let i = 0; i < 10; i++) {
    await new Promise(requestAnimationFrame);
  }

  // Wait another Nms (default: 1000) for the user to be very sure. (Sorry speed readers!)
  const securityDelayMs = RPMGetIntPref("security.dialog_enable_delay", 1000);
  await new Promise(go => setTimeout(go, securityDelayMs));

  if (wasReset) {
    return;
  }

  // Enable and un-gray-out.
  exceptionDialogButton.disabled = false;
}

function disallowCertOverridesIfNeeded() {
  // Disallow overrides if this is a Strict-Transport-Security
  // host and the cert is bad (STS Spec section 7.3) or if the
  // certerror is in a frame (bug 633691).
  if (gHasSts || window != top) {
    document.getElementById("exceptionDialogButton").hidden = true;
  }
  if (gHasSts) {
    const stsExplanation = document.getElementById("badStsCertExplanation");
    document.l10n.setAttributes(
      stsExplanation,
      "certerror-what-should-i-do-bad-sts-cert-explanation",
      { hostname: HOST_NAME }
    );
    stsExplanation.hidden = false;

    document.l10n.setAttributes(
      document.getElementById("returnButton"),
      "neterror-return-to-previous-page-button"
    );
    document.l10n.setAttributes(
      document.getElementById("advancedPanelReturnButton"),
      "neterror-return-to-previous-page-button"
    );
  }
}

function recordTRREventTelemetry(
  warningPageType,
  trrMode,
  trrDomain,
  skipReason
) {
  RPMRecordGleanEvent("securityDohNeterror", "loadDohwarning", {
    value: warningPageType,
    mode: trrMode,
    provider_key: trrDomain,
    skip_reason: skipReason,
  });

  const netErrorButtonDiv = document.getElementById("netErrorButtonContainer");
  const buttons = netErrorButtonDiv.querySelectorAll("button");
  for (let b of buttons) {
    b.addEventListener("click", function (e) {
      let target = e.originalTarget;
      let telemetryId = target.dataset.telemetryId;
      RPMRecordGleanEvent(
        "securityDohNeterror",
        "click" +
          telemetryId
            .split("_")
            .map(word => word[0].toUpperCase() + word.slice(1))
            .join(""),
        {
          value: warningPageType,
          mode: trrMode,
          provider_key: trrDomain,
          skip_reason: skipReason,
        }
      );
    });
  }
}

function setResponseStatus(shortDesc) {
  let responseStatus;
  let responseStatusText;
  try {
    const netErrorInfo = document.getNetErrorInfo();
    responseStatus = netErrorInfo.responseStatus;
    responseStatusText = netErrorInfo.responseStatusText;
  } catch (ex) {
    return;
  }

  if (responseStatus >= 400) {
    let responseStatusLabel = document.createElement("p");
    responseStatusLabel.id = "response-status-label"; // id for testing
    document.l10n.setAttributes(
      responseStatusLabel,
      "neterror-response-status-code",
      {
        responsestatus: responseStatus,
        responsestatustext: responseStatusText ?? "",
      }
    );
    shortDesc.appendChild(responseStatusLabel);
  }
}

function initPage() {
  // We show an offline support page in case of a system-wide error,
  // when a user cannot connect to the internet and access the SUMO website.
  // For example, clock error, which causes certerrors across the web or
  // a security software conflict where the user is unable to connect
  // to the internet.
  // The URL that prompts us to show an offline support page should have the following
  // format: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/supportPageSlug",
  // so we can extract the support page slug.
  let baseURL = RPMGetFormatURLPref("app.support.baseURL");
  if (document.location.href.startsWith(baseURL)) {
    let supportPageSlug = document.location.pathname.split("/").pop();
    RPMSendAsyncMessage("DisplayOfflineSupportPage", {
      supportPageSlug,
    });
  }

  const className = getCSSClass();
  if (className) {
    document.body.classList.add(className);
  }

  const isTRROnlyFailure = gErrorCode == "dnsNotFound" && RPMIsTRROnlyFailure();

  let isNativeFallbackWarning = false;
  if (RPMGetBoolPref("network.trr.display_fallback_warning")) {
    isNativeFallbackWarning =
      gErrorCode == "dnsNotFound" && RPMIsNativeFallbackFailure();
  }

  const docTitle = document.querySelector("title");
  const bodyTitle = document.querySelector(".title-text");
  const shortDesc = document.getElementById("errorShortDesc");

  if (gIsCertError) {
    const isStsError = window !== window.top || gHasSts;
    const errArgs = { hostname: HOST_NAME };
    if (isCaptive()) {
      document.l10n.setAttributes(
        docTitle,
        "neterror-captive-portal-page-title"
      );
      document.l10n.setAttributes(bodyTitle, "captivePortal-title");
      document.l10n.setAttributes(
        shortDesc,
        "neterror-captive-portal",
        errArgs
      );
      initPageCaptivePortal();
    } else {
      if (isStsError) {
        document.l10n.setAttributes(docTitle, "certerror-sts-page-title");
        document.l10n.setAttributes(bodyTitle, "nssBadCert-sts-title");
        document.l10n.setAttributes(shortDesc, "certerror-sts-intro", errArgs);
      } else {
        document.l10n.setAttributes(docTitle, "certerror-page-title");
        document.l10n.setAttributes(bodyTitle, "nssBadCert-title");
        document.l10n.setAttributes(shortDesc, "certerror-intro", errArgs);
      }
      initPageCertError();
    }

    initCertErrorPageActions();
    setTechnicalDetailsOnCertError();
    return;
  }

  document.body.classList.add("neterror");

  let longDesc = document.getElementById("errorLongDesc");
  const tryAgain = document.getElementById("netErrorButtonContainer");
  tryAgain.hidden = false;
  const learnMore = document.getElementById("learnMoreContainer");
  const learnMoreLink = document.getElementById("learnMoreLink");
  learnMoreLink.setAttribute("href", baseURL + "connection-not-secure");

  let pageTitleId = "neterror-page-title";
  let bodyTitleId = gErrorCode + "-title";

  switch (gErrorCode) {
    case "blockedByPolicy":
      pageTitleId = "neterror-blocked-by-policy-page-title";
      document.body.classList.add("blocked");

      // Remove the "Try again" button from pages that don't need it.
      // For pages blocked by policy, trying again won't help.
      tryAgain.hidden = true;
      break;

    case "cspBlocked":
    case "xfoBlocked": {
      bodyTitleId = "csp-xfo-error-title";

      // Remove the "Try again" button for XFO and CSP violations,
      // since it's almost certainly useless. (Bug 553180)
      tryAgain.hidden = true;

      // Adding a button for opening websites blocked for CSP and XFO violations
      // in a new window. (Bug 1461195)
      document.getElementById("errorShortDesc").hidden = true;

      document.l10n.setAttributes(longDesc, "csp-xfo-blocked-long-desc", {
        hostname: HOST_NAME,
      });
      longDesc = null;

      document.getElementById("openInNewWindowContainer").hidden =
        RPMGetBoolPref("security.xfocsp.hideOpenInNewWindow");

      const openInNewWindowButton = document.getElementById(
        "openInNewWindowButton"
      );
      openInNewWindowButton.addEventListener("click", function () {
        const url = document.location.href;
        window.open(url, "_blank", "noopener,noreferrer");
      });

      // Add a learn more link
      learnMore.hidden = false;
      learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page");

      setupBlockingReportingUI();
      break;
    }

    case "dnsNotFound":
      pageTitleId = "neterror-dns-not-found-title";
      if (!isTRROnlyFailure) {
        RPMCheckAlternateHostAvailable();
      }

      break;
    case "inadequateSecurityError":
      // Remove the "Try again" button from pages that don't need it.
      // For HTTP/2 inadequate security, trying again won't help.
      tryAgain.hidden = true;
      break;

    case "malformedURI":
      pageTitleId = "neterror-malformed-uri-page-title";
      // Remove the "Try again" button from pages that don't need it.
      tryAgain.hidden = true;
      break;

    // TLS errors and non-overridable certificate errors (e.g. pinning
    // failures) are of type nssFailure2.
    case "nssFailure2": {
      learnMore.hidden = false;

      const netErrorInfo = document.getNetErrorInfo();
      void recordSecurityUITelemetry(
        "securityUiTlserror",
        "loadAbouttlserror",
        netErrorInfo
      );
      const errorCode = netErrorInfo.errorCodeString;
      switch (errorCode) {
        case "SSL_ERROR_UNSUPPORTED_VERSION":
        case "SSL_ERROR_PROTOCOL_VERSION_ALERT": {
          const tlsNotice = document.getElementById("tlsVersionNotice");
          tlsNotice.hidden = false;
          document.l10n.setAttributes(tlsNotice, "cert-error-old-tls-version");
        }
        // fallthrough

        case "SSL_ERROR_NO_CIPHERS_SUPPORTED":
        case "SSL_ERROR_NO_CYPHER_OVERLAP":
        case "SSL_ERROR_SSL_DISABLED":
          RPMAddMessageListener("HasChangedCertPrefs", msg => {
            if (msg.data.hasChangedCertPrefs) {
              // Configuration overrides might have caused this; offer to reset.
              showPrefChangeContainer();
            }
          });
          RPMSendAsyncMessage("GetChangedCertPrefs");
      }

      break;
    }

    case "sslv3Used":
      learnMore.hidden = false;
      document.body.className = "certerror";
      break;
  }

  if (!KNOWN_ERROR_TITLE_IDS.has(bodyTitleId)) {
    console.error("No strings exist for error:", gErrorCode);
    bodyTitleId = "generic-title";
  }

  // The TRR errors may present options that direct users to settings only available on Firefox Desktop
  if (RPMIsFirefox()) {
    if (isTRROnlyFailure) {
      document.body.className = "certerror"; // Shows warning icon
      pageTitleId = "dns-not-found-trr-only-title2";
      document.l10n.setAttributes(docTitle, pageTitleId);
      bodyTitleId = "dns-not-found-trr-only-title2";
      document.l10n.setAttributes(bodyTitle, bodyTitleId);

      shortDesc.textContent = "";
      let skipReason = RPMGetTRRSkipReason();

      // enable buttons
      let trrExceptionButton = document.getElementById("trrExceptionButton");
      trrExceptionButton.addEventListener("click", () => {
        RPMSendQuery("Browser:AddTRRExcludedDomain", {
          hostname: HOST_NAME,
        }).then(() => {
          retryThis(trrExceptionButton);
        });
      });

      let isTrrServerError = true;
      if (RPMIsSiteSpecificTRRError()) {
        // Only show the exclude button if the failure is specific to this
        // domain. If the TRR server is inaccessible we don't want to allow
        // the user to add an exception just for this domain.
        trrExceptionButton.hidden = false;
        isTrrServerError = false;
      }
      let trrSettingsButton = document.getElementById("trrSettingsButton");
      trrSettingsButton.addEventListener("click", () => {
        RPMSendAsyncMessage("OpenTRRPreferences");
      });
      trrSettingsButton.hidden = false;
      let message = document.getElementById("trrOnlyMessage");
      document.l10n.setAttributes(
        message,
        "neterror-dns-not-found-trr-only-reason2",
        {
          hostname: HOST_NAME,
        }
      );

      let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
      let args = { trrDomain: RPMGetTRRDomain() };
      if (
        skipReason == "TRR_FAILED" ||
        skipReason == "TRR_CHANNEL_DNS_FAIL" ||
        skipReason == "TRR_UNKNOWN_CHANNEL_FAILURE" ||
        skipReason == "TRR_NET_REFUSED" ||
        skipReason == "TRR_NET_INTERRUPT" ||
        skipReason == "TRR_NET_INADEQ_SEQURITY"
      ) {
        descriptionTag = "neterror-dns-not-found-trr-only-could-not-connect";
      } else if (skipReason == "TRR_TIMEOUT") {
        descriptionTag = "neterror-dns-not-found-trr-only-timeout";
      } else if (
        skipReason == "TRR_BROWSER_IS_OFFLINE" ||
        skipReason == "TRR_NO_CONNECTIVITY"
      ) {
        descriptionTag = "neterror-dns-not-found-trr-offline";
      } else if (
        skipReason == "TRR_NO_ANSWERS" ||
        skipReason == "TRR_NXDOMAIN" ||
        skipReason == "TRR_RCODE_FAIL"
      ) {
        descriptionTag = "neterror-dns-not-found-trr-unknown-host2";
      } else if (
        skipReason == "TRR_DECODE_FAILED" ||
        skipReason == "TRR_SERVER_RESPONSE_ERR"
      ) {
        descriptionTag = "neterror-dns-not-found-trr-server-problem";
      } else if (skipReason == "TRR_BAD_URL") {
        descriptionTag = "neterror-dns-not-found-bad-trr-url";
      } else if (skipReason == "TRR_SYSTEM_SLEEP_MODE") {
        descriptionTag = "neterror-dns-not-found-system-sleep";
      }

      let trrMode = RPMGetIntPref("network.trr.mode");
      recordTRREventTelemetry(
        "TRROnlyFailure",
        trrMode,
        args.trrDomain,
        skipReason
      );

      let description = document.getElementById("trrOnlyDescription");
      document.l10n.setAttributes(description, descriptionTag, args);

      const trrLearnMoreContainer = document.getElementById(
        "trrLearnMoreContainer"
      );
      trrLearnMoreContainer.hidden = false;
      let trrOnlyLearnMoreLink = document.getElementById(
        "trrOnlylearnMoreLink"
      );
      if (isTrrServerError) {
        // Go to DoH settings page
        trrOnlyLearnMoreLink.href = "about:preferences#privacy-doh";
        trrOnlyLearnMoreLink.addEventListener("click", event => {
          event.preventDefault();
          RPMSendAsyncMessage("OpenTRRPreferences");
          RPMRecordGleanEvent("securityDohNeterror", "clickSettingsButton", {
            value: "TRROnlyFailure",
            mode: trrMode,
            provider_key: args.trrDomain,
            skip_reason: skipReason,
          });
        });
      } else {
        // This will be replaced at a later point with a link to an offline support page
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1806257
        trrOnlyLearnMoreLink.href =
          RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
          skipReason.toLowerCase().replaceAll("_", "-");
      }

      let div = document.getElementById("trrOnlyContainer");
      div.hidden = false;

      return;
    } else if (isNativeFallbackWarning) {
      showNativeFallbackWarning();
      return;
    }
  }

  document.l10n.setAttributes(docTitle, pageTitleId);
  document.l10n.setAttributes(bodyTitle, bodyTitleId);

  shortDesc.textContent = getDescription();
  setFocus("#netErrorButtonContainer > .try-again");

  if (longDesc) {
    const parts = getNetErrorDescParts();
    setNetErrorMessageFromParts(longDesc, parts);
  }

  setResponseStatus(shortDesc);
  setNetErrorMessageFromCode();
}

function showNativeFallbackWarning() {
  const docTitle = document.querySelector("title");
  const bodyTitle = document.querySelector(".title-text");
  const shortDesc = document.getElementById("errorShortDesc");

  let pageTitleId = "neterror-page-title";
  let bodyTitleId = gErrorCode + "-title";

  document.body.className = "certerror"; // Shows warning icon
  pageTitleId = "dns-not-found-native-fallback-title2";
  document.l10n.setAttributes(docTitle, pageTitleId);

  bodyTitleId = "dns-not-found-native-fallback-title2";
  document.l10n.setAttributes(bodyTitle, bodyTitleId);

  shortDesc.textContent = "";
  let nativeFallbackIgnoreButton = document.getElementById(
    "nativeFallbackIgnoreButton"
  );
  nativeFallbackIgnoreButton.addEventListener("click", () => {
    RPMSetPref("network.trr.display_fallback_warning", false);
    retryThis(nativeFallbackIgnoreButton);
  });

  let continueThisTimeButton = document.getElementById(
    "nativeFallbackContinueThisTimeButton"
  );
  continueThisTimeButton.addEventListener("click", () => {
    RPMSetTRRDisabledLoadFlags();
    document.location.reload();
  });
  continueThisTimeButton.hidden = false;

  nativeFallbackIgnoreButton.hidden = false;
  let message = document.getElementById("nativeFallbackMessage");
  document.l10n.setAttributes(
    message,
    "neterror-dns-not-found-native-fallback-reason2",
    {
      hostname: HOST_NAME,
    }
  );
  let skipReason = RPMGetTRRSkipReason();
  let descriptionTag = "neterror-dns-not-found-trr-unknown-problem";
  let args = { trrDomain: RPMGetTRRDomain() };

  if (skipReason.includes("HEURISTIC_TRIPPED")) {
    descriptionTag = "neterror-dns-not-found-native-fallback-heuristic";
  } else if (skipReason == "TRR_NOT_CONFIRMED") {
    descriptionTag = "neterror-dns-not-found-native-fallback-not-confirmed2";
  }

  let description = document.getElementById("nativeFallbackDescription");
  document.l10n.setAttributes(description, descriptionTag, args);

  let learnMoreContainer = document.getElementById(
    "nativeFallbackLearnMoreContainer"
  );
  learnMoreContainer.hidden = false;

  let learnMoreLink = document.getElementById("nativeFallbackLearnMoreLink");
  learnMoreLink.href =
    RPMGetFormatURLPref("network.trr_ui.skip_reason_learn_more_url") +
    skipReason.toLowerCase().replaceAll("_", "-");

  let div = document.getElementById("nativeFallbackContainer");
  div.hidden = false;

  recordTRREventTelemetry(
    "NativeFallbackWarning",
    RPMGetIntPref("network.trr.mode"),
    args.trrDomain,
    skipReason
  );
}
/**
 * Builds HTML elements from `parts` and appends them to `parentElement`.
 *
 * @param {HTMLElement} parentElement
 * @param {Array<["li" | "p" | "span", string, Record<string, string> | undefined]>} parts
 */
function setNetErrorMessageFromParts(parentElement, parts) {
  let list = null;

  for (let [tag, l10nId, l10nArgs] of parts) {
    const elem = document.createElement(tag);
    elem.dataset.l10nId = l10nId;
    if (l10nArgs) {
      elem.dataset.l10nArgs = JSON.stringify(l10nArgs);
    }

    if (tag === "li") {
      if (!list) {
        list = document.createElement("ul");
        parentElement.appendChild(list);
      }
      list.appendChild(elem);
    } else {
      if (list) {
        list = null;
      }
      parentElement.appendChild(elem);
    }
  }
}

/**
 * Returns an array of tuples determining the parts of an error message:
 * - HTML tag name
 * - l10n id
 * - l10n args (optional)
 *
 * @returns { Array<["li" | "p" | "span", string, Record<string, string> | undefined]> }
 */
function getNetErrorDescParts() {
  switch (gErrorCode) {
    case "connectionFailure":
    case "netInterrupt":
    case "netReset":
    case "netTimeout":
    case "serverError": {
      let errorTags = [
        ["li", "neterror-load-error-try-again"],
        ["li", "neterror-load-error-connection"],
        ["li", "neterror-load-error-firewall"],
      ];
      if (RPMShowOSXLocalNetworkPermissionWarning()) {
        errorTags.push(["li", "neterror-load-osx-permission"]);
      }
      return errorTags;
    }

    case "blockedByPolicy":
    case "deniedPortAccess":
    case "malformedURI":
      return [];

    case "captivePortal":
      return [["p", ""]];
    case "contentEncodingError":
      return [["li", "neterror-content-encoding-error"]];
    case "corruptedContentErrorv2":
      return [
        ["p", "neterror-corrupted-content-intro"],
        ["li", "neterror-corrupted-content-contact-website"],
      ];
    case "dnsNotFound":
      return [
        ["span", "neterror-dns-not-found-hint-header"],
        ["li", "neterror-dns-not-found-hint-try-again"],
        ["li", "neterror-dns-not-found-hint-check-network"],
        ["li", "neterror-dns-not-found-hint-firewall"],
      ];
    case "fileAccessDenied":
      return [["li", "neterror-access-denied"]];
    case "fileNotFound":
      return [
        ["li", "neterror-file-not-found-filename"],
        ["li", "neterror-file-not-found-moved"],
      ];
    case "inadequateSecurityError":
      return [
        ["p", "neterror-inadequate-security-intro", { hostname: HOST_NAME }],
        ["p", "neterror-inadequate-security-code"],
      ];
    case "mitm": {
      const failedCertInfo = document.getFailedCertSecurityInfo();
      const errArgs = {
        hostname: HOST_NAME,
        mitm: getMitmName(failedCertInfo),
      };
      return [["span", "certerror-mitm", errArgs]];
    }
    case "netOffline":
      return [["li", "neterror-net-offline"]];
    case "networkProtocolError":
      return [
        ["p", "neterror-network-protocol-error-intro"],
        ["li", "neterror-network-protocol-error-contact-website"],
      ];
    case "notCached":
      return [
        ["p", "neterror-not-cached-intro"],
        ["li", "neterror-not-cached-sensitive"],
        ["li", "neterror-not-cached-try-again"],
      ];
    case "nssFailure2":
      return [
        ["li", "neterror-nss-failure-not-verified"],
        ["li", "neterror-nss-failure-contact-website"],
      ];
    case "proxyConnectFailure":
      return [
        ["li", "neterror-proxy-connect-failure-settings"],
        ["li", "neterror-proxy-connect-failure-contact-admin"],
      ];
    case "proxyResolveFailure":
      return [
        ["li", "neterror-proxy-resolve-failure-settings"],
        ["li", "neterror-proxy-resolve-failure-connection"],
        ["li", "neterror-proxy-resolve-failure-firewall"],
      ];
    case "redirectLoop":
      return [["li", "neterror-redirect-loop"]];
    case "sslv3Used":
      return [["span", "neterror-sslv3-used"]];
    case "unknownProtocolFound":
      return [["li", "neterror-unknown-protocol"]];
    case "unknownSocketType":
      return [
        ["li", "neterror-unknown-socket-type-psm-installed"],
        ["li", "neterror-unknown-socket-type-server-config"],
      ];
    case "unsafeContentType":
      return [["li", "neterror-unsafe-content-type"]];

    default:
      return [["p", "neterror-generic-error"]];
  }
}

function setNetErrorMessageFromCode() {
  let errorCode;
  try {
    errorCode = document.getNetErrorInfo().errorCodeString;
  } catch (ex) {
    return;
  }

  if (!errorCode) {
    return;
  }

  let errorMessage;
  if (errorCode) {
    const l10nId = errorCode.replace(/_/g, "-").toLowerCase();
    if (KNOWN_ERROR_MESSAGE_IDS.has(l10nId)) {
      const l10n = new Localization([ERROR_MESSAGES_FTL], true);
      errorMessage = l10n.formatValueSync(l10nId);
    }

    const shortDesc2 = document.getElementById("errorShortDesc2");
    document.l10n.setAttributes(shortDesc2, "cert-error-code-prefix", {
      error: errorCode,
    });
  } else {
    console.warn("This error page has no error code in its security info");
  }

  let hostname = HOST_NAME;
  const { port } = document.location;
  if (port && port != 443) {
    hostname += ":" + port;
  }

  const shortDesc = document.getElementById("errorShortDesc");
  document.l10n.setAttributes(shortDesc, "cert-error-ssl-connection-error", {
    errorMessage: errorMessage ?? errorCode ?? "",
    hostname,
  });
}

function setupBlockingReportingUI() {
  let checkbox = document.getElementById("automaticallyReportBlockingInFuture");

  let reportingAutomatic = RPMGetBoolPref(
    "security.xfocsp.errorReporting.automatic"
  );
  checkbox.checked = !!reportingAutomatic;

  checkbox.addEventListener("change", function ({ target: { checked } }) {
    RPMSetPref("security.xfocsp.errorReporting.automatic", checked);

    // If we're enabling reports, send a report for this failure.
    if (checked) {
      reportBlockingError();
    }
  });

  let reportingEnabled = RPMGetBoolPref(
    "security.xfocsp.errorReporting.enabled"
  );

  if (reportingEnabled) {
    // Display blocking error reporting UI for XFO error and CSP error.
    document.getElementById("blockingErrorReporting").hidden = false;

    if (reportingAutomatic) {
      reportBlockingError();
    }
  }
}

function reportBlockingError() {
  // We only report if we are in a frame.
  if (window === window.top) {
    return;
  }

  let err = gErrorCode;
  // Ensure we only deal with XFO and CSP here.
  if (!["xfoBlocked", "cspBlocked"].includes(err)) {
    return;
  }

  let xfo_header = RPMGetHttpResponseHeader("X-Frame-Options");
  let csp_header = RPMGetHttpResponseHeader("Content-Security-Policy");

  // Extract the 'CSP: frame-ancestors' from the CSP header.
  let reg = /(?:^|\s)frame-ancestors\s([^;]*)[$]*/i;
  let match = reg.exec(csp_header);
  csp_header = match ? match[1] : "";

  // If it's the csp error page without the CSP: frame-ancestors, this means
  // this error page is not triggered by CSP: frame-ancestors. So, we bail out
  // early.
  if (err === "cspBlocked" && !csp_header) {
    return;
  }

  let xfoAndCspInfo = {
    error_type: err === "xfoBlocked" ? "xfo" : "csp",
    xfo_header,
    csp_header,
  };

  // Trimming the tail colon symbol.
  let scheme = document.location.protocol.slice(0, -1);

  RPMSendAsyncMessage("ReportBlockingError", {
    scheme,
    host: document.location.host,
    port: parseInt(document.location.port) || -1,
    path: document.location.pathname,
    xfoAndCspInfo,
  });
}

function initPageCaptivePortal() {
  document.body.className = "captiveportal";
  document.getElementById("returnButton").hidden = true;
  const openButton = document.getElementById("openPortalLoginPageButton");
  openButton.hidden = false;
  openButton.addEventListener("click", () => {
    RPMSendAsyncMessage("Browser:OpenCaptivePortalPage");
  });

  setFocus("#openPortalLoginPageButton");
  setupAdvancedButton();
  disallowCertOverridesIfNeeded();

  // When the portal is freed, an event is sent by the parent process
  // that we can pick up and attempt to reload the original page.
  RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => {
    document.location.reload();
  });
}

function initPageCertError() {
  document.body.classList.add("certerror");

  setFocus("#returnButton");
  setupAdvancedButton();
  disallowCertOverridesIfNeeded();

  const hideAddExceptionButton = RPMGetBoolPref(
    "security.certerror.hideAddException",
    false
  );
  if (hideAddExceptionButton) {
    document.getElementById("exceptionDialogButton").hidden = true;
  }

  const els = document.querySelectorAll("[data-telemetry-id]");
  for (let el of els) {
    el.addEventListener("click", recordClickTelemetry);
  }

  const failedCertInfo = document.getFailedCertSecurityInfo();
  void recordSecurityUITelemetry(
    "securityUiCerterror",
    "loadAboutcerterror",
    failedCertInfo
  );

  setCertErrorDetails();
}

function recordClickTelemetry(e) {
  let target = e.originalTarget;
  let telemetryId = target.dataset.telemetryId;
  let failedCertInfo = document.getFailedCertSecurityInfo();
  void recordSecurityUITelemetry(
    "securityUiCerterror",
    "click" +
      telemetryId
        .split("_")
        .map(word => word[0].toUpperCase() + word.slice(1))
        .join(""),
    failedCertInfo
  );
}

function initCertErrorPageActions() {
  document.getElementById("certErrorAndCaptivePortalButtonContainer").hidden =
    false;
  document
    .getElementById("returnButton")
    .addEventListener("click", onReturnButtonClick);
  document
    .getElementById("advancedPanelReturnButton")
    .addEventListener("click", onReturnButtonClick);
  document
    .getElementById("copyToClipboardTop")
    .addEventListener("click", copyPEMToClipboard);
  document
    .getElementById("copyToClipboardBottom")
    .addEventListener("click", copyPEMToClipboard);
  document
    .getElementById("exceptionDialogButton")
    .addEventListener("click", addCertException);
}

function addCertException() {
  const isPermanent =
    !RPMIsWindowPrivate() &&
    RPMGetBoolPref("security.certerrors.permanentOverride");
  document.addCertException(!isPermanent).then(
    () => {
      location.reload();
    },
    () => {}
  );
}

function onReturnButtonClick() {
  RPMSendAsyncMessage("Browser:SSLErrorGoBack");
}

function copyPEMToClipboard() {
  const errorText = document.getElementById("certificateErrorText");
  navigator.clipboard.writeText(errorText.textContent);
}

function setCertErrorDetails() {
  // Check if the connection is being man-in-the-middled. When the parent
  // detects an intercepted connection, the page may be reloaded with a new
  // error code (MOZILLA_PKIX_ERROR_MITM_DETECTED).
  const failedCertInfo = document.getFailedCertSecurityInfo();
  const mitmPrimingEnabled = RPMGetBoolPref(
    "security.certerrors.mitm.priming.enabled"
  );
  if (
    mitmPrimingEnabled &&
    failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
    // Only do this check for top-level failures.
    window.parent == window
  ) {
    RPMSendAsyncMessage("Browser:PrimeMitm");
  }

  document.body.setAttribute("code", failedCertInfo.errorCodeString);

  const learnMore = document.getElementById("learnMoreContainer");
  learnMore.hidden = false;
  const learnMoreLink = document.getElementById("learnMoreLink");
  const baseURL = RPMGetFormatURLPref("app.support.baseURL");
  learnMoreLink.href = baseURL + "connection-not-secure";

  const bodyTitle = document.querySelector(".title-text");
  const shortDesc = document.getElementById("errorShortDesc");
  const shortDesc2 = document.getElementById("errorShortDesc2");

  let whatToDoParts = null;

  switch (failedCertInfo.errorCodeString) {
    case "SSL_ERROR_BAD_CERT_DOMAIN":
      whatToDoParts = [
        ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
      ];
      break;

    case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT": // FIXME - this would have thrown?
      break;

    case "SEC_ERROR_UNKNOWN_ISSUER":
      whatToDoParts = [
        ["p", "certerror-unknown-issuer-what-can-you-do-about-it-website"],
        [
          "p",
          "certerror-unknown-issuer-what-can-you-do-about-it-contact-admin",
        ],
      ];
      break;

    // This error code currently only exists for the Symantec distrust
    // in Firefox 63, so we add copy explaining that to the user.
    // In case of future distrusts of that scale we might need to add
    // additional parameters that allow us to identify the affected party
    // without replicating the complex logic from certverifier code.
    case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED": {
      document.l10n.setAttributes(
        shortDesc2,
        "cert-error-symantec-distrust-description",
        { hostname: HOST_NAME }
      );

      // FIXME - this does nothing
      const adminDesc = document.createElement("p");
      document.l10n.setAttributes(
        adminDesc,
        "cert-error-symantec-distrust-admin"
      );

      learnMoreLink.href = baseURL + "symantec-warning";
      break;
    }

    case "MOZILLA_PKIX_ERROR_MITM_DETECTED": {
      const autoEnabledEnterpriseRoots = RPMGetBoolPref(
        "security.enterprise_roots.auto-enabled",
        false
      );
      if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) {
        RPMSendAsyncMessage("Browser:ResetEnterpriseRootsPref");
      }

      learnMoreLink.href = baseURL + "security-error";

      document.l10n.setAttributes(bodyTitle, "certerror-mitm-title");

      document.l10n.setAttributes(shortDesc, "certerror-mitm", {
        hostname: HOST_NAME,
        mitm: getMitmName(failedCertInfo),
      });

      const id3 = gHasSts
        ? "certerror-mitm-what-can-you-do-about-it-attack-sts"
        : "certerror-mitm-what-can-you-do-about-it-attack";
      whatToDoParts = [
        ["li", "certerror-mitm-what-can-you-do-about-it-antivirus"],
        ["li", "certerror-mitm-what-can-you-do-about-it-corporate"],
        ["li", id3, { mitm: getMitmName(failedCertInfo) }],
      ];
      break;
    }

    case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
      learnMoreLink.href = baseURL + "security-error";
      break;

    // In case the certificate expired we make sure the system clock
    // matches the remote-settings service (blocklist via Kinto) ping time
    // and is not before the build date.
    case "SEC_ERROR_EXPIRED_CERTIFICATE":
    case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
    case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE":
    case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE": {
      learnMoreLink.href = baseURL + "time-errors";

      // We check against the remote-settings server time first if available, because that allows us
      // to give the user an approximation of what the correct time is.
      const difference = RPMGetIntPref(
        "services.settings.clock_skew_seconds",
        0
      );
      const lastFetched =
        RPMGetIntPref("services.settings.last_update_seconds", 0) * 1000;

      // This is set to true later if the user's system clock is at fault for this error.
      let clockSkew = false;

      const now = Date.now();
      const certRange = {
        notBefore: failedCertInfo.certValidityRangeNotBefore,
        notAfter: failedCertInfo.certValidityRangeNotAfter,
      };
      const approximateDate = now - difference * 1000;
      // If the difference is more than a day, we last fetched the date in the last 5 days,
      // and adjusting the date per the interval would make the cert valid, warn the user:
      if (
        Math.abs(difference) > 60 * 60 * 24 &&
        now - lastFetched <= 60 * 60 * 24 * 5 * 1000 &&
        certRange.notBefore < approximateDate &&
        certRange.notAfter > approximateDate
      ) {
        clockSkew = true;
        // If there is no clock skew with Kinto servers, check against the build date.
        // (The Kinto ping could have happened when the time was still right, or not at all)
      } else {
        const appBuildID = RPMGetAppBuildID();
        const year = parseInt(appBuildID.substr(0, 4), 10);
        const month = parseInt(appBuildID.substr(4, 2), 10) - 1;
        const day = parseInt(appBuildID.substr(6, 2), 10);

        const buildDate = new Date(year, month, day);

        // We don't check the notBefore of the cert with the build date,
        // as it is of course almost certain that it is now later than the build date,
        // so we shouldn't exclude the possibility that the cert has become valid
        // since the build date.
        if (buildDate > now && new Date(certRange.notAfter) > buildDate) {
          clockSkew = true;
        }
      }

      if (clockSkew) {
        document.body.classList.add("clockSkewError");
        document.l10n.setAttributes(bodyTitle, "clockSkewError-title");
        document.l10n.setAttributes(shortDesc, "neterror-clock-skew-error", {
          hostname: HOST_NAME,
          now,
        });
        document.getElementById("returnButton").hidden = true;
        document.getElementById("certErrorTryAgainButton").hidden = false;
        document.getElementById("advancedButton").hidden = true;

        document.getElementById("advancedPanelReturnButton").hidden = true;
        document.getElementById("advancedPanelTryAgainButton").hidden = false;
        document.getElementById("exceptionDialogButton").hidden = true;
        break;
      }

      document.l10n.setAttributes(shortDesc, "certerror-expired-cert-intro", {
        hostname: HOST_NAME,
      });

      // The secondary description mentions expired certificates explicitly
      // and should only be shown if the certificate has actually expired
      // instead of being not yet valid.
      if (failedCertInfo.errorCodeString == "SEC_ERROR_EXPIRED_CERTIFICATE") {
        const sd2Id = gHasSts
          ? "certerror-expired-cert-sts-second-para"
          : "certerror-expired-cert-second-para";
        document.l10n.setAttributes(shortDesc2, sd2Id);
        if (
          Math.abs(difference) <= 60 * 60 * 24 &&
          now - lastFetched <= 60 * 60 * 24 * 5 * 1000
        ) {
          whatToDoParts = [
            ["p", "certerror-bad-cert-domain-what-can-you-do-about-it"],
          ];
        }
      }

      whatToDoParts ??= [
        [
          "p",
          "certerror-expired-cert-what-can-you-do-about-it-clock",
          { hostname: HOST_NAME, now },
        ],
        [
          "p",
          "certerror-expired-cert-what-can-you-do-about-it-contact-website",
        ],
      ];
      break;
    }
    case "MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY":
      whatToDoParts = [
        [
          "p",
          "cert-error-trust-certificate-transparency-what-can-you-do-about-it",
        ],
      ];
      break;
  }

  if (whatToDoParts) {
    setNetErrorMessageFromParts(
      document.getElementById("errorWhatToDoText"),
      whatToDoParts
    );
    document.getElementById("errorWhatToDo").hidden = false;
  }
}

// The optional argument is only here for testing purposes.
function setTechnicalDetailsOnCertError(
  failedCertInfo = document.getFailedCertSecurityInfo()
) {
  let technicalInfo = document.getElementById("badCertTechnicalInfo");
  technicalInfo.textContent = "";

  function addLabel(l10nId, args = null, attrs = null) {
    let elem = document.createElement("label");
    technicalInfo.appendChild(elem);

    let newLines = document.createTextNode("\n \n");
    technicalInfo.appendChild(newLines);

    if (attrs) {
      let link = document.createElement("a");
      for (let [attr, value] of Object.entries(attrs)) {
        link.setAttribute(attr, value);
      }
      elem.appendChild(link);
    }

    document.l10n.setAttributes(elem, l10nId, args);
  }

  function addErrorCodeLink() {
    addLabel(
      "cert-error-code-prefix-link",
      { error: failedCertInfo.errorCodeString },
      {
        title: failedCertInfo.errorCodeString,
        id: "errorCode",
        "data-l10n-name": "error-code-link",
        "data-telemetry-id": "error_code_link",
        href: "#certificateErrorDebugInformation",
      }
    );

    // We're attaching the event listener to the parent element and not on
    // the errorCodeLink itself because event listeners cannot be attached
    // to fluent DOM overlays.
    technicalInfo.addEventListener("click", event => {
      if (event.target.id === "errorCode") {
        event.preventDefault();
        toggleCertErrorDebugInfoVisibility();
        recordClickTelemetry(event);
      }
    });
  }

  let hostname = HOST_NAME;
  const { port } = document.location;
  if (port && port != 443) {
    hostname += ":" + port;
  }

  switch (failedCertInfo.overridableErrorCategory) {
    case "trust-error":
      switch (failedCertInfo.errorCodeString) {
        case "MOZILLA_PKIX_ERROR_MITM_DETECTED":
          addLabel("cert-error-mitm-intro");
          addLabel("cert-error-mitm-mozilla");
          addLabel("cert-error-mitm-connection");
          break;
        case "SEC_ERROR_UNKNOWN_ISSUER":
          addLabel("cert-error-trust-unknown-issuer-intro");
          addLabel("cert-error-trust-unknown-issuer", { hostname });
          break;
        case "SEC_ERROR_CA_CERT_INVALID":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-cert-invalid");
          break;
        case "SEC_ERROR_UNTRUSTED_ISSUER":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-untrusted-issuer");
          break;
        case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-signature-algorithm-disabled");
          break;
        case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-expired-issuer");
          break;
        case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-self-signed");
          break;
        case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED":
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-trust-symantec");
          break;
        case "MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY":
          addLabel("cert-error-trust-certificate-transparency", { hostname });
          break;
        default:
          addLabel("cert-error-intro", { hostname });
          addLabel("cert-error-untrusted-default");
      }
      addErrorCodeLink();
      break;

    case "expired-or-not-yet-valid": {
      const notBefore = failedCertInfo.validNotBefore;
      const notAfter = failedCertInfo.validNotAfter;
      if (notBefore && Date.now() < notAfter) {
        addLabel("cert-error-not-yet-valid-now", {
          hostname,
          "not-before-local-time": formatter.format(new Date(notBefore)),
        });
      } else {
        addLabel("cert-error-expired-now", {
          hostname,
          "not-after-local-time": formatter.format(new Date(notAfter)),
        });
      }
      addErrorCodeLink();
      break;
    }

    case "domain-mismatch":
      getSubjectAltNames(failedCertInfo).then(subjectAltNames => {
        if (!subjectAltNames.length) {
          addLabel("cert-error-domain-mismatch", { hostname });
        } else if (subjectAltNames.length > 1) {
          const names = subjectAltNames.join(", ");
          addLabel("cert-error-domain-mismatch-multiple", {
            hostname,
            "subject-alt-names": names,
          });
        } else {
          const altName = subjectAltNames[0];

          // If the alt name is a wildcard domain ("*.example.com")
          // let's use "www" instead.  "*.example.com" isn't going to
          // get anyone anywhere useful. bug 432491
          const okHost = altName.replace(/^\*\./, "www.");

          // Let's check if we want to make this a link.
          const showLink =
            /* case #1:
             * example.com uses an invalid security certificate.
             *
             * The certificate is only valid for www.example.com
             *
             * Make sure to include the "." ahead of thisHost so that a
             * MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
             *
             * We'd normally just use a RegExp here except that we lack a
             * library function to escape them properly (bug 248062), and
             * domain names are famous for having '.' characters in them,
             * which would allow spurious and possibly hostile matches.
             */
            okHost.endsWith("." + HOST_NAME) ||
            /* case #2:
             * browser.garage.maemo.org uses an invalid security certificate.
             *
             * The certificate is only valid for garage.maemo.org
             */
            HOST_NAME.endsWith("." + okHost);

          const l10nArgs = { hostname, "alt-name": altName };
          if (showLink) {
            // Set the link if we want it.
            const proto = document.location.protocol + "//";
            addLabel("cert-error-domain-mismatch-single", l10nArgs, {
              href: proto + okHost,
              "data-l10n-name": "domain-mismatch-link",
              id: "cert_domain_link",
            });

            // If we set a link, meaning there's something helpful for
            // the user here, expand the section by default
            if (getCSSClass() != "expertBadCert") {
              revealAdvancedPanelSlowlyAsync();
            }
          } else {
            addLabel("cert-error-domain-mismatch-single-nolink", l10nArgs);
          }
        }
        addErrorCodeLink();
      });
      break;
  }

  getFailedCertificatesAsPEMString().then(pemString => {
    const errorText = document.getElementById("certificateErrorText");
    errorText.textContent = pemString;
  });
}

/* Only focus if we're the toplevel frame; otherwise we
   don't want to call attention to ourselves!
*/
function setFocus(selector, position = "afterbegin") {
  if (window.top == window) {
    var button = document.querySelector(selector);
    button.parentNode.insertAdjacentElement(position, button);
    // It's possible setFocus was called via the DOMContentLoaded event
    // handler and that the button has no frame. Things without a frame cannot
    // be focused. We use a requestAnimationFrame to queue up the focus to occur
    // once the button has its frame.
    requestAnimationFrame(() => {
      button.focus({ focusVisible: false });
    });
  }
}

function shouldUseFeltPrivacyRefresh() {
  if (!FELT_PRIVACY_REFRESH) {
    return false;
  }

  let failedCertInfo;
  try {
    failedCertInfo = document.getFailedCertSecurityInfo();
  } catch {
    return false;
  }

  return NetErrorCard.ERROR_CODES.has(failedCertInfo.errorCodeString);
}

if (!shouldUseFeltPrivacyRefresh()) {
  for (let button of document.querySelectorAll(".try-again")) {
    button.addEventListener("click", function () {
      retryThis(this);
    });
  }

  initPage();

  // Dispatch this event so tests can detect that we finished loading the error page.
  document.dispatchEvent(
    new CustomEvent("AboutNetErrorLoad", { bubbles: true })
  );
} else {
  customElements.define("net-error-card", NetErrorCard);
  document.body.classList.add("felt-privacy-body");
  document.body.replaceChildren(document.createElement("net-error-card"));
}
