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

ChromeUtils.defineESModuleGetters(this, {
  NativeManifests: "resource://gre/modules/NativeManifests.sys.mjs",
  ctypes: "resource://gre/modules/ctypes.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  this,
  "pkcs11db",
  "@mozilla.org/security/pkcs11moduledb;1",
  "nsIPKCS11ModuleDB"
);

// eslint-disable-next-line mozilla/reject-importGlobalProperties
Cu.importGlobalProperties(["PathUtils"]);

var { DefaultMap } = ExtensionUtils;

const findModuleByPath = function (path) {
  for (let module of pkcs11db.listModules()) {
    if (module && module.libName === path) {
      return module;
    }
  }
  return null;
};

this.pkcs11 = class extends ExtensionAPI {
  getAPI(context) {
    let manifestCache = new DefaultMap(async name => {
      let hostInfo = await NativeManifests.lookupManifest(
        "pkcs11",
        name,
        context
      );
      if (hostInfo) {
        // We don't normalize the absolute path below because
        // `Path.normalize` throws when the target file doesn't
        // exist, and that might be the case on non Windows
        // builds.
        let absolutePath = PathUtils.isAbsolute(hostInfo.manifest.path)
          ? hostInfo.manifest.path
          : PathUtils.joinRelative(
              PathUtils.parent(hostInfo.path),
              hostInfo.manifest.path
            );

        if (AppConstants.platform === "win") {
          // On Windows, `hostInfo.manifest.path` is expected to be a normalized
          // absolute path. On other platforms, this path may be relative but we
          // cannot use `PathUtils.normalize()` on non-absolute paths.
          absolutePath = PathUtils.normalize(absolutePath);
          hostInfo.manifest.path = absolutePath;
        }

        // PathUtils.filename throws if the path is not an absolute path.
        // The result is expected to be the basename of the file (without
        // the dir path and the extension) so it is fine to use an absolute
        // path that may not be normalized (non-Windows platforms).
        let manifestLib = PathUtils.filename(absolutePath);

        if (AppConstants.platform !== "linux") {
          manifestLib = manifestLib.toLowerCase(manifestLib);
        }
        if (
          manifestLib !== ctypes.libraryName("nssckbi") &&
          manifestLib !== ctypes.libraryName("osclientcerts") &&
          manifestLib !== ctypes.libraryName("ipcclientcerts")
        ) {
          return hostInfo.manifest;
        }
      }
      return Promise.reject({ message: `No such PKCS#11 module ${name}` });
    });
    return {
      pkcs11: {
        /**
         * Verify whether a given PKCS#11 module is installed.
         *
         * @param {string} name The name of the module, as specified in
         *                      the manifest file.
         * @returns {Promise} A Promise that resolves to true if the package
         *                    is installed, or false if it is not. May be
         *                    rejected if the module could not be found.
         */
        async isModuleInstalled(name) {
          let manifest = await manifestCache.get(name);
          return findModuleByPath(manifest.path) !== null;
        },
        /**
         * Install a PKCS#11 module
         *
         * @param {string} name The name of the module, as specified in
         *                      the manifest file.
         * @param {integer} [flags = 0] Any flags to be passed on to the
         *                              nsIPKCS11ModuleDB.addModule method
         * @returns {Promise} When the Promise resolves, the module will have
         *                    been installed. When it is rejected, the module
         *                    either is already installed or could not be
         *                    installed for some reason.
         */
        async installModule(name, flags = 0) {
          let manifest = await manifestCache.get(name);
          if (!manifest.description) {
            return Promise.reject({
              message: `The description field in the manifest for PKCS#11 module ${name} must have a value`,
            });
          }
          pkcs11db.addModule(manifest.description, manifest.path, flags, 0);
        },
        /**
         * Uninstall a PKCS#11 module
         *
         * @param {string} name The name of the module, as specified in
         *                      the manifest file.
         * @returns {Promise}. When the Promise resolves, the module will have
         *                     been uninstalled. When it is rejected, the
         *                     module either was not installed or could not be
         *                     uninstalled for some reason.
         */
        async uninstallModule(name) {
          let manifest = await manifestCache.get(name);
          let module = findModuleByPath(manifest.path);
          if (!module) {
            return Promise.reject({
              message: `The PKCS#11 module ${name} is not loaded`,
            });
          }
          pkcs11db.deleteModule(module.name);
        },
        /**
         * Get a list of slots for a given PKCS#11 module, with
         * information on the token (if any) in the slot.
         *
         * The PKCS#11 standard defines slots as an abstract concept
         * that may or may not have at most one token. In practice, when
         * using PKCS#11 for smartcards (the most likely use case of
         * PKCS#11 for Firefox), a slot corresponds to a cardreader, and
         * a token corresponds to a card.
         *
         * @param {string} name The name of the PKCS#11 module, as
         *                 specified in the manifest file.
         * @returns {Promise} A promise that resolves to an array of objects
         *                    with two properties.  The `name` object contains
         *                    the name of the slot; the `token` object is null
         *                    if there is no token in the slot, or is an object
         *                    describing various properties of the token if
         *                    there is.
         */
        async getModuleSlots(name) {
          let manifest = await manifestCache.get(name);
          let module = findModuleByPath(manifest.path);
          if (!module) {
            return Promise.reject({
              message: `The module ${name} is not installed`,
            });
          }
          let rv = [];
          for (let slot of module.listSlots()) {
            let token = slot.getToken();
            let slotobj = {
              name: slot.name,
              token: null,
            };
            if (slot.status != 1 /* SLOT_NOT_PRESENT */) {
              slotobj.token = {
                name: token.tokenName,
                manufacturer: token.tokenManID,
                HWVersion: token.tokenHWVersion,
                FWVersion: token.tokenFWVersion,
                serial: token.tokenSerialNumber,
                isLoggedIn: token.isLoggedIn(),
              };
            }
            rv.push(slotobj);
          }
          return rv;
        },
      },
    };
  }
};
