"use strict";

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);

ChromeUtils.defineModuleGetter(
  this,
  "OSCrypto",
  "resource://gre/modules/OSCrypto.jsm"
);

const IE7_FORM_PASSWORDS_MIGRATOR_NAME = "IE7FormPasswords";
const LOGINS_KEY =
  "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
const EXTENSION = "-backup";
const TESTED_WEBSITES = {
  twitter: {
    uri: makeURI("https://twitter.com"),
    hash: "A89D42BC6406E27265B1AD0782B6F376375764A301",
    data: [
      12,
      0,
      0,
      0,
      56,
      0,
      0,
      0,
      38,
      0,
      0,
      0,
      87,
      73,
      67,
      75,
      24,
      0,
      0,
      0,
      2,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      68,
      36,
      67,
      124,
      118,
      212,
      208,
      1,
      8,
      0,
      0,
      0,
      18,
      0,
      0,
      0,
      68,
      36,
      67,
      124,
      118,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      97,
      0,
      98,
      0,
      99,
      0,
      100,
      0,
      101,
      0,
      102,
      0,
      103,
      0,
      104,
      0,
      0,
      0,
      49,
      0,
      50,
      0,
      51,
      0,
      52,
      0,
      53,
      0,
      54,
      0,
      55,
      0,
      56,
      0,
      57,
      0,
      0,
      0,
    ],
    logins: [
      {
        username: "abcdefgh",
        password: "123456789",
        origin: "https://twitter.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439325854000,
        timeLastUsed: 1439325854000,
        timePasswordChanged: 1439325854000,
        timesUsed: 1,
      },
    ],
  },
  facebook: {
    uri: makeURI("https://www.facebook.com/"),
    hash: "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796",
    data: [
      12,
      0,
      0,
      0,
      152,
      0,
      0,
      0,
      160,
      0,
      0,
      0,
      87,
      73,
      67,
      75,
      24,
      0,
      0,
      0,
      8,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      88,
      182,
      125,
      18,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      20,
      0,
      0,
      0,
      88,
      182,
      125,
      18,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      40,
      0,
      0,
      0,
      134,
      65,
      33,
      37,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      60,
      0,
      0,
      0,
      134,
      65,
      33,
      37,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      80,
      0,
      0,
      0,
      45,
      242,
      246,
      62,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      100,
      0,
      0,
      0,
      45,
      242,
      246,
      62,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      120,
      0,
      0,
      0,
      28,
      10,
      193,
      80,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      140,
      0,
      0,
      0,
      28,
      10,
      193,
      80,
      121,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      117,
      0,
      115,
      0,
      101,
      0,
      114,
      0,
      110,
      0,
      97,
      0,
      109,
      0,
      101,
      0,
      48,
      0,
      0,
      0,
      112,
      0,
      97,
      0,
      115,
      0,
      115,
      0,
      119,
      0,
      111,
      0,
      114,
      0,
      100,
      0,
      48,
      0,
      0,
      0,
      117,
      0,
      115,
      0,
      101,
      0,
      114,
      0,
      110,
      0,
      97,
      0,
      109,
      0,
      101,
      0,
      49,
      0,
      0,
      0,
      112,
      0,
      97,
      0,
      115,
      0,
      115,
      0,
      119,
      0,
      111,
      0,
      114,
      0,
      100,
      0,
      49,
      0,
      0,
      0,
      117,
      0,
      115,
      0,
      101,
      0,
      114,
      0,
      110,
      0,
      97,
      0,
      109,
      0,
      101,
      0,
      50,
      0,
      0,
      0,
      112,
      0,
      97,
      0,
      115,
      0,
      115,
      0,
      119,
      0,
      111,
      0,
      114,
      0,
      100,
      0,
      50,
      0,
      0,
      0,
      117,
      0,
      115,
      0,
      101,
      0,
      114,
      0,
      110,
      0,
      97,
      0,
      109,
      0,
      101,
      0,
      51,
      0,
      0,
      0,
      112,
      0,
      97,
      0,
      115,
      0,
      115,
      0,
      119,
      0,
      111,
      0,
      114,
      0,
      100,
      0,
      51,
      0,
      0,
      0,
    ],
    logins: [
      {
        username: "username0",
        password: "password0",
        origin: "https://www.facebook.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439326966000,
        timeLastUsed: 1439326966000,
        timePasswordChanged: 1439326966000,
        timesUsed: 1,
      },
      {
        username: "username1",
        password: "password1",
        origin: "https://www.facebook.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439326997000,
        timeLastUsed: 1439326997000,
        timePasswordChanged: 1439326997000,
        timesUsed: 1,
      },
      {
        username: "username2",
        password: "password2",
        origin: "https://www.facebook.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439327040000,
        timeLastUsed: 1439327040000,
        timePasswordChanged: 1439327040000,
        timesUsed: 1,
      },
      {
        username: "username3",
        password: "password3",
        origin: "https://www.facebook.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439327070000,
        timeLastUsed: 1439327070000,
        timePasswordChanged: 1439327070000,
        timesUsed: 1,
      },
    ],
  },
  live: {
    uri: makeURI("https://login.live.com/"),
    hash: "7B506F2D6B81D939A8E0456F036EE8970856FF705E",
    data: [
      12,
      0,
      0,
      0,
      56,
      0,
      0,
      0,
      44,
      0,
      0,
      0,
      87,
      73,
      67,
      75,
      24,
      0,
      0,
      0,
      2,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      212,
      17,
      219,
      140,
      148,
      212,
      208,
      1,
      9,
      0,
      0,
      0,
      20,
      0,
      0,
      0,
      212,
      17,
      219,
      140,
      148,
      212,
      208,
      1,
      11,
      0,
      0,
      0,
      114,
      0,
      105,
      0,
      97,
      0,
      100,
      0,
      104,
      0,
      49,
      6,
      74,
      6,
      39,
      6,
      54,
      6,
      0,
      0,
      39,
      6,
      66,
      6,
      49,
      6,
      35,
      6,
      80,
      0,
      192,
      0,
      223,
      0,
      119,
      0,
      246,
      0,
      114,
      0,
      100,
      0,
      0,
      0,
    ],
    logins: [
      {
        username: "riadhرياض",
        password: "اقرأPÀßwörd",
        origin: "https://login.live.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439338767000,
        timeLastUsed: 1439338767000,
        timePasswordChanged: 1439338767000,
        timesUsed: 1,
      },
    ],
  },
  reddit: {
    uri: makeURI("http://www.reddit.com/"),
    hash: "B644028D1C109A91EC2C4B9D1F145E55A1FAE42065",
    data: [
      12,
      0,
      0,
      0,
      152,
      0,
      0,
      0,
      212,
      0,
      0,
      0,
      87,
      73,
      67,
      75,
      24,
      0,
      0,
      0,
      8,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      32,
      8,
      234,
      114,
      153,
      212,
      208,
      1,
      1,
      0,
      0,
      0,
      4,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      6,
      0,
      0,
      0,
      97,
      93,
      131,
      116,
      153,
      212,
      208,
      1,
      3,
      0,
      0,
      0,
      14,
      0,
      0,
      0,
      97,
      93,
      131,
      116,
      153,
      212,
      208,
      1,
      16,
      0,
      0,
      0,
      48,
      0,
      0,
      0,
      88,
      150,
      78,
      174,
      153,
      212,
      208,
      1,
      4,
      0,
      0,
      0,
      58,
      0,
      0,
      0,
      88,
      150,
      78,
      174,
      153,
      212,
      208,
      1,
      29,
      0,
      0,
      0,
      118,
      0,
      0,
      0,
      79,
      102,
      137,
      34,
      154,
      212,
      208,
      1,
      15,
      0,
      0,
      0,
      150,
      0,
      0,
      0,
      79,
      102,
      137,
      34,
      154,
      212,
      208,
      1,
      30,
      0,
      0,
      0,
      97,
      0,
      0,
      0,
      0,
      0,
      252,
      140,
      173,
      138,
      146,
      48,
      0,
      0,
      66,
      0,
      105,
      0,
      116,
      0,
      116,
      0,
      101,
      0,
      32,
      0,
      98,
      0,
      101,
      0,
      115,
      0,
      116,
      0,
      228,
      0,
      116,
      0,
      105,
      0,
      103,
      0,
      101,
      0,
      110,
      0,
      0,
      0,
      205,
      145,
      110,
      127,
      198,
      91,
      1,
      120,
      0,
      0,
      31,
      4,
      48,
      4,
      64,
      4,
      62,
      4,
      59,
      4,
      76,
      4,
      32,
      0,
      67,
      4,
      65,
      4,
      63,
      4,
      53,
      4,
      72,
      4,
      61,
      4,
      62,
      4,
      32,
      0,
      65,
      4,
      49,
      4,
      64,
      4,
      62,
      4,
      72,
      4,
      53,
      4,
      61,
      4,
      46,
      0,
      32,
      0,
      18,
      4,
      62,
      4,
      57,
      4,
      66,
      4,
      56,
      4,
      0,
      0,
      40,
      6,
      51,
      6,
      69,
      6,
      32,
      0,
      39,
      6,
      68,
      6,
      68,
      6,
      71,
      6,
      32,
      0,
      39,
      6,
      68,
      6,
      49,
      6,
      45,
      6,
      69,
      6,
      70,
      6,
      0,
      0,
      118,
      0,
      101,
      0,
      117,
      0,
      105,
      0,
      108,
      0,
      108,
      0,
      101,
      0,
      122,
      0,
      32,
      0,
      108,
      0,
      101,
      0,
      32,
      0,
      118,
      0,
      233,
      0,
      114,
      0,
      105,
      0,
      102,
      0,
      105,
      0,
      101,
      0,
      114,
      0,
      32,
      0,
      224,
      0,
      32,
      0,
      110,
      0,
      111,
      0,
      117,
      0,
      118,
      0,
      101,
      0,
      97,
      0,
      117,
      0,
      0,
      0,
    ],
    logins: [
      // This login is present in the data, but should be stripped out
      // by the validation rules of the importer:
      // {
      //   "username": "a",
      //   "password": "",
      //   "origin": "http://www.reddit.com",
      //   "formActionOrigin": "",
      //   "httpRealm": null,
      //   "usernameField": "",
      //   "passwordField": ""
      // },
      {
        username: "購読を",
        password: "Bitte bestätigen",
        origin: "http://www.reddit.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439340874000,
        timeLastUsed: 1439340874000,
        timePasswordChanged: 1439340874000,
        timesUsed: 1,
      },
      {
        username: "重置密码",
        password: "Пароль успешно сброшен. Войти",
        origin: "http://www.reddit.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439340971000,
        timeLastUsed: 1439340971000,
        timePasswordChanged: 1439340971000,
        timesUsed: 1,
      },
      {
        username: "بسم الله الرحمن",
        password: "veuillez le vérifier à nouveau",
        origin: "http://www.reddit.com",
        formActionOrigin: "",
        httpRealm: null,
        usernameField: "",
        passwordField: "",
        timeCreated: 1439341166000,
        timeLastUsed: 1439341166000,
        timePasswordChanged: 1439341166000,
        timesUsed: 1,
      },
    ],
  },
};

