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:
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user