/* 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/. */

"use strict";

const {
  Component,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const {
  L10N,
} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
const {
  fetchNetworkUpdatePacket,
  getUrlHost,
} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");

// Components
const TreeViewClass = ChromeUtils.importESModule(
  "resource://devtools/client/shared/components/tree/TreeView.mjs"
).default;
const PropertiesView = createFactory(
  require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js")
);

loader.lazyGetter(this, "Rep", function () {
  return ChromeUtils.importESModule(
    "resource://devtools/client/shared/components/reps/index.mjs"
  ).REPS.Rep;
});
loader.lazyGetter(this, "MODE", function () {
  return ChromeUtils.importESModule(
    "resource://devtools/client/shared/components/reps/index.mjs"
  ).MODE;
});

const { div, span } = dom;
const NOT_AVAILABLE = L10N.getStr("netmonitor.security.notAvailable");
const ERROR_LABEL = L10N.getStr("netmonitor.security.error");
const CIPHER_SUITE_LABEL = L10N.getStr("netmonitor.security.cipherSuite");
const WARNING_CIPHER_LABEL = L10N.getStr("netmonitor.security.warning.cipher");
const ENABLED_LABEL = L10N.getStr("netmonitor.security.enabled");
const DISABLED_LABEL = L10N.getStr("netmonitor.security.disabled");
const CONNECTION_LABEL = L10N.getStr("netmonitor.security.connection");
const PROTOCOL_VERSION_LABEL = L10N.getStr(
  "netmonitor.security.protocolVersion"
);
const KEA_GROUP_LABEL = L10N.getStr("netmonitor.security.keaGroup");
const KEA_GROUP_NONE = L10N.getStr("netmonitor.security.keaGroup.none");
const KEA_GROUP_CUSTOM = L10N.getStr("netmonitor.security.keaGroup.custom");
const KEA_GROUP_UNKNOWN = L10N.getStr("netmonitor.security.keaGroup.unknown");
const SIGNATURE_SCHEME_LABEL = L10N.getStr(
  "netmonitor.security.signatureScheme"
);
const SIGNATURE_SCHEME_NONE = L10N.getStr(
  "netmonitor.security.signatureScheme.none"
);
const SIGNATURE_SCHEME_UNKNOWN = L10N.getStr(
  "netmonitor.security.signatureScheme.unknown"
);
const HSTS_LABEL = L10N.getStr("netmonitor.security.hsts");
const HPKP_LABEL = L10N.getStr("netmonitor.security.hpkp");
const CERTIFICATE_LABEL = L10N.getStr("netmonitor.security.certificate");
const CERTIFICATE_TRANSPARENCY_LABEL = L10N.getStr(
  "certmgr.certificateTransparency.label"
);
const CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT = L10N.getStr(
  "certmgr.certificateTransparency.status.ok"
);
const CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS = L10N.getStr(
  "certmgr.certificateTransparency.status.notEnoughSCTS"
);
const CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS = L10N.getStr(
  "certmgr.certificateTransparency.status.notDiverseSCTS"
);
const SUBJECT_INFO_LABEL = L10N.getStr("certmgr.subjectinfo.label");
const CERT_DETAIL_COMMON_NAME_LABEL = L10N.getStr("certmgr.certdetail.cn");
const CERT_DETAIL_ORG_LABEL = L10N.getStr("certmgr.certdetail.o");
const CERT_DETAIL_ORG_UNIT_LABEL = L10N.getStr("certmgr.certdetail.ou");
const ISSUER_INFO_LABEL = L10N.getStr("certmgr.issuerinfo.label");
const PERIOD_OF_VALIDITY_LABEL = L10N.getStr("certmgr.periodofvalidity.label");
const BEGINS_LABEL = L10N.getStr("certmgr.begins");
const EXPIRES_LABEL = L10N.getStr("certmgr.expires");
const FINGERPRINTS_LABEL = L10N.getStr("certmgr.fingerprints.label");
const SHA256_FINGERPRINT_LABEL = L10N.getStr(
  "certmgr.certdetail.sha256fingerprint"
);
const SHA1_FINGERPRINT_LABEL = L10N.getStr(
  "certmgr.certdetail.sha1fingerprint"
);

/*
 * Localize special values for key exchange group name,
 * certificate signature scheme, and certificate
 * transparency status.
 */
const formatSecurityInfo = securityInfo => {
  const formattedSecurityInfo = { ...securityInfo };

  const formatters = {
    keaGroupName: value => {
      if (value === "none") {
        return KEA_GROUP_NONE;
      }
      if (value === "custom") {
        return KEA_GROUP_CUSTOM;
      }
      if (value === "unknown group") {
        return KEA_GROUP_UNKNOWN;
      }
      return value;
    },
    signatureSchemeName: value => {
      if (value === "none") {
        return SIGNATURE_SCHEME_NONE;
      }
      if (value === "unknown signature") {
        return SIGNATURE_SCHEME_UNKNOWN;
      }
      return value;
    },
    certificateTransparency: value => {
      if (value === 5) {
        return CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT;
      }
      if (value === 6) {
        return CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS;
      }
      if (value === 7) {
        return CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS;
      }
      return value;
    },
  };

  return Object.keys(formatters).reduce((acc, key) => {
    const formatter = formatters[key];
    acc[key] = formatter(acc[key]);
    return acc;
  }, formattedSecurityInfo);
};

const getConnectionLabel = securityInfo => ({
  [PROTOCOL_VERSION_LABEL]: securityInfo.protocolVersion || NOT_AVAILABLE,
  [CIPHER_SUITE_LABEL]: securityInfo.cipherSuite || NOT_AVAILABLE,
  [KEA_GROUP_LABEL]: securityInfo.keaGroupName || NOT_AVAILABLE,
  [SIGNATURE_SCHEME_LABEL]: securityInfo.signatureSchemeName || NOT_AVAILABLE,
});

const getHostHeaderLabel = securityInfo => ({
  [HSTS_LABEL]: securityInfo.hsts ? ENABLED_LABEL : DISABLED_LABEL,
  [HPKP_LABEL]: securityInfo.hpkp ? ENABLED_LABEL : DISABLED_LABEL,
});

const getCertificateLabel = securityInfo => {
  const { fingerprint, issuer, subject, validity } = securityInfo.cert;

  return {
    [SUBJECT_INFO_LABEL]: {
      [CERT_DETAIL_COMMON_NAME_LABEL]: subject?.commonName || NOT_AVAILABLE,
      [CERT_DETAIL_ORG_LABEL]: subject?.organization || NOT_AVAILABLE,
      [CERT_DETAIL_ORG_UNIT_LABEL]: subject?.organizationUnit || NOT_AVAILABLE,
    },
    [ISSUER_INFO_LABEL]: {
      [CERT_DETAIL_COMMON_NAME_LABEL]: issuer?.commonName || NOT_AVAILABLE,
      [CERT_DETAIL_ORG_LABEL]: issuer?.organization || NOT_AVAILABLE,
      [CERT_DETAIL_ORG_UNIT_LABEL]: issuer?.organizationUnit || NOT_AVAILABLE,
    },
    [PERIOD_OF_VALIDITY_LABEL]: {
      [BEGINS_LABEL]: validity?.start || NOT_AVAILABLE,
      [EXPIRES_LABEL]: validity?.end || NOT_AVAILABLE,
    },
    [FINGERPRINTS_LABEL]: {
      [SHA256_FINGERPRINT_LABEL]: fingerprint?.sha256 || NOT_AVAILABLE,
      [SHA1_FINGERPRINT_LABEL]: fingerprint?.sha1 || NOT_AVAILABLE,
    },
    [CERTIFICATE_TRANSPARENCY_LABEL]:
      securityInfo.certificateTransparency || NOT_AVAILABLE,
  };
};

const getObject = ({ securityInfo, url }) => {
  if (securityInfo.state !== "secure" && securityInfo.state !== "weak") {
    return {
      [ERROR_LABEL]: securityInfo.errorMessage || NOT_AVAILABLE,
    };
  }

  const HOST_HEADER_LABEL = L10N.getFormatStr(
    "netmonitor.security.hostHeader",
    getUrlHost(url)
  );
  const formattedSecurityInfo = formatSecurityInfo(securityInfo);

  return {
    [CONNECTION_LABEL]: getConnectionLabel(formattedSecurityInfo),
    [HOST_HEADER_LABEL]: getHostHeaderLabel(formattedSecurityInfo),
    [CERTIFICATE_LABEL]: getCertificateLabel(formattedSecurityInfo),
  };
};

/*
 * Security panel component
 * If the site is being served over HTTPS, you get an extra tab labeled "Security".
 * This contains details about the secure connection used including the protocol,
 * the cipher suite, and certificate details
 */
class SecurityPanel extends Component {
  static get propTypes() {
    return {
      connector: PropTypes.object.isRequired,
      openLink: PropTypes.func,
      request: PropTypes.object.isRequired,
    };
  }

  componentDidMount() {
    const { request, connector } = this.props;
    fetchNetworkUpdatePacket(connector.requestData, request, ["securityInfo"]);
  }

  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
  UNSAFE_componentWillReceiveProps(nextProps) {
    const { request, connector } = nextProps;
    fetchNetworkUpdatePacket(connector.requestData, request, ["securityInfo"]);
  }

  renderValue(props, weaknessReasons = []) {
    const { member, value } = props;

    // Hide object summary
    if (typeof member.value === "object") {
      return null;
    }

    return span(
      { className: "security-info-value" },
      member.name === ERROR_LABEL
        ? // Display multiline text for security error for a label using a rep.
          value
        : Rep(
            Object.assign(props, {
              // FIXME: A workaround for the issue in StringRep
              // Force StringRep to crop the text everytime
              member: Object.assign({}, member, { open: false }),
              mode: MODE.TINY,
              cropLimit: 60,
              noGrip: true,
            })
          ),
      weaknessReasons.includes("cipher") && member.name === CIPHER_SUITE_LABEL
        ? // Display an extra warning icon after the cipher suite
          div({
            id: "security-warning-cipher",
            className: "security-warning-icon",
            title: WARNING_CIPHER_LABEL,
          })
        : null
    );
  }

  render() {
    const { request } = this.props;
    const { securityInfo, url } = request;

    if (!securityInfo || !url) {
      return null;
    }

    const object = getObject({ securityInfo, url });
    return div(
      { className: "panel-container security-panel" },
      PropertiesView({
        object,
        renderValue: props =>
          this.renderValue(props, securityInfo.weaknessReasons),
        enableFilter: false,
        expandedNodes: TreeViewClass.getExpandedNodes(object),
      })
    );
  }
}

module.exports = SecurityPanel;