const TESTED_URLS = [
  "http://a.foo.com",
  "http://b.foo.com",
  "http://c.foo.com",
  "http://www.test.net",
  "http://www.test.net/home",
  "http://www.test.net/index",
  "https://a.bar.com",
  "https://b.bar.com",
  "https://c.bar.com",
];

var nsIWindowsRegKey = Ci.nsIWindowsRegKey;
var Storage2Key;

/*
 * If the key value exists, it's going to be backed up and replaced, so the value could be restored.
 * Otherwise a new value is going to be created.
 */
function backupAndStore(key, name, value) {
  if (key.hasValue(name)) {
    // backup the the current value
    let type = key.getValueType(name);
    // create a new value using use the current value name followed by EXTENSION as its new name
    switch (type) {
      case nsIWindowsRegKey.TYPE_STRING:
        key.writeStringValue(name + EXTENSION, key.readStringValue(name));
        break;
      case nsIWindowsRegKey.TYPE_BINARY:
        key.writeBinaryValue(name + EXTENSION, key.readBinaryValue(name));
        break;
      case nsIWindowsRegKey.TYPE_INT:
        key.writeIntValue(name + EXTENSION, key.readIntValue(name));
        break;
      case nsIWindowsRegKey.TYPE_INT64:
        key.writeInt64Value(name + EXTENSION, key.readInt64Value(name));
        break;
    }
  }
  key.writeBinaryValue(name, value);
}

