summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginHelper.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/LoginHelper.jsm')
-rw-r--r--toolkit/components/passwordmgr/LoginHelper.jsm725
1 files changed, 725 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/LoginHelper.jsm b/toolkit/components/passwordmgr/LoginHelper.jsm
new file mode 100644
index 000000000..e0c4d872b
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -0,0 +1,725 @@
+/* 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/. */
+
+/**
+ * Contains functions shared by different Login Manager components.
+ *
+ * This JavaScript module exists in order to share code between the different
+ * XPCOM components that constitute the Login Manager, including implementations
+ * of nsILoginManager and nsILoginManagerStorage.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginHelper",
+];
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// LoginHelper
+
+/**
+ * Contains functions shared by different Login Manager components.
+ */
+this.LoginHelper = {
+ /**
+ * Warning: these only update if a logger was created.
+ */
+ debug: Services.prefs.getBoolPref("signon.debug"),
+ formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
+ showInsecureFieldWarning: Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled"),
+
+ createLogger(aLogPrefix) {
+ let getMaxLogLevel = () => {
+ return this.debug ? "debug" : "warn";
+ };
+
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+ let consoleOptions = {
+ maxLogLevel: getMaxLogLevel(),
+ prefix: aLogPrefix,
+ };
+ let logger = new ConsoleAPI(consoleOptions);
+
+ // Watch for pref changes and update this.debug and the maxLogLevel for created loggers
+ Services.prefs.addObserver("signon.", () => {
+ this.debug = Services.prefs.getBoolPref("signon.debug");
+ this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
+ this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
+ this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
+ logger.maxLogLevel = getMaxLogLevel();
+ }, false);
+
+ Services.prefs.addObserver("security.insecure_field_warning.", () => {
+ this.showInsecureFieldWarning = Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled");
+ }, false);
+
+ return logger;
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject hostnames that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkHostnameValue(aHostname) {
+ // Nulls are invalid, as they don't round-trip well. Newlines are also
+ // invalid for any field stored as plaintext, and a hostname made of a
+ // single dot cannot be stored in the legacy format.
+ if (aHostname == "." ||
+ aHostname.indexOf("\r") != -1 ||
+ aHostname.indexOf("\n") != -1 ||
+ aHostname.indexOf("\0") != -1) {
+ throw new Error("Invalid hostname");
+ }
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject logins that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkLoginValues(aLogin) {
+ function badCharacterPresent(l, c) {
+ return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
+ (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
+ l.hostname.indexOf(c) != -1 ||
+ l.usernameField.indexOf(c) != -1 ||
+ l.passwordField.indexOf(c) != -1);
+ }
+
+ // Nulls are invalid, as they don't round-trip well.
+ // Mostly not a formatting problem, although ".\0" can be quirky.
+ if (badCharacterPresent(aLogin, "\0")) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // In theory these nulls should just be rolled up into the encrypted
+ // values, but nsISecretDecoderRing doesn't use nsStrings, so the
+ // nulls cause truncation. Check for them here just to avoid
+ // unexpected round-trip surprises.
+ if (aLogin.username.indexOf("\0") != -1 ||
+ aLogin.password.indexOf("\0") != -1) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // Newlines are invalid for any field stored as plaintext.
+ if (badCharacterPresent(aLogin, "\r") ||
+ badCharacterPresent(aLogin, "\n")) {
+ throw new Error("login values can't contain newlines");
+ }
+
+ // A line with just a "." can have special meaning.
+ if (aLogin.usernameField == "." ||
+ aLogin.formSubmitURL == ".") {
+ throw new Error("login values can't be periods");
+ }
+
+ // A hostname with "\ \(" won't roundtrip.
+ // eg host="foo (", realm="bar" --> "foo ( (bar)"
+ // vs host="foo", realm=" (bar" --> "foo ( (bar)"
+ if (aLogin.hostname.indexOf(" (") != -1) {
+ throw new Error("bad parens in hostname");
+ }
+ },
+
+ /**
+ * Returns a new XPCOM property bag with the provided properties.
+ *
+ * @param {Object} aProperties
+ * Each property of this object is copied to the property bag. This
+ * parameter can be omitted to return an empty property bag.
+ *
+ * @return A new property bag, that is an instance of nsIWritablePropertyBag,
+ * nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
+ */
+ newPropertyBag(aProperties) {
+ let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag);
+ if (aProperties) {
+ for (let [name, value] of Object.entries(aProperties)) {
+ propertyBag.setProperty(name, value);
+ }
+ }
+ return propertyBag.QueryInterface(Ci.nsIPropertyBag)
+ .QueryInterface(Ci.nsIPropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag2);
+ },
+
+ /**
+ * Helper to avoid the `count` argument and property bags when calling
+ * Services.logins.searchLogins from JS.
+ *
+ * @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
+ * @return {nsILoginInfo[]} - The result of calling searchLogins.
+ */
+ searchLoginsWithObject(aSearchOptions) {
+ return Services.logins.searchLogins({}, this.newPropertyBag(aSearchOptions));
+ },
+
+ /**
+ * @param {String} aLoginOrigin - An origin value from a stored login's
+ * hostname or formSubmitURL properties.
+ * @param {String} aSearchOrigin - The origin that was are looking to match
+ * with aLoginOrigin. This would normally come
+ * from a form or page that we are considering.
+ * @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
+ * from the login (aLoginOrigin) is a
+ * match for the origin we're looking
+ * for (aSearchOrigin).
+ */
+ isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ if (aLoginOrigin == aSearchOrigin) {
+ return true;
+ }
+
+ if (!aOptions) {
+ return false;
+ }
+
+ if (aOptions.schemeUpgrades) {
+ try {
+ let loginURI = Services.io.newURI(aLoginOrigin, null, null);
+ let searchURI = Services.io.newURI(aSearchOrigin, null, null);
+ if (loginURI.scheme == "http" && searchURI.scheme == "https" &&
+ loginURI.hostPort == searchURI.hostPort) {
+ return true;
+ }
+ } catch (ex) {
+ // newURI will throw for some values e.g. chrome://FirefoxAccounts
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ doLoginsMatch(aLogin1, aLogin2, {
+ ignorePassword = false,
+ ignoreSchemes = false,
+ }) {
+ if (aLogin1.httpRealm != aLogin2.httpRealm ||
+ aLogin1.username != aLogin2.username)
+ return false;
+
+ if (!ignorePassword && aLogin1.password != aLogin2.password)
+ return false;
+
+ if (ignoreSchemes) {
+ let hostname1URI = Services.io.newURI(aLogin1.hostname, null, null);
+ let hostname2URI = Services.io.newURI(aLogin2.hostname, null, null);
+ if (hostname1URI.hostPort != hostname2URI.hostPort)
+ return false;
+
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ Services.io.newURI(aLogin1.formSubmitURL, null, null).hostPort !=
+ Services.io.newURI(aLogin2.formSubmitURL, null, null).hostPort)
+ return false;
+ } else {
+ if (aLogin1.hostname != aLogin2.hostname)
+ return false;
+
+ // If either formSubmitURL is blank (but not null), then match.
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ aLogin1.formSubmitURL != aLogin2.formSubmitURL)
+ return false;
+ }
+
+ // The .usernameField and .passwordField values are ignored.
+
+ return true;
+ },
+
+ /**
+ * Creates a new login object that results by modifying the given object with
+ * the provided data.
+ *
+ * @param aOldStoredLogin
+ * Existing nsILoginInfo object to modify.
+ * @param aNewLoginData
+ * The new login values, either as nsILoginInfo or nsIProperyBag.
+ *
+ * @return The newly created nsILoginInfo object.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
+ function bagHasProperty(aPropName) {
+ try {
+ aNewLoginData.getProperty(aPropName);
+ return true;
+ } catch (ex) { }
+ return false;
+ }
+
+ aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ let newLogin;
+ if (aNewLoginData instanceof Ci.nsILoginInfo) {
+ // Clone the existing login to get its nsILoginMetaInfo, then init it
+ // with the replacement nsILoginInfo data from the new login.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.init(aNewLoginData.hostname,
+ aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
+ aNewLoginData.username, aNewLoginData.password,
+ aNewLoginData.usernameField, aNewLoginData.passwordField);
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ if (newLogin.password != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ } else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
+ // Clone the existing login, along with all its properties.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ // (Done before the main property updates, lest the caller be
+ // explicitly updating both .password and .timePasswordChanged)
+ if (bagHasProperty("password")) {
+ let newPassword = aNewLoginData.getProperty("password");
+ if (newPassword != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ }
+
+ let propEnum = aNewLoginData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // nsILoginInfo
+ case "hostname":
+ case "httpRealm":
+ case "formSubmitURL":
+ case "username":
+ case "password":
+ case "usernameField":
+ case "passwordField":
+ // nsILoginMetaInfo
+ case "guid":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ newLogin[prop.name] = prop.value;
+ break;
+
+ // Fake property, allows easy incrementing.
+ case "timesUsedIncrement":
+ newLogin.timesUsed += prop.value;
+ break;
+
+ // Fail if caller requests setting an unknown property.
+ default:
+ throw new Error("Unexpected propertybag item: " + prop.name);
+ }
+ }
+ } else {
+ throw new Error("newLoginData needs an expected interface!");
+ }
+
+ // Sanity check the login
+ if (newLogin.hostname == null || newLogin.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (newLogin.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (newLogin.password == null || newLogin.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (newLogin.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (newLogin.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (newLogin.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+ // Throws if there are bogus values.
+ this.checkLoginValues(newLogin);
+
+ return newLogin;
+ },
+
+ /**
+ * Removes duplicates from a list of logins while preserving the sort order.
+ *
+ * @param {nsILoginInfo[]} logins
+ * A list of logins we want to deduplicate.
+ * @param {string[]} [uniqueKeys = ["username", "password"]]
+ * A list of login attributes to use as unique keys for the deduplication.
+ * @param {string[]} [resolveBy = ["timeLastUsed"]]
+ * Ordered array of keyword strings used to decide which of the
+ * duplicates should be used. "scheme" would prefer the login that has
+ * a scheme matching `preferredOrigin`'s if there are two logins with
+ * the same `uniqueKeys`. The default preference to distinguish two
+ * logins is `timeLastUsed`. If there is no preference between two
+ * logins, the first one found wins.
+ * @param {string} [preferredOrigin = undefined]
+ * String representing the origin to use for preferring one login over
+ * another when they are dupes. This is used with "scheme" for
+ * `resolveBy` so the scheme from this origin will be preferred.
+ *
+ * @returns {nsILoginInfo[]} list of unique logins.
+ */
+ dedupeLogins(logins, uniqueKeys = ["username", "password"],
+ resolveBy = ["timeLastUsed"],
+ preferredOrigin = undefined) {
+ const KEY_DELIMITER = ":";
+
+ if (!preferredOrigin && resolveBy.includes("scheme")) {
+ throw new Error("dedupeLogins: `preferredOrigin` is required in order to " +
+ "prefer schemes which match it.");
+ }
+
+ let preferredOriginScheme;
+ if (preferredOrigin) {
+ try {
+ preferredOriginScheme = Services.io.newURI(preferredOrigin, null, null).scheme;
+ } catch (ex) {
+ // Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
+ }
+ }
+
+ if (!preferredOriginScheme && resolveBy.includes("scheme")) {
+ log.warn("dedupeLogins: Deduping with a scheme preference but couldn't " +
+ "get the preferred origin scheme.");
+ }
+
+ // We use a Map to easily lookup logins by their unique keys.
+ let loginsByKeys = new Map();
+
+ // Generate a unique key string from a login.
+ function getKey(login, uniqueKeys) {
+ return uniqueKeys.reduce((prev, key) => prev + KEY_DELIMITER + login[key], "");
+ }
+
+ /**
+ * @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
+ * `existingLogin`.
+ *
+ * `resolveBy` is a sorted array so we can return true the first time `login` is preferred
+ * over the existingLogin.
+ */
+ function isLoginPreferred(existingLogin, login) {
+ if (!resolveBy || resolveBy.length == 0) {
+ // If there is no preference, prefer the existing login.
+ return false;
+ }
+
+ for (let preference of resolveBy) {
+ switch (preference) {
+ case "scheme": {
+ if (!preferredOriginScheme) {
+ break;
+ }
+
+ try {
+ // Only `hostname` is currently considered
+ let existingLoginURI = Services.io.newURI(existingLogin.hostname, null, null);
+ let loginURI = Services.io.newURI(login.hostname, null, null);
+ // If the schemes of the two logins are the same or neither match the
+ // preferredOriginScheme then we have no preference and look at the next resolveBy.
+ if (loginURI.scheme == existingLoginURI.scheme ||
+ (loginURI.scheme != preferredOriginScheme &&
+ existingLoginURI.scheme != preferredOriginScheme)) {
+ break;
+ }
+
+ return loginURI.scheme == preferredOriginScheme;
+ } catch (ex) {
+ // Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
+ log.debug("dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
+ existingLogin.hostname, login.hostname,
+ "preferredOrigin:", preferredOrigin, ex);
+ }
+ break;
+ }
+ case "timeLastUsed":
+ case "timePasswordChanged": {
+ // If we find a more recent login for the same key, replace the existing one.
+ let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ let storedLoginDate = existingLogin.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ if (loginDate == storedLoginDate) {
+ break;
+ }
+
+ return loginDate > storedLoginDate;
+ }
+ default: {
+ throw new Error("dedupeLogins: Invalid resolveBy preference: " + preference);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ for (let login of logins) {
+ let key = getKey(login, uniqueKeys);
+
+ if (loginsByKeys.has(key)) {
+ if (!isLoginPreferred(loginsByKeys.get(key), login)) {
+ // If there is no preference for the new login, use the existing one.
+ continue;
+ }
+ }
+ loginsByKeys.set(key, login);
+ }
+
+ // Return the map values in the form of an array.
+ return [...loginsByKeys.values()];
+ },
+
+ /**
+ * Open the password manager window.
+ *
+ * @param {Window} window
+ * the window from where we want to open the dialog
+ *
+ * @param {string} [filterString=""]
+ * the filterString parameter to pass to the login manager dialog
+ */
+ openPasswordManager(window, filterString = "") {
+ let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
+ if (win) {
+ win.setFilter(filterString);
+ win.focus();
+ } else {
+ window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
+ "Toolkit:PasswordManager", "",
+ {filterString : filterString});
+ }
+ },
+
+ /**
+ * Checks if a field type is username compatible.
+ *
+ * @param {Element} element
+ * the field we want to check.
+ *
+ * @returns {Boolean} true if the field type is one
+ * of the username types.
+ */
+ isUsernameFieldType(element) {
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement))
+ return false;
+
+ let fieldType = (element.hasAttribute("type") ?
+ element.getAttribute("type").toLowerCase() :
+ element.type);
+ if (fieldType == "text" ||
+ fieldType == "email" ||
+ fieldType == "url" ||
+ fieldType == "tel" ||
+ fieldType == "number") {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Add the login to the password manager if a similar one doesn't already exist. Merge it
+ * otherwise with the similar existing ones.
+ * @param {Object} loginData - the data about the login that needs to be added.
+ * @returns {nsILoginInfo} the newly added login, or null if no login was added.
+ * Note that we will also return null if an existing login
+ * was modified.
+ */
+ maybeImportLogin(loginData) {
+ // create a new login
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ login.init(loginData.hostname,
+ loginData.formSubmitURL || (typeof(loginData.httpRealm) == "string" ? null : ""),
+ typeof(loginData.httpRealm) == "string" ? loginData.httpRealm : null,
+ loginData.username,
+ loginData.password,
+ loginData.usernameElement || "",
+ loginData.passwordElement || "");
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.timeCreated = loginData.timeCreated;
+ login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
+ login.timePasswordChanged = loginData.timePasswordChanged || loginData.timeCreated;
+ login.timesUsed = loginData.timesUsed || 1;
+ // While here we're passing formSubmitURL and httpRealm, they could be empty/null and get
+ // ignored in that case, leading to multiple logins for the same username.
+ let existingLogins = Services.logins.findLogins({}, login.hostname,
+ login.formSubmitURL,
+ login.httpRealm);
+ // Check for an existing login that matches *including* the password.
+ // If such a login exists, we do not need to add a new login.
+ if (existingLogins.some(l => login.matches(l, false /* ignorePassword */))) {
+ return null;
+ }
+ // Now check for a login with the same username, where it may be that we have an
+ // updated password.
+ let foundMatchingLogin = false;
+ for (let existingLogin of existingLogins) {
+ if (login.username == existingLogin.username) {
+ foundMatchingLogin = true;
+ existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ if (login.password != existingLogin.password &
+ login.timePasswordChanged > existingLogin.timePasswordChanged) {
+ // if a login with the same username and different password already exists and it's older
+ // than the current one, update its password and timestamp.
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("password", login.password);
+ propBag.setProperty("timePasswordChanged", login.timePasswordChanged);
+ Services.logins.modifyLogin(existingLogin, propBag);
+ }
+ }
+ }
+ // if the new login is an update or is older than an exiting login, don't add it.
+ if (foundMatchingLogin) {
+ return null;
+ }
+ return Services.logins.addLogin(login);
+ },
+
+ /**
+ * Convert an array of nsILoginInfo to vanilla JS objects suitable for
+ * sending over IPC.
+ *
+ * NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
+ */
+ loginsToVanillaObjects(logins) {
+ return logins.map(this.loginToVanillaObject);
+ },
+
+ /**
+ * Same as above, but for a single login.
+ */
+ loginToVanillaObject(login) {
+ let obj = {};
+ for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
+ if (typeof login[i] !== 'function') {
+ obj[i] = login[i];
+ }
+ }
+
+ return obj;
+ },
+
+ /**
+ * Convert an object received from IPC into an nsILoginInfo (with guid).
+ */
+ vanillaObjectToLogin(login) {
+ let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ formLogin.init(login.hostname, login.formSubmitURL,
+ login.httpRealm, login.username,
+ login.password, login.usernameField,
+ login.passwordField);
+
+ formLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ for (let prop of ["guid", "timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
+ formLogin[prop] = login[prop];
+ }
+ return formLogin;
+ },
+
+ /**
+ * As above, but for an array of objects.
+ */
+ vanillaObjectsToLogins(logins) {
+ return logins.map(this.vanillaObjectToLogin);
+ },
+
+ removeLegacySignonFiles() {
+ const {Constants, Path, File} = Cu.import("resource://gre/modules/osfile.jsm").OS;
+
+ const profileDir = Constants.Path.profileDir;
+ const defaultSignonFilePrefs = new Map([
+ ["signon.SignonFileName", "signons.txt"],
+ ["signon.SignonFileName2", "signons2.txt"],
+ ["signon.SignonFileName3", "signons3.txt"]
+ ]);
+ const toDeletes = new Set();
+
+ for (let [pref, val] of defaultSignonFilePrefs.entries()) {
+ toDeletes.add(Path.join(profileDir, val));
+
+ try {
+ let signonFile = Services.prefs.getCharPref(pref);
+
+ toDeletes.add(Path.join(profileDir, signonFile));
+ Services.prefs.clearUserPref(pref);
+ } catch (e) {}
+ }
+
+ for (let file of toDeletes) {
+ File.remove(file);
+ }
+ },
+
+ /**
+ * Returns true if the user has a master password set and false otherwise.
+ */
+ isMasterPasswordSet() {
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+ getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ if (!slot) {
+ return false;
+ }
+ let hasMP = slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
+ slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
+ return hasMP;
+ },
+
+ /**
+ * Send a notification when stored data is changed.
+ */
+ notifyStorageChanged(changeType, data) {
+ let dataObject = data;
+ // Can't pass a raw JS string or array though notifyObservers(). :-(
+ if (Array.isArray(data)) {
+ dataObject = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ for (let i = 0; i < data.length; i++) {
+ dataObject.appendElement(data[i], false);
+ }
+ } else if (typeof(data) == "string") {
+ dataObject = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ dataObject.data = data;
+ }
+ Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginHelper");
+ return logger;
+});