1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

rework error handling & presentation

This commit is contained in:
John Harrington
2025-12-05 16:28:18 -07:00
parent 9ff43a66c5
commit 386cf03c42
11 changed files with 164 additions and 21 deletions

View File

@@ -15,6 +15,17 @@ pub mod sandbox {
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
// Before requesting access outside of sandbox, determine if browser is actually installed
const BROWSER_BUNDLE_IDS: &[(&str, &str)] = &[
("Chrome", "com.google.Chrome"),
("Chromium", "org.chromium.Chromium"),
("Microsoft Edge", "com.microsoft.edgemac"),
("Brave", "com.brave.Browser"),
("Arc", "company.thebrowser.Browser"),
("Opera", "com.operasoftware.Opera"),
("Vivaldi", "com.vivaldi.Vivaldi"),
];
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum CommandResult<T> {
@@ -61,6 +72,13 @@ pub mod sandbox {
.find(|b| b.name == browser_name)
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
if !is_browser_installed(browser_name).await? {
return Err(anyhow!(
"chromiumImporterBrowserNotInstalled:{}",
browser_name
));
}
// For macOS, data_dir is always a single-element array
let relative_path = config
.data_dir
@@ -163,6 +181,44 @@ pub mod sandbox {
});
}
}
async fn is_browser_installed(browser_name: &str) -> Result<bool> {
let bundle_id = BROWSER_BUNDLE_IDS
.iter()
.find(|(name, _)| *name == browser_name)
.map(|(_, id)| *id);
let bundle_id = match bundle_id {
Some(id) => id,
None => return Ok(true),
};
let input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "check_browser_installed".to_string(),
params: serde_json::json!({
"bundleId": bundle_id,
}),
};
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<CheckBrowserInstalledResponse> = serde_json::from_str(&output)
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
match result {
CommandResult::Success { value } => Ok(value.is_installed),
CommandResult::Error { error } => Err(anyhow!("{}", error)),
}
}
#[derive(Debug, Deserialize)]
struct CheckBrowserInstalledResponse {
#[serde(rename = "isInstalled")]
is_installed: bool,
}
}
//

View File

@@ -14,7 +14,7 @@
}
- (NSString *)requestAccessToBrowserDir:(NSString *)browserName relativePath:(NSString *)relativePath {
if (!relativePath) {
return nil;
}
@@ -53,14 +53,7 @@
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];
// Invalid directory selected caller will handle
return nil;
}

View File

@@ -0,0 +1,8 @@
#ifndef CHECK_BROWSER_INSTALLED_H
#define CHECK_BROWSER_INSTALLED_H
#import <Foundation/Foundation.h>
void checkBrowserInstalledCommand(void *context, NSDictionary *params);
#endif

View File

@@ -0,0 +1,29 @@
#import <Foundation/Foundation.h>
#import <CoreServices/CoreServices.h>
#import "../../interop.h"
#import "check_browser_installed.h"
void checkBrowserInstalledCommand(void* context, NSDictionary *params) {
NSString *bundleId = params[@"bundleId"];
if (!bundleId) {
return _return(context, _error(@"Missing required parameter: bundleId"));
}
CFURLRef appURL = NULL;
OSStatus status = LSFindApplicationForInfo(
kLSUnknownCreator,
(__bridge CFStringRef)bundleId,
NULL,
NULL,
&appURL
);
BOOL isInstalled = (status == noErr && appURL != NULL);
if (appURL != NULL) {
CFRelease(appURL);
}
_return(context, _success(@{@"isInstalled": @(isInstalled)}));
}

View File

@@ -15,7 +15,7 @@ void requestAccessCommand(void* context, NSDictionary *params) {
NSString *bookmarkData = [manager requestAccessToBrowserDir:browserName relativePath:relativePath];
if (bookmarkData == nil) {
return _return(context, _error(@"User denied access or selected an invalid browser directory"));
return _return(context, _error(@"browserAccessDenied"));
}
_return(context, _success(@{@"bookmark": bookmarkData}));

View File

@@ -3,6 +3,7 @@
#import "commands/has_stored_access.h"
#import "commands/start_access.h"
#import "commands/stop_access.h"
#import "commands/check_browser_installed.h"
#import "../interop.h"
#import "../utils.h"
#import "run_chromium_command.h"
@@ -19,6 +20,8 @@ void runChromiumCommand(void* context, NSDictionary *input) {
return startAccessCommand(context, params);
} else if ([command isEqual:@"stop_access"]) {
return stopAccessCommand(context, params);
} else if ([command isEqual:@"check_browser_installed"]) {
return checkBrowserInstalledCommand(context, params);
}
_return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command]));

View File

@@ -61,8 +61,26 @@ export class ImportDesktopComponent {
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"));
} catch (error) {
const rawMessage = error instanceof Error ? error.message : "";
// Check verbose error chain for specific i18n key indicating browser not installed
const browserNotInstalledMatch = rawMessage.match(
/chromiumImporterBrowserNotInstalled:([^:]+)/,
);
let message: string;
if (browserNotInstalledMatch) {
message = this.i18nService.t(
"chromiumImporterBrowserNotInstalled",
browserNotInstalledMatch[1],
);
} else {
// Invalid folder, explicit permission denial, or system error
message = this.i18nService.t("browserAccessDenied");
}
throw new Error(message);
}
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
}

View File

@@ -4299,5 +4299,14 @@
},
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
"message": "Set an unlock method to change your timeout action"
},
"chromiumImporterBrowserNotInstalled": {
"message": "$BROWSER$ is not installed. Please install $BROWSER$ before importing passwords from it.",
"placeholders": {
"browser": {
"content": "$1",
"example": "Chrome"
}
}
}
}