// Remove all values where their names are members of the names array from the key of registry
function removeAllValues(key, names) {
  for (let name of names) {
    key.removeValue(name);
  }
}

// Restore all the backed up values
function restore(key) {
  let count = key.valueCount;
  let names = []; // the names of the key values
  for (let i = 0; i < count; ++i) {
    names.push(key.getValueName(i));
  }

  for (let name of names) {
    // backed up values have EXTENSION at the end of their names
    if (name.lastIndexOf(EXTENSION) == name.length - EXTENSION.length) {
      let valueName = name.substr(0, name.length - EXTENSION.length);
      let type = key.getValueType(name);
      // create a new value using the name before the backup and removed the backed up one
      switch (type) {
        case nsIWindowsRegKey.TYPE_STRING:
          key.writeStringValue(valueName, key.readStringValue(name));
          key.removeValue(name);
          break;
        case nsIWindowsRegKey.TYPE_BINARY:
          key.writeBinaryValue(valueName, key.readBinaryValue(name));
          key.removeValue(name);
          break;
        case nsIWindowsRegKey.TYPE_INT:
          key.writeIntValue(valueName, key.readIntValue(name));
          key.removeValue(name);
          break;
        case nsIWindowsRegKey.TYPE_INT64:
          key.writeInt64Value(valueName, key.readInt64Value(name));
          key.removeValue(name);
          break;
      }
    }
  }
}

