/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "FIPSUtils",
  "@mozilla.org/security/fipsutils;1",
  Ci.nsIFIPSUtils
);

var secmoddb;
var skip_enable_buttons = false;

/* Do the initial load of all PKCS# modules and list them. */
async function LoadModules() {
  secmoddb = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
    Ci.nsIPKCS11ModuleDB
  );
  await RefreshDeviceList();

  document
    .getElementById("device_tree")
    .addEventListener("select", () => enableButtons());
  document
    .getElementById("devicemanager")
    .addEventListener("command", async event => {
      switch (event.target.id) {
        case "login_button":
          doLogin();
          break;
        case "logout_button":
          doLogout();
          break;
        case "change_pw_button":
          changePassword();
          break;
        case "load_button":
          await doLoad();
          break;
        case "unload_button":
          await doUnload();
          break;
        case "fipsbutton":
          await toggleFIPS();
          break;
        default:
          // Default means that we are not handling a command so we should
          // probably let people know.
          throw new Error("Unhandled command event");
      }
    });

  Services.obs.notifyObservers(window, "device-manager-loaded");
}

async function doPrompt(l10n_id) {
  let [msg] = await document.l10n.formatValues([{ id: l10n_id }]);
  Services.prompt.alert(window, null, msg);
}

async function doConfirm(l10n_id) {
  let [msg] = await document.l10n.formatValues([{ id: l10n_id }]);
  return Services.prompt.confirm(window, null, msg);
}

async function RefreshDeviceList() {
  for (let module of await secmoddb.listModules()) {
    AddModule(module, module.slots);
  }

  // Set the text on the FIPS button.
  SetFIPSButton();
}

function SetFIPSButton() {
  var fipsButton = document.getElementById("fipsbutton");
  if (lazy.FIPSUtils.isFIPSEnabled) {
    document.l10n.setAttributes(fipsButton, "devmgr-button-disable-fips");
  } else {
    document.l10n.setAttributes(fipsButton, "devmgr-button-enable-fips");
  }

  var can_toggle = lazy.FIPSUtils.canToggleFIPS;
  if (can_toggle) {
    fipsButton.removeAttribute("disabled");
  } else {
    fipsButton.setAttribute("disabled", "true");
  }
}

/* Add a module to the tree.  slots is the array of slots in the module,
 * to be represented as children.
 */
function AddModule(module, slots) {
  var tree = document.getElementById("device_list");
  var item = document.createXULElement("treeitem");
  var row = document.createXULElement("treerow");
  var cell = document.createXULElement("treecell");
  cell.setAttribute("label", module.name);
  row.appendChild(cell);
  item.appendChild(row);
  var parent = document.createXULElement("treechildren");
  for (let slot of slots) {
    var child_item = document.createXULElement("treeitem");
    var child_row = document.createXULElement("treerow");
    var child_cell = document.createXULElement("treecell");
    child_cell.setAttribute("label", slot.name);
    child_row.appendChild(child_cell);
    child_item.appendChild(child_row);
    child_item.setAttribute("pk11kind", "slot");
    // 'slot' is an attribute on any HTML element, hence 'slotObject' instead.
    child_item.slotObject = slot;
    parent.appendChild(child_item);
  }
  item.appendChild(parent);
  item.setAttribute("pk11kind", "module");
  item.module = module;
  item.setAttribute("open", "true");
  item.setAttribute("container", "true");
  tree.appendChild(item);
}

var selected_slot;
var selected_module;

/* get the slot selected by the user (can only be one-at-a-time) */
function getSelectedItem() {
  let tree = document.getElementById("device_tree");
  if (tree.currentIndex < 0) {
    return;
  }
  let item = tree.view.getItemAtIndex(tree.currentIndex);
  selected_slot = null;
  selected_module = null;
  if (item) {
    let kind = item.getAttribute("pk11kind");
    if (kind == "slot") {
      selected_slot = item.slotObject;
    } else {
      // (kind == "module")
      selected_module = item.module;
    }
  }
}

