1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-28 15:23:53 +00:00

Merge branch 'main' into PM-29919-Add-dropdown-to-select-email-verification-and-emails-field-to-Send-when-creating-or-editing-a-Send

This commit is contained in:
bmbitwarden
2026-01-22 18:00:06 -05:00
committed by GitHub
13 changed files with 193 additions and 2 deletions

View File

@@ -24,12 +24,13 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
const IMPORTERS: &[(&str, &str)] = &[
("arccsv", "Arc"),
("bravecsv", "Brave"),
("chromecsv", "Chrome"),
("chromiumcsv", "Chromium"),
("bravecsv", "Brave"),
("edgecsv", "Microsoft Edge"),
("operacsv", "Opera"),
("vivaldicsv", "Vivaldi"),
("edgecsv", "Microsoft Edge"),
];
let supported: HashSet<&'static str> =
@@ -91,6 +92,7 @@ mod tests {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let expected: HashSet<String> = HashSet::from([
"arccsv".to_string(),
"chromecsv".to_string(),
"chromiumcsv".to_string(),
"bravecsv".to_string(),
@@ -113,6 +115,7 @@ mod tests {
fn macos_specific_loaders_match_const_array() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let ids = [
"arccsv",
"chromecsv",
"chromiumcsv",
"bravecsv",

View File

@@ -195,6 +195,8 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
return "Brave";
} else if (format === "vivaldicsv") {
return "Vivaldi";
} else if (format === "arccsv") {
return "Arc";
}
return "Chrome";
}

View File

@@ -0,0 +1,139 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ArcCsvImporter } from "./arc-csv-importer";
import { data as missingNameAndUrlData } from "./spec-data/arc-csv/missing-name-and-url-data.csv";
import { data as missingNameWithUrlData } from "./spec-data/arc-csv/missing-name-with-url-data.csv";
import { data as passwordWithNoteData } from "./spec-data/arc-csv/password-with-note-data.csv";
import { data as simplePasswordData } from "./spec-data/arc-csv/simple-password-data.csv";
import { data as subdomainData } from "./spec-data/arc-csv/subdomain-data.csv";
import { data as urlWithWwwData } from "./spec-data/arc-csv/url-with-www-data.csv";
const CipherData = [
{
title: "should parse password",
csv: simplePasswordData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should parse password with note",
csv: passwordWithNoteData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/",
}),
],
}),
notes: "This is a test note",
type: 1,
}),
},
{
title: "should strip www. prefix from name",
csv: urlWithWwwData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://www.example.com/",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should extract name from URL when name is missing",
csv: missingNameWithUrlData,
expected: Object.assign(new CipherView(), {
name: "example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com/login",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: "should use -- as name when both name and URL are missing",
csv: missingNameAndUrlData,
expected: Object.assign(new CipherView(), {
name: "--",
login: Object.assign(new LoginView(), {
username: null,
password: "password123",
uris: null,
}),
notes: null,
type: 1,
}),
},
{
title: "should preserve subdomain in name",
csv: subdomainData,
expected: Object.assign(new CipherView(), {
name: "login.example.com",
login: Object.assign(new LoginView(), {
username: "user@example.com",
password: "password123",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://login.example.com/auth",
}),
],
}),
notes: null,
type: 1,
}),
},
];
describe("Arc CSV Importer", () => {
CipherData.forEach((data) => {
it(data.title, async () => {
jest.useFakeTimers().setSystemTime(data.expected.creationDate);
const importer = new ArcCsvImporter();
const result = await importer.parse(data.csv);
expect(result != null).toBe(true);
expect(result.ciphers.length).toBeGreaterThan(0);
const cipher = result.ciphers.shift();
let property: keyof typeof data.expected;
for (property in data.expected) {
if (Object.prototype.hasOwnProperty.call(data.expected, property)) {
expect(Object.prototype.hasOwnProperty.call(cipher, property)).toBe(true);
expect(cipher[property]).toEqual(data.expected[property]);
}
}
});
});
});

View File

@@ -0,0 +1,30 @@
import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
export class ArcCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
const url = this.getValueOrDefault(value.url);
cipher.name = this.getValueOrDefault(this.nameFromUrl(url) ?? "", "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.url);
cipher.notes = this.getValueOrDefault(value.note);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,3 +1,4 @@
export { ArcCsvImporter } from "./arc-csv-importer";
export { AscendoCsvImporter } from "./ascendo-csv-importer";
export { AvastCsvImporter, AvastJsonImporter } from "./avast";
export { AviraCsvImporter } from "./avira-csv-importer";

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,,,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
,https://example.com/login,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,This is a test note`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
example.com,https://example.com/,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
login.example.com,https://login.example.com/auth,user@example.com,password123,`;

View File

@@ -0,0 +1,2 @@
export const data = `name,url,username,password,note
www.example.com,https://www.example.com/,user@example.com,password123,`;

View File

@@ -46,6 +46,7 @@ export const regularImportOptions = [
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "arccsv", name: "Arc" },
{ id: "edgecsv", name: "Edge" },
{ id: "operacsv", name: "Opera" },
{ id: "vivaldicsv", name: "Vivaldi" },

View File

@@ -31,6 +31,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res
import { KeyService } from "@bitwarden/key-management";
import {
ArcCsvImporter,
AscendoCsvImporter,
AvastCsvImporter,
AvastJsonImporter,
@@ -256,6 +257,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "arccsv":
return new ArcCsvImporter();
case "edgecsv":
case "chromecsv":
case "operacsv":