function checkLoginsAreEqual(passwordManagerLogin, IELogin, id) {
  passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
  for (let attribute in IELogin) {
    Assert.equal(
      passwordManagerLogin[attribute],
      IELogin[attribute],
      "The two logins ID " + id + " have the same " + attribute
    );
  }
}

function createRegistryPath(path) {
  let loginPath = path.split("\\");
  let parentKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
    nsIWindowsRegKey
  );
  let currentPath = [];
  for (let currentKey of loginPath) {
    parentKey.open(
      nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
      currentPath.join("\\"),
      nsIWindowsRegKey.ACCESS_ALL
    );

    if (!parentKey.hasChild(currentKey)) {
      parentKey.createChild(currentKey, 0);
    }
    currentPath.push(currentKey);
    parentKey.close();
  }
}

function getFirstResourceOfType(type) {
  let migrator = Cc[
    "@mozilla.org/profile/migrator;1?app=browser&type=ie"
  ].createInstance(Ci.nsISupports).wrappedJSObject;
  let migrators = migrator.getResources();
  for (let m of migrators) {
    if (m.name == IE7_FORM_PASSWORDS_MIGRATOR_NAME && m.type == type) {
      return m;
    }
  }
  throw new Error("failed to find the " + type + " migrator");
}

function makeURI(aURL) {
  return Services.io.newURI(aURL);
}

