зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1687852 Resolve how we handle duplicate logins when importing from CSV file r=sfoster,tgiles
Differential Revision: https://phabricator.services.mozilla.com/D102639
This commit is contained in:
Родитель
5ac1094bb5
Коммит
cc083bcdcf
|
@ -162,7 +162,17 @@ class LoginCSVImport {
|
|||
columnName.toLocaleLowerCase()
|
||||
);
|
||||
if (fieldName) {
|
||||
fieldsInFile.add(fieldName);
|
||||
if (!fieldsInFile.has(fieldName)) {
|
||||
fieldsInFile.add(fieldName);
|
||||
} else {
|
||||
TelemetryStopwatch.cancelKeyed(
|
||||
"FX_MIGRATION_LOGINS_IMPORT_MS",
|
||||
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
|
||||
);
|
||||
throw new ImportFailedException(
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -185,22 +195,6 @@ class LoginCSVImport {
|
|||
throw new ImportFailedException(ImportFailedErrorType.FILE_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
const uniqueLoginIdentifiers = new Set();
|
||||
for (const csvObject of parsedLines) {
|
||||
// TODO: handle duplicates without guid column. Bug 1687852
|
||||
|
||||
if (csvObject.guid) {
|
||||
if (uniqueLoginIdentifiers.has(csvObject.guid)) {
|
||||
throw new ImportFailedException(
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
csvObject.guid
|
||||
);
|
||||
} else {
|
||||
uniqueLoginIdentifiers.add(csvObject.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let loginsToImport = parsedLines.map(csvObject => {
|
||||
return LoginCSVImport._getVanillaLoginFromCSVObject(
|
||||
csvObject,
|
||||
|
|
|
@ -24,6 +24,314 @@ ChromeUtils.defineModuleGetter(
|
|||
"resource://gre/modules/OSKeyStore.jsm"
|
||||
);
|
||||
|
||||
/**
|
||||
* A helper class to deal with CSV import rows.
|
||||
*/
|
||||
class ImportRowProcessor {
|
||||
uniqueLoginIdentifiers = new Set();
|
||||
originToRows = new Map();
|
||||
summary = [];
|
||||
|
||||
/**
|
||||
* Validates if the login data contains a GUID that was already found in a previous row in the current import.
|
||||
* If this is the case, the summary will be updated with an error.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
* @returns {boolean} True if there is an error, false otherwise.
|
||||
*/
|
||||
checkNonUniqueGuidError(loginData) {
|
||||
if (loginData.guid) {
|
||||
if (this.uniqueLoginIdentifiers.has(loginData.guid)) {
|
||||
this.addLoginToSummary({ ...loginData }, "error");
|
||||
return true;
|
||||
}
|
||||
this.uniqueLoginIdentifiers.add(loginData.guid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the login data contains invalid fields that are mandatory like origin and password.
|
||||
* If this is the case, the summary will be updated with an error.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
* @returns {boolean} True if there is an error, false otherwise.
|
||||
*/
|
||||
checkMissingMandatoryFieldsError(loginData) {
|
||||
loginData.origin = LoginHelper.getLoginOrigin(loginData.origin);
|
||||
if (!loginData.origin) {
|
||||
this.addLoginToSummary({ ...loginData }, "error_invalid_origin");
|
||||
return true;
|
||||
}
|
||||
if (!loginData.password) {
|
||||
this.addLoginToSummary({ ...loginData }, "error_invalid_password");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if there is already an existing entry with similar values.
|
||||
* If there are similar values but not identical, a new "modified" entry will be added to the summary.
|
||||
* If there are identical values, a new "no_change" entry will be added to the summary
|
||||
* If either of these is the case, it will return true.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
|
||||
*/
|
||||
async checkExistingEntry(loginData) {
|
||||
if (loginData.guid) {
|
||||
// First check for `guid` matches if it's set.
|
||||
// `guid` matches will allow every kind of update, including reverting
|
||||
// to older passwords which can be useful if the user wants to recover
|
||||
// an old password.
|
||||
let existingLogins = await Services.logins.searchLoginsAsync({
|
||||
guid: loginData.guid,
|
||||
origin: loginData.origin, // Ignored outside of GV.
|
||||
});
|
||||
|
||||
if (existingLogins.length) {
|
||||
log.debug("maybeImportLogins: Found existing login with GUID");
|
||||
// There should only be one `guid` match.
|
||||
let existingLogin = existingLogins[0].QueryInterface(
|
||||
Ci.nsILoginMetaInfo
|
||||
);
|
||||
|
||||
if (
|
||||
loginData.username !== existingLogin.username ||
|
||||
loginData.password !== existingLogin.password ||
|
||||
loginData.httpRealm !== existingLogin.httpRealm ||
|
||||
loginData.formActionOrigin !== existingLogin.formActionOrigin ||
|
||||
`${loginData.timeCreated}` !== `${existingLogin.timeCreated}` ||
|
||||
`${loginData.timePasswordChanged}` !==
|
||||
`${existingLogin.timePasswordChanged}`
|
||||
) {
|
||||
// Use a property bag rather than an nsILoginInfo so we don't clobber
|
||||
// properties that the import source doesn't provide.
|
||||
let propBag = LoginHelper.newPropertyBag(loginData);
|
||||
this.addLoginToSummary({ ...existingLogin }, "modified", propBag);
|
||||
return true;
|
||||
}
|
||||
this.addLoginToSummary({ ...existingLogin }, "no_change");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if there is a conflict with previous rows based on the origin.
|
||||
* We need to check the logins that we've already decided to add, to see if this is a duplicate.
|
||||
* If this is the case, we mark this one as "no_change" in the summary and return true.
|
||||
* @param {object} login
|
||||
* A login object.
|
||||
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
|
||||
*/
|
||||
checkConflictingOriginWithPreviousRows(login) {
|
||||
let rowsPerOrigin = this.originToRows.get(login.origin) || [];
|
||||
if (!rowsPerOrigin.length) {
|
||||
this.originToRows.set(login.origin, rowsPerOrigin);
|
||||
} else if (
|
||||
rowsPerOrigin.some(r =>
|
||||
login.matches(r.login, false /* ignorePassword */)
|
||||
)
|
||||
) {
|
||||
this.addLoginToSummary(login, "no_change");
|
||||
return true;
|
||||
} else {
|
||||
for (let row of rowsPerOrigin) {
|
||||
let newLogin = row.login;
|
||||
if (login.username == newLogin.username) {
|
||||
this.addLoginToSummary(login, "no_change");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if there is a conflict with existing logins based on the origin.
|
||||
* If this is the case and there are some changes, we mark it as "modified" in the summary.
|
||||
* If it matches an existing login without any extra modifications, we mark it as "no_change".
|
||||
* For both cases we return true.
|
||||
* @param {object} login
|
||||
* A login object.
|
||||
* @returns {boolean} True if the entry is similar or identical to another previously processed entry, false otherwise.
|
||||
*/
|
||||
checkConflictingWithExistingLogins(login) {
|
||||
// While here we're passing formActionOrigin 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.origin,
|
||||
login.formActionOrigin,
|
||||
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 */))
|
||||
) {
|
||||
this.addLoginToSummary(login, "no_change");
|
||||
return true;
|
||||
}
|
||||
// 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);
|
||||
this.addLoginToSummary({ ...existingLogin }, "modified", propBag);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the new login is an update or is older than an exiting login, don't add it.
|
||||
if (foundMatchingLogin) {
|
||||
this.addLoginToSummary(login, "no_change");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if there are any invalid values using LoginHelper.checkLoginValues.
|
||||
* If this is the case we mark it as "error" and return true.
|
||||
* @param {object} login
|
||||
* A login object.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
* @returns {boolean} True if there is a validation error we return true, false otherwise.
|
||||
*/
|
||||
checkLoginValuesError(login, loginData) {
|
||||
try {
|
||||
// Ensure we only send checked logins through, since the validation is optimized
|
||||
// out from the bulk APIs below us.
|
||||
LoginHelper.checkLoginValues(login);
|
||||
} catch (e) {
|
||||
this.addLoginToSummary({ ...loginData }, "error");
|
||||
Cu.reportError(e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new login from loginData.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
* @returns {object} A login object.
|
||||
*/
|
||||
createNewLogin(loginData) {
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
|
||||
Ci.nsILoginInfo
|
||||
);
|
||||
login.init(
|
||||
loginData.origin,
|
||||
loginData.formActionOrigin,
|
||||
loginData.httpRealm,
|
||||
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;
|
||||
login.guid = loginData.guid || null;
|
||||
return login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the action and realm field of the loginData.
|
||||
* @param {object} loginData
|
||||
* An vanilla object for the login without any methods.
|
||||
*/
|
||||
cleanupActionAndRealmFields(loginData) {
|
||||
loginData.formActionOrigin =
|
||||
LoginHelper.getLoginOrigin(loginData.formActionOrigin, true) ||
|
||||
(typeof loginData.httpRealm == "string" ? null : "");
|
||||
|
||||
loginData.httpRealm =
|
||||
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a login to the summary.
|
||||
* @param {object} login
|
||||
* A login object.
|
||||
* @param {string} result
|
||||
* The result type. One of "added", "modified", "error", "error_invalid_origin", "error_invalid_password" or "no_change".
|
||||
* @param {object} propBag
|
||||
* An optional parameter with the properties bag.
|
||||
*/
|
||||
addLoginToSummary(login, result, propBag) {
|
||||
let rows = this.originToRows.get(login.origin) || [];
|
||||
const newSummaryRow = { result, login, propBag };
|
||||
rows.push(newSummaryRow);
|
||||
this.summary.push(newSummaryRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all then rows where more than two match the same origin. It mutates the internal state of the processor.
|
||||
* It makes sure that if the `timePasswordChanged` field is present it will be used to decide if it's a "no_change" or "added".
|
||||
* The entry with the oldest `timePasswordChanged` will be "added", the rest will be "no_change".
|
||||
*/
|
||||
markLastTimePasswordChangedAsModified() {
|
||||
for (let rowsGroupedByOrigin of this.originToRows.values()) {
|
||||
if (rowsGroupedByOrigin.length > 1) {
|
||||
let lastTimePasswordChanged = rowsGroupedByOrigin[0];
|
||||
for (let i = 1; i < rowsGroupedByOrigin.length; i++) {
|
||||
const row = rowsGroupedByOrigin[i];
|
||||
if (
|
||||
(row.login.password != lastTimePasswordChanged.login.password) &
|
||||
(row.login.timePasswordChanged >
|
||||
lastTimePasswordChanged.login.timePasswordChanged)
|
||||
) {
|
||||
lastTimePasswordChanged.result = "no_change";
|
||||
lastTimePasswordChanged = row;
|
||||
}
|
||||
}
|
||||
lastTimePasswordChanged.result = "added";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all then rows where more than two match the same origin. It mutates the internal state of the processor.
|
||||
* It makes sure that if the `timePasswordChanged` field is present it will be used to decide if it's a "no_change" or "added".
|
||||
* The entry with the oldest `timePasswordChanged` will be "added", the rest will be "no_change".
|
||||
* @returns {Object[]} An entry for each processed row containing how the row was processed and the login data.
|
||||
*/
|
||||
async processLoginsAndBuildSummary() {
|
||||
this.markLastTimePasswordChangedAsModified();
|
||||
for (let summaryRow of this.summary) {
|
||||
if (summaryRow.result === "added") {
|
||||
summaryRow.login = await Services.logins.addLogin(summaryRow.login);
|
||||
} else if (summaryRow.result === "modified") {
|
||||
Services.logins.modifyLogin(summaryRow.login, summaryRow.propBag);
|
||||
}
|
||||
}
|
||||
return this.summary;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains functions shared by different Login Manager components.
|
||||
*/
|
||||
|
@ -969,203 +1277,36 @@ this.LoginHelper = {
|
|||
* 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.
|
||||
* @returns {Object[]} An entry for each processed row containing how the row was processed and the login data.
|
||||
*/
|
||||
async maybeImportLogins(loginDatas) {
|
||||
let summary = [];
|
||||
let loginsToAdd = [];
|
||||
let loginMap = new Map();
|
||||
const processor = new ImportRowProcessor();
|
||||
for (let rawLoginData of loginDatas) {
|
||||
// Do some sanitization on a clone of the loginData.
|
||||
let loginData = ChromeUtils.shallowClone(rawLoginData);
|
||||
loginData.origin = this.getLoginOrigin(loginData.origin);
|
||||
if (!loginData.origin) {
|
||||
summary.push({
|
||||
result: "error_invalid_origin",
|
||||
login: { ...loginData },
|
||||
});
|
||||
if (processor.checkNonUniqueGuidError(loginData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!loginData.password) {
|
||||
summary.push({
|
||||
result: "error_invalid_password",
|
||||
login: { ...loginData },
|
||||
});
|
||||
if (processor.checkMissingMandatoryFieldsError(loginData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
loginData.formActionOrigin =
|
||||
this.getLoginOrigin(loginData.formActionOrigin, true) ||
|
||||
(typeof loginData.httpRealm == "string" ? null : "");
|
||||
|
||||
loginData.httpRealm =
|
||||
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null;
|
||||
|
||||
if (loginData.guid) {
|
||||
// First check for `guid` matches if it's set.
|
||||
// `guid` matches will allow every kind of update, including reverting
|
||||
// to older passwords which can be useful if the user wants to recover
|
||||
// an old password.
|
||||
let existingLogins = await Services.logins.searchLoginsAsync({
|
||||
guid: loginData.guid,
|
||||
origin: loginData.origin, // Ignored outside of GV.
|
||||
});
|
||||
|
||||
if (existingLogins.length) {
|
||||
log.debug("maybeImportLogins: Found existing login with GUID");
|
||||
// There should only be one `guid` match.
|
||||
let existingLogin = existingLogins[0].QueryInterface(
|
||||
Ci.nsILoginMetaInfo
|
||||
);
|
||||
|
||||
// Use a property bag rather than an nsILoginInfo so we don't clobber
|
||||
// properties that the import source doesn't provide.
|
||||
let propBag = this.newPropertyBag(loginData);
|
||||
if (
|
||||
loginData.username !== existingLogin.username ||
|
||||
loginData.password !== existingLogin.password ||
|
||||
loginData.httpRealm !== existingLogin.httpRealm ||
|
||||
loginData.formActionOrigin !== existingLogin.formActionOrigin ||
|
||||
`${loginData.timeCreated}` !== `${existingLogin.timeCreated}` ||
|
||||
`${loginData.timePasswordChanged}` !==
|
||||
`${existingLogin.timePasswordChanged}`
|
||||
) {
|
||||
summary.push({ result: "modified", login: { ...existingLogin } });
|
||||
Services.logins.modifyLogin(existingLogin, propBag);
|
||||
// Updated a login so we're done.
|
||||
} else {
|
||||
summary.push({ result: "no_change", login: { ...existingLogin } });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// create a new login
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
|
||||
Ci.nsILoginInfo
|
||||
);
|
||||
login.init(
|
||||
loginData.origin,
|
||||
loginData.formActionOrigin,
|
||||
loginData.httpRealm,
|
||||
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;
|
||||
login.guid = loginData.guid || null;
|
||||
|
||||
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) {
|
||||
summary.push({
|
||||
result: "error",
|
||||
login: { ...loginData },
|
||||
});
|
||||
Cu.reportError(e);
|
||||
processor.cleanupActionAndRealmFields(loginData);
|
||||
if (await processor.checkExistingEntry(loginData)) {
|
||||
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.origin) || [];
|
||||
if (!newLogins) {
|
||||
loginMap.set(login.origin, newLogins);
|
||||
} else {
|
||||
if (newLogins.some(l => login.matches(l, false /* ignorePassword */))) {
|
||||
summary.push({ result: "no_change", login });
|
||||
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) {
|
||||
summary.push({ result: "no_change", login });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// While here we're passing formActionOrigin 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.origin,
|
||||
login.formActionOrigin,
|
||||
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 */))
|
||||
) {
|
||||
summary.push({ result: "no_change", login });
|
||||
let login = processor.createNewLogin(loginData);
|
||||
if (processor.checkLoginValuesError(login, loginData)) {
|
||||
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
|
||||
);
|
||||
summary.push({ result: "modified", login: { ...existingLogin } });
|
||||
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) {
|
||||
summary.push({ result: "no_change", login: { login } });
|
||||
if (processor.checkConflictingOriginWithPreviousRows(login)) {
|
||||
continue;
|
||||
}
|
||||
newLogins.push(login);
|
||||
loginsToAdd.push(login);
|
||||
if (processor.checkConflictingWithExistingLogins(login)) {
|
||||
continue;
|
||||
}
|
||||
processor.addLoginToSummary(login, "added");
|
||||
}
|
||||
if (loginsToAdd.length) {
|
||||
let addedLogins = await Services.logins.addLogins(loginsToAdd);
|
||||
for (let addedLogin of addedLogins) {
|
||||
summary.push({ result: "added", login: { ...addedLogin } });
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
return processor.processLoginsAndBuildSummary();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1188,7 +1329,6 @@ this.LoginHelper = {
|
|||
obj[i] = login[i];
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
|
@ -1225,8 +1365,12 @@ this.LoginHelper = {
|
|||
/**
|
||||
* As above, but for an array of objects.
|
||||
*/
|
||||
vanillaObjectsToLogins(logins) {
|
||||
return logins.map(this.vanillaObjectToLogin);
|
||||
vanillaObjectsToLogins(vanillaObjects) {
|
||||
const logins = [];
|
||||
for (const vanillaObject of vanillaObjects) {
|
||||
logins.push(this.vanillaObjectToLogin(vanillaObject));
|
||||
}
|
||||
return logins;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -164,22 +164,15 @@ add_task(async function test_import_with_duplicate_columns() {
|
|||
"https://mozilla.org,https://mozilla.org,jdoe@example.com,qwerty",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/CONFLICTING_VALUES_ERROR/,
|
||||
"Check that the errorType is file format error"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "https://mozilla.org",
|
||||
password: "qwerty",
|
||||
passwordField: "",
|
||||
timesUsed: 1,
|
||||
username: "jdoe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that no login was added with duplicate columns of differing values"
|
||||
[],
|
||||
"Check that no login was added from a file with duplicated columns"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -515,36 +508,90 @@ add_task(async function test_import_summary_contains_added_login() {
|
|||
});
|
||||
|
||||
/**
|
||||
* Imports login data summary contains modified logins.
|
||||
* Imports login data summary contains modified logins without guid.
|
||||
*/
|
||||
add_task(async function test_import_summary_contains_modified_login() {
|
||||
add_task(async function test_import_summary_modified_login_without_guid() {
|
||||
let initialDataFile = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://modifiedwithoutguid.example.com,gini@example.com,initial_password,My realm,,,1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
await LoginCSVImport.importFromCSV(initialDataFile);
|
||||
|
||||
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://modifiedwithoutguid.example.com,gini@example.com,modified_password,My realm,,,1589617814635,1589710449871,1589617846999",
|
||||
]);
|
||||
|
||||
let [modifiedWithoutGuid] = await LoginCSVImport.importFromCSV(csvFile.path);
|
||||
|
||||
equal(
|
||||
modifiedWithoutGuid.result,
|
||||
"modified",
|
||||
`Check that the login was modified when there was no guid data`
|
||||
);
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.authLogin({
|
||||
formActionOrigin: null,
|
||||
guid: null,
|
||||
httpRealm: "My realm",
|
||||
origin: "https://modifiedwithoutguid.example.com",
|
||||
password: "modified_password",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timeLastUsed: 1589710449871,
|
||||
timePasswordChanged: 1589617846999,
|
||||
timesUsed: 1,
|
||||
username: "gini@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that logins were updated with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data summary contains modified logins with guid.
|
||||
*/
|
||||
add_task(async function test_import_summary_modified_login_with_guid() {
|
||||
let initialDataFile = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://modifiedwithguid.example.com,jane@example.com,initial_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0001},1589617814635,1589710449871,1589617846802",
|
||||
"https://modifiedwithoutguid.example.com,jane@example.com,initial_password,My realm,,,1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
await LoginCSVImport.importFromCSV(initialDataFile);
|
||||
|
||||
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://modified.example.com,jane@example.com,modified_password,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0001},1589617814635,1589710449871,1589617846999",
|
||||
"https://modifiedwithoutguid.example.com,jane@example.com,modified_password,My realm,,,1589617814635,1589710449871,1589617846999",
|
||||
]);
|
||||
|
||||
let [
|
||||
modifiedWithGuid,
|
||||
modifiedWithoutGuid,
|
||||
] = await LoginCSVImport.importFromCSV(csvFile.path);
|
||||
let [modifiedWithGuid] = await LoginCSVImport.importFromCSV(csvFile.path);
|
||||
|
||||
equal(
|
||||
modifiedWithGuid.result,
|
||||
"modified",
|
||||
`Check that the login was modified when it had the same guid`
|
||||
);
|
||||
equal(
|
||||
modifiedWithoutGuid.result,
|
||||
"modified",
|
||||
`Check that the login was modified when there was no guid data`
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.authLogin({
|
||||
formActionOrigin: null,
|
||||
guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb0001}",
|
||||
httpRealm: "My realm",
|
||||
origin: "https://modified.example.com",
|
||||
password: "modified_password",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timeLastUsed: 1589710449871,
|
||||
timePasswordChanged: 1589617846999,
|
||||
timesUsed: 1,
|
||||
username: "jane@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that logins were updated with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -632,7 +679,7 @@ add_task(async function test_import_summary_with_non_csv_file() {
|
|||
});
|
||||
|
||||
/**
|
||||
* Imports login with wrong file type will have correct errorType.
|
||||
* Imports login multiple url and user will import the first and skip the second.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_url_user_multiple_values() {
|
||||
let csvFilePath = await setupCsv([
|
||||
|
@ -641,44 +688,103 @@ add_task(async function test_import_summary_with_url_user_multiple_values() {
|
|||
"https://example.com,jane@example.com,password2,My realm",
|
||||
]);
|
||||
|
||||
let errorType;
|
||||
try {
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
} catch (e) {
|
||||
if (e instanceof ImportFailedException) {
|
||||
errorType = e.errorType;
|
||||
}
|
||||
}
|
||||
let initialLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
let results = await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
let afterImportLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
equal(results.length, 2, `Check that we got a result for each imported row`);
|
||||
equal(results[0].result, "added", `Check that the first login was added`);
|
||||
equal(
|
||||
errorType,
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
`Check that the errorType is file format error in case of duplicate entries`
|
||||
results[1].result,
|
||||
"no_change",
|
||||
`Check that the second login was skipped`
|
||||
);
|
||||
}).skip(); // TODO: Bug 1687852, resolve duplicates when importing
|
||||
equal(initialLoginCount, 0, `Check that initially we had no logins`);
|
||||
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login with wrong file type will have correct errorType.
|
||||
* Imports login with duplicated guid values throws error.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_multiple_guid_values() {
|
||||
add_task(async function test_import_summary_with_duplicated_guid_values() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example1.com,jane1@example.com,password1,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
|
||||
"https://example2.com,jane2@example.com,password2,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
let initialLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
let errorType;
|
||||
try {
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
} catch (e) {
|
||||
if (e instanceof ImportFailedException) {
|
||||
errorType = e.errorType;
|
||||
}
|
||||
}
|
||||
let results = await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
let afterImportLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
equal(
|
||||
errorType,
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
`Check that the errorType is file format error in case of duplicate entries`
|
||||
);
|
||||
equal(results.length, 2, `Check that we got a result for each imported row`);
|
||||
equal(results[0].result, "added", `Check that the first login was added`);
|
||||
equal(results[1].result, "error", `Check that the second login was an error`);
|
||||
equal(initialLoginCount, 0, `Check that initially we had no logins`);
|
||||
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login with different passwords will pick up the newest one and ignore the oldest one.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_different_time_changed() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example.com,eve@example.com,old password,1589617814635,1589710449800,1589617846800",
|
||||
"https://example.com,eve@example.com,new password,1589617814635,1589710449801,1589617846801",
|
||||
]);
|
||||
let initialLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
let results = await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
let afterImportLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
equal(results.length, 2, `Check that we got a result for each imported row`);
|
||||
equal(
|
||||
results[0].result,
|
||||
"no_change",
|
||||
`Check that the oldest password is skipped`
|
||||
);
|
||||
equal(
|
||||
results[1].login.password,
|
||||
"new password",
|
||||
`Check that the newest password is imported`
|
||||
);
|
||||
equal(
|
||||
results[1].result,
|
||||
"added",
|
||||
`Check that the newest password result is correct`
|
||||
);
|
||||
equal(initialLoginCount, 0, `Check that initially we had no logins`);
|
||||
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports duplicate logins as one without an error.
|
||||
*/
|
||||
add_task(async function test_import_duplicate_logins_as_one() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"name,url,username,password",
|
||||
"somesite,https://example.com/,user@example.com,asdasd123123",
|
||||
"somesite,https://example.com/,user@example.com,asdasd123123",
|
||||
]);
|
||||
let initialLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
let results = await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
let afterImportLoginCount = Services.logins.getAllLogins().length;
|
||||
|
||||
equal(results.length, 2, `Check that we got a result for each imported row`);
|
||||
equal(
|
||||
results[0].result,
|
||||
"added",
|
||||
`Check that the first login login was added`
|
||||
);
|
||||
equal(
|
||||
results[1].result,
|
||||
"no_change",
|
||||
`Check that the second login was not changed`
|
||||
);
|
||||
|
||||
equal(initialLoginCount, 0, `Check that initially we had no logins`);
|
||||
equal(afterImportLoginCount, 1, `Check that we imported only one login`);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче