1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 11:13:46 +00:00

[PM-11882] Handled identity item and unsupported items during ProtonPass import. (#10967)

This commit is contained in:
Aftab Ali
2024-09-18 21:58:47 +05:30
committed by GitHub
parent 2d7fb035d4
commit 0f3d8a6f89
6 changed files with 504 additions and 6 deletions

View File

@@ -0,0 +1,66 @@
import { processNames } from "./protonpass-import-utils";
describe("processNames", () => {
it("should use only fullName to map names if it contains at least three words, ignoring individual name fields", () => {
const result = processNames("Alice Beth Carter", "Kevin", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should map extra words to the middle name if fullName contains more than three words", () => {
const result = processNames("Alice Beth Middle Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth Middle",
mappedLastName: "Carter",
});
});
it("should map names correctly even if fullName has words separated by more than one space", () => {
const result = processNames("Alice Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "",
mappedLastName: "Carter",
});
});
it("should handle a single name in fullName and use middleName and lastName to populate rest of names", () => {
const result = processNames("Alice", "", "Beth", "Carter");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should correctly map fullName when it only contains two words", () => {
const result = processNames("Alice Carter", "", "", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "",
mappedLastName: "Carter",
});
});
it("should map middle name from middleName if fullName only contains two words", () => {
const result = processNames("Alice Carter", "", "Beth", "");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
it("should fall back to firstName, middleName, and lastName if fullName is empty", () => {
const result = processNames("", "Alice", "Beth", "Carter");
expect(result).toEqual({
mappedFirstName: "Alice",
mappedMiddleName: "Beth",
mappedLastName: "Carter",
});
});
});

View File

@@ -0,0 +1,21 @@
export function processNames(
fullname: string | null,
firstname: string | null,
middlename: string | null,
lastname: string | null,
) {
let mappedFirstName = firstname;
let mappedMiddleName = middlename;
let mappedLastName = lastname;
if (fullname) {
const parts = fullname.trim().split(/\s+/);
// Assign parts to first, middle, and last name based on the number of parts
mappedFirstName = parts[0] || firstname;
mappedLastName = parts.length > 1 ? parts[parts.length - 1] : lastname;
mappedMiddleName = parts.length > 2 ? parts.slice(1, -1).join(" ") : middlename;
}
return { mappedFirstName, mappedMiddleName, mappedLastName };
}

View File

@@ -1,24 +1,110 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { ImportResult } from "../../models/import-result";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { processNames } from "./protonpass-import-utils";
import {
ProtonPassCreditCardItemContent,
ProtonPassIdentityItemContent,
ProtonPassIdentityItemExtraSection,
ProtonPassItemExtraField,
ProtonPassItemState,
ProtonPassJsonFile,
ProtonPassLoginItemContent,
} from "./types/protonpass-json-type";
export class ProtonPassJsonImporter extends BaseImporter implements Importer {
private mappedIdentityItemKeys = [
"fullName",
"firstName",
"middleName",
"lastName",
"email",
"phoneNumber",
"company",
"socialSecurityNumber",
"passportNumber",
"licenseNumber",
"organization",
"streetAddress",
"floor",
"county",
"city",
"stateOrProvince",
"zipOrPostalCode",
"countryOrRegion",
];
private identityItemExtraFieldsKeys = [
"extraPersonalDetails",
"extraAddressDetails",
"extraContactDetails",
"extraWorkDetails",
"extraSections",
];
constructor(private i18nService: I18nService) {
super();
}
private processIdentityItemUnmappedAndExtraFields(
cipher: CipherView,
identityItem: ProtonPassIdentityItemContent,
) {
Object.keys(identityItem).forEach((key) => {
if (
!this.mappedIdentityItemKeys.includes(key) &&
!this.identityItemExtraFieldsKeys.includes(key)
) {
this.processKvp(
cipher,
key,
identityItem[key as keyof ProtonPassIdentityItemContent] as string,
);
return;
}
if (this.identityItemExtraFieldsKeys.includes(key)) {
if (key !== "extraSections") {
const extraFields = identityItem[
key as keyof ProtonPassIdentityItemContent
] as ProtonPassItemExtraField[];
extraFields?.forEach((extraField) => {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
});
} else {
const extraSections = identityItem[
key as keyof ProtonPassIdentityItemContent
] as ProtonPassIdentityItemExtraSection[];
extraSections?.forEach((extraSection) => {
extraSection.sectionFields?.forEach((extraField) => {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
});
});
}
}
});
}
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results: ProtonPassJsonFile = JSON.parse(data);
@@ -38,7 +124,6 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer {
if (item.state == ProtonPassItemState.TRASHED) {
continue;
}
this.processFolder(result, vault.name);
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(item.data.metadata.name, "--");
@@ -96,8 +181,55 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer {
break;
}
case "identity": {
const identityContent = item.data.content as ProtonPassIdentityItemContent;
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
const { mappedFirstName, mappedMiddleName, mappedLastName } = processNames(
this.getValueOrDefault(identityContent.fullName),
this.getValueOrDefault(identityContent.firstName),
this.getValueOrDefault(identityContent.middleName),
this.getValueOrDefault(identityContent.lastName),
);
cipher.identity.firstName = mappedFirstName;
cipher.identity.middleName = mappedMiddleName;
cipher.identity.lastName = mappedLastName;
cipher.identity.email = this.getValueOrDefault(identityContent.email);
cipher.identity.phone = this.getValueOrDefault(identityContent.phoneNumber);
cipher.identity.company = this.getValueOrDefault(identityContent.company);
cipher.identity.ssn = this.getValueOrDefault(identityContent.socialSecurityNumber);
cipher.identity.passportNumber = this.getValueOrDefault(identityContent.passportNumber);
cipher.identity.licenseNumber = this.getValueOrDefault(identityContent.licenseNumber);
const address3 =
`${identityContent.floor ?? ""} ${identityContent.county ?? ""}`.trim();
cipher.identity.address1 = this.getValueOrDefault(identityContent.organization);
cipher.identity.address2 = this.getValueOrDefault(identityContent.streetAddress);
cipher.identity.address3 = this.getValueOrDefault(address3);
cipher.identity.city = this.getValueOrDefault(identityContent.city);
cipher.identity.state = this.getValueOrDefault(identityContent.stateOrProvince);
cipher.identity.postalCode = this.getValueOrDefault(identityContent.zipOrPostalCode);
cipher.identity.country = this.getValueOrDefault(identityContent.countryOrRegion);
this.processIdentityItemUnmappedAndExtraFields(cipher, identityContent);
for (const extraField of item.data.extraFields) {
this.processKvp(
cipher,
extraField.fieldName,
extraField.data.content,
extraField.type === "hidden" ? FieldType.Hidden : FieldType.Text,
);
}
break;
}
default:
continue;
}
this.processFolder(result, vault.name);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
}

View File

@@ -36,8 +36,11 @@ export type ProtonPassItemData = {
metadata: ProtonPassItemMetadata;
extraFields: ProtonPassItemExtraField[];
platformSpecific?: any;
type: "login" | "alias" | "creditCard" | "note";
content: ProtonPassLoginItemContent | ProtonPassCreditCardItemContent;
type: "login" | "alias" | "creditCard" | "note" | "identity";
content:
| ProtonPassLoginItemContent
| ProtonPassCreditCardItemContent
| ProtonPassIdentityItemContent;
};
export type ProtonPassItemMetadata = {
@@ -74,3 +77,48 @@ export type ProtonPassCreditCardItemContent = {
expirationDate?: string;
pin?: string;
};
export type ProtonPassIdentityItemExtraSection = {
sectionName?: string;
sectionFields?: ProtonPassItemExtraField[];
};
export type ProtonPassIdentityItemContent = {
fullName?: string;
email?: string;
phoneNumber?: string;
firstName?: string;
middleName?: string;
lastName?: string;
birthdate?: string;
gender?: string;
extraPersonalDetails?: ProtonPassItemExtraField[];
organization?: string;
streetAddress?: string;
zipOrPostalCode?: string;
city?: string;
stateOrProvince?: string;
countryOrRegion?: string;
floor?: string;
county?: string;
extraAddressDetails?: ProtonPassItemExtraField[];
socialSecurityNumber?: string;
passportNumber?: string;
licenseNumber?: string;
website?: string;
xHandle?: string;
secondPhoneNumber?: string;
linkedin?: string;
reddit?: string;
facebook?: string;
yahoo?: string;
instagram?: string;
extraContactDetails?: ProtonPassItemExtraField[];
company?: string;
jobTitle?: string;
personalWebsite?: string;
workPhoneNumber?: string;
workEmail?: string;
extraWorkDetails?: ProtonPassItemExtraField[];
extraSections?: ProtonPassIdentityItemExtraSection[];
};