function enableButtons() {
  if (skip_enable_buttons) {
    return;
  }

  var login_toggle = true;
  var logout_toggle = true;
  var pw_toggle = true;
  var unload_toggle = true;
  getSelectedItem();
  if (selected_module) {
    unload_toggle = false;
    showModuleInfo();
  } else if (selected_slot) {
    // here's the workaround - login functions are all with token,
    // so grab the token type
    var selected_token = selected_slot.getToken();
    if (selected_token != null) {
      if (selected_token.needsLogin() || !selected_token.needsUserInit) {
        pw_toggle = false;
        if (selected_token.needsLogin()) {
          if (selected_token.isLoggedIn()) {
            logout_toggle = false;
          } else {
            login_toggle = false;
          }
        }
      }

      if (
        !Services.policies.isAllowed("createMasterPassword") &&
        selected_token.isInternalKeyToken &&
        !selected_token.hasPassword
      ) {
        pw_toggle = "true";
      }
    }
    showSlotInfo();
  }
  document
    .getElementById("login_button")
    .toggleAttribute("disabled", login_toggle);
  document
    .getElementById("logout_button")
    .toggleAttribute("disabled", logout_toggle);
  document
    .getElementById("change_pw_button")
    .toggleAttribute("disabled", pw_toggle);
  document
    .getElementById("unload_button")
    .toggleAttribute("disabled", unload_toggle);
}

// clear the display of information for the slot
function ClearInfoList() {
  let infoList = document.getElementById("info_list");
  while (infoList.hasChildNodes()) {
    infoList.firstChild.remove();
  }
}

function ClearDeviceList() {
  ClearInfoList();

  skip_enable_buttons = true;
  var tree = document.getElementById("device_tree");
  tree.view.selection.clearSelection();
  skip_enable_buttons = false;

  // Remove the existing listed modules so that a refresh doesn't display the
  // module that just changed.
  let deviceList = document.getElementById("device_list");
  while (deviceList.hasChildNodes()) {
    deviceList.firstChild.remove();
  }
}

// show a list of info about a slot
function showSlotInfo() {
  var present = true;
  ClearInfoList();
  switch (selected_slot.status) {
    case Ci.nsIPKCS11Slot.SLOT_DISABLED:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-disabled" },
        "tok_status"
      );
      present = false;
      break;
    case Ci.nsIPKCS11Slot.SLOT_NOT_PRESENT:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-not-present" },
        "tok_status"
      );
      present = false;
      break;
    case Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-uninitialized" },
        "tok_status"
      );
      break;
    case Ci.nsIPKCS11Slot.SLOT_NOT_LOGGED_IN:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-not-logged-in" },
        "tok_status"
      );
      break;
    case Ci.nsIPKCS11Slot.SLOT_LOGGED_IN:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-logged-in" },
        "tok_status"
      );
      break;
    case Ci.nsIPKCS11Slot.SLOT_READY:
      AddInfoRow(
        "devinfo-status",
        { l10nID: "devinfo-status-ready" },
        "tok_status"
      );
      break;
    default:
      return;
  }
  AddInfoRow("devinfo-desc", { label: selected_slot.desc }, "slot_desc");
  AddInfoRow("devinfo-man-id", { label: selected_slot.manID }, "slot_manID");
  AddInfoRow(
    "devinfo-hwversion",
    { label: selected_slot.HWVersion },
    "slot_hwv"
  );
  AddInfoRow(
    "devinfo-fwversion",
    { label: selected_slot.FWVersion },
    "slot_fwv"
  );
  if (present) {
    showTokenInfo();
  }
}

function showModuleInfo() {
  ClearInfoList();
  AddInfoRow("devinfo-modname", { label: selected_module.name }, "module_name");
  AddInfoRow(
    "devinfo-modpath",
    { label: selected_module.libName },
    "module_path"
  );
}

// add a row to the info list, as [col1 col2] (ex.: ["status" "logged in"])
function AddInfoRow(l10nID, col2, cell_id) {
  var tree = document.getElementById("info_list");
  var item = document.createXULElement("treeitem");
  var row = document.createXULElement("treerow");
  var cell1 = document.createXULElement("treecell");
  document.l10n.setAttributes(cell1, l10nID);
  cell1.setAttribute("crop", "never");
  row.appendChild(cell1);
  var cell2 = document.createXULElement("treecell");
  if (col2.l10nID) {
    document.l10n.setAttributes(cell2, col2.l10nID);
  } else {
    cell2.setAttribute("label", col2.label);
  }
  cell2.setAttribute("crop", "never");
  cell2.setAttribute("id", cell_id);
  row.appendChild(cell2);
  item.appendChild(row);
  tree.appendChild(item);
}

// log in to a slot
function doLogin() {
  getSelectedItem();
  // here's the workaround - login functions are with token
  var selected_token = selected_slot.getToken();
  try {
    selected_token.login(false);
    var tok_status = document.getElementById("tok_status");
    if (selected_token.isLoggedIn()) {
      document.l10n.setAttributes(tok_status, "devinfo-status-logged-in");
    } else {
      document.l10n.setAttributes(tok_status, "devinfo-status-not-logged-in");
    }
  } catch (e) {
    doPrompt("login-failed");
  }
  enableButtons();
}

// log out of a slot
function doLogout() {
  getSelectedItem();
  // here's the workaround - login functions are with token
  var selected_token = selected_slot.getToken();
  try {
    selected_token.logoutAndDropAuthenticatedResources();
    var tok_status = document.getElementById("tok_status");
    if (selected_token.isLoggedIn()) {
      document.l10n.setAttributes(tok_status, "devinfo-status-logged-in");
    } else {
      document.l10n.setAttributes(tok_status, "devinfo-status-not-logged-in");
    }
  } catch (e) {}
  enableButtons();
}

// load a new device
async function doLoad() {
  window.browsingContext.topChromeWindow.open(
    "load_device.xhtml",
    "loaddevice",
    "chrome,centerscreen,modal"
  );
  ClearDeviceList();
  await RefreshDeviceList();
}

async function deleteSelected() {
  getSelectedItem();
  if (selected_module && (await doConfirm("del-module-warning"))) {
    try {
      await secmoddb.deleteModule(selected_module.name);
    } catch (e) {
      doPrompt("del-module-error");
      return false;
    }
    selected_module = null;
    return true;
  }
  return false;
}

async function doUnload() {
  if (await deleteSelected()) {
    ClearDeviceList();
    await RefreshDeviceList();
  }
}

function changePassword() {
  getSelectedItem();
  let params = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance(
    Ci.nsIDialogParamBlock
  );
  let objects = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
  objects.appendElement(selected_slot.getToken());
  params.objects = objects;
  window.browsingContext.topChromeWindow.openDialog(
    "changepassword.xhtml",
    "",
    "chrome,centerscreen,modal",
    params
  );
  showSlotInfo();
  enableButtons();
}

// -------------------------------------   Old code

function showTokenInfo() {
  var selected_token = selected_slot.getToken();
  AddInfoRow("devinfo-label", { label: selected_token.tokenName }, "tok_label");
  AddInfoRow(
    "devinfo-man-id",
    { label: selected_token.tokenManID },
    "tok_manID"
  );
  AddInfoRow(
    "devinfo-serialnum",
    { label: selected_token.tokenSerialNumber },
    "tok_sNum"
  );
  AddInfoRow(
    "devinfo-hwversion",
    { label: selected_token.tokenHWVersion },
    "tok_hwv"
  );
  AddInfoRow(
    "devinfo-fwversion",
    { label: selected_token.tokenFWVersion },
    "tok_fwv"
  );
}

async function toggleFIPS() {
  if (!lazy.FIPSUtils.isFIPSEnabled) {
    // A restriction of FIPS mode is, the password must be set
    // In FIPS mode the password must be non-empty.
    // This is different from what we allow in NON-Fips mode.

    var tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
      Ci.nsIPK11TokenDB
    );
    var internal_token = tokendb.getInternalKeyToken(); // nsIPK11Token
    if (!internal_token.hasPassword) {
      // Token has either no or an empty password.
      doPrompt("fips-nonempty-primary-password-required");
      return;
    }
  }

  try {
    lazy.FIPSUtils.toggleFIPSMode();
  } catch (e) {
    doPrompt("unable-to-toggle-fips");
    return;
  }

  // Remove the existing listed modules so that a refresh doesn't display the
  // module that just changed.
  ClearDeviceList();

  await RefreshDeviceList();
}

window.addEventListener("load", async () => LoadModules());
