From 7015663c3828b747444679d30d6e7e729fdf016d Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Fri, 17 Oct 2025 09:46:10 -0400 Subject: [PATCH] [PM-25521] Move importer metadata to native code (#16695) * Add importer metadata to native code * Impl napi code in ts * Impl napi code in ts * Fix clippy * Fix clippy * remove ts util tests * Check for installed browsers * PR fixes * test fix * fix clippy * fix tests * Bug fix * clippy fix * Correct tests * fix clippy * fix clippy * Correct tests * Correct tests * [PM-25521] Wire up loading metadata on desktop (#16813) * Initial commit * Fix issues regarding now unused feature flag * Fixed ts-strict issues --------- Co-authored-by: Daniel James Smith Co-authored-by: adudek-bw * Remove logic to skip Brave as that now happens via the native code * Define default capabilities which can be overwritten by specifc client/platform * Fix DI issues * Do not overwrite existing importers, just add new ones or update existing ones * feat: [PM-25521] return metadata directly (not as JSON) (#16882) * feat: return metadata directly (not as JSON) * Fix broken builds Move getMetaData into chromium_importer Remove chromium_importer_metadata and any related service Parse object from native instead of json * Run cargo fmt * Fix cargo dependency sort order * Use exposed type from NAPI instead of redefining it. * Run cargo fmt --------- Co-authored-by: Daniel James Smith * Only enable chromium loader for installed and supported browsers --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Daniel James Smith Co-authored-by: Andreas Coroiu --- .../browser/src/background/main.background.ts | 24 ++- .../import/import-browser-v2.component.ts | 19 +- .../service-container/service-container.ts | 24 ++- apps/desktop/desktop_native/Cargo.lock | 2 + .../bitwarden_chromium_importer/Cargo.toml | 2 + .../src/chromium.rs | 42 ++-- .../bitwarden_chromium_importer/src/lib.rs | 7 + .../bitwarden_chromium_importer/src/linux.rs | 2 +- .../bitwarden_chromium_importer/src/macos.rs | 2 +- .../src/metadata.rs | 203 ++++++++++++++++++ .../src/windows.rs | 14 +- apps/desktop/desktop_native/napi/index.d.ts | 11 + apps/desktop/desktop_native/napi/src/lib.rs | 14 +- .../tools/import/chromium-importer.service.ts | 4 + .../import/desktop-import-metadata.service.ts | 66 ++++++ .../tools/import/import-desktop.component.ts | 18 +- apps/desktop/src/app/tools/preload.ts | 4 + .../app/tools/import/import-web.component.ts | 19 +- .../src/components/import.component.ts | 11 +- .../src/components/importer-providers.ts | 5 +- libs/importer/src/components/index.ts | 1 + libs/importer/src/index.ts | 1 + libs/importer/src/metadata/importers.ts | 20 +- .../default-import-metadata.service.ts | 51 +++++ .../import-metadata.service.abstraction.ts | 11 + .../services/import-metadata.service.spec.ts | 110 ++++++++++ .../services/import.service.abstraction.ts | 5 - .../src/services/import.service.spec.ts | 153 ------------- libs/importer/src/services/import.service.ts | 54 +---- libs/importer/src/services/index.ts | 3 + libs/importer/src/util.spec.ts | 60 ------ libs/importer/src/util.ts | 16 +- 32 files changed, 641 insertions(+), 337 deletions(-) create mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs create mode 100644 apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts create mode 100644 libs/importer/src/services/default-import-metadata.service.ts create mode 100644 libs/importer/src/services/import-metadata.service.abstraction.ts create mode 100644 libs/importer/src/services/import-metadata.service.spec.ts delete mode 100644 libs/importer/src/util.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7181287aaa..ef661a0024 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -221,8 +221,10 @@ import { UsernameGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; import { + DefaultImportMetadataService, ImportApiService, ImportApiServiceAbstraction, + ImportMetadataServiceAbstraction, ImportService, ImportServiceAbstraction, } from "@bitwarden/importer-core"; @@ -368,6 +370,7 @@ export default class MainBackground { authService: AuthServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; + importMetadataService: ImportMetadataServiceAbstraction; importService: ImportServiceAbstraction; exportApiService: VaultExportApiService; exportService: VaultExportServiceAbstraction; @@ -1088,6 +1091,18 @@ export default class MainBackground { this.importApiService = new ImportApiService(this.apiService); + this.importMetadataService = new DefaultImportMetadataService( + createSystemServiceProvider( + new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), + this.stateProvider, + this.policyService, + buildExtensionRegistry(), + this.logService, + this.platformUtilsService, + this.configService, + ), + ); + this.importService = new ImportService( this.cipherService, this.folderService, @@ -1099,15 +1114,6 @@ export default class MainBackground { this.pinService, this.accountService, this.restrictedItemTypesService, - createSystemServiceProvider( - new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), - this.stateProvider, - this.policyService, - buildExtensionRegistry(), - this.logService, - this.platformUtilsService, - this.configService, - ), ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts index 506dae2fb1..6397ddc185 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts @@ -4,7 +4,16 @@ import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; -import { ImportComponent } from "@bitwarden/importer-ui"; +import { + DefaultImportMetadataService, + ImportMetadataServiceAbstraction, +} from "@bitwarden/importer-core"; +import { + ImportComponent, + ImporterProviders, + SYSTEM_SERVICE_PROVIDER, +} from "@bitwarden/importer-ui"; +import { safeProvider } from "@bitwarden/ui-common"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; @@ -25,6 +34,14 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopupHeaderComponent, PopOutComponent, ], + providers: [ + ...ImporterProviders, + safeProvider({ + provide: ImportMetadataServiceAbstraction, + useClass: DefaultImportMetadataService, + deps: [SYSTEM_SERVICE_PROVIDER], + }), + ], }) export class ImportBrowserV2Component { protected disabled = false; diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 5e4de8d856..02009ad7aa 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -151,8 +151,10 @@ import { PasswordGenerationServiceAbstraction, } from "@bitwarden/generator-legacy"; import { + DefaultImportMetadataService, ImportApiService, ImportApiServiceAbstraction, + ImportMetadataServiceAbstraction, ImportService, ImportServiceAbstraction, } from "@bitwarden/importer-core"; @@ -252,6 +254,7 @@ export class ServiceContainer { auditService: AuditService; importService: ImportServiceAbstraction; importApiService: ImportApiServiceAbstraction; + importMetadataService: ImportMetadataServiceAbstraction; exportService: VaultExportServiceAbstraction; vaultExportApiService: VaultExportApiService; individualExportService: IndividualVaultExportServiceAbstraction; @@ -845,6 +848,18 @@ export class ServiceContainer { this.importApiService = new ImportApiService(this.apiService); + this.importMetadataService = new DefaultImportMetadataService( + createSystemServiceProvider( + new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), + this.stateProvider, + this.policyService, + buildExtensionRegistry(), + this.logService, + this.platformUtilsService, + this.configService, + ), + ); + this.importService = new ImportService( this.cipherService, this.folderService, @@ -856,15 +871,6 @@ export class ServiceContainer { this.pinService, this.accountService, this.restrictedItemTypesService, - createSystemServiceProvider( - new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), - this.stateProvider, - this.policyService, - buildExtensionRegistry(), - this.logService, - this.platformUtilsService, - this.configService, - ), ); this.individualExportService = new IndividualVaultExportService( diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4fe1dc8cd8..053e2fc380 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -450,6 +450,8 @@ dependencies = [ "cbc", "hex", "homedir", + "napi", + "napi-derive", "oo7", "pbkdf2", "rand 0.9.1", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index d1efbd006f..656c3ad150 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -14,6 +14,8 @@ base64 = { workspace = true } cbc = { workspace = true, features = ["alloc"] } hex = { workspace = true } homedir = { workspace = true } +napi = { workspace = true } +napi-derive = { workspace = true } pbkdf2 = "=0.12.2" rand = { workspace = true } rusqlite = { version = "=0.37.0", features = ["bundled"] } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 8179a10213..094500e6d4 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -11,7 +11,7 @@ use rusqlite::{params, Connection}; #[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "windows", path = "windows.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")] -mod platform; +pub mod platform; // // Public API @@ -50,18 +50,26 @@ pub enum LoginImportResult { Failure(LoginImportFailure), } -// TODO: Make thus async -pub fn get_installed_browsers() -> Result> { - let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len()); +pub trait InstalledBrowserRetriever { + fn get_installed_browsers() -> Result>; +} - for (browser, config) in SUPPORTED_BROWSER_MAP.iter() { - let data_dir = get_browser_data_dir(config)?; - if data_dir.exists() { - browsers.push((*browser).to_string()); +pub struct DefaultInstalledBrowserRetriever {} + +impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { + // TODO: Make thus async + fn get_installed_browsers() -> Result> { + let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len()); + + for (browser, config) in SUPPORTED_BROWSER_MAP.iter() { + let data_dir = get_browser_data_dir(config)?; + if data_dir.exists() { + browsers.push((*browser).to_string()); + } } - } - Ok(browsers) + Ok(browsers) + } } // TODO: Make thus async @@ -104,13 +112,13 @@ pub async fn import_logins( // Private // -#[derive(Debug)] -struct BrowserConfig { - name: &'static str, - data_dir: &'static str, +#[derive(Debug, Clone, Copy)] +pub struct BrowserConfig { + pub name: &'static str, + pub data_dir: &'static str, } -static SUPPORTED_BROWSER_MAP: LazyLock< +pub static SUPPORTED_BROWSER_MAP: LazyLock< std::collections::HashMap<&'static str, &'static BrowserConfig>, > = LazyLock::new(|| { platform::SUPPORTED_BROWSERS @@ -132,12 +140,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result { // #[async_trait] -trait CryptoService: Send { +pub trait CryptoService: Send { async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result; } #[derive(serde::Deserialize, Clone)] -struct LocalState { +pub struct LocalState { profile: AllProfiles, #[allow(dead_code)] os_crypt: Option, diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs index b0a399d632..84f140d234 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs @@ -1 +1,8 @@ +#[macro_use] +extern crate napi_derive; + pub mod chromium; +pub mod metadata; +pub mod util; + +pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index 0ead034a4b..be3bcdb1e1 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -6,7 +6,7 @@ use oo7::XDG_SCHEMA_ATTRIBUTE; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; -mod util; +use crate::util; // // Public API diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index d9aeff68f2..bcb2c00500 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -4,7 +4,7 @@ use security_framework::passwords::get_generic_password; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; -mod util; +use crate::util; // // Public API diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs new file mode 100644 index 0000000000..a95a86ef0e --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs @@ -0,0 +1,203 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS}; + +#[napi(object)] +/// Mechanisms that load data into the importer +pub struct NativeImporterMetadata { + /// Identifies the importer + pub id: String, + /// Describes the strategies used to obtain imported data + pub loaders: Vec<&'static str>, + /// Identifies the instructions for the importer + pub instructions: &'static str, +} + +/// Returns a map of supported importers based on the current platform. +/// +/// Only browsers listed in PLATFORM_SUPPORTED_BROWSERS will have the "chromium" loader. +/// All importers will have the "file" loader. +pub fn get_supported_importers( +) -> HashMap { + let mut map = HashMap::new(); + + // Check for installed browsers + let installed_browsers = T::get_installed_browsers().unwrap_or_default(); + + const IMPORTERS: [(&str, &str); 6] = [ + ("chromecsv", "Chrome"), + ("chromiumcsv", "Chromium"), + ("bravecsv", "Brave"), + ("operacsv", "Opera"), + ("vivaldicsv", "Vivaldi"), + ("edgecsv", "Microsoft Edge"), + ]; + + let supported: HashSet<&'static str> = + PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect(); + + for (id, browser_name) in IMPORTERS { + let mut loaders: Vec<&'static str> = vec!["file"]; + if supported.contains(browser_name) { + loaders.push("chromium"); + } + + if installed_browsers.contains(&browser_name.to_string()) { + map.insert( + id.to_string(), + NativeImporterMetadata { + id: id.to_string(), + loaders, + instructions: "chromium", + }, + ); + } + } + + map +} + +/* + Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage +*/ +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + use crate::chromium::{InstalledBrowserRetriever, SUPPORTED_BROWSER_MAP}; + + pub struct MockInstalledBrowserRetriever {} + + impl InstalledBrowserRetriever for MockInstalledBrowserRetriever { + fn get_installed_browsers() -> Result, anyhow::Error> { + Ok(SUPPORTED_BROWSER_MAP + .keys() + .map(|browser| browser.to_string()) + .collect()) + } + } + + fn map_keys(map: &HashMap) -> HashSet { + map.keys().cloned().collect() + } + + fn get_loaders( + map: &HashMap, + id: &str, + ) -> HashSet<&'static str> { + map.get(id) + .map(|m| m.loaders.iter().copied().collect::>()) + .unwrap_or_default() + } + + #[cfg(target_os = "macos")] + #[test] + fn macos_returns_all_known_importers() { + let map = get_supported_importers::(); + + let expected: HashSet = HashSet::from([ + "chromecsv".to_string(), + "chromiumcsv".to_string(), + "bravecsv".to_string(), + "operacsv".to_string(), + "vivaldicsv".to_string(), + "edgecsv".to_string(), + ]); + assert_eq!(map.len(), expected.len()); + assert_eq!(map_keys(&map), expected); + + for (key, meta) in map.iter() { + assert_eq!(&meta.id, key); + assert_eq!(meta.instructions, "chromium"); + assert!(meta.loaders.iter().any(|l| *l == "file")); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn macos_specific_loaders_match_const_array() { + let map = get_supported_importers::(); + let ids = [ + "chromecsv", + "chromiumcsv", + "bravecsv", + "operacsv", + "vivaldicsv", + "edgecsv", + ]; + for id in ids { + let loaders = get_loaders(&map, id); + assert!(loaders.contains("file")); + assert!(loaders.contains("chromium"), "missing chromium for {id}"); + } + } + + #[cfg(target_os = "linux")] + #[test] + fn returns_all_known_importers() { + let map = get_supported_importers::(); + + let expected: HashSet = HashSet::from([ + "chromecsv".to_string(), + "chromiumcsv".to_string(), + "bravecsv".to_string(), + "operacsv".to_string(), + ]); + assert_eq!(map.len(), expected.len()); + assert_eq!(map_keys(&map), expected); + + for (key, meta) in map.iter() { + assert_eq!(&meta.id, key); + assert_eq!(meta.instructions, "chromium"); + assert!(meta.loaders.iter().any(|l| *l == "file")); + } + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_specific_loaders_match_const_array() { + let map = get_supported_importers::(); + let ids = ["chromecsv", "chromiumcsv", "bravecsv", "operacsv"]; + + for id in ids { + let loaders = get_loaders(&map, id); + assert!(loaders.contains("file")); + assert!(loaders.contains("chromium"), "missing chromium for {id}"); + } + } + + #[cfg(target_os = "windows")] + #[test] + fn returns_all_known_importers() { + let map = get_supported_importers::(); + + let expected: HashSet = HashSet::from([ + "chromiumcsv".to_string(), + "edgecsv".to_string(), + "operacsv".to_string(), + "vivaldicsv".to_string(), + ]); + assert_eq!(map.len(), expected.len()); + assert_eq!(map_keys(&map), expected); + + for (key, meta) in map.iter() { + assert_eq!(&meta.id, key); + assert_eq!(meta.instructions, "chromium"); + assert!(meta.loaders.iter().any(|l| *l == "file")); + } + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_specific_loaders_match_const_array() { + let map = get_supported_importers::(); + let ids = ["chromiumcsv", "edgecsv", "operacsv", "vivaldicsv"]; + + for id in ids { + let loaders = get_loaders(&map, id); + assert!(loaders.contains("file")); + assert!(loaders.contains("chromium"), "missing chromium for {id}"); + } + } +} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index e7dffe93db..b9c1c9a4cc 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -9,18 +9,14 @@ use windows::Win32::Foundation::{LocalFree, HLOCAL}; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; -#[allow(dead_code)] -mod util; +use crate::util; // // Public API // -pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [ - BrowserConfig { - name: "Chrome", - data_dir: "AppData/Local/Google/Chrome/User Data", - }, +// IMPORTANT adjust array size when enabling / disabling chromium importers here +pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [ BrowserConfig { name: "Chromium", data_dir: "AppData/Local/Chromium/User Data", @@ -29,10 +25,6 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [ name: "Microsoft Edge", data_dir: "AppData/Local/Microsoft/Edge/User Data", }, - BrowserConfig { - name: "Brave", - data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data", - }, BrowserConfig { name: "Opera", data_dir: "AppData/Roaming/Opera Software/Opera Stable", diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 030bf4c964..c822017c1a 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -3,6 +3,15 @@ /* auto-generated by NAPI-RS */ +/** Mechanisms that load data into the importer */ +export interface NativeImporterMetadata { + /** Identifies the importer */ + id: string + /** Describes the strategies used to obtain imported data */ + loaders: Array + /** Identifies the instructions for the importer */ + instructions: string +} export declare namespace passwords { /** The error message returned when a password is not found during retrieval or deletion. */ export const PASSWORD_NOT_FOUND: string @@ -228,6 +237,8 @@ export declare namespace chromium_importer { login?: Login failure?: LoginImportFailure } + /** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */ + export function getMetadata(): Record export function getInstalledBrowsers(): Array export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 327c7c1c8e..3e6a5f00ae 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -944,8 +944,12 @@ pub mod logging { #[napi] pub mod chromium_importer { + use bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever; + use bitwarden_chromium_importer::chromium::InstalledBrowserRetriever; use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult; use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo; + use bitwarden_chromium_importer::metadata::NativeImporterMetadata; + use std::collections::HashMap; #[napi(object)] pub struct ProfileInfo { @@ -1007,9 +1011,17 @@ pub mod chromium_importer { } } + #[napi] + /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. + pub fn get_metadata() -> HashMap { + bitwarden_chromium_importer::metadata::get_supported_importers::< + DefaultInstalledBrowserRetriever, + >() + } + #[napi] pub fn get_installed_browsers() -> napi::Result> { - bitwarden_chromium_importer::chromium::get_installed_browsers() + bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers() .map_err(|e| napi::Error::from_reason(e.to_string())) } diff --git a/apps/desktop/src/app/tools/import/chromium-importer.service.ts b/apps/desktop/src/app/tools/import/chromium-importer.service.ts index 56f31c359d..5273eef4b5 100644 --- a/apps/desktop/src/app/tools/import/chromium-importer.service.ts +++ b/apps/desktop/src/app/tools/import/chromium-importer.service.ts @@ -4,6 +4,10 @@ import { chromium_importer } from "@bitwarden/desktop-napi"; export class ChromiumImporterService { constructor() { + ipcMain.handle("chromium_importer.getMetadata", async (event) => { + return await chromium_importer.getMetadata(); + }); + ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => { return await chromium_importer.getInstalledBrowsers(); }); diff --git a/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts b/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts new file mode 100644 index 0000000000..fc2c2ff118 --- /dev/null +++ b/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts @@ -0,0 +1,66 @@ +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import type { NativeImporterMetadata } from "@bitwarden/desktop-napi"; +import { + ImportType, + DefaultImportMetadataService, + ImportMetadataServiceAbstraction, + DataLoader, + ImporterMetadata, + InstructionLink, + Instructions, + Loader, +} from "@bitwarden/importer-core"; + +export class DesktopImportMetadataService + extends DefaultImportMetadataService + implements ImportMetadataServiceAbstraction +{ + constructor(system: SystemServiceProvider) { + super(system); + } + + async init(): Promise { + const metadata = await ipc.tools.chromiumImporter.getMetadata(); + await this.parseNativeMetaData(metadata); + await super.init(); + } + + private async parseNativeMetaData(raw: Record): Promise { + const entries = Object.entries(raw).map(([id, meta]) => { + const loaders = meta.loaders.map(this.mapLoader); + const instructions = this.mapInstructions(meta.instructions); + const mapped: ImporterMetadata = { + type: id as ImportType, + loaders, + ...(instructions ? { instructions } : {}), + }; + return [id, mapped] as const; + }); + + // Do not overwrite existing importers, just add new ones or update existing ones + this.importers = { + ...this.importers, + ...Object.fromEntries(entries), + }; + } + + private mapLoader(name: string): DataLoader { + switch (name) { + case "file": + return Loader.file; + case "chromium": + return Loader.chromium; + default: + throw new Error(`Unknown loader from native module: ${name}`); + } + } + + private mapInstructions(name: string): InstructionLink | undefined { + switch (name) { + case "chromium": + return Instructions.chromium; + default: + return undefined; + } + } +} diff --git a/apps/desktop/src/app/tools/import/import-desktop.component.ts b/apps/desktop/src/app/tools/import/import-desktop.component.ts index f096471f77..fefeb43901 100644 --- a/apps/desktop/src/app/tools/import/import-desktop.component.ts +++ b/apps/desktop/src/app/tools/import/import-desktop.component.ts @@ -3,7 +3,15 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; -import { ImportComponent } from "@bitwarden/importer-ui"; +import { ImportMetadataServiceAbstraction } from "@bitwarden/importer-core"; +import { + ImportComponent, + ImporterProviders, + SYSTEM_SERVICE_PROVIDER, +} from "@bitwarden/importer-ui"; +import { safeProvider } from "@bitwarden/ui-common"; + +import { DesktopImportMetadataService } from "./desktop-import-metadata.service"; @Component({ templateUrl: "import-desktop.component.html", @@ -15,6 +23,14 @@ import { ImportComponent } from "@bitwarden/importer-ui"; ButtonModule, ImportComponent, ], + providers: [ + ...ImporterProviders, + safeProvider({ + provide: ImportMetadataServiceAbstraction, + useClass: DesktopImportMetadataService, + deps: [SYSTEM_SERVICE_PROVIDER], + }), + ], }) export class ImportDesktopComponent { protected disabled = false; diff --git a/apps/desktop/src/app/tools/preload.ts b/apps/desktop/src/app/tools/preload.ts index 574c27ac9f..4d629c992a 100644 --- a/apps/desktop/src/app/tools/preload.ts +++ b/apps/desktop/src/app/tools/preload.ts @@ -1,6 +1,10 @@ import { ipcRenderer } from "electron"; +import type { NativeImporterMetadata } from "@bitwarden/desktop-napi"; + const chromiumImporter = { + getMetadata: (): Promise> => + ipcRenderer.invoke("chromium_importer.getMetadata"), getInstalledBrowsers: (): Promise => ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"), getAvailableProfiles: (browser: string): Promise => diff --git a/apps/web/src/app/tools/import/import-web.component.ts b/apps/web/src/app/tools/import/import-web.component.ts index 7883769389..17c586a1b9 100644 --- a/apps/web/src/app/tools/import/import-web.component.ts +++ b/apps/web/src/app/tools/import/import-web.component.ts @@ -1,7 +1,16 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { ImportComponent } from "@bitwarden/importer-ui"; +import { + DefaultImportMetadataService, + ImportMetadataServiceAbstraction, +} from "@bitwarden/importer-core"; +import { + ImportComponent, + ImporterProviders, + SYSTEM_SERVICE_PROVIDER, +} from "@bitwarden/importer-ui"; +import { safeProvider } from "@bitwarden/ui-common"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; @@ -9,6 +18,14 @@ import { SharedModule } from "../../shared"; @Component({ templateUrl: "import-web.component.html", imports: [SharedModule, ImportComponent, HeaderModule], + providers: [ + ...ImporterProviders, + safeProvider({ + provide: ImportMetadataServiceAbstraction, + useClass: DefaultImportMetadataService, + deps: [SYSTEM_SERVICE_PROVIDER], + }), + ], }) export class ImportWebComponent { protected loading = false; diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index d98cd81714..3c6dc22dc2 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -72,7 +72,11 @@ import { import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata"; import { ImportOption, ImportResult, ImportType } from "../models"; -import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services"; +import { + ImportCollectionServiceAbstraction, + ImportMetadataServiceAbstraction, + ImportServiceAbstraction, +} from "../services"; import { ImportChromeComponent } from "./chrome"; import { @@ -236,6 +240,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { protected accountService: AccountService, private restrictedItemTypesService: RestrictedItemTypesService, private destroyRef: DestroyRef, + protected importMetadataService: ImportMetadataServiceAbstraction, ) {} protected get importBlockedByPolicy(): boolean { @@ -254,9 +259,11 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } async ngOnInit() { + await this.importMetadataService.init(); + this.setImportOptions(); - this.importService + this.importMetadataService .metadata$(this.formGroup.controls.format.valueChanges) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ diff --git a/libs/importer/src/components/importer-providers.ts b/libs/importer/src/components/importer-providers.ts index b00bd65211..c48f7c1b09 100644 --- a/libs/importer/src/components/importer-providers.ts +++ b/libs/importer/src/components/importer-providers.ts @@ -37,7 +37,9 @@ import { // FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module // to a general module. -const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken("SystemServices"); +export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken( + "SystemServices", +); /** Import service factories */ export const ImporterProviders: SafeProvider[] = [ @@ -85,7 +87,6 @@ export const ImporterProviders: SafeProvider[] = [ PinServiceAbstraction, AccountService, RestrictedItemTypesService, - SYSTEM_SERVICE_PROVIDER, ], }), ]; diff --git a/libs/importer/src/components/index.ts b/libs/importer/src/components/index.ts index a2e59f1714..366b7c3621 100644 --- a/libs/importer/src/components/index.ts +++ b/libs/importer/src/components/index.ts @@ -1,3 +1,4 @@ export * from "./dialog"; +export * from "./importer-providers"; export { ImportComponent } from "./import.component"; diff --git a/libs/importer/src/index.ts b/libs/importer/src/index.ts index 1d7a88d316..a24d22e12b 100644 --- a/libs/importer/src/index.ts +++ b/libs/importer/src/index.ts @@ -1,4 +1,5 @@ export * from "./models"; +export * from "./metadata"; export * from "./services"; export { Importer } from "./importers/importer"; diff --git a/libs/importer/src/metadata/importers.ts b/libs/importer/src/metadata/importers.ts index efd5eafe7d..b2c13a8404 100644 --- a/libs/importer/src/metadata/importers.ts +++ b/libs/importer/src/metadata/importers.ts @@ -2,26 +2,32 @@ import { deepFreeze } from "@bitwarden/common/tools/util"; import { ImportType } from "../models"; -import { Loader, Instructions } from "./data"; +import { Instructions, Loader } from "./data"; import { ImporterMetadata } from "./types"; -// FIXME: load this data from rust code +export type ImportersMetadata = Partial>; + +/** List of all supported importers and their default capabilities + * Note: the loaders listed here are the ones that are supported in all clients. + * Specific clients may have additional loaders available based on platform capabilities. + */ const importers = [ + { id: "bitwardenjson", loaders: [Loader.file], instructions: Instructions.unique }, // chromecsv import depends upon operating system, so ironically it doesn't support chromium { id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium }, - { id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium }, + { id: "operacsv", loaders: [Loader.file], instructions: Instructions.chromium }, { id: "vivaldicsv", - loaders: [Loader.file, Loader.chromium], + loaders: [Loader.file], instructions: Instructions.chromium, }, - { id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium }, - { id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium }, + { id: "bravecsv", loaders: [Loader.file], instructions: Instructions.chromium }, + { id: "edgecsv", loaders: [Loader.file], instructions: Instructions.chromium }, // FIXME: add other formats and remove `Partial` from export ] as const; /** Describes which loaders are available for each import type */ -export const Importers: Partial> = deepFreeze( +export const Importers: ImportersMetadata = deepFreeze( Object.fromEntries(importers.map((i) => [i.id, i])), ); diff --git a/libs/importer/src/services/default-import-metadata.service.ts b/libs/importer/src/services/default-import-metadata.service.ts new file mode 100644 index 0000000000..a9e767178a --- /dev/null +++ b/libs/importer/src/services/default-import-metadata.service.ts @@ -0,0 +1,51 @@ +import { map, Observable } from "rxjs"; + +import { SemanticLogger } from "@bitwarden/common/tools/log"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; + +import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata"; +import { ImportType } from "../models/import-options"; +import { availableLoaders } from "../util"; + +import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction"; + +export class DefaultImportMetadataService implements ImportMetadataServiceAbstraction { + protected importers: ImportersMetadata = Importers; + private logger: SemanticLogger; + + constructor(protected system: SystemServiceProvider) { + this.logger = system.log({ type: "ImportMetadataService" }); + } + + async init(): Promise { + // no-op for default implementation + } + + metadata$(type$: Observable): Observable { + const client = this.system.environment.getClientType(); + const capabilities$ = type$.pipe( + map((type) => { + if (!this.importers) { + return { type, loaders: [] }; + } + + const loaders = availableLoaders(this.importers, type, client); + + if (!loaders || loaders.length === 0) { + return { type, loaders: [] }; + } + + const capabilities: ImporterMetadata = { type, loaders }; + if (type in this.importers) { + capabilities.instructions = this.importers[type]?.instructions; + } + + this.logger.debug({ importType: type, capabilities }, "capabilities updated"); + + return capabilities; + }), + ); + + return capabilities$; + } +} diff --git a/libs/importer/src/services/import-metadata.service.abstraction.ts b/libs/importer/src/services/import-metadata.service.abstraction.ts new file mode 100644 index 0000000000..500dcfcef1 --- /dev/null +++ b/libs/importer/src/services/import-metadata.service.abstraction.ts @@ -0,0 +1,11 @@ +import { Observable } from "rxjs"; + +import { ImporterMetadata } from "../metadata"; +import { ImportType } from "../models/import-options"; + +export abstract class ImportMetadataServiceAbstraction { + abstract init(): Promise; + + /** describes the features supported by a format */ + abstract metadata$: (type$: Observable) => Observable; +} diff --git a/libs/importer/src/services/import-metadata.service.spec.ts b/libs/importer/src/services/import-metadata.service.spec.ts new file mode 100644 index 0000000000..25ce41251b --- /dev/null +++ b/libs/importer/src/services/import-metadata.service.spec.ts @@ -0,0 +1,110 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Subject, firstValueFrom } from "rxjs"; + +import { ClientType } from "@bitwarden/client-type"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; + +import { ImporterMetadata, Instructions } from "../metadata"; +import { ImportType } from "../models"; + +import { DefaultImportMetadataService } from "./default-import-metadata.service"; +import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction"; + +describe("ImportMetadataService", () => { + let sut: ImportMetadataServiceAbstraction; + let systemServiceProvider: MockProxy; + + beforeEach(() => { + const configService = mock(); + + const environment = mock(); + environment.getClientType.mockReturnValue(ClientType.Desktop); + + systemServiceProvider = mock({ + configService, + environment, + log: jest.fn().mockReturnValue({ debug: jest.fn() }), + }); + + sut = new DefaultImportMetadataService(systemServiceProvider); + }); + + describe("metadata$", () => { + let typeSubject: Subject; + let mockLogger: { debug: jest.Mock }; + + beforeEach(() => { + typeSubject = new Subject(); + mockLogger = { debug: jest.fn() }; + + const configService = mock(); + + const environment = mock(); + environment.getClientType.mockReturnValue(ClientType.Desktop); + + systemServiceProvider = mock({ + configService, + environment, + log: jest.fn().mockReturnValue(mockLogger), + }); + + // Recreate the service with the updated mocks for logging tests + sut = new DefaultImportMetadataService(systemServiceProvider); + }); + + afterEach(() => { + typeSubject.complete(); + }); + + it("should emit metadata when type$ emits", async () => { + const testType: ImportType = "chromecsv"; + + const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); + typeSubject.next(testType); + + const result = await metadataPromise; + + expect(result).toEqual({ + type: testType, + loaders: expect.any(Array), + instructions: Instructions.chromium, + }); + expect(result.type).toBe(testType); + }); + + it("should update when type$ changes", async () => { + const emissions: ImporterMetadata[] = []; + const subscription = sut.metadata$(typeSubject).subscribe((metadata) => { + emissions.push(metadata); + }); + + typeSubject.next("chromecsv"); + typeSubject.next("bravecsv"); + + // Wait for emissions + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(emissions).toHaveLength(2); + expect(emissions[0].type).toBe("chromecsv"); + expect(emissions[1].type).toBe("bravecsv"); + + subscription.unsubscribe(); + }); + + it("should log debug information with correct data", async () => { + const testType: ImportType = "chromecsv"; + + const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); + typeSubject.next(testType); + + await metadataPromise; + + expect(mockLogger.debug).toHaveBeenCalledWith( + { importType: testType, capabilities: expect.any(Object) }, + "capabilities updated", + ); + }); + }); +}); diff --git a/libs/importer/src/services/import.service.abstraction.ts b/libs/importer/src/services/import.service.abstraction.ts index ee0d1ed33a..d8f1f6ccd5 100644 --- a/libs/importer/src/services/import.service.abstraction.ts +++ b/libs/importer/src/services/import.service.abstraction.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -8,7 +7,6 @@ import { CollectionView } from "@bitwarden/admin-console/common"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { Importer } from "../importers/importer"; -import { ImporterMetadata } from "../metadata"; import { ImportOption, ImportType } from "../models/import-options"; import { ImportResult } from "../models/import-result"; @@ -17,9 +15,6 @@ export abstract class ImportServiceAbstraction { regularImportOptions: readonly ImportOption[]; getImportOptions: () => ImportOption[]; - /** describes the features supported by a format */ - metadata$: (type$: Observable) => Observable; - import: ( importer: Importer, fileContents: string, diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 2a3963e19d..fd710056e8 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -1,20 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, Subject, firstValueFrom } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { ClientType } from "@bitwarden/client-type"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -25,8 +18,6 @@ import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; import { Importer } from "../importers/importer"; -import { ImporterMetadata, Instructions, Loader } from "../metadata"; -import { ImportType } from "../models"; import { ImportResult } from "../models/import-result"; import { ImportApiServiceAbstraction } from "./import-api.service.abstraction"; @@ -44,7 +35,6 @@ describe("ImportService", () => { let pinService: MockProxy; let accountService: MockProxy; let restrictedItemTypesService: MockProxy; - let systemServiceProvider: MockProxy; beforeEach(() => { cipherService = mock(); @@ -57,18 +47,6 @@ describe("ImportService", () => { pinService = mock(); restrictedItemTypesService = mock(); - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false)); - - const environment = mock(); - environment.getClientType.mockReturnValue(ClientType.Desktop); - - systemServiceProvider = mock({ - configService, - environment, - log: jest.fn().mockReturnValue({ debug: jest.fn() }), - }); - importService = new ImportService( cipherService, folderService, @@ -80,7 +58,6 @@ describe("ImportService", () => { pinService, accountService, restrictedItemTypesService, - systemServiceProvider, ); }); @@ -268,136 +245,6 @@ describe("ImportService", () => { expect(importResult.folderRelationships[1]).toEqual([0, 1]); }); }); - - describe("metadata$", () => { - let featureFlagSubject: BehaviorSubject; - let typeSubject: Subject; - let mockLogger: { debug: jest.Mock }; - - beforeEach(() => { - featureFlagSubject = new BehaviorSubject(false); - typeSubject = new Subject(); - mockLogger = { debug: jest.fn() }; - - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject); - - const environment = mock(); - environment.getClientType.mockReturnValue(ClientType.Desktop); - - systemServiceProvider = mock({ - configService, - environment, - log: jest.fn().mockReturnValue(mockLogger), - }); - - // Recreate the service with the updated mocks for logging tests - importService = new ImportService( - cipherService, - folderService, - importApiService, - i18nService, - collectionService, - keyService, - encryptService, - pinService, - accountService, - restrictedItemTypesService, - systemServiceProvider, - ); - }); - - afterEach(() => { - featureFlagSubject.complete(); - typeSubject.complete(); - }); - - it("should emit metadata when type$ emits", async () => { - const testType: ImportType = "chromecsv"; - - const metadataPromise = firstValueFrom(importService.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result).toEqual({ - type: testType, - loaders: expect.any(Array), - instructions: Instructions.chromium, - }); - expect(result.type).toBe(testType); - }); - - it("should include all loaders when chromium feature flag is enabled", async () => { - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(true); - - const metadataPromise = firstValueFrom(importService.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should update when type$ changes", async () => { - const emissions: ImporterMetadata[] = []; - const subscription = importService.metadata$(typeSubject).subscribe((metadata) => { - emissions.push(metadata); - }); - - typeSubject.next("chromecsv"); - typeSubject.next("bravecsv"); - - // Wait for emissions - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(emissions).toHaveLength(2); - expect(emissions[0].type).toBe("chromecsv"); - expect(emissions[1].type).toBe("bravecsv"); - - subscription.unsubscribe(); - }); - - it("should update when both type$ and feature flag change", async () => { - const emissions: ImporterMetadata[] = []; - - const subscription = importService.metadata$(typeSubject).subscribe((metadata) => { - emissions.push(metadata); - }); - - // Initial emission - typeSubject.next("chromecsv"); - - // Change both at the same time - featureFlagSubject.next(true); - typeSubject.next("bravecsv"); - - // Wait for emissions - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(emissions.length).toBeGreaterThanOrEqual(2); - const lastEmission = emissions[emissions.length - 1]; - expect(lastEmission.type).toBe("bravecsv"); - - subscription.unsubscribe(); - }); - - it("should log debug information with correct data", async () => { - const testType: ImportType = "chromecsv"; - - const metadataPromise = firstValueFrom(importService.metadata$(typeSubject)); - typeSubject.next(testType); - - await metadataPromise; - - expect(mockLogger.debug).toHaveBeenCalledWith( - { importType: testType, capabilities: expect.any(Object) }, - "capabilities updated", - ); - }); - }); }); function createCipher(options: Partial = {}) { diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index c17490ed4a..3efe327e31 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -11,7 +11,6 @@ import { } from "@bitwarden/admin-console/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { DeviceType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request"; @@ -20,8 +19,6 @@ import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -98,7 +95,6 @@ import { PasswordDepot17XmlImporter, } from "../importers"; import { Importer } from "../importers/importer"; -import { ImporterMetadata, Importers, Loader } from "../metadata"; import { featuredImportOptions, ImportOption, @@ -108,15 +104,12 @@ import { import { ImportResult } from "../models/import-result"; import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction"; import { ImportServiceAbstraction } from "../services/import.service.abstraction"; -import { availableLoaders as availableLoaders } from "../util"; export class ImportService implements ImportServiceAbstraction { featuredImportOptions = featuredImportOptions as readonly ImportOption[]; regularImportOptions = regularImportOptions as readonly ImportOption[]; - private logger: SemanticLogger; - constructor( private cipherService: CipherService, private folderService: FolderService, @@ -128,55 +121,12 @@ export class ImportService implements ImportServiceAbstraction { private pinService: PinServiceAbstraction, private accountService: AccountService, private restrictedItemTypesService: RestrictedItemTypesService, - private system: SystemServiceProvider, - ) { - this.logger = system.log({ type: "ImportService" }); - } + ) {} getImportOptions(): ImportOption[] { return this.featuredImportOptions.concat(this.regularImportOptions); } - metadata$(type$: Observable): Observable { - const client = this.system.environment.getClientType(); - const capabilities$ = combineLatest([type$]).pipe( - map(([type]) => { - let loaders = availableLoaders(type, client); - - // Mac App Store is currently disabled due to sandboxing. - let isUnsupported = this.system.environment.isMacAppStore(); - - // disable the chromium loader for Brave on Windows only - if (type === "bravecsv") { - try { - const device = this.system.environment.getDevice(); - const isWindowsDesktop = device === DeviceType.WindowsDesktop; - if (isWindowsDesktop) { - isUnsupported = true; - } - } catch { - isUnsupported = true; - } - } - // If the browser is unsupported, remove the chromium loader - if (isUnsupported) { - loaders = loaders?.filter((loader) => loader !== Loader.chromium); - } - - const capabilities: ImporterMetadata = { type, loaders }; - if (type in Importers) { - capabilities.instructions = Importers[type].instructions; - } - - this.logger.debug({ importType: type, capabilities }, "capabilities updated"); - - return capabilities; - }), - ); - - return capabilities$; - } - async import( importer: Importer, fileContents: string, diff --git a/libs/importer/src/services/index.ts b/libs/importer/src/services/index.ts index 13568f886c..ec53826add 100644 --- a/libs/importer/src/services/index.ts +++ b/libs/importer/src/services/index.ts @@ -4,4 +4,7 @@ export { ImportApiService } from "./import-api.service"; export { ImportServiceAbstraction } from "./import.service.abstraction"; export { ImportService } from "./import.service"; +export { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction"; +export { DefaultImportMetadataService } from "./default-import-metadata.service"; + export { ImportCollectionServiceAbstraction } from "./import-collection.service.abstraction"; diff --git a/libs/importer/src/util.spec.ts b/libs/importer/src/util.spec.ts deleted file mode 100644 index 5a68e3cea1..0000000000 --- a/libs/importer/src/util.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ClientType } from "@bitwarden/client-type"; - -import { Loader } from "./metadata"; -import { availableLoaders } from "./util"; - -describe("availableLoaders", () => { - describe("given valid import types", () => { - it("returns available loaders when client supports all loaders", () => { - const result = availableLoaders("operacsv", ClientType.Desktop); - - expect(result).toEqual([Loader.file, Loader.chromium]); - }); - - it("returns filtered loaders when client supports some loaders", () => { - const result = availableLoaders("operacsv", ClientType.Browser); - - expect(result).toEqual([Loader.file]); - }); - - it("returns single loader for import types with one loader", () => { - const result = availableLoaders("chromecsv", ClientType.Desktop); - - expect(result).toEqual([Loader.file]); - }); - - it("returns all supported loaders for multi-loader import types", () => { - const result = availableLoaders("bravecsv", ClientType.Desktop); - - expect(result).toEqual([Loader.file, Loader.chromium]); - }); - }); - - describe("given unknown import types", () => { - it("returns undefined when import type is not found in metadata", () => { - const result = availableLoaders("nonexistent" as any, ClientType.Desktop); - - expect(result).toBeUndefined(); - }); - }); - - describe("given different client types", () => { - it("returns appropriate loaders for Browser client", () => { - const result = availableLoaders("operacsv", ClientType.Browser); - - expect(result).toEqual([Loader.file]); - }); - - it("returns appropriate loaders for Web client", () => { - const result = availableLoaders("chromecsv", ClientType.Web); - - expect(result).toEqual([Loader.file]); - }); - - it("returns appropriate loaders for CLI client", () => { - const result = availableLoaders("vivaldicsv", ClientType.Cli); - - expect(result).toEqual([Loader.file]); - }); - }); -}); diff --git a/libs/importer/src/util.ts b/libs/importer/src/util.ts index 0a76b7e753..7a5f4cbf1e 100644 --- a/libs/importer/src/util.ts +++ b/libs/importer/src/util.ts @@ -1,6 +1,6 @@ import { ClientType } from "@bitwarden/client-type"; -import { LoaderAvailability, Importers } from "./metadata"; +import { LoaderAvailability, ImportersMetadata } from "./metadata"; import { ImportType } from "./models"; /** Lookup the loaders supported by a specific client. @@ -8,12 +8,20 @@ import { ImportType } from "./models"; * @returns `undefined` when metadata is not defined for the type, or * an array identifying the supported clients. */ -export function availableLoaders(type: ImportType, client: ClientType) { - if (!(type in Importers)) { +export function availableLoaders( + importersMetadata: ImportersMetadata, + type: ImportType, + client: ClientType, +) { + if (!importersMetadata) { return undefined; } - const capabilities = Importers[type]?.loaders ?? []; + if (!(type in importersMetadata)) { + return undefined; + } + + const capabilities = importersMetadata[type]?.loaders ?? []; const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client)); return available; }