add_task(async function setup() {
  if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
    Assert.throws(
      () => getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS),
      /failed to find/,
      "The migrator doesn't exist for win8+"
    );
    return;
  }
  // create the path to Storage2 in the registry if it doest exist.
  createRegistryPath(LOGINS_KEY);
  Storage2Key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
    nsIWindowsRegKey
  );
  Storage2Key.open(
    nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
    LOGINS_KEY,
    nsIWindowsRegKey.ACCESS_ALL
  );

  // create a dummy value otherwise the migrator doesn't exist
  if (!Storage2Key.hasValue("dummy")) {
    Storage2Key.writeBinaryValue("dummy", "dummy");
  }
});

add_task(async function test_passwordsNotAvailable() {
  if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
    return;
  }

  let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
  Assert.ok(migrator.exists, "The migrator has to exist");
  let logins = Services.logins.getAllLogins();
  Assert.equal(
    logins.length,
    0,
    "There are no logins at the beginning of the test"
  );

  let uris = []; // the uris of the migrated logins
  for (let url of TESTED_URLS) {
    uris.push(makeURI(url));
    // in this test, there is no IE login data in the registry, so after the migration, the number
    // of logins in the store should be 0
    await migrator._migrateURIs(uris);
    logins = Services.logins.getAllLogins();
    Assert.equal(
      logins.length,
      0,
      "There are no logins after doing the migration without adding values to the registry"
    );
  }
});

add_task(async function test_passwordsAvailable() {
  if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
    return;
  }

  let crypto = new OSCrypto();
  let hashes = []; // the hashes of all migrator websites, this is going to be used for the clean up

  registerCleanupFunction(() => {
    Services.logins.removeAllLogins();
    logins = Services.logins.getAllLogins();
    Assert.equal(logins.length, 0, "There are no logins after the cleanup");
    // remove all the values created in this test from the registry
    removeAllValues(Storage2Key, hashes);
    // restore all backed up values
    restore(Storage2Key);

    // clean the dummy value
    if (Storage2Key.hasValue("dummy")) {
      Storage2Key.removeValue("dummy");
    }
    Storage2Key.close();
    crypto.finalize();
  });

  let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
  Assert.ok(migrator.exists, "The migrator has to exist");
  let logins = Services.logins.getAllLogins();
  Assert.equal(
    logins.length,
    0,
    "There are no logins at the beginning of the test"
  );

  let uris = []; // the uris of the migrated logins

  let loginCount = 0;
  for (let current in TESTED_WEBSITES) {
    let website = TESTED_WEBSITES[current];
    // backup the current the registry value if it exists and replace the existing value/create a
    // new value with the encrypted data
    backupAndStore(
      Storage2Key,
      website.hash,
      crypto.encryptData(crypto.arrayToString(website.data), website.uri.spec)
    );
    Assert.ok(migrator.exists, "The migrator has to exist");
    uris.push(website.uri);
    hashes.push(website.hash);

    await migrator._migrateURIs(uris);
    logins = Services.logins.getAllLogins();
    // check that the number of logins in the password manager has increased as expected which means
    // that all the values for the current website were imported
    loginCount += website.logins.length;
    Assert.equal(
      logins.length,
      loginCount,
      "The number of logins has increased after the migration"
    );
    // NB: because telemetry records any login data passed to the login manager, it
    // also gets told about logins that are duplicates or invalid (for one reason
    // or another) and so its counts might exceed those of the login manager itself.
    Assert.greaterOrEqual(
      MigrationUtils._importQuantities.logins,
      loginCount,
      "Telemetry quantities equal or exceed the actual import."
    );
    // Reset - this normally happens at the start of a new migration, but we're calling
    // the migrator directly so can't rely on that:
    MigrationUtils._importQuantities.logins = 0;

    let startIndex = loginCount - website.logins.length;
    // compares the imported password manager logins with their expected logins
    for (let i = 0; i < website.logins.length; i++) {
      checkLoginsAreEqual(
        logins[startIndex + i],
        website.logins[i],
        " " + current + " - " + i + " "
      );
    }
  }
});
