diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.html b/apps/browser/src/autofill/popup/settings/notifications.component.html
index 86fe4923df8..c6446012d0c 100644
--- a/apps/browser/src/autofill/popup/settings/notifications.component.html
+++ b/apps/browser/src/autofill/popup/settings/notifications.component.html
@@ -50,7 +50,7 @@
{{ "excludedDomains" | i18n }}
-
+
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index abc3aa3a1d7..36143682fa2 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -9969,5 +9969,8 @@
},
"domainClaimed": {
"message": "Domain claimed"
+ },
+ "organizationNameMaxLength": {
+ "message": "Organization name cannot exceed 50 characters."
}
}
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html
index a08f5710f1e..78f2cb41bef 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html
+++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html
@@ -43,6 +43,14 @@
{{ "organizationName" | i18n }}
+
+ {{ "organizationNameMaxLength" | i18n }}
+
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts
index 18910491a0c..2a27b1b32f3 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts
+++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts
@@ -47,7 +47,7 @@ export class CreateClientDialogComponent implements OnInit {
protected discountPercentage: number;
protected formGroup = new FormGroup({
clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]),
- organizationName: new FormControl("", [Validators.required]),
+ organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]),
seats: new FormControl(null, [Validators.required, Validators.min(1)]),
});
protected loading = true;
diff --git a/libs/importer/spec/passwordxp-csv-importer.spec.ts b/libs/importer/spec/passwordxp-csv-importer.spec.ts
index f707b1138c5..fda323450c6 100644
--- a/libs/importer/spec/passwordxp-csv-importer.spec.ts
+++ b/libs/importer/spec/passwordxp-csv-importer.spec.ts
@@ -3,10 +3,46 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { PasswordXPCsvImporter } from "../src/importers";
import { ImportResult } from "../src/models/import-result";
+import { dutchHeaders } from "./test-data/passwordxp-csv/dutch-headers";
+import { germanHeaders } from "./test-data/passwordxp-csv/german-headers";
import { noFolder } from "./test-data/passwordxp-csv/no-folder.csv";
import { withFolders } from "./test-data/passwordxp-csv/passwordxp-with-folders.csv";
import { withoutFolders } from "./test-data/passwordxp-csv/passwordxp-without-folders.csv";
+async function importLoginWithCustomFields(importer: PasswordXPCsvImporter, csvData: string) {
+ const result: ImportResult = await importer.parse(csvData);
+ expect(result.success).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.Login);
+ expect(cipher.name).toBe("Title2");
+ expect(cipher.notes).toBe("Test Notes");
+ expect(cipher.login.username).toBe("Username2");
+ expect(cipher.login.password).toBe("12345678");
+ expect(cipher.login.uris[0].uri).toBe("http://URL2.com");
+
+ expect(cipher.fields.length).toBe(5);
+ let field = cipher.fields.shift();
+ expect(field.name).toBe("Account");
+ expect(field.value).toBe("Account2");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Modified");
+ expect(field.value).toBe("27-3-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Created");
+ expect(field.value).toBe("27-3-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Expire on");
+ expect(field.value).toBe("27-5-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Modified by");
+ expect(field.value).toBe("someone");
+}
+
describe("PasswordXPCsvImporter", () => {
let importer: PasswordXPCsvImporter;
@@ -20,6 +56,12 @@ describe("PasswordXPCsvImporter", () => {
expect(result.success).toBe(false);
});
+ it("should return success false if CSV headers did not get translated", async () => {
+ const data = germanHeaders.replace("Titel;", "UnknownTitle;");
+ const result: ImportResult = await importer.parse(data);
+ expect(result.success).toBe(false);
+ });
+
it("should skip rows starting with >>>", async () => {
const result: ImportResult = await importer.parse(noFolder);
expect(result.success).toBe(true);
@@ -61,38 +103,16 @@ describe("PasswordXPCsvImporter", () => {
expect(cipher.login.uris[0].uri).toBe("http://test");
});
- it("should parse CSV data and import unmapped columns as custom fields", async () => {
- const result: ImportResult = await importer.parse(withoutFolders);
- expect(result.success).toBe(true);
+ it("should parse CSV data with English headers and import unmapped columns as custom fields", async () => {
+ await importLoginWithCustomFields(importer, withoutFolders);
+ });
- const cipher = result.ciphers.shift();
- expect(cipher.type).toBe(CipherType.Login);
- expect(cipher.name).toBe("Title2");
- expect(cipher.notes).toBe("Test Notes");
- expect(cipher.login.username).toBe("Username2");
- expect(cipher.login.password).toBe("12345678");
- expect(cipher.login.uris[0].uri).toBe("http://URL2.com");
+ it("should parse CSV data with German headers and import unmapped columns as custom fields", async () => {
+ await importLoginWithCustomFields(importer, germanHeaders);
+ });
- expect(cipher.fields.length).toBe(5);
- let field = cipher.fields.shift();
- expect(field.name).toBe("Account");
- expect(field.value).toBe("Account2");
-
- field = cipher.fields.shift();
- expect(field.name).toBe("Modified");
- expect(field.value).toBe("27-3-2024 08:11:21");
-
- field = cipher.fields.shift();
- expect(field.name).toBe("Created");
- expect(field.value).toBe("27-3-2024 08:11:21");
-
- field = cipher.fields.shift();
- expect(field.name).toBe("Expire on");
- expect(field.value).toBe("27-5-2024 08:11:21");
-
- field = cipher.fields.shift();
- expect(field.name).toBe("Modified by");
- expect(field.value).toBe("someone");
+ it("should parse CSV data with Dutch headers and import unmapped columns as custom fields", async () => {
+ await importLoginWithCustomFields(importer, dutchHeaders);
});
it("should parse CSV data with folders and assign items to them", async () => {
diff --git a/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts
new file mode 100644
index 00000000000..9cab04f1e6d
--- /dev/null
+++ b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts
@@ -0,0 +1,7 @@
+export const dutchHeaders = `Titel;Gebruikersnaam;Account;URL;Wachtwoord;Gewijzigd;Gemaakt;Verloopt op;Beschrijving;Gewijzigd door
+>>>
+Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone
+Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2;
+Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1;
+test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3;
+`;
diff --git a/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts
new file mode 100644
index 00000000000..a6ac21c76d6
--- /dev/null
+++ b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts
@@ -0,0 +1,7 @@
+export const germanHeaders = `Titel;Benutzername;Konto;URL;Passwort;Geändert am;Erstellt am;Läuft ab am;Beschreibung;Geändert von
+>>>
+Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone
+Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2;
+Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1;
+test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3;
+`;
diff --git a/libs/importer/src/importers/index.ts b/libs/importer/src/importers/index.ts
index 19b22cfa80d..1ba3a0d9eb8 100644
--- a/libs/importer/src/importers/index.ts
+++ b/libs/importer/src/importers/index.ts
@@ -45,7 +45,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer";
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";
-export { PasswordXPCsvImporter } from "./passwordxp-csv-importer";
+export { PasswordXPCsvImporter } from "./passsordxp/passwordxp-csv-importer";
export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer";
export { PsonoJsonImporter } from "./psono/psono-json-importer";
export { RememBearCsvImporter } from "./remembear-csv-importer";
diff --git a/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts
new file mode 100644
index 00000000000..7f9c219de56
--- /dev/null
+++ b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts
@@ -0,0 +1,10 @@
+export const dutchHeaderTranslations: { [key: string]: string } = {
+ Titel: "Title",
+ Gebruikersnaam: "Username",
+ Wachtwoord: "Password",
+ Gewijzigd: "Modified",
+ Gemaakt: "Created",
+ "Verloopt op": "Expire on",
+ Beschrijving: "Description",
+ "Gewijzigd door": "Modified by",
+};
diff --git a/libs/importer/src/importers/passsordxp/german-csv-headers.ts b/libs/importer/src/importers/passsordxp/german-csv-headers.ts
new file mode 100644
index 00000000000..584ad0badca
--- /dev/null
+++ b/libs/importer/src/importers/passsordxp/german-csv-headers.ts
@@ -0,0 +1,11 @@
+export const germanHeaderTranslations: { [key: string]: string } = {
+ Titel: "Title",
+ Benutzername: "Username",
+ Konto: "Account",
+ Passwort: "Password",
+ "Geändert am": "Modified",
+ "Erstellt am": "Created",
+ "Läuft ab am": "Expire on",
+ Beschreibung: "Description",
+ "Geändert von": "Modified by",
+};
diff --git a/libs/importer/src/importers/passwordxp-csv-importer.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts
similarity index 68%
rename from libs/importer/src/importers/passwordxp-csv-importer.ts
rename to libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts
index 461432e98d4..226a284ec91 100644
--- a/libs/importer/src/importers/passwordxp-csv-importer.ts
+++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts
@@ -1,12 +1,28 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
-import { ImportResult } from "../models/import-result";
-
-import { BaseImporter } from "./base-importer";
-import { Importer } from "./importer";
+import { ImportResult } from "../../models/import-result";
+import { BaseImporter } from "../base-importer";
+import { Importer } from "../importer";
const _mappedColumns = new Set(["Title", "Username", "URL", "Password", "Description"]);
+import { dutchHeaderTranslations } from "./dutch-csv-headers";
+import { germanHeaderTranslations } from "./german-csv-headers";
+
+/* Translates the headers from non-English to English
+ * This is necessary because the parser only maps English headers to ciphers
+ * Currently only supports German and Dutch translations
+ */
+function translateIntoEnglishHeaders(header: string): string {
+ const translations: { [key: string]: string } = {
+ // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing.
+ "User name": "Username",
+ ...germanHeaderTranslations,
+ ...dutchHeaderTranslations,
+ };
+
+ return translations[header] || header;
+}
/**
* PasswordXP CSV importer
@@ -17,15 +33,22 @@ export class PasswordXPCsvImporter extends BaseImporter implements Importer {
* @param data
*/
parse(data: string): Promise {
- // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing.
- data = data.replace(";User name;", ";Username;");
-
const result = new ImportResult();
- const results = this.parseCsv(data, true, { skipEmptyLines: true });
+ const results = this.parseCsv(data, true, {
+ skipEmptyLines: true,
+ transformHeader: translateIntoEnglishHeaders,
+ });
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
+
+ // If the first row (header check) does not contain the column "Title", then the data is invalid (no translation found)
+ if (!results[0].Title) {
+ result.success = false;
+ return Promise.resolve(result);
+ }
+
let currentFolderName = "";
results.forEach((row) => {
// Skip rows starting with '>>>' as they indicate items following have no folder assigned to them