/* 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";

XPCOMUtils.defineLazyServiceGetter(
  this,
  "idn",
  "@mozilla.org/network/idn-service;1",
  Ci.nsIIDNService
);

var { ExtensionError } = ExtensionUtils;

const invalidHostnameError = hostname =>
  new ExtensionError(`Invalid hostname: ${hostname}`);

const removeIPv6Brackets = hostname =>
  hostname.startsWith("[") && hostname.endsWith("]") && hostname.includes(":")
    ? hostname.slice(1, -1)
    : hostname;

// Note: hostname should not be an IP address.
function ensureValidHostname(hostname) {
  // "*" is accepted by domainToASCII but should be invalid.
  if (hostname && !hostname.includes("*")) {
    try {
      return idn.domainToASCII(hostname);
    } catch {}
  }
  throw invalidHostnameError(hostname);
}

this.publicSuffix = class extends ExtensionAPI {
  getAPI() {
    return {
      publicSuffix: {
        isKnownSuffix: function (hostname) {
          try {
            const suffix = Services.eTLD.getKnownPublicSuffixFromHost(hostname);
            if (suffix) {
              return suffix === idn.convertUTF8toACE(hostname);
            }
          } catch {}
          return false;
        },

        getKnownSuffix: function (hostname) {
          let suffix;
          try {
            suffix =
              Services.eTLD.getKnownPublicSuffixFromHost(
                removeIPv6Brackets(hostname)
              ) || null;
          } catch (e) {
            if (e.result === Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
              if (hostname.includes(":") && !hostname.startsWith("[")) {
                // IPv6 address without brackets.
                throw invalidHostnameError(hostname);
              }
              return null;
            }
            if (e.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
              throw invalidHostnameError(hostname);
            }
          }

          // Now that we know the hostname is not an IP address,
          // check for invalid characters
          ensureValidHostname(hostname);

          return suffix || null;
        },

        getDomain: function (hostname, options) {
          let domain;
          try {
            // Note: returned domain may have an unknown suffix
            domain =
              Services.eTLD.getBaseDomainFromHost(
                removeIPv6Brackets(hostname)
              ) || null;
          } catch (e) {
            if (e.result === Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
              if (hostname.includes(":") && !hostname.startsWith("[")) {
                // IPv6 address without brackets.
                throw invalidHostnameError(hostname);
              }
              return options.allowIPAddress ? hostname : null;
            }
            if (e.result !== Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
              throw invalidHostnameError(hostname);
            }
          }

          // Now that we know the hostname is not an IP address,
          // check for invalid characters
          const ascii = ensureValidHostname(hostname);
          if (!domain) {
            if (!options.allowPlainSuffix) {
              return null;
            }
            domain = ascii;
          } else if (
            !options.allowUnknownSuffix &&
            !Services.eTLD.hasKnownPublicSuffixFromHost(domain)
          ) {
            return null;
          }
          if (options.encoding === "display") {
            domain = idn.domainToDisplay(domain);
          }
          return domain;
        },
      },
    };
  }
};
