mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge 13547d5af0 into d32365fbba
This commit is contained in:
commit
061f5f9cf3
1
apps/desktop/desktop_native/Cargo.lock
generated
1
apps/desktop/desktop_native/Cargo.lock
generated
@ -610,6 +610,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"desktop_objc",
|
||||
"dirs",
|
||||
"hex",
|
||||
"oo7",
|
||||
|
||||
@ -28,7 +28,11 @@ let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platfo
|
||||
function buildNapiModule(target, release = true) {
|
||||
const targetArg = target ? `--target ${target}` : "";
|
||||
const releaseArg = release ? "--release" : "";
|
||||
child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") });
|
||||
child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, "napi"),
|
||||
env: process.env // Pass environment variables including SANDBOX_BUILD
|
||||
});
|
||||
}
|
||||
|
||||
function buildProxyBin(target, release = true) {
|
||||
|
||||
@ -18,9 +18,11 @@ serde_json = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
desktop_objc = { path = "../objc" }
|
||||
pbkdf2 = "=0.12.2"
|
||||
security-framework = { workspace = true }
|
||||
sha1 = "=0.10.6"
|
||||
tokio = { workspace = true, features = ["rt"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
aes-gcm = { workspace = true }
|
||||
|
||||
@ -51,19 +51,24 @@ pub enum LoginImportResult {
|
||||
}
|
||||
|
||||
pub trait InstalledBrowserRetriever {
|
||||
fn get_installed_browsers() -> Result<Vec<String>>;
|
||||
fn get_installed_browsers(mas_build: bool) -> Result<Vec<String>>;
|
||||
}
|
||||
|
||||
pub struct DefaultInstalledBrowserRetriever {}
|
||||
|
||||
impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
|
||||
fn get_installed_browsers() -> Result<Vec<String>> {
|
||||
fn get_installed_browsers(mas_build: bool) -> Result<Vec<String>> {
|
||||
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||
|
||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||
let data_dir = get_and_validate_data_dir(config);
|
||||
if data_dir.is_ok() {
|
||||
if mas_build {
|
||||
// show all browsers for MAS builds, user will grant access when selected
|
||||
browsers.push((*browser).to_string());
|
||||
} else {
|
||||
// When not in sandbox check file system directly
|
||||
let data_dir = get_and_validate_data_dir(config)?;
|
||||
if data_dir.exists() {
|
||||
browsers.push((*browser).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,15 +76,45 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> {
|
||||
pub async fn get_available_profiles(
|
||||
browser_name: &str,
|
||||
mas_build: bool,
|
||||
) -> Result<Vec<ProfileInfo>> {
|
||||
// MAS builds need to resume security-scoped access before reading browser files
|
||||
#[cfg(target_os = "macos")]
|
||||
let _access = if mas_build {
|
||||
Some(platform::sandbox::ScopedBrowserAccess::resume(browser_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (_, local_state) = load_local_state_for_browser(browser_name)?;
|
||||
Ok(get_profile_info(&local_state))
|
||||
}
|
||||
|
||||
/// Request access to browser directory (MAS builds only)
|
||||
/// This shows the permission dialog and creates a security-scoped bookmark
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn request_browser_access(browser_name: &str, mas_build: bool) -> Result<()> {
|
||||
if mas_build {
|
||||
platform::sandbox::ScopedBrowserAccess::request_only(browser_name).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn import_logins(
|
||||
browser_name: &String,
|
||||
profile_id: &String,
|
||||
browser_name: &str,
|
||||
profile_id: &str,
|
||||
_mas_build: bool,
|
||||
) -> Result<Vec<LoginImportResult>> {
|
||||
// MAS builds will use the formerly created security bookmark
|
||||
#[cfg(target_os = "macos")]
|
||||
let _access = if _mas_build {
|
||||
Some(platform::sandbox::ScopedBrowserAccess::resume(browser_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
|
||||
|
||||
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
|
||||
@ -177,9 +212,9 @@ struct OsCrypt {
|
||||
app_bound_encrypted_key: Option<String>,
|
||||
}
|
||||
|
||||
fn load_local_state_for_browser(browser_name: &String) -> Result<(PathBuf, LocalState)> {
|
||||
fn load_local_state_for_browser(browser_name: &str) -> Result<(PathBuf, LocalState)> {
|
||||
let config = SUPPORTED_BROWSER_MAP
|
||||
.get(browser_name.as_str())
|
||||
.get(browser_name)
|
||||
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||
|
||||
let data_dir = get_and_validate_data_dir(config)?;
|
||||
@ -222,11 +257,7 @@ struct EncryptedLogin {
|
||||
encrypted_note: Vec<u8>,
|
||||
}
|
||||
|
||||
fn get_logins(
|
||||
browser_dir: &Path,
|
||||
profile_id: &String,
|
||||
filename: &str,
|
||||
) -> Result<Vec<EncryptedLogin>> {
|
||||
fn get_logins(browser_dir: &Path, profile_id: &str, filename: &str) -> Result<Vec<EncryptedLogin>> {
|
||||
let login_data_path = browser_dir.join(profile_id).join(filename);
|
||||
|
||||
// Sometimes database files are not present, so nothing to import
|
||||
|
||||
@ -38,7 +38,7 @@ pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
];
|
||||
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
browser_name: &str,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
let config = KEYRING_CONFIG
|
||||
|
||||
@ -7,6 +7,164 @@ use crate::{
|
||||
util,
|
||||
};
|
||||
|
||||
//
|
||||
// Sandbox specific (for Mac App Store Builds)
|
||||
//
|
||||
|
||||
pub mod sandbox {
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum CommandResult<T> {
|
||||
#[serde(rename = "success")]
|
||||
Success { value: T },
|
||||
#[serde(rename = "error")]
|
||||
Error { error: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RequestAccessResponse {
|
||||
#[allow(dead_code)]
|
||||
bookmark: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HasStoredAccessResponse {
|
||||
#[serde(rename = "hasAccess")]
|
||||
has_access: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct StartAccessResponse {
|
||||
#[allow(dead_code)]
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CommandInput {
|
||||
namespace: String,
|
||||
command: String,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
|
||||
pub struct ScopedBrowserAccess {
|
||||
browser_name: String,
|
||||
}
|
||||
|
||||
impl ScopedBrowserAccess {
|
||||
/// Request access to browser directory and create a security bookmark if access is approved
|
||||
pub async fn request_only(browser_name: &str) -> Result<()> {
|
||||
let config = crate::chromium::platform::SUPPORTED_BROWSERS
|
||||
.iter()
|
||||
.find(|b| b.name == browser_name)
|
||||
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||
|
||||
// For macOS, data_dir is always a single-element array
|
||||
let relative_path = config
|
||||
.data_dir
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("No data directory configured for browser"))?;
|
||||
|
||||
let input = CommandInput {
|
||||
namespace: "chromium_importer".to_string(),
|
||||
command: "request_access".to_string(),
|
||||
params: serde_json::json!({
|
||||
"browserName": browser_name,
|
||||
"relativePath": relative_path,
|
||||
}),
|
||||
};
|
||||
|
||||
let output = desktop_objc::run_command(serde_json::to_string(&input)?)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
|
||||
|
||||
let result: CommandResult<RequestAccessResponse> = serde_json::from_str(&output)
|
||||
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
|
||||
|
||||
match result {
|
||||
CommandResult::Success { .. } => Ok(()),
|
||||
CommandResult::Error { error } => Err(anyhow!("{}", error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resume browser directory access using previously created security bookmark
|
||||
pub async fn resume(browser_name: &str) -> Result<Self> {
|
||||
// First check if we have stored access
|
||||
let has_access_input = CommandInput {
|
||||
namespace: "chromium_importer".to_string(),
|
||||
command: "has_stored_access".to_string(),
|
||||
params: serde_json::json!({
|
||||
"browserName": browser_name,
|
||||
}),
|
||||
};
|
||||
|
||||
let has_access_output =
|
||||
desktop_objc::run_command(serde_json::to_string(&has_access_input)?)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
|
||||
|
||||
let has_access_result: CommandResult<HasStoredAccessResponse> =
|
||||
serde_json::from_str(&has_access_output)
|
||||
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
|
||||
|
||||
let has_access = match has_access_result {
|
||||
CommandResult::Success { value } => value.has_access,
|
||||
CommandResult::Error { error } => return Err(anyhow!("{}", error)),
|
||||
};
|
||||
|
||||
if !has_access {
|
||||
return Err(anyhow!("Access has not been granted for this browser"));
|
||||
}
|
||||
|
||||
// Start accessing the browser
|
||||
let start_input = CommandInput {
|
||||
namespace: "chromium_importer".to_string(),
|
||||
command: "start_access".to_string(),
|
||||
params: serde_json::json!({
|
||||
"browserName": browser_name,
|
||||
}),
|
||||
};
|
||||
|
||||
let start_output = desktop_objc::run_command(serde_json::to_string(&start_input)?)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
|
||||
|
||||
let start_result: CommandResult<StartAccessResponse> =
|
||||
serde_json::from_str(&start_output)
|
||||
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
|
||||
|
||||
match start_result {
|
||||
CommandResult::Success { .. } => Ok(Self {
|
||||
browser_name: browser_name.to_string(),
|
||||
}),
|
||||
CommandResult::Error { error } => Err(anyhow!("{}", error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScopedBrowserAccess {
|
||||
fn drop(&mut self) {
|
||||
let browser_name = self.browser_name.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let input = CommandInput {
|
||||
namespace: "chromium_importer".to_string(),
|
||||
command: "stop_access".to_string(),
|
||||
params: serde_json::json!({
|
||||
"browserName": browser_name,
|
||||
}),
|
||||
};
|
||||
|
||||
if let Ok(input_json) = serde_json::to_string(&input) {
|
||||
let _ = desktop_objc::run_command(input_json).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
@ -43,7 +201,7 @@ pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
];
|
||||
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
browser_name: &str,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
let config = KEYCHAIN_CONFIG
|
||||
|
||||
@ -17,11 +17,12 @@ pub struct NativeImporterMetadata {
|
||||
/// Only browsers listed in PLATFORM_SUPPORTED_BROWSERS will have the "chromium" loader.
|
||||
/// All importers will have the "file" loader.
|
||||
pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
mas_build: bool,
|
||||
) -> HashMap<String, NativeImporterMetadata> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
// Check for installed browsers
|
||||
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
|
||||
let installed_browsers = T::get_installed_browsers(mas_build).unwrap_or_default();
|
||||
|
||||
const IMPORTERS: &[(&str, &str)] = &[
|
||||
("chromecsv", "Chrome"),
|
||||
@ -67,7 +68,7 @@ mod tests {
|
||||
pub struct MockInstalledBrowserRetriever {}
|
||||
|
||||
impl InstalledBrowserRetriever for MockInstalledBrowserRetriever {
|
||||
fn get_installed_browsers() -> Result<Vec<String>, anyhow::Error> {
|
||||
fn get_installed_browsers(_mas_build: bool) -> Result<Vec<String>, anyhow::Error> {
|
||||
Ok(SUPPORTED_BROWSER_MAP
|
||||
.keys()
|
||||
.map(|browser| browser.to_string())
|
||||
@ -91,7 +92,7 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"chromecsv".to_string(),
|
||||
@ -114,7 +115,7 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
let ids = [
|
||||
"chromecsv",
|
||||
"chromiumcsv",
|
||||
@ -133,7 +134,7 @@ mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"chromecsv".to_string(),
|
||||
@ -154,7 +155,7 @@ mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn linux_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
let ids = ["chromecsv", "chromiumcsv", "bravecsv", "operacsv"];
|
||||
|
||||
for id in ids {
|
||||
@ -167,7 +168,7 @@ mod tests {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"bravecsv".to_string(),
|
||||
@ -190,7 +191,7 @@ mod tests {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
|
||||
let ids = [
|
||||
"bravecsv",
|
||||
"chromecsv",
|
||||
|
||||
7
apps/desktop/desktop_native/napi/index.d.ts
vendored
7
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -253,9 +253,10 @@ export declare namespace chromium_importer {
|
||||
instructions: string
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
export function getMetadata(masBuild: boolean): Record<string, NativeImporterMetadata>
|
||||
export function getAvailableProfiles(browser: string, masBuild: boolean): Promise<Array<ProfileInfo>>
|
||||
export function importLogins(browser: string, profileId: string, masBuild: boolean): Promise<Array<LoginImportResult>>
|
||||
export function requestBrowserAccess(browser: string, masBuild: boolean): Promise<void>
|
||||
}
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
|
||||
@ -1158,16 +1158,22 @@ pub mod chromium_importer {
|
||||
|
||||
#[napi]
|
||||
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
|
||||
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
|
||||
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>()
|
||||
.into_iter()
|
||||
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
|
||||
.collect()
|
||||
pub fn get_metadata(mas_build: bool) -> HashMap<String, NativeImporterMetadata> {
|
||||
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>(
|
||||
mas_build,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
|
||||
chromium_importer::chromium::get_available_profiles(&browser)
|
||||
pub async fn get_available_profiles(
|
||||
browser: String,
|
||||
mas_build: bool,
|
||||
) -> napi::Result<Vec<ProfileInfo>> {
|
||||
chromium_importer::chromium::get_available_profiles(&browser, mas_build)
|
||||
.await
|
||||
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
@ -1176,12 +1182,25 @@ pub mod chromium_importer {
|
||||
pub async fn import_logins(
|
||||
browser: String,
|
||||
profile_id: String,
|
||||
mas_build: bool,
|
||||
) -> napi::Result<Vec<LoginImportResult>> {
|
||||
chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||||
chromium_importer::chromium::import_logins(&browser, &profile_id, mas_build)
|
||||
.await
|
||||
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn request_browser_access(_browser: String, _mas_build: bool) -> napi::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return chromium_importer::chromium::request_browser_access(&_browser, _mas_build)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
@ -10,7 +10,7 @@ default = []
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use glob::glob;
|
||||
|
||||
// Compile Objective-C files
|
||||
let mut builder = cc::Build::new();
|
||||
|
||||
// Auto compile all .m files in the src/native directory
|
||||
// Compile all .m files in the src/native directory
|
||||
for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") {
|
||||
let path = entry.expect("Failed to read glob entry");
|
||||
builder.file(path.clone());
|
||||
@ -12,7 +14,11 @@ fn main() {
|
||||
|
||||
builder
|
||||
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
|
||||
.compile("autofill");
|
||||
.compile("objc_code");
|
||||
|
||||
// Link required frameworks
|
||||
println!("cargo:rustc-link-lib=framework=Foundation");
|
||||
println!("cargo:rustc-link-lib=framework=AppKit");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "utils.h"
|
||||
#import "interop.h"
|
||||
@ -0,0 +1,25 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BrowserAccessManager : NSObject
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
/// Request access to a specific browser's directory
|
||||
/// Returns security bookmark data (used to persist permissions) as base64 string, or nil if user declined
|
||||
- (nullable NSString *)requestAccessToBrowserDir:(NSString *)browserName relativePath:(NSString *)relativePath;
|
||||
|
||||
/// Check if we have stored bookmark for browser (doesn't verify it's still valid)
|
||||
- (BOOL)hasStoredAccess:(NSString *)browserName;
|
||||
|
||||
/// Start accessing a browser directory using stored bookmark
|
||||
/// Returns the resolved path, or nil if bookmark is invalid/revoked
|
||||
- (nullable NSString *)startAccessingBrowser:(NSString *)browserName;
|
||||
|
||||
/// Stop accessing a browser directory (must be called after startAccessingBrowser)
|
||||
- (void)stopAccessingBrowser:(NSString *)browserName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,164 @@
|
||||
#import "browser_access_manager.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@implementation BrowserAccessManager {
|
||||
NSString *_bookmarkKey;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_bookmarkKey = @"com.bitwarden.chromiumImporter.bookmarks";
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)requestAccessToBrowserDir:(NSString *)browserName relativePath:(NSString *)relativePath {
|
||||
|
||||
if (!relativePath) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *homeDir = [[NSFileManager defaultManager] homeDirectoryForCurrentUser];
|
||||
NSURL *browserPath = [homeDir URLByAppendingPathComponent:relativePath];
|
||||
|
||||
// NSOpenPanel must be run on the main thread
|
||||
__block NSURL *selectedURL = nil;
|
||||
__block NSModalResponse panelResult = NSModalResponseCancel;
|
||||
|
||||
void (^showPanel)(void) = ^{
|
||||
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
||||
openPanel.message = [NSString stringWithFormat:
|
||||
@"Please select your %@ data folder\n\nExpected location:\n%@",
|
||||
browserName, browserPath.path];
|
||||
openPanel.prompt = @"Grant Access";
|
||||
openPanel.allowsMultipleSelection = NO;
|
||||
openPanel.canChooseDirectories = YES;
|
||||
openPanel.canChooseFiles = NO;
|
||||
openPanel.directoryURL = browserPath;
|
||||
|
||||
panelResult = [openPanel runModal];
|
||||
selectedURL = openPanel.URL;
|
||||
};
|
||||
|
||||
if ([NSThread isMainThread]) {
|
||||
showPanel();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), showPanel);
|
||||
}
|
||||
|
||||
if (panelResult != NSModalResponseOK || !selectedURL) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *localStatePath = [selectedURL URLByAppendingPathComponent:@"Local State"];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:localStatePath.path]) {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
alert.messageText = @"Invalid Folder";
|
||||
alert.informativeText = [NSString stringWithFormat:
|
||||
@"The selected folder doesn't appear to be a valid %@ data directory. Please select the correct folder.",
|
||||
browserName];
|
||||
alert.alertStyle = NSAlertStyleWarning;
|
||||
[alert runModal];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Access is temporary right now, persist it by creating a security bookmark
|
||||
NSError *error = nil;
|
||||
NSData *bookmarkData = [selectedURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
|
||||
includingResourceValuesForKeys:nil
|
||||
relativeToURL:nil
|
||||
error:&error];
|
||||
|
||||
if (!bookmarkData) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[self saveBookmark:bookmarkData forBrowser:browserName];
|
||||
return [bookmarkData base64EncodedStringWithOptions:0];
|
||||
}
|
||||
|
||||
- (BOOL)hasStoredAccess:(NSString *)browserName {
|
||||
return [self loadBookmarkForBrowser:browserName] != nil;
|
||||
}
|
||||
|
||||
- (NSString *)startAccessingBrowser:(NSString *)browserName {
|
||||
NSData *bookmarkData = [self loadBookmarkForBrowser:browserName];
|
||||
if (!bookmarkData) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL isStale = NO;
|
||||
NSError *error = nil;
|
||||
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
|
||||
options:NSURLBookmarkResolutionWithSecurityScope
|
||||
relativeToURL:nil
|
||||
bookmarkDataIsStale:&isStale
|
||||
error:&error];
|
||||
|
||||
if (!url) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (isStale) {
|
||||
NSData *newBookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
|
||||
includingResourceValuesForKeys:nil
|
||||
relativeToURL:nil
|
||||
error:&error];
|
||||
|
||||
if (!newBookmarkData) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[self saveBookmark:newBookmarkData forBrowser:browserName];
|
||||
}
|
||||
|
||||
if (![url startAccessingSecurityScopedResource]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return url.path;
|
||||
}
|
||||
|
||||
- (void)stopAccessingBrowser:(NSString *)browserName {
|
||||
NSData *bookmarkData = [self loadBookmarkForBrowser:browserName];
|
||||
if (!bookmarkData) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isStale = NO;
|
||||
NSError *error = nil;
|
||||
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
|
||||
options:NSURLBookmarkResolutionWithSecurityScope
|
||||
relativeToURL:nil
|
||||
bookmarkDataIsStale:&isStale
|
||||
error:&error];
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
- (NSString *)bookmarkKeyFor:(NSString *)browserName {
|
||||
return [NSString stringWithFormat:@"%@.%@", _bookmarkKey, browserName];
|
||||
}
|
||||
|
||||
- (void)saveBookmark:(NSData *)data forBrowser:(NSString *)browserName {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
NSString *key = [self bookmarkKeyFor:browserName];
|
||||
[defaults setObject:data forKey:key];
|
||||
[defaults synchronize];
|
||||
}
|
||||
|
||||
- (NSData *)loadBookmarkForBrowser:(NSString *)browserName {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
NSString *key = [self bookmarkKeyFor:browserName];
|
||||
return [defaults dataForKey:key];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -0,0 +1,8 @@
|
||||
#ifndef HAS_STORED_ACCESS_H
|
||||
#define HAS_STORED_ACCESS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void hasStoredAccessCommand(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,17 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "../../interop.h"
|
||||
#import "../browser_access_manager.h"
|
||||
#import "has_stored_access.h"
|
||||
|
||||
void hasStoredAccessCommand(void* context, NSDictionary *params) {
|
||||
NSString *browserName = params[@"browserName"];
|
||||
|
||||
if (!browserName) {
|
||||
return _return(context, _error(@"Missing required parameter: browserName"));
|
||||
}
|
||||
|
||||
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
|
||||
BOOL hasAccess = [manager hasStoredAccess:browserName];
|
||||
|
||||
_return(context, _success(@{@"hasAccess": @(hasAccess)}));
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
#ifndef REQUEST_ACCESS_H
|
||||
#define REQUEST_ACCESS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void requestAccessCommand(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,22 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "../../interop.h"
|
||||
#import "../browser_access_manager.h"
|
||||
#import "request_access.h"
|
||||
|
||||
void requestAccessCommand(void* context, NSDictionary *params) {
|
||||
NSString *browserName = params[@"browserName"];
|
||||
NSString *relativePath = params[@"relativePath"];
|
||||
|
||||
if (!browserName || !relativePath) {
|
||||
return _return(context, _error(@"Missing required parameters: browserName and relativePath"));
|
||||
}
|
||||
|
||||
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
|
||||
NSString *bookmarkData = [manager requestAccessToBrowserDir:browserName relativePath:relativePath];
|
||||
|
||||
if (bookmarkData == nil) {
|
||||
return _return(context, _error(@"User denied access or selected an invalid browser directory"));
|
||||
}
|
||||
|
||||
_return(context, _success(@{@"bookmark": bookmarkData}));
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
#ifndef START_ACCESS_H
|
||||
#define START_ACCESS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void startAccessCommand(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,21 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "../../interop.h"
|
||||
#import "../browser_access_manager.h"
|
||||
#import "start_access.h"
|
||||
|
||||
void startAccessCommand(void* context, NSDictionary *params) {
|
||||
NSString *browserName = params[@"browserName"];
|
||||
|
||||
if (!browserName) {
|
||||
return _return(context, _error(@"Missing required parameter: browserName"));
|
||||
}
|
||||
|
||||
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
|
||||
NSString *resolvedPath = [manager startAccessingBrowser:browserName];
|
||||
|
||||
if (resolvedPath == nil) {
|
||||
return _return(context, _error(@"Failed to start accessing browser. Bookmark may be invalid or revoked"));
|
||||
}
|
||||
|
||||
_return(context, _success(@{@"path": resolvedPath}));
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
#ifndef STOP_ACCESS_H
|
||||
#define STOP_ACCESS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void stopAccessCommand(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,17 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "../../interop.h"
|
||||
#import "../browser_access_manager.h"
|
||||
#import "stop_access.h"
|
||||
|
||||
void stopAccessCommand(void* context, NSDictionary *params) {
|
||||
NSString *browserName = params[@"browserName"];
|
||||
|
||||
if (!browserName) {
|
||||
return _return(context, _error(@"Missing required parameter: browserName"));
|
||||
}
|
||||
|
||||
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
|
||||
[manager stopAccessingBrowser:browserName];
|
||||
|
||||
_return(context, _success(@{}));
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
#ifndef RUN_CHROMIUM_COMMAND_H
|
||||
#define RUN_CHROMIUM_COMMAND_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void runChromiumCommand(void* context, NSDictionary *input);
|
||||
|
||||
#endif
|
||||
@ -0,0 +1,25 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "commands/request_access.h"
|
||||
#import "commands/has_stored_access.h"
|
||||
#import "commands/start_access.h"
|
||||
#import "commands/stop_access.h"
|
||||
#import "../interop.h"
|
||||
#import "../utils.h"
|
||||
#import "run_chromium_command.h"
|
||||
|
||||
void runChromiumCommand(void* context, NSDictionary *input) {
|
||||
NSString *command = input[@"command"];
|
||||
NSDictionary *params = input[@"params"];
|
||||
|
||||
if ([command isEqual:@"request_access"]) {
|
||||
return requestAccessCommand(context, params);
|
||||
} else if ([command isEqual:@"has_stored_access"]) {
|
||||
return hasStoredAccessCommand(context, params);
|
||||
} else if ([command isEqual:@"start_access"]) {
|
||||
return startAccessCommand(context, params);
|
||||
} else if ([command isEqual:@"stop_access"]) {
|
||||
return stopAccessCommand(context, params);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command]));
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "autofill/run_autofill_command.h"
|
||||
#import "chromium_importer/run_chromium_command.h"
|
||||
#import "interop.h"
|
||||
#import "utils.h"
|
||||
|
||||
@ -8,6 +9,8 @@ void pickAndRunCommand(void* context, NSDictionary *input) {
|
||||
|
||||
if ([namespace isEqual:@"autofill"]) {
|
||||
return runAutofillCommand(context, input);
|
||||
} else if ([namespace isEqual:@"chromium_importer"]) {
|
||||
return runChromiumCommand(context, input);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace]));
|
||||
|
||||
@ -2,20 +2,31 @@ import { ipcMain } from "electron";
|
||||
|
||||
import { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { isMacAppStore } from "../../../utils";
|
||||
|
||||
export class ChromiumImporterService {
|
||||
constructor() {
|
||||
ipcMain.handle("chromium_importer.getMetadata", async (event) => {
|
||||
return await chromium_importer.getMetadata();
|
||||
ipcMain.handle("chromium_importer.getMetadata", async (event, isMas: boolean) => {
|
||||
return await chromium_importer.getMetadata(isMas);
|
||||
});
|
||||
|
||||
// Used on Mac OS App Store builds to request permissions to browser entries outside the sandbox
|
||||
ipcMain.handle("chromium_importer.requestBrowserAccess", async (event, browser: string) => {
|
||||
if (chromium_importer.requestBrowserAccess) {
|
||||
return await chromium_importer.requestBrowserAccess(browser, isMacAppStore());
|
||||
}
|
||||
// requestBrowserAccess not found, returning with no-op
|
||||
return;
|
||||
});
|
||||
|
||||
ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => {
|
||||
return await chromium_importer.getAvailableProfiles(browser);
|
||||
return await chromium_importer.getAvailableProfiles(browser, isMacAppStore());
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"chromium_importer.importLogins",
|
||||
async (event, browser: string, profileId: string) => {
|
||||
return await chromium_importer.importLogins(browser, profileId);
|
||||
return await chromium_importer.importLogins(browser, profileId, isMacAppStore());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,7 +20,8 @@ export class DesktopImportMetadataService
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const metadata = await ipc.tools.chromiumImporter.getMetadata();
|
||||
const isMas = ipc.platform.isMacAppStore;
|
||||
const metadata = await ipc.tools.chromiumImporter.getMetadata(isMas);
|
||||
await this.parseNativeMetaData(metadata);
|
||||
await super.init();
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
<bit-dialog #dialog dialogSize="large" background="alt">
|
||||
<span bitDialogTitle>{{ "importData" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<tools-import
|
||||
(formLoading)="this.loading = $event"
|
||||
(formDisabled)="this.disabled = $event"
|
||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
></tools-import>
|
||||
<div class="tw-relative">
|
||||
<tools-import
|
||||
(formLoading)="this.loading = $event"
|
||||
(formDisabled)="this.disabled = $event"
|
||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
[class.tw-invisible]="loading"
|
||||
></tools-import>
|
||||
<div *ngIf="loading" class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
|
||||
@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
import type { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
import { ImportMetadataServiceAbstraction } from "@bitwarden/importer-core";
|
||||
@ -39,7 +40,13 @@ export class ImportDesktopComponent {
|
||||
protected disabled = false;
|
||||
protected loading = false;
|
||||
|
||||
constructor(public dialogRef: DialogRef) {}
|
||||
protected readonly onLoadProfilesFromBrowser = this._onLoadProfilesFromBrowser.bind(this);
|
||||
protected readonly onImportFromBrowser = this._onImportFromBrowser.bind(this);
|
||||
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Callback that is called after a successful import.
|
||||
@ -48,11 +55,19 @@ export class ImportDesktopComponent {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
protected onLoadProfilesFromBrowser(browser: string): Promise<chromium_importer.ProfileInfo[]> {
|
||||
private async _onLoadProfilesFromBrowser(
|
||||
browser: string,
|
||||
): Promise<chromium_importer.ProfileInfo[]> {
|
||||
try {
|
||||
// Request browser access (required for sandboxed builds, no-op otherwise)
|
||||
await ipc.tools.chromiumImporter.requestBrowserAccess(browser);
|
||||
} catch {
|
||||
throw new Error(this.i18nService.t("browserAccessDenied"));
|
||||
}
|
||||
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
|
||||
}
|
||||
|
||||
protected onImportFromBrowser(
|
||||
private _onImportFromBrowser(
|
||||
browser: string,
|
||||
profile: string,
|
||||
): Promise<chromium_importer.LoginImportResult[]> {
|
||||
|
||||
@ -3,8 +3,13 @@ import { ipcRenderer } from "electron";
|
||||
import type { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
|
||||
const chromiumImporter = {
|
||||
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
|
||||
ipcRenderer.invoke("chromium_importer.getMetadata"),
|
||||
getMetadata: (
|
||||
isMas: boolean,
|
||||
): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
|
||||
ipcRenderer.invoke("chromium_importer.getMetadata", isMas),
|
||||
// Request browser access for Mac OS App Store (sandboxed) builds (no-op in non-sandboxed builds)
|
||||
requestBrowserAccess: (browser: string): Promise<void> =>
|
||||
ipcRenderer.invoke("chromium_importer.requestBrowserAccess", browser),
|
||||
getAvailableProfiles: (browser: string): Promise<chromium_importer.ProfileInfo[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser),
|
||||
importLogins: (
|
||||
|
||||
@ -4255,5 +4255,8 @@
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"browserAccessDenied": {
|
||||
"message": "Either an invalid browser directory was selected, or access was denied"
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +105,10 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() csvDataLoaded = new EventEmitter<string>();
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() error = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private controlContainer: ControlContainer,
|
||||
@ -112,8 +116,20 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
effect(async () => {
|
||||
this.profileList = await this.onLoadProfilesFromBrowser(this.getBrowserName(this.format()));
|
||||
// FIXME: Add error handling and display when profiles could not be loaded/retrieved
|
||||
// Callback is set via @Input after constructor, so check it exists
|
||||
if (this.onLoadProfilesFromBrowser) {
|
||||
try {
|
||||
this.profileList = await this.onLoadProfilesFromBrowser(
|
||||
this.getBrowserName(this.format()),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logService.error("Error loading profiles from browser:", error);
|
||||
const keyOrMessage = this.getValidationErrorI18nKey(error);
|
||||
this.error.emit(
|
||||
keyOrMessage === "errorOccurred" ? this.i18nService.t("errorOccurred") : keyOrMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,9 +185,11 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logService.error(`Chromium importer error: ${error}`);
|
||||
const keyOrMessage = this.getValidationErrorI18nKey(error);
|
||||
return {
|
||||
errors: {
|
||||
message: this.i18nService.t(this.getValidationErrorI18nKey(error)),
|
||||
message:
|
||||
keyOrMessage === "errorOccurred" ? this.i18nService.t("errorOccurred") : keyOrMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -180,10 +198,7 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
|
||||
|
||||
private getValidationErrorI18nKey(error: any): string {
|
||||
const message = typeof error === "string" ? error : error?.message;
|
||||
switch (message) {
|
||||
default:
|
||||
return "errorOccurred";
|
||||
}
|
||||
return message || "errorOccurred";
|
||||
}
|
||||
|
||||
private getBrowserName(format: ImportType): string {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user