зеркало из https://github.com/mozilla/gecko-dev.git
782 строки
28 KiB
JavaScript
782 строки
28 KiB
JavaScript
/* 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";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"LoginHelper",
|
|
];
|
|
|
|
// Globals
|
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
// LoginHelper
|
|
|
|
/**
|
|
* Contains functions shared by different Login Manager components.
|
|
*/
|
|
var 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"),
|
|
|
|
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 = ChromeUtils.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();
|
|
});
|
|
|
|
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.includes("\r") ||
|
|
aHostname.includes("\n") ||
|
|
aHostname.includes("\0")) {
|
|
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.includes(c)) ||
|
|
(l.httpRealm && l.httpRealm.includes(c)) ||
|
|
l.hostname.includes(c) ||
|
|
l.usernameField.includes(c) ||
|
|
l.passwordField.includes(c));
|
|
}
|
|
|
|
// 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.includes("\0") ||
|
|
aLogin.password.includes("\0")) {
|
|
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.includes(" (")) {
|
|
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} aURL
|
|
* @returns {string} which is the hostPort of aURL if supported by the scheme
|
|
* otherwise, returns the original aURL.
|
|
*/
|
|
maybeGetHostPortForURL(aURL) {
|
|
try {
|
|
let uri = Services.io.newURI(aURL);
|
|
return uri.hostPort;
|
|
} catch (ex) {
|
|
// No need to warn for javascript:/data:/about:/chrome:/etc.
|
|
}
|
|
return aURL;
|
|
},
|
|
|
|
/**
|
|
* @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);
|
|
let searchURI = Services.io.newURI(aSearchOrigin);
|
|
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 login1HostPort = this.maybeGetHostPortForURL(aLogin1.hostname);
|
|
let login2HostPort = this.maybeGetHostPortForURL(aLogin2.hostname);
|
|
if (login1HostPort != login2HostPort)
|
|
return false;
|
|
|
|
if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
|
|
this.maybeGetHostPortForURL(aLogin1.formSubmitURL) !=
|
|
this.maybeGetHostPortForURL(aLogin2.formSubmitURL)) {
|
|
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).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);
|
|
let loginURI = Services.io.newURI(login.hostname);
|
|
// 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});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 (ChromeUtils.getClassName(element) !== "HTMLInputElement")
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* For each login, 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[]} loginDatas - For each login, the data that needs to be added.
|
|
* @returns {nsILoginInfo[]} the newly added logins, filtered if no login was added.
|
|
*/
|
|
async maybeImportLogins(loginDatas) {
|
|
let loginsToAdd = [];
|
|
let loginMap = new Map();
|
|
for (let loginData of loginDatas) {
|
|
// 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;
|
|
|
|
try {
|
|
// Ensure we only send checked logins through, since the validation is optimized
|
|
// out from the bulk APIs below us.
|
|
this.checkLoginValues(login);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
continue;
|
|
}
|
|
|
|
// First, we need to check the logins that we've already decided to add, to
|
|
// see if this is a duplicate. This should mirror the logic below for
|
|
// existingLogins, but only for the array of logins we're adding.
|
|
let newLogins = loginMap.get(login.hostname) || [];
|
|
if (!newLogins) {
|
|
loginMap.set(login.hostname, newLogins);
|
|
} else {
|
|
if (newLogins.some(l => login.matches(l, false /* ignorePassword */))) {
|
|
continue;
|
|
}
|
|
let foundMatchingNewLogin = false;
|
|
for (let newLogin of newLogins) {
|
|
if (login.username == newLogin.username) {
|
|
foundMatchingNewLogin = true;
|
|
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
if (login.password != newLogin.password &
|
|
login.timePasswordChanged > newLogin.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.
|
|
newLogin.password = login.password;
|
|
newLogin.timePasswordChanged = login.timePasswordChanged;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundMatchingNewLogin) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 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 */))) {
|
|
continue;
|
|
}
|
|
// 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) {
|
|
continue;
|
|
}
|
|
|
|
newLogins.push(login);
|
|
loginsToAdd.push(login);
|
|
}
|
|
if (!loginsToAdd.length) {
|
|
return [];
|
|
}
|
|
return Services.logins.addLogins(loginsToAdd);
|
|
},
|
|
|
|
/**
|
|
* 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} = ChromeUtils.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 tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
|
|
.getService(Ci.nsIPK11TokenDB);
|
|
let token = tokenDB.getInternalKeyToken();
|
|
return token.hasPassword;
|
|
},
|
|
|
|
/**
|
|
* 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]);
|
|
}
|
|
} 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.defineLazyPreferenceGetter(LoginHelper, "showInsecureFieldWarning",
|
|
"security.insecure_field_warning.contextual.enabled");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
|
let logger = LoginHelper.createLogger("LoginHelper");
|
|
return logger;
|
|
});
|