diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index cae80c9..85da717 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -1,6 +1,6 @@ use crate::api_client::is_browser_version_nightly; -use crate::browser_runner::{BrowserProfile, BrowserRunner}; use crate::browser_version_service::{BrowserVersionInfo, BrowserVersionService}; +use crate::profile::BrowserProfile; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -30,7 +30,6 @@ pub struct AutoUpdateState { pub struct AutoUpdater { version_service: BrowserVersionService, - browser_runner: BrowserRunner, settings_manager: SettingsManager, } @@ -38,7 +37,6 @@ impl AutoUpdater { pub fn new() -> Self { Self { version_service: BrowserVersionService::new(), - browser_runner: BrowserRunner::new(), settings_manager: SettingsManager::new(), } } @@ -51,8 +49,8 @@ impl AutoUpdater { let mut browser_versions: HashMap> = HashMap::new(); // Group profiles by browser - let profiles = self - .browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; let mut browser_profiles: HashMap> = HashMap::new(); @@ -294,8 +292,8 @@ impl AutoUpdater { browser: &str, new_version: &str, ) -> Result, Box> { - let profiles = self - .browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; @@ -312,10 +310,7 @@ impl AutoUpdater { // Check if this is an update (newer version) if self.is_version_newer(new_version, &profile.version) { // Update the profile version - match self - .browser_runner - .update_profile_version(&profile.name, new_version) - { + match profile_manager.update_profile_version(&profile.name, new_version) { Ok(_) => { updated_profiles.push(profile.name); } @@ -361,8 +356,8 @@ impl AutoUpdater { &self, ) -> Result, Box> { // Load current profiles - let profiles = self - .browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to load profiles: {e}"))?; diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index af8e8e6..b792260 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1,12 +1,13 @@ +use crate::platform_browser; +use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use directories::BaseDirs; -use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::fs::{self, create_dir_all}; +use std::fs::create_dir_all; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; -use sysinfo::{Pid, System}; +use sysinfo::System; use tauri::Emitter; use crate::browser::{create_browser, BrowserType, ProxySettings}; @@ -18,1040 +19,11 @@ use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BrowserProfile { - pub id: uuid::Uuid, - pub name: String, - pub browser: String, - pub version: String, - #[serde(default)] - pub proxy_id: Option, // Reference to stored proxy - #[serde(default)] - pub process_id: Option, - #[serde(default)] - pub last_launch: Option, - #[serde(default = "default_release_type")] - pub release_type: String, // "stable" or "nightly" - #[serde(default)] - pub camoufox_config: Option, // Camoufox configuration - #[serde(default)] - pub group_id: Option, // Reference to profile group -} - -fn default_release_type() -> String { - "stable".to_string() -} - // Global state to track currently downloading browser-version pairs lazy_static::lazy_static! { static ref DOWNLOADING_BROWSERS: Arc>> = Arc::new(Mutex::new(HashSet::new())); } -// Platform-specific modules -#[cfg(target_os = "macos")] -mod macos { - use super::*; - use std::ffi::OsString; - use std::process::Command; - - pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool { - match browser_type { - "mullvad-browser" => { - let has_mullvad_in_exe = exe_name.contains("mullvad"); - let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); - let has_mullvad_in_cmd = cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Mullvad Browser.app") - || arg_str.contains("mullvad") - || arg_str.contains("Mullvad") - || arg_str.contains("/Applications/Mullvad Browser.app/") - || arg_str.contains("MullvadBrowser") - }); - - has_mullvad_in_exe || (has_firefox_exe && has_mullvad_in_cmd) - } - "tor-browser" => { - let has_tor_in_exe = exe_name.contains("tor"); - let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); - let has_tor_in_cmd = cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Tor Browser.app") - || arg_str.contains("tor-browser") - || arg_str.contains("TorBrowser") - || arg_str.contains("/Applications/Tor Browser.app/") - || arg_str.contains("TorBrowser-Data") - }); - - has_tor_in_exe || (has_firefox_exe && has_tor_in_cmd) - } - _ => false, - } - } - - pub async fn launch_browser_process( - executable_path: &std::path::Path, - args: &[String], - ) -> Result> { - println!("Launching browser on macOS: {executable_path:?} with args: {args:?}"); - Ok(Command::new(executable_path).args(args).spawn()?) - } - - pub async fn open_url_in_existing_browser_firefox_like( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - let pid = profile.process_id.unwrap(); - let profile_data_path = profile.get_profile_data_path(profiles_dir); - - // First try: Use Firefox remote command - println!("Trying Firefox remote command for PID: {pid}"); - let browser = create_browser(browser_type); - if let Ok(executable_path) = browser.get_executable_path(browser_dir) { - let remote_args = vec![ - "-profile".to_string(), - profile_data_path.to_string_lossy().to_string(), - "-new-tab".to_string(), - url.to_string(), - ]; - - let remote_output = Command::new(executable_path).args(&remote_args).output(); - - match remote_output { - Ok(output) if output.status.success() => { - println!("Firefox remote command succeeded"); - return Ok(()); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!( - "Firefox remote command failed with stderr: {stderr}, trying AppleScript fallback" - ); - } - Err(e) => { - println!("Firefox remote command error: {e}, trying AppleScript fallback"); - } - } - } - - // Fallback: Use AppleScript - let escaped_url = url - .replace("\"", "\\\"") - .replace("\\", "\\\\") - .replace("'", "\\'"); - - let script = format!( - r#" -try - tell application "System Events" - -- Find the exact process by PID - set targetProcess to (first application process whose unix id is {pid}) - - -- Verify the process exists - if not (exists targetProcess) then - error "No process found with PID {pid}" - end if - - -- Get the process name for verification - set processName to name of targetProcess - - -- Bring the process to the front first - set frontmost of targetProcess to true - delay 1.0 - - -- Check if the process has any visible windows - set windowList to windows of targetProcess - set hasVisibleWindow to false - repeat with w in windowList - if visible of w is true then - set hasVisibleWindow to true - exit repeat - end if - end repeat - - if not hasVisibleWindow then - -- No visible windows, create a new one - tell targetProcess - keystroke "n" using command down - delay 2.0 - end tell - end if - - -- Ensure the process is frontmost again - set frontmost of targetProcess to true - delay 0.5 - - -- Focus on the address bar and open URL - tell targetProcess - -- Open a new tab - keystroke "t" using command down - delay 1.5 - - -- Focus address bar (Cmd+L) - keystroke "l" using command down - delay 0.5 - - -- Type the URL - keystroke "{escaped_url}" - delay 0.5 - - -- Press Enter to navigate - keystroke return - end tell - - return "Successfully opened URL in " & processName & " (PID: {pid})" - end tell -on error errMsg number errNum - return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" -end try - "# - ); - - println!("Executing AppleScript fallback for Firefox-based browser (PID: {pid})..."); - let output = Command::new("osascript").args(["-e", &script]).output()?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - println!("AppleScript failed: {error_msg}"); - return Err( - format!( - "Both Firefox remote command and AppleScript failed. AppleScript error: {error_msg}" - ) - .into(), - ); - } else { - println!("AppleScript succeeded"); - } - - Ok(()) - } - - pub async fn open_url_in_existing_browser_tor_mullvad( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - _profiles_dir: &Path, - ) -> Result<(), Box> { - let pid = profile.process_id.unwrap(); - - println!("Opening URL in TOR/Mullvad browser using file-based approach (PID: {pid})"); - - // Method 1: Try using a temporary HTML file approach - println!("Attempting file-based URL opening for TOR/Mullvad browser"); - - let temp_dir = std::env::temp_dir(); - let temp_file_name = format!("donut_browser_url_{}.html", std::process::id()); - let temp_file_path = temp_dir.join(&temp_file_name); - - let html_content = format!( - r#" - - - - - Redirecting... - - - -

Redirecting to {url}...

- -"# - ); - - match std::fs::write(&temp_file_path, html_content) { - Ok(()) => { - println!("Created temporary HTML file: {temp_file_path:?}"); - - let browser = create_browser(browser_type.clone()); - if let Ok(executable_path) = browser.get_executable_path(browser_dir) { - let open_result = Command::new("open") - .args([ - "-a", - executable_path.to_str().unwrap(), - temp_file_path.to_str().unwrap(), - ]) - .output(); - - // Clean up the temporary file after a short delay - let temp_file_path_clone = temp_file_path.clone(); - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - let _ = std::fs::remove_file(temp_file_path_clone); - }); - - match open_result { - Ok(output) if output.status.success() => { - println!("Successfully opened URL using file-based approach"); - return Ok(()); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("File-based approach failed: {stderr}"); - } - Err(e) => { - println!("File-based approach error: {e}"); - } - } - } - - let _ = std::fs::remove_file(&temp_file_path); - } - Err(e) => { - println!("Failed to create temporary HTML file: {e}"); - } - } - - // Method 2: Try using the 'open' command directly with the URL - println!("Attempting direct URL opening with 'open' command"); - - let browser = create_browser(browser_type.clone()); - if let Ok(executable_path) = browser.get_executable_path(browser_dir) { - let direct_open_result = Command::new("open") - .args(["-a", executable_path.to_str().unwrap(), url]) - .output(); - - match direct_open_result { - Ok(output) if output.status.success() => { - println!("Successfully opened URL using direct 'open' command"); - return Ok(()); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("Direct 'open' command failed: {stderr}"); - } - Err(e) => { - println!("Direct 'open' command error: {e}"); - } - } - } - - // If all methods fail, return a helpful error message - Err( - format!( - "Failed to open URL in existing TOR/Mullvad browser (PID: {pid}). All methods failed:\n\ - 1. File-based approach failed\n\ - 2. Direct 'open' command failed\n\ - \n\ - This may be due to browser security restrictions or the browser process may have changed.\n\ - Try closing and reopening the browser, or manually paste the URL: {url}" - ) - .into(), - ) - } - - pub async fn open_url_in_existing_browser_chromium( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - _profiles_dir: &Path, - ) -> Result<(), Box> { - let pid = profile.process_id.unwrap(); - - // First, try using the browser's built-in URL opening capability - println!("Trying Chromium URL opening for PID: {pid}"); - - let browser = create_browser(browser_type); - if let Ok(executable_path) = browser.get_executable_path(browser_dir) { - let profile_data_path = profile.get_profile_data_path(_profiles_dir); - let remote_output = Command::new(executable_path) - .args([ - &format!("--user-data-dir={}", profile_data_path.to_string_lossy()), - url, - ]) - .output(); - - match remote_output { - Ok(output) if output.status.success() => { - println!("Chromium URL opening succeeded"); - return Ok(()); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("Chromium URL opening failed: {stderr}, trying AppleScript"); - } - Err(e) => { - println!("Chromium URL opening error: {e}, trying AppleScript"); - } - } - } - - // Fallback to AppleScript - let escaped_url = url - .replace("\"", "\\\"") - .replace("\\", "\\\\") - .replace("'", "\\'"); - - let script = format!( - r#" -try - tell application "System Events" - -- Find the exact process by PID - set targetProcess to (first application process whose unix id is {pid}) - - -- Verify the process exists - if not (exists targetProcess) then - error "No process found with PID {pid}" - end if - - -- Get the process name for verification - set processName to name of targetProcess - - -- Bring the process to the front first - set frontmost of targetProcess to true - delay 1.0 - - -- Check if the process has any visible windows - set windowList to windows of targetProcess - set hasVisibleWindow to false - repeat with w in windowList - if visible of w is true then - set hasVisibleWindow to true - exit repeat - end if - end repeat - - if not hasVisibleWindow then - -- No visible windows, create a new one - tell targetProcess - keystroke "n" using command down - delay 2.0 - end tell - end if - - -- Ensure the process is frontmost again - set frontmost of targetProcess to true - delay 0.5 - - -- Focus on the address bar and open URL - tell targetProcess - -- Open a new tab - keystroke "t" using command down - delay 1.5 - - -- Focus address bar (Cmd+L) - keystroke "l" using command down - delay 0.5 - - -- Type the URL - keystroke "{escaped_url}" - delay 0.5 - - -- Press Enter to navigate - keystroke return - end tell - - return "Successfully opened URL in " & processName & " (PID: {pid})" - end tell -on error errMsg number errNum - return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" -end try - "# - ); - - println!("Executing AppleScript for Chromium-based browser (PID: {pid})..."); - let output = Command::new("osascript").args(["-e", &script]).output()?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - println!("AppleScript failed: {error_msg}"); - return Err( - format!("Failed to open URL in existing Chromium-based browser: {error_msg}").into(), - ); - } else { - println!("AppleScript succeeded"); - } - - Ok(()) - } - - pub async fn kill_browser_process_impl( - pid: u32, - ) -> Result<(), Box> { - println!("Attempting to kill browser process with PID: {pid}"); - - // First try SIGTERM (graceful shutdown) - let output = Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .output() - .map_err(|e| format!("Failed to execute kill command: {e}"))?; - - if !output.status.success() { - // If SIGTERM fails, try SIGKILL (force kill) - let output = Command::new("kill") - .args(["-KILL", &pid.to_string()]) - .output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to kill process {}: {}", - pid, - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - } - - println!("Successfully killed browser process with PID: {pid}"); - Ok(()) - } -} - -#[cfg(target_os = "windows")] -mod windows { - use super::*; - use std::ffi::OsString; - use std::process::Command; - - pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool { - let exe_lower = exe_name.to_lowercase(); - - // Check for Firefox-based browsers first by executable name - let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe"); - - if !is_firefox_family { - return false; - } - - // Check command arguments for profile paths and browser-specific indicators - let cmd_line = cmd - .iter() - .map(|s| s.to_string_lossy().to_lowercase()) - .collect::>() - .join(" "); - - match browser_type { - "tor-browser" => { - // Check for TOR browser specific paths and arguments - cmd_line.contains("tor") - || cmd_line.contains("browser\\torbrowser") - || cmd_line.contains("tor-browser") - || cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb")) - } - "mullvad-browser" => { - // Check for Mullvad browser specific paths and arguments - cmd_line.contains("mullvad") - || cmd_line.contains("browser\\mullvadbrowser") - || cmd_line.contains("mullvad-browser") - || cmd_line.contains("profile") && cmd_line.contains("mullvad") - } - _ => false, - } - } - - pub async fn launch_browser_process( - executable_path: &std::path::Path, - args: &[String], - ) -> Result> { - println!( - "Launching browser on Windows: {:?} with args: {:?}", - executable_path, args - ); - - // Check if the executable exists - if !executable_path.exists() { - return Err(format!("Browser executable not found: {:?}", executable_path).into()); - } - - // On Windows, set up the command with proper working directory - let mut cmd = Command::new(executable_path); - cmd.args(args); - - // Set working directory to the executable's directory for better compatibility - if let Some(parent_dir) = executable_path.parent() { - cmd.current_dir(parent_dir); - } - - // For Windows 7 compatibility, set some environment variables - cmd.env( - "PROCESSOR_ARCHITECTURE", - std::env::var("PROCESSOR_ARCHITECTURE").unwrap_or_else(|_| "x86".to_string()), - ); - - // Ensure proper PATH for DLL loading - if let Some(exe_dir) = executable_path.parent() { - let mut path_var = std::env::var("PATH").unwrap_or_default(); - if !path_var.is_empty() { - path_var = format!("{};{}", exe_dir.display(), path_var); - } else { - path_var = exe_dir.display().to_string(); - } - cmd.env("PATH", path_var); - } - - // Launch the process - let child = cmd - .spawn() - .map_err(|e| format!("Failed to launch browser process: {}", e))?; - - println!( - "Successfully launched browser process with PID: {}", - child.id() - ); - Ok(child) - } - - pub async fn open_url_in_existing_browser_firefox_like( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - let browser = create_browser(browser_type); - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - - let profile_data_path = profile.get_profile_data_path(profiles_dir); - - // For Windows, try using the -requestPending approach for Firefox - let mut cmd = Command::new(executable_path); - cmd.args([ - "-profile", - &profile_data_path.to_string_lossy(), - "-requestPending", - "-new-tab", - url, - ]); - - // Set working directory - if let Some(parent_dir) = browser_dir - .parent() - .or_else(|| browser_dir.ancestors().nth(1)) - { - cmd.current_dir(parent_dir); - } - - let output = cmd.output()?; - - if !output.status.success() { - // Fallback: try without -requestPending - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - let mut fallback_cmd = Command::new(executable_path); - let profile_data_path = profile.get_profile_data_path(profiles_dir); - fallback_cmd.args([ - "-profile", - &profile_data_path.to_string_lossy(), - "-new-tab", - url, - ]); - - if let Some(parent_dir) = browser_dir - .parent() - .or_else(|| browser_dir.ancestors().nth(1)) - { - fallback_cmd.current_dir(parent_dir); - } - - let fallback_output = fallback_cmd.output()?; - - if !fallback_output.status.success() { - return Err( - format!( - "Failed to open URL in existing browser: {}", - String::from_utf8_lossy(&fallback_output.stderr) - ) - .into(), - ); - } - } - - Ok(()) - } - - pub async fn open_url_in_existing_browser_tor_mullvad( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - // On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line - // even with -no-remote, by launching a new instance that hands off to existing one - let browser = create_browser(browser_type.clone()); - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - - let mut cmd = Command::new(&executable_path); - let profile_data_path = profile.get_profile_data_path(profiles_dir); - cmd.args(["-profile", &profile_data_path.to_string_lossy(), url]); - - // Set working directory - if let Some(parent_dir) = browser_dir - .parent() - .or_else(|| browser_dir.ancestors().nth(1)) - { - cmd.current_dir(parent_dir); - } - - let output = cmd.output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.", - browser_type.as_str(), - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - - Ok(()) - } - - pub async fn open_url_in_existing_browser_chromium( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - let browser = create_browser(browser_type.clone()); - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - - let mut cmd = Command::new(&executable_path); - cmd.args([ - &format!( - "--user-data-dir={}", - profile - .get_profile_data_path(profiles_dir) - .to_string_lossy() - ), - "--new-window", - url, - ]); - - // Set working directory - if let Some(parent_dir) = browser_dir - .parent() - .or_else(|| browser_dir.ancestors().nth(1)) - { - cmd.current_dir(parent_dir); - } - - let output = cmd.output()?; - - if !output.status.success() { - // Try fallback without --new-window - let mut fallback_cmd = Command::new(&executable_path); - fallback_cmd.args([ - &format!( - "--user-data-dir={}", - profile - .get_profile_data_path(profiles_dir) - .to_string_lossy() - ), - url, - ]); - - if let Some(parent_dir) = browser_dir - .parent() - .or_else(|| browser_dir.ancestors().nth(1)) - { - fallback_cmd.current_dir(parent_dir); - } - - let fallback_output = fallback_cmd.output()?; - - if !fallback_output.status.success() { - return Err( - format!( - "Failed to open URL in existing Chromium-based browser: {}", - String::from_utf8_lossy(&fallback_output.stderr) - ) - .into(), - ); - } - } - - Ok(()) - } - - pub async fn kill_browser_process_impl( - pid: u32, - ) -> Result<(), Box> { - // First try using sysinfo (cross-platform approach) - let system = System::new_all(); - if let Some(process) = system.process(Pid::from(pid as usize)) { - if process.kill() { - println!("Successfully killed browser process with PID: {pid}"); - return Ok(()); - } - } - - // Fallback to Windows-specific process termination - use std::process::Command; - - // Try taskkill command as fallback - let output = Command::new("taskkill") - .args(["/F", "/PID", &pid.to_string()]) - .output(); - - match output { - Ok(result) => { - if result.status.success() { - println!("Successfully killed browser process with PID: {pid} using taskkill"); - Ok(()) - } else { - Err( - format!( - "Failed to kill process {} with taskkill: {}", - pid, - String::from_utf8_lossy(&result.stderr) - ) - .into(), - ) - } - } - Err(e) => Err(format!("Failed to execute taskkill for process {}: {}", pid, e).into()), - } - } -} - -#[cfg(target_os = "linux")] -mod linux { - use super::*; - use std::ffi::OsString; - use std::process::Command; - - pub fn is_tor_or_mullvad_browser( - _exe_name: &str, - _cmd: &[OsString], - _browser_type: &str, - ) -> bool { - // Linux implementation would go here - false - } - - pub async fn launch_browser_process( - executable_path: &std::path::Path, - args: &[String], - ) -> Result> { - println!( - "Launching browser on Linux: {:?} with args: {:?}", - executable_path, args - ); - - // Check if the executable exists and is executable - if !executable_path.exists() { - return Err(format!("Browser executable not found: {:?}", executable_path).into()); - } - - // Check if we can read the executable to detect architecture issues early - if let Err(e) = std::fs::File::open(executable_path) { - return Err(format!("Cannot access browser executable: {}", e).into()); - } - - // Ensure the executable has proper permissions - if let Err(e) = std::fs::metadata(executable_path) { - return Err(format!("Cannot get executable metadata: {}", e).into()); - } - - // On Linux, we might need to set LD_LIBRARY_PATH for some browsers - let mut cmd = Command::new(executable_path); - cmd.args(args); - - // For Firefox-based browsers, ensure library path includes the installation directory - if let Some(install_dir) = executable_path.parent() { - let mut ld_library_path = Vec::new(); - - // Add multiple potential library directories - let lib_dirs = [ - install_dir.join("lib"), - install_dir.join("../lib"), // Parent directory lib - install_dir.join("../../lib"), // Grandparent directory lib - install_dir.to_path_buf(), // Installation directory itself - ]; - - for lib_dir in &lib_dirs { - if lib_dir.exists() { - ld_library_path.push(lib_dir.to_string_lossy().to_string()); - } - } - - // For Firefox specifically, add common system library paths that might be needed - let firefox_lib_paths = [ - "/usr/lib/firefox", - "/usr/lib/x86_64-linux-gnu", - "/usr/lib/aarch64-linux-gnu", - "/lib/x86_64-linux-gnu", - "/lib/aarch64-linux-gnu", - ]; - - for lib_path in &firefox_lib_paths { - let path = std::path::Path::new(lib_path); - if path.exists() { - ld_library_path.push(lib_path.to_string()); - } - } - - // Preserve existing LD_LIBRARY_PATH - if let Ok(existing_path) = std::env::var("LD_LIBRARY_PATH") { - ld_library_path.push(existing_path); - } - - // Set the combined LD_LIBRARY_PATH - if !ld_library_path.is_empty() { - cmd.env("LD_LIBRARY_PATH", ld_library_path.join(":")); - println!("Set LD_LIBRARY_PATH to: {}", ld_library_path.join(":")); - } - } - - // Additional Linux-specific environment variables for better compatibility - cmd.env( - "DISPLAY", - std::env::var("DISPLAY").unwrap_or(":0".to_string()), - ); - - // Set MOZ_ENABLE_WAYLAND for better Wayland support - if std::env::var("WAYLAND_DISPLAY").is_ok() { - cmd.env("MOZ_ENABLE_WAYLAND", "1"); - } - - // Disable GPU acceleration if running in headless environments - if std::env::var("DISPLAY").is_err() || std::env::var("WAYLAND_DISPLAY").is_err() { - println!("No display detected, browser may fail to start"); - } - - // Attempt to spawn with better error handling for architecture issues - match cmd.spawn() { - Ok(child) => Ok(child), - Err(e) => { - // Detect architecture mismatch errors - if e.kind() == std::io::ErrorKind::Other { - let error_msg = e.to_string(); - if error_msg.contains("Exec format error") { - return Err(format!( - "Architecture mismatch: The browser executable is not compatible with your system architecture ({}). \ - This typically happens when trying to run x86_64 binaries on ARM64 systems. \ - Please use a browser that supports your architecture, such as Zen Browser or Brave. \ - Executable: {:?}", - std::env::consts::ARCH, - executable_path - ).into()); - } else if error_msg.contains("No such file or directory") { - return Err(format!( - "Executable or required library not found. This might be due to missing dependencies or incorrect executable path. \ - Try installing missing libraries or verify the browser installation. \ - Executable: {:?}, Error: {}", - executable_path, error_msg - ).into()); - } - } - Err(format!("Failed to launch browser: {}", e).into()) - } - } - } - - pub async fn open_url_in_existing_browser_firefox_like( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - let browser = create_browser(browser_type); - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - - let profile_data_path = profile.get_profile_data_path(profiles_dir); - let output = Command::new(executable_path) - .args([ - "-profile", - &profile_data_path.to_string_lossy(), - "-new-tab", - url, - ]) - .output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to open URL in existing browser: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - - Ok(()) - } - - pub async fn open_url_in_existing_browser_tor_mullvad( - _profile: &BrowserProfile, - _url: &str, - _browser_type: BrowserType, - _browser_dir: &Path, - _profiles_dir: &Path, - ) -> Result<(), Box> { - Err("Opening URLs in existing Firefox-based browsers is not supported on Linux when using -no-remote".into()) - } - - pub async fn open_url_in_existing_browser_chromium( - profile: &BrowserProfile, - url: &str, - browser_type: BrowserType, - browser_dir: &Path, - profiles_dir: &Path, - ) -> Result<(), Box> { - let browser = create_browser(browser_type); - let executable_path = browser - .get_executable_path(browser_dir) - .map_err(|e| format!("Failed to get executable path: {}", e))?; - - let profile_data_path = profile.get_profile_data_path(profiles_dir); - let output = Command::new(executable_path) - .args([ - &format!("--user-data-dir={}", profile_data_path.to_string_lossy()), - url, - ]) - .output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to open URL in existing Chromium-based browser: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - - Ok(()) - } - - pub async fn kill_browser_process_impl( - pid: u32, - ) -> Result<(), Box> { - let system = System::new_all(); - if let Some(process) = system.process(Pid::from(pid as usize)) { - if !process.kill() { - return Err(format!("Failed to kill process {}", pid).into()); - } - } else { - return Err(format!("Process {} not found", pid).into()); - } - - println!("Successfully killed browser process with PID: {pid}"); - Ok(()) - } -} - pub struct BrowserRunner { base_dirs: BaseDirs, } @@ -1063,241 +35,10 @@ impl BrowserRunner { } } - /// Helper function to kill a browser process by PID only (used during migration) - async fn kill_browser_process_by_pid( - pid: u32, - ) -> Result<(), Box> { - println!("Attempting to kill browser process with PID: {pid} during migration"); - - // Kill the process using platform-specific implementation - #[cfg(target_os = "macos")] - macos::kill_browser_process_impl(pid).await?; - - #[cfg(target_os = "windows")] - windows::kill_browser_process_impl(pid).await?; - - #[cfg(target_os = "linux")] - linux::kill_browser_process_impl(pid).await?; - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - return Err("Unsupported platform".into()); - - println!("Successfully killed browser process with PID: {pid} during migration"); - Ok(()) - } - /// Migrate old profile structure to new UUID-based structure pub async fn migrate_profiles_to_uuid(&self) -> Result, Box> { - let profiles_dir = self.get_profiles_dir(); - if !profiles_dir.exists() { - return Ok(vec![]); - } - - let mut migrated_profiles = Vec::new(); - - // Scan for old-format profile files (*.json files directly in profiles directory) - for entry in fs::read_dir(&profiles_dir)? { - let entry = entry?; - let path = entry.path(); - - // Look for .json files that are NOT in UUID directories - if path.is_file() && path.extension().is_some_and(|ext| ext == "json") { - let content = fs::read_to_string(&path)?; - - // Try to parse as old profile format (without UUID) - let mut old_profile: serde_json::Value = serde_json::from_str(&content)?; - - // Skip if it already has an id field (already migrated) - if old_profile.get("id").is_some() { - continue; - } - - // Generate new UUID for this profile - let profile_id = uuid::Uuid::new_v4(); - - // Extract profile name before mutating - let profile_name = old_profile["name"] - .as_str() - .unwrap_or("unknown") - .to_string(); - - // Check if there's a running browser process for this profile and kill it - if let Some(process_id_value) = old_profile.get("process_id") { - if let Some(pid) = process_id_value.as_u64() { - let pid = pid as u32; - println!("Found running browser process (PID: {pid}) for profile '{profile_name}' during migration"); - - // Kill the process before migration - if let Err(e) = Self::kill_browser_process_by_pid(pid).await { - println!( - "Warning: Failed to kill browser process (PID: {pid}) during migration: {e}" - ); - // Continue with migration even if kill fails - the process might already be dead - } else { - println!( - "Successfully killed browser process (PID: {pid}) for profile '{profile_name}'" - ); - } - - // Clear the process_id since we killed it - old_profile["process_id"] = serde_json::Value::Null; - } - } - - let snake_case_name = profile_name.to_lowercase().replace(" ", "_"); - let old_profile_dir = profiles_dir.join(&snake_case_name); - - // Create new UUID directory and profile subdirectory - let new_profile_dir = profiles_dir.join(profile_id.to_string()); - let new_profile_data_dir = new_profile_dir.join("profile"); - create_dir_all(&new_profile_dir)?; - create_dir_all(&new_profile_data_dir)?; - - // Now update the profile with UUID (no need to store profile_path anymore) - old_profile["id"] = serde_json::Value::String(profile_id.to_string()); - - // Handle proxy migration - extract proxy to separate storage if it exists - if let Some(proxy_value) = old_profile.get("proxy").cloned() { - if !proxy_value.is_null() { - // Try to deserialize the proxy settings - if let Ok(proxy_settings) = serde_json::from_value::(proxy_value) { - // Create a stored proxy with the profile name (all proxies are now enabled by default) - let proxy_name = format!("{profile_name} Proxy"); - match PROXY_MANAGER.create_stored_proxy(proxy_name.clone(), proxy_settings.clone()) { - Ok(stored_proxy) => { - // Update profile to reference the stored proxy - old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); - println!( - "Migrated proxy for profile '{}' to stored proxy '{}'", - profile_name, stored_proxy.name - ); - } - Err(e) => { - println!("Warning: Failed to migrate proxy for profile '{profile_name}': {e}"); - // If creation fails (e.g., name collision), try to find existing proxy with same settings - let existing_proxies = PROXY_MANAGER.get_stored_proxies(); - if let Some(existing_proxy) = existing_proxies.iter().find(|p| { - p.proxy_settings.proxy_type == proxy_settings.proxy_type - && p.proxy_settings.host == proxy_settings.host - && p.proxy_settings.port == proxy_settings.port - && p.proxy_settings.username == proxy_settings.username - && p.proxy_settings.password == proxy_settings.password - }) { - old_profile["proxy_id"] = serde_json::Value::String(existing_proxy.id.clone()); - println!( - "Reused existing proxy '{}' for profile '{}'", - existing_proxy.name, profile_name - ); - } else { - // Try with a different name if the original failed due to name collision - let alt_proxy_name = format!( - "{profile_name} Proxy {}", - &uuid::Uuid::new_v4().to_string()[..8] - ); - match PROXY_MANAGER - .create_stored_proxy(alt_proxy_name.clone(), proxy_settings.clone()) - { - Ok(stored_proxy) => { - old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); - println!( - "Migrated proxy for profile '{}' to stored proxy '{}' with fallback name", - profile_name, stored_proxy.name - ); - } - Err(e2) => { - println!("Error: Could not migrate proxy for profile '{profile_name}' even with fallback name: {e2}"); - } - } - } - } - } - } else { - println!( - "Warning: Could not deserialize proxy settings for profile '{profile_name}'" - ); - } - } - } - - // Always remove the old proxy field after migration attempt, whether successful or not - if old_profile - .as_object_mut() - .unwrap() - .remove("proxy") - .is_some() - { - println!("Removed legacy proxy field from profile '{profile_name}'"); - } - - // Move old profile directory contents to new UUID/profile directory if it exists - if old_profile_dir.exists() && old_profile_dir.is_dir() { - // Copy all contents from old directory to new profile subdirectory - for entry in fs::read_dir(&old_profile_dir)? { - let entry = entry?; - let source_path = entry.path(); - let dest_path = new_profile_data_dir.join(entry.file_name()); - - if source_path.is_dir() { - Self::copy_directory_recursive(&source_path, &dest_path)?; - } else { - fs::copy(&source_path, &dest_path)?; - } - } - - // Remove old profile directory - fs::remove_dir_all(&old_profile_dir)?; - println!( - "Migrated profile directory: {} -> {}", - old_profile_dir.display(), - new_profile_data_dir.display() - ); - } - - // Save migrated profile as metadata.json in UUID directory - let metadata_file = new_profile_dir.join("metadata.json"); - let json = serde_json::to_string_pretty(&old_profile)?; - fs::write(&metadata_file, json)?; - - // Remove old profile JSON file - fs::remove_file(&path)?; - - migrated_profiles.push(profile_name.clone()); - println!("Migrated profile '{profile_name}' to UUID: {profile_id}"); - } - } - - if !migrated_profiles.is_empty() { - println!( - "Successfully migrated {} profiles to UUID format", - migrated_profiles.len() - ); - } - - Ok(migrated_profiles) - } - - /// Recursively copy directory contents - fn copy_directory_recursive( - source: &Path, - destination: &Path, - ) -> Result<(), Box> { - if !destination.exists() { - create_dir_all(destination)?; - } - - for entry in fs::read_dir(source)? { - let entry = entry?; - let source_path = entry.path(); - let dest_path = destination.join(entry.file_name()); - - if source_path.is_dir() { - Self::copy_directory_recursive(&source_path, &dest_path)?; - } else { - fs::copy(&source_path, &dest_path)?; - } - } - - Ok(()) + let profile_manager = ProfileManager::new(); + profile_manager.migrate_profiles_to_uuid().await } // Helper function to check if a process matches TOR/Mullvad browser @@ -1308,13 +49,13 @@ impl BrowserRunner { browser_type: &str, ) -> bool { #[cfg(target_os = "macos")] - return macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); + return platform_browser::macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(target_os = "windows")] - return windows::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); + return platform_browser::windows::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(target_os = "linux")] - return linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); + return platform_browser::linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { @@ -1335,289 +76,8 @@ impl BrowserRunner { } pub fn get_profiles_dir(&self) -> PathBuf { - let mut path = self.base_dirs.data_local_dir().to_path_buf(); - path.push(if cfg!(debug_assertions) { - "DonutBrowserDev" - } else { - "DonutBrowser" - }); - path.push("profiles"); - path - } - - pub fn create_profile( - &self, - name: &str, - browser: &str, - version: &str, - release_type: &str, - proxy_id: Option, - camoufox_config: Option, - ) -> Result> { - self.create_profile_with_group( - name, - browser, - version, - release_type, - proxy_id, - camoufox_config, - None, - ) - } - - #[allow(clippy::too_many_arguments)] - pub fn create_profile_with_group( - &self, - name: &str, - browser: &str, - version: &str, - release_type: &str, - proxy_id: Option, - camoufox_config: Option, - group_id: Option, - ) -> Result> { - println!("Attempting to create profile: {name}"); - - // Check if a profile with this name already exists (case insensitive) - let existing_profiles = self.list_profiles()?; - if existing_profiles - .iter() - .any(|p| p.name.to_lowercase() == name.to_lowercase()) - { - return Err(format!("Profile with name '{name}' already exists").into()); - } - - // Generate a new UUID for this profile - let profile_id = uuid::Uuid::new_v4(); - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile_id.to_string()); - let profile_data_dir = profile_uuid_dir.join("profile"); - let profile_file = profile_uuid_dir.join("metadata.json"); - - // Create profile directory with UUID and profile subdirectory - create_dir_all(&profile_uuid_dir)?; - create_dir_all(&profile_data_dir)?; - - let profile = BrowserProfile { - id: profile_id, - name: name.to_string(), - browser: browser.to_string(), - version: version.to_string(), - proxy_id: proxy_id.clone(), - process_id: None, - last_launch: None, - release_type: release_type.to_string(), - camoufox_config: camoufox_config.clone(), - group_id: group_id.clone(), - }; - - // Save profile info - self.save_profile(&profile)?; - - // Verify the profile was saved correctly - if !profile_file.exists() { - return Err(format!("Failed to create profile file for '{name}'").into()); - } - - println!("Profile '{name}' created successfully with ID: {profile_id}"); - - // Create user.js with common Firefox preferences and apply proxy settings if provided - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?; - } else { - // Proxy ID provided but not found, disable proxy - self.disable_proxy_settings_in_profile(&profile_data_dir)?; - } - } else { - // Create user.js with common Firefox preferences but no proxy - self.disable_proxy_settings_in_profile(&profile_data_dir)?; - } - - // Perform cleanup after profile creation to remove any unused binaries - if let Err(e) = self.cleanup_unused_binaries_internal() { - println!("Warning: Failed to cleanup unused binaries after profile creation: {e}"); - } - - Ok(profile) - } - - pub async fn update_profile_proxy( - &self, - app_handle: tauri::AppHandle, - profile_name: &str, - proxy_id: Option, - ) -> Result> { - // Find the profile by name - let profiles = - self - .list_profiles() - .map_err(|e| -> Box { - format!("Failed to list profiles: {e}").into() - })?; - - let mut profile = profiles - .into_iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| -> Box { - format!("Profile {profile_name} not found").into() - })?; - - // Check if browser is running to manage proxy accordingly - let browser_is_running = profile.process_id.is_some() - && self - .check_browser_status(app_handle.clone(), &profile) - .await?; - - // If browser is running, stop existing proxy - if browser_is_running && profile.proxy_id.is_some() { - if let Some(pid) = profile.process_id { - let _ = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await; - } - } - - // Update proxy settings - profile.proxy_id = proxy_id.clone(); - - // Save the updated profile - self - .save_profile(&profile) - .map_err(|e| -> Box { - format!("Failed to save profile: {e}").into() - })?; - - // Handle proxy startup/configuration - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - if browser_is_running { - // Browser is running and proxy is enabled, start new proxy - if let Some(pid) = profile.process_id { - match PROXY_MANAGER - .start_proxy(app_handle.clone(), &proxy_settings, pid, Some(profile_name)) - .await - { - Ok(internal_proxy_settings) => { - let browser_runner = BrowserRunner::new(); - let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - - // Apply the proxy settings with the internal proxy to the profile directory - browser_runner - .apply_proxy_settings_to_profile( - &profile_path, - &proxy_settings, - Some(&internal_proxy_settings), - ) - .map_err(|e| format!("Failed to update profile proxy: {e}"))?; - - println!("Successfully started proxy for profile: {}", profile.name); - - // Give the proxy a moment to fully start up - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } - Err(e) => { - eprintln!("Failed to start proxy: {e}"); - // Apply proxy settings without internal proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; - } - } - } else { - // No PID available, apply proxy settings without internal proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; - } - } else { - // Proxy disabled or browser not running, just apply settings - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; - } - } else { - // Proxy ID provided but proxy not found, disable proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .disable_proxy_settings_in_profile(&profile_path) - .map_err(|e| -> Box { - format!("Failed to disable proxy settings: {e}").into() - })?; - } - } else { - // No proxy ID provided, disable proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .disable_proxy_settings_in_profile(&profile_path) - .map_err(|e| -> Box { - format!("Failed to disable proxy settings: {e}").into() - })?; - } - - Ok(profile) - } - - pub fn update_profile_version( - &self, - profile_name: &str, - version: &str, - ) -> Result> { - // Find the profile by name - let profiles = self.list_profiles()?; - let mut profile = profiles - .into_iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile {profile_name} not found"))?; - - // Check if the browser is currently running - if profile.process_id.is_some() { - return Err( - "Cannot update version while browser is running. Please stop the browser first.".into(), - ); - } - - // Verify the new version is downloaded - let browser_type = BrowserType::from_str(&profile.browser) - .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; - let browser = create_browser(browser_type.clone()); - let binaries_dir = self.get_binaries_dir(); - - if !browser.is_version_downloaded(version, &binaries_dir) { - return Err(format!("Browser version {version} is not downloaded").into()); - } - - // Update version - profile.version = version.to_string(); - - // Update the release_type based on the version and browser - profile.release_type = - if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) { - "nightly".to_string() - } else { - "stable".to_string() - }; - - // Save the updated profile - self.save_profile(&profile)?; - - // Always perform cleanup after profile version update to remove unused binaries - let _ = self.cleanup_unused_binaries_internal(); - - Ok(profile) + let profile_manager = ProfileManager::new(); + profile_manager.get_profiles_dir() } /// Internal method to cleanup unused binaries (used by auto-cleanup) @@ -1649,232 +109,24 @@ impl BrowserRunner { Ok(cleaned_up) } - pub fn assign_profiles_to_group( - &self, - profile_names: Vec, - group_id: Option, - ) -> Result<(), Box> { - let profiles = self.list_profiles()?; - - for profile_name in profile_names { - let mut profile = profiles - .iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile '{profile_name}' not found"))? - .clone(); - - // Check if browser is running - if profile.process_id.is_some() { - return Err(format!( - "Cannot modify group for profile '{profile_name}' while browser is running. Please stop the browser first." - ).into()); - } - - profile.group_id = group_id.clone(); - self.save_profile(&profile)?; - } - - Ok(()) - } - - pub fn delete_multiple_profiles( - &self, - profile_names: Vec, - ) -> Result<(), Box> { - let profiles = self.list_profiles()?; - - for profile_name in profile_names { - let profile = profiles - .iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; - - // Check if browser is running - if profile.process_id.is_some() { - return Err( - format!( - "Cannot delete profile '{profile_name}' while browser is running. Please stop the browser first." - ) - .into(), - ); - } - - // Delete the profile - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - - if profile_uuid_dir.exists() { - std::fs::remove_dir_all(&profile_uuid_dir)?; - } - } - - // Always perform cleanup after profile deletion to remove unused binaries - if let Err(e) = self.cleanup_unused_binaries_internal() { - println!("Warning: Failed to cleanup unused binaries: {e}"); - } - - Ok(()) - } - - fn get_common_firefox_preferences(&self) -> Vec { - vec![ - // Disable default browser check - "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), - "user_pref(\"app.update.enabled\", false);".to_string(), - "user_pref(\"app.update.auto\", false);".to_string(), - "user_pref(\"app.update.mode\", 2);".to_string(), - "user_pref(\"app.update.promptWaitTime\", 0);".to_string(), - "user_pref(\"app.update.service.enabled\", false);".to_string(), - "user_pref(\"app.update.silent\", true);".to_string(), - "user_pref(\"app.update.checkInstallTime\", false);".to_string(), - "user_pref(\"app.update.url\", \"\");".to_string(), - "user_pref(\"app.update.url.manual\", \"\");".to_string(), - "user_pref(\"app.update.url.details\", \"\");".to_string(), - "user_pref(\"app.update.url.override\", \"\");".to_string(), - "user_pref(\"app.update.interval\", 9999999999);".to_string(), - "user_pref(\"app.update.background.interval\", 9999999999);".to_string(), - "user_pref(\"app.update.download.attemptOnce\", false);".to_string(), - "user_pref(\"app.update.idletime\", -1);".to_string(), - ] - } - fn apply_proxy_settings_to_profile( &self, profile_data_path: &Path, proxy: &ProxySettings, internal_proxy: Option<&ProxySettings>, ) -> Result<(), Box> { - let user_js_path = profile_data_path.join("user.js"); - let mut preferences = Vec::new(); - - // Get the UUID directory (parent of profile data directory) - let uuid_dir = profile_data_path - .parent() - .ok_or("Invalid profile path - cannot find UUID directory")?; - - // Add common Firefox preferences (like disabling default browser check) - preferences.extend(self.get_common_firefox_preferences()); - - // Use embedded PAC template instead of reading from file - const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) { - return "{{proxy_url}}"; -}"#; - - // Format proxy URL based on type and whether we have an internal proxy - let proxy_url = if let Some(internal) = internal_proxy { - // Use internal proxy as the primary proxy - format!("HTTP {}:{}", internal.host, internal.port) - } else { - // Use user-configured proxy directly - match proxy.proxy_type.as_str() { - "http" => format!("HTTP {}:{}", proxy.host, proxy.port), - "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), - "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), - "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), - _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), - } - }; - - // Replace placeholders in PAC file - let pac_content = PAC_TEMPLATE - .replace("{{proxy_url}}", &proxy_url) - .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file - - // Save PAC file in UUID directory - let pac_path = uuid_dir.join("proxy.pac"); - fs::write(&pac_path, pac_content)?; - - // Configure Firefox to use the PAC file - preferences.extend([ - "user_pref(\"network.proxy.type\", 2);".to_string(), - format!( - "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", - pac_path.to_string_lossy() - ), - "user_pref(\"network.proxy.failover_direct\", false);".to_string(), - "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), - "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), - "user_pref(\"signon.autologin.proxy\", true);".to_string(), - "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), - "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), - "user_pref(\"network.auth-use-sspi\", false);".to_string(), - ]); - - // Write settings to user.js file - fs::write(user_js_path, preferences.join("\n"))?; - - Ok(()) - } - - pub fn disable_proxy_settings_in_profile( - &self, - profile_data_path: &Path, - ) -> Result<(), Box> { - let user_js_path = profile_data_path.join("user.js"); - let mut preferences = Vec::new(); - - // Get the UUID directory (parent of profile data directory) - let uuid_dir = profile_data_path - .parent() - .ok_or("Invalid profile path - cannot find UUID directory")?; - - // Add common Firefox preferences (like disabling default browser check) - preferences.extend(self.get_common_firefox_preferences()); - - preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); - preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); - - // Create a direct proxy PAC file in UUID directory - let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }"; - let pac_path = uuid_dir.join("proxy.pac"); - fs::write(&pac_path, pac_content)?; - preferences.push(format!( - "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", - pac_path.to_string_lossy() - )); - - fs::write(user_js_path, preferences.join("\n"))?; - - Ok(()) + let profile_manager = ProfileManager::new(); + profile_manager.apply_proxy_settings_to_profile(profile_data_path, proxy, internal_proxy) } pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - let profile_file = profile_uuid_dir.join("metadata.json"); - - // Ensure the UUID directory exists - create_dir_all(&profile_uuid_dir)?; - - let json = serde_json::to_string_pretty(profile)?; - fs::write(profile_file, json)?; - - Ok(()) + let profile_manager = ProfileManager::new(); + profile_manager.save_profile(profile) } pub fn list_profiles(&self) -> Result, Box> { - let profiles_dir = self.get_profiles_dir(); - if !profiles_dir.exists() { - return Ok(vec![]); - } - - let mut profiles = Vec::new(); - for entry in fs::read_dir(profiles_dir)? { - let entry = entry?; - let path = entry.path(); - - // Look for UUID directories containing metadata.json - if path.is_dir() { - let metadata_file = path.join("metadata.json"); - if metadata_file.exists() { - let content = fs::read_to_string(metadata_file)?; - let profile: BrowserProfile = serde_json::from_str(&content)?; - profiles.push(profile); - } - } - } - - Ok(profiles) + let profile_manager = ProfileManager::new(); + profile_manager.list_profiles() } pub async fn launch_browser( @@ -2043,17 +295,17 @@ impl BrowserRunner { let child = { #[cfg(target_os = "macos")] { - macos::launch_browser_process(&executable_path, &browser_args).await? + platform_browser::macos::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(target_os = "windows")] { - windows::launch_browser_process(&executable_path, &browser_args).await? + platform_browser::windows::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(target_os = "linux")] { - linux::launch_browser_process(&executable_path, &browser_args).await? + platform_browser::linux::launch_browser_process(&executable_path, &browser_args).await? } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] @@ -2227,7 +479,7 @@ impl BrowserRunner { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); - return macos::open_url_in_existing_browser_firefox_like( + return platform_browser::macos::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, @@ -2240,7 +492,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); - return windows::open_url_in_existing_browser_firefox_like( + return platform_browser::windows::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, @@ -2253,7 +505,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); - return linux::open_url_in_existing_browser_firefox_like( + return platform_browser::linux::open_url_in_existing_browser_firefox_like( &updated_profile, url, browser_type, @@ -2270,7 +522,7 @@ impl BrowserRunner { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); - return macos::open_url_in_existing_browser_tor_mullvad( + return platform_browser::macos::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, @@ -2283,7 +535,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); - return windows::open_url_in_existing_browser_tor_mullvad( + return platform_browser::windows::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, @@ -2296,7 +548,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); - return linux::open_url_in_existing_browser_tor_mullvad( + return platform_browser::linux::open_url_in_existing_browser_tor_mullvad( &updated_profile, url, browser_type, @@ -2313,7 +565,7 @@ impl BrowserRunner { #[cfg(target_os = "macos")] { let profiles_dir = self.get_profiles_dir(); - return macos::open_url_in_existing_browser_chromium( + return platform_browser::macos::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, @@ -2326,7 +578,7 @@ impl BrowserRunner { #[cfg(target_os = "windows")] { let profiles_dir = self.get_profiles_dir(); - return windows::open_url_in_existing_browser_chromium( + return platform_browser::windows::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, @@ -2339,7 +591,7 @@ impl BrowserRunner { #[cfg(target_os = "linux")] { let profiles_dir = self.get_profiles_dir(); - return linux::open_url_in_existing_browser_chromium( + return platform_browser::linux::open_url_in_existing_browser_chromium( &updated_profile, url, browser_type, @@ -2474,35 +726,6 @@ impl BrowserRunner { } } - pub fn rename_profile( - &self, - old_name: &str, - new_name: &str, - ) -> Result> { - // Check if new name already exists (case insensitive) - let existing_profiles = self.list_profiles()?; - if existing_profiles - .iter() - .any(|p| p.name.to_lowercase() == new_name.to_lowercase()) - { - return Err(format!("Profile with name '{new_name}' already exists").into()); - } - - // Find the profile by old name - let mut profile = existing_profiles - .into_iter() - .find(|p| p.name == old_name) - .ok_or_else(|| format!("Profile '{old_name}' not found"))?; - - // Update profile name (no need to move directories since we use UUID) - profile.name = new_name.to_string(); - - // Save profile with new name - self.save_profile(&profile)?; - - Ok(profile) - } - fn save_process_info( &self, profile: &BrowserProfile, @@ -2515,38 +738,8 @@ impl BrowserRunner { } pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { - println!("Attempting to delete profile: {profile_name}"); - - // Find the profile by name - let profiles = self.list_profiles()?; - let profile = profiles - .into_iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; - - // Check if browser is running - if profile.process_id.is_some() { - return Err( - "Cannot delete profile while browser is running. Please stop the browser first.".into(), - ); - } - - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - - // Delete the entire UUID directory (contains both metadata.json and profile data) - if profile_uuid_dir.exists() { - println!("Deleting profile directory: {}", profile_uuid_dir.display()); - fs::remove_dir_all(&profile_uuid_dir)?; - println!("Profile directory deleted successfully"); - } - - // Verify deletion was successful - if profile_uuid_dir.exists() { - return Err(format!("Failed to completely delete profile '{profile_name}'").into()); - } - - println!("Profile '{profile_name}' deleted successfully"); + let profile_manager = ProfileManager::new(); + profile_manager.delete_profile(profile_name)?; // Always perform cleanup after profile deletion to remove unused binaries if let Err(e) = self.cleanup_unused_binaries_internal() { @@ -2561,196 +754,10 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - // Handle camoufox profiles using the same fast approach as other browsers - // No special handling needed - camoufox uses the same process checking logic - - // For non-camoufox browsers, use the existing logic - let mut inner_profile = profile.clone(); - let system = System::new_all(); - let mut is_running = false; - let mut found_pid: Option = None; - - // First check if the stored PID is still valid - if let Some(pid) = profile.process_id { - if let Some(process) = system.process(Pid::from(pid as usize)) { - let cmd = process.cmd(); - // Verify this process is actually our browser with the correct profile - let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_data_path_str = profile_data_path.to_string_lossy(); - let profile_path_match = cmd.iter().any(|s| { - let arg = s.to_str().unwrap_or(""); - // For Firefox-based browsers (including camoufox), check for exact profile path match - if profile.browser == "camoufox" { - // Camoufox uses user_data_dir like Chromium browsers - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } else if profile.browser == "tor-browser" - || profile.browser == "firefox" - || profile.browser == "firefox-developer" - || profile.browser == "mullvad-browser" - || profile.browser == "zen" - { - arg == profile_data_path_str - || arg == format!("-profile={profile_data_path_str}") - || (arg == "-profile" - && cmd - .iter() - .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) - } else { - // For Chromium-based browsers, check for user-data-dir - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } - }); - - if profile_path_match { - is_running = true; - found_pid = Some(pid); - println!( - "Found existing browser process with PID: {} for profile: {}", - pid, profile.name - ); - } else { - println!("PID {pid} exists but doesn't match our profile path exactly, searching for correct process..."); - } - } else { - println!("Stored PID {pid} no longer exists, searching for browser process..."); - } - } - - // If we didn't find the browser with the stored PID, search all processes - if !is_running { - for (pid, process) in system.processes() { - let cmd = process.cmd(); - if cmd.len() >= 2 { - // Check if this is the right browser executable first - let exe_name = process.name().to_string_lossy().to_lowercase(); - let is_correct_browser = match profile.browser.as_str() { - "firefox" => { - exe_name.contains("firefox") - && !exe_name.contains("developer") - && !exe_name.contains("tor") - && !exe_name.contains("mullvad") - && !exe_name.contains("camoufox") - } - "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), - "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"), - "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"), - "zen" => exe_name.contains("zen"), - "chromium" => exe_name.contains("chromium"), - "brave" => exe_name.contains("brave"), - "camoufox" => { - exe_name.contains("camoufox") - || (exe_name.contains("firefox") - && cmd - .iter() - .any(|arg| arg.to_str().unwrap_or("").contains("camoufox"))) - } - _ => false, - }; - - if !is_correct_browser { - continue; - } - - // Check for profile path match - let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_data_path_str = profile_data_path.to_string_lossy(); - let profile_path_match = cmd.iter().any(|s| { - let arg = s.to_str().unwrap_or(""); - // For Firefox-based browsers, check for exact profile path match - if profile.browser == "camoufox" { - // Camoufox uses user_data_dir like Chromium browsers - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } else if profile.browser == "tor-browser" - || profile.browser == "firefox" - || profile.browser == "firefox-developer" - || profile.browser == "mullvad-browser" - || profile.browser == "zen" - { - arg == profile_data_path_str - || arg == format!("-profile={profile_data_path_str}") - || (arg == "-profile" - && cmd - .iter() - .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) - } else { - // For Chromium-based browsers, check for user-data-dir - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } - }); - - if profile_path_match { - // Found a matching process - found_pid = Some(pid.as_u32()); - is_running = true; - println!( - "Found browser process with PID: {} for profile: {}", - pid.as_u32(), - profile.name - ); - break; - } - } - } - } - - // Update the process ID if we found a different one - if let Some(pid) = found_pid { - if inner_profile.process_id != Some(pid) { - inner_profile.process_id = Some(pid); - if let Err(e) = self.save_profile(&inner_profile) { - println!("Warning: Failed to update profile with new PID: {e}"); - } - } - } else if inner_profile.process_id.is_some() { - // Clear the PID if no process found - inner_profile.process_id = None; - if let Err(e) = self.save_profile(&inner_profile) { - println!("Warning: Failed to clear profile PID: {e}"); - } - } - - // Emit profile update event to frontend - if let Err(e) = app_handle.emit("profile-updated", &inner_profile) { - println!("Warning: Failed to emit profile update event: {e}"); - } - - Ok(is_running) - } - - pub fn update_camoufox_config( - &self, - profile_name: &str, - config: crate::camoufox::CamoufoxConfig, - ) -> Result<(), Box> { - // Find the profile by name - let profiles = self.list_profiles()?; - let mut profile = profiles - .into_iter() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile {profile_name} not found"))?; - - // Check if the browser is currently running - if profile.process_id.is_some() { - return Err( - "Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(), - ); - } - - // Update the Camoufox configuration - profile.camoufox_config = Some(config); - - // Save the updated profile - self.save_profile(&profile)?; - - println!("Camoufox configuration updated for profile '{profile_name}'."); - - Ok(()) + let profile_manager = ProfileManager::new(); + profile_manager + .check_browser_status(app_handle, profile) + .await } pub async fn kill_browser_process( @@ -2928,13 +935,13 @@ impl BrowserRunner { // Kill the process using platform-specific implementation #[cfg(target_os = "macos")] - macos::kill_browser_process_impl(pid).await?; + platform_browser::macos::kill_browser_process_impl(pid).await?; #[cfg(target_os = "windows")] - windows::kill_browser_process_impl(pid).await?; + platform_browser::windows::kill_browser_process_impl(pid).await?; #[cfg(target_os = "linux")] - linux::kill_browser_process_impl(pid).await?; + platform_browser::linux::kill_browser_process_impl(pid).await?; #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); @@ -3290,13 +1297,6 @@ impl BrowserRunner { } } -impl BrowserProfile { - /// Get the path to the profile data directory (profiles/{uuid}/profile) - pub fn get_profile_data_path(&self, profiles_dir: &Path) -> PathBuf { - profiles_dir.join(self.id.to_string()).join("profile") - } -} - #[tauri::command] pub fn create_browser_profile( name: String, @@ -3306,8 +1306,8 @@ pub fn create_browser_profile( proxy_id: Option, camoufox_config: Option, ) -> Result { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .create_profile( &name, &browser, @@ -3321,8 +1321,8 @@ pub fn create_browser_profile( #[tauri::command] pub fn list_browser_profiles() -> Result, String> { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) } @@ -3426,8 +1426,8 @@ pub async fn update_profile_proxy( profile_name: String, proxy_id: Option, ) -> Result { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .update_profile_proxy(app_handle, &profile_name, proxy_id) .await .map_err(|e| format!("Failed to update profile: {e}")) @@ -3438,8 +1438,8 @@ pub fn update_profile_version( profile_name: String, version: String, ) -> Result { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .update_profile_version(&profile_name, &version) .map_err(|e| format!("Failed to update profile version: {e}")) } @@ -3449,8 +1449,8 @@ pub async fn check_browser_status( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .check_browser_status(app_handle, &profile) .await .map_err(|e| format!("Failed to check browser status: {e}")) @@ -3462,10 +1462,10 @@ pub fn rename_profile( old_name: &str, new_name: &str, ) -> Result { - let browser_runner = BrowserRunner::new(); - browser_runner + let profile_manager = ProfileManager::new(); + profile_manager .rename_profile(old_name, new_name) - .map_err(|e| format!("Failed to delete profile: {e}")) + .map_err(|e| format!("Failed to rename profile: {e}")) } #[tauri::command] @@ -3618,6 +1618,17 @@ pub fn create_browser_profile_new( ) } +#[tauri::command] +pub async fn update_camoufox_config( + profile_name: String, + config: CamoufoxConfig, +) -> Result<(), String> { + let profile_manager = ProfileManager::new(); + profile_manager + .update_camoufox_config(&profile_name, config) + .map_err(|e| format!("Failed to update Camoufox config: {e}")) +} + #[tauri::command] pub async fn fetch_browser_versions_with_count( browser_str: String, @@ -3670,7 +1681,6 @@ pub async fn ensure_all_binaries_exist( #[cfg(test)] mod tests { use super::*; - use crate::browser::ProxySettings; use tempfile::TempDir; fn create_test_browser_runner() -> (BrowserRunner, TempDir) { @@ -3708,10 +1718,11 @@ mod tests { } #[test] - fn test_create_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); + fn test_profile_operations_via_profile_manager() { + let (_runner, _temp_dir) = create_test_browser_runner(); + let profile_manager = ProfileManager::new(); - let profile = runner + let profile = profile_manager .create_profile("Test Profile", "firefox", "139.0", "stable", None, None) .unwrap(); @@ -3720,229 +1731,21 @@ mod tests { assert_eq!(profile.version, "139.0"); assert!(profile.proxy_id.is_none()); assert!(profile.process_id.is_none()); - } - #[test] - fn test_create_profile_with_proxy() { - let (runner, _temp_dir) = create_test_browser_runner(); - - let _proxy = ProxySettings { - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: 8080, - username: None, - password: None, - }; - - let profile = runner - .create_profile( - "Test Profile with Proxy", - "firefox", - "139.0", - "stable", - None, // Tests now use separate proxy storage system - None, // No camoufox config for this test - ) - .unwrap(); - - assert_eq!(profile.name, "Test Profile with Proxy"); - // Note: Proxy settings are now stored separately in the proxy storage system - assert!(profile.proxy_id.is_none()); // No proxy assigned in this test - } - - #[test] - fn test_save_and_load_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - let profile = runner - .create_profile("Test Save Load", "firefox", "139.0", "stable", None, None) - .unwrap(); - - // Save the profile - runner.save_profile(&profile).unwrap(); - - // Load profiles and verify - let profiles = runner.list_profiles().unwrap(); + // Test listing profiles + let profiles = profile_manager.list_profiles().unwrap(); assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "Test Save Load"); - assert_eq!(profiles[0].browser, "firefox"); - assert_eq!(profiles[0].version, "139.0"); - } + assert_eq!(profiles[0].name, "Test Profile"); - #[test] - fn test_rename_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile - let _ = runner - .create_profile("Original Name", "firefox", "139.0", "stable", None, None) + // Test renaming profile + let renamed_profile = profile_manager + .rename_profile("Test Profile", "Renamed Profile") .unwrap(); + assert_eq!(renamed_profile.name, "Renamed Profile"); - // Rename profile - let renamed_profile = runner.rename_profile("Original Name", "New Name").unwrap(); - - assert_eq!(renamed_profile.name, "New Name"); - - // Verify old profile is gone and new one exists - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - assert_eq!(profiles[0].name, "New Name"); - } - - #[test] - fn test_delete_profile() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile - let _ = runner - .create_profile("To Delete", "firefox", "139.0", "stable", None, None) - .unwrap(); - - // Verify profile exists - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 1); - - // Delete profile - runner.delete_profile("To Delete").unwrap(); - - // Verify profile is gone - let profiles = runner.list_profiles().unwrap(); + // Test deleting profile + profile_manager.delete_profile("Renamed Profile").unwrap(); + let profiles = profile_manager.list_profiles().unwrap(); assert_eq!(profiles.len(), 0); } - - #[test] - fn test_profile_name_sanitization() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile with spaces and special characters - let profile = runner - .create_profile( - "Test Profile With Spaces", - "firefox", - "139.0", - "stable", - None, - None, - ) - .unwrap(); - - // Profile path should contain UUID and end with /profile - let profiles_dir = runner.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - assert!(profile_data_path - .to_string_lossy() - .contains(&profile.id.to_string())); - assert!(profile_data_path.to_string_lossy().ends_with("/profile")); - // Profile name should remain unchanged - assert_eq!(profile.name, "Test Profile With Spaces"); - // Profile should have a valid UUID - assert!(uuid::Uuid::parse_str(&profile.id.to_string()).is_ok()); - } - - #[test] - fn test_multiple_profiles() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create multiple profiles - let _ = runner - .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) - .unwrap(); - let _ = runner - .create_profile("Profile 2", "chromium", "1465660", "stable", None, None) - .unwrap(); - let _ = runner - .create_profile("Profile 3", "brave", "v1.81.9", "stable", None, None) - .unwrap(); - - // List profiles - let profiles = runner.list_profiles().unwrap(); - assert_eq!(profiles.len(), 3); - - let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); - assert!(profile_names.contains(&"Profile 1")); - assert!(profile_names.contains(&"Profile 2")); - assert!(profile_names.contains(&"Profile 3")); - } - - #[test] - fn test_profile_validation() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Test that we can't rename to an existing profile name - let _ = runner - .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) - .unwrap(); - let _ = runner - .create_profile("Profile 2", "firefox", "139.0", "stable", None, None) - .unwrap(); - - // Try to rename profile2 to profile1's name (should fail) - let result = runner.rename_profile("Profile 2", "Profile 1"); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("already exists")); - } - - #[test] - fn test_firefox_default_browser_preferences() { - let (runner, _temp_dir) = create_test_browser_runner(); - - // Create profile without proxy - let profile = runner - .create_profile( - "Test Firefox Preferences", - "firefox", - "139.0", - "stable", - None, - None, - ) - .unwrap(); - - // Check that user.js file was created with default browser preference - let profiles_dir = runner.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let user_js_path = profile_data_path.join("user.js"); - assert!(user_js_path.exists()); - - let user_js_content = std::fs::read_to_string(user_js_path).unwrap(); - assert!(user_js_content.contains("browser.shell.checkDefaultBrowser")); - assert!(user_js_content.contains("false")); - - // Verify automatic update disabling preferences are present - assert!(user_js_content.contains("app.update.enabled")); - assert!(user_js_content.contains("app.update.auto")); - - // Create profile with proxy (proxy object unused in new architecture) - let _proxy = ProxySettings { - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: 8080, - username: None, - password: None, - }; - - let profile_with_proxy = runner - .create_profile( - "Test Firefox Preferences Proxy", - "firefox", - "139.0", - "stable", - None, // Tests now use separate proxy storage system - None, // No camoufox config for this test - ) - .unwrap(); - - // Check that user.js file contains both proxy settings and default browser preference - let profile_with_proxy_data_path = profile_with_proxy.get_profile_data_path(&profiles_dir); - let user_js_path_proxy = profile_with_proxy_data_path.join("user.js"); - assert!(user_js_path_proxy.exists()); - - let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap(); - assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser")); - assert!(user_js_content_proxy.contains("network.proxy.type")); - - // Verify automatic update disabling preferences are present even with proxy - assert!(user_js_content_proxy.contains("app.update.enabled")); - assert!(user_js_content_proxy.contains("app.update.auto")); - } } diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs index 0b65f0b..78301e5 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox.rs @@ -1,4 +1,4 @@ -use crate::browser_runner::BrowserProfile; +use crate::profile::BrowserProfile; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; diff --git a/src-tauri/src/downloaded_browsers.rs b/src-tauri/src/downloaded_browsers.rs index f1be4c8..3415fa4 100644 --- a/src-tauri/src/downloaded_browsers.rs +++ b/src-tauri/src/downloaded_browsers.rs @@ -201,7 +201,7 @@ impl DownloadedBrowsersRegistry { /// Get all browsers and versions referenced by active profiles pub fn get_active_browser_versions( &self, - profiles: &[crate::browser_runner::BrowserProfile], + profiles: &[crate::profile::BrowserProfile], ) -> Vec<(String, String)> { profiles .iter() @@ -251,7 +251,7 @@ impl DownloadedBrowsersRegistry { /// Get all browsers and versions that are currently running pub fn get_running_browser_versions( &self, - profiles: &[crate::browser_runner::BrowserProfile], + profiles: &[crate::profile::BrowserProfile], ) -> Vec<(String, String)> { profiles .iter() diff --git a/src-tauri/src/group_manager.rs b/src-tauri/src/group_manager.rs index 8e9b24e..1ca150e 100644 --- a/src-tauri/src/group_manager.rs +++ b/src-tauri/src/group_manager.rs @@ -140,7 +140,7 @@ impl GroupManager { pub fn get_groups_with_profile_counts( &self, - profiles: &[crate::browser_runner::BrowserProfile], + profiles: &[crate::profile::BrowserProfile], ) -> Result, Box> { let groups = self.get_all_groups()?; let mut group_counts = HashMap::new(); @@ -186,9 +186,7 @@ lazy_static::lazy_static! { } // Helper function to get groups with counts -pub fn get_groups_with_counts( - profiles: &[crate::browser_runner::BrowserProfile], -) -> Vec { +pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec { let group_manager = GROUP_MANAGER.lock().unwrap(); group_manager .get_groups_with_profile_counts(profiles) @@ -206,8 +204,8 @@ pub async fn get_profile_groups() -> Result, String> { #[tauri::command] pub async fn get_groups_with_profile_counts() -> Result, String> { - let browser_runner = crate::browser_runner::BrowserRunner::new(); - let profiles = browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + let profiles = profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; Ok(get_groups_with_counts(&profiles)) @@ -242,16 +240,16 @@ pub async fn assign_profiles_to_group( profile_names: Vec, group_id: Option, ) -> Result<(), String> { - let browser_runner = crate::browser_runner::BrowserRunner::new(); - browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + profile_manager .assign_profiles_to_group(profile_names, group_id) .map_err(|e| format!("Failed to assign profiles to group: {e}")) } #[tauri::command] pub async fn delete_selected_profiles(profile_names: Vec) -> Result<(), String> { - let browser_runner = crate::browser_runner::BrowserRunner::new(); - browser_runner + let profile_manager = crate::profile::ProfileManager::new(); + profile_manager .delete_multiple_profiles(profile_names) .map_err(|e| format!("Failed to delete profiles: {e}")) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7255975..289e725 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,7 +20,8 @@ mod downloaded_browsers; mod extraction; mod geoip_downloader; mod group_manager; - +mod platform_browser; +mod profile; mod profile_importer; mod proxy_manager; mod settings_manager; @@ -36,7 +37,8 @@ use browser_runner::{ fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first, get_browser_release_types, get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile, launch_browser_profile, - list_browser_profiles, rename_profile, update_profile_proxy, update_profile_version, + list_browser_profiles, rename_profile, update_camoufox_config, update_profile_proxy, + update_profile_version, }; use settings_manager::{ @@ -174,17 +176,6 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { .map_err(|e| format!("Failed to delete stored proxy: {e}")) } -#[tauri::command] -async fn update_camoufox_config( - profile_name: String, - config: crate::camoufox::CamoufoxConfig, -) -> Result<(), String> { - let browser_runner = browser_runner::BrowserRunner::new(); - browser_runner - .update_camoufox_config(&profile_name, config) - .map_err(|e| format!("Failed to update Camoufox config: {e}")) -} - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let args: Vec = env::args().collect(); diff --git a/src-tauri/src/platform_browser.rs b/src-tauri/src/platform_browser.rs new file mode 100644 index 0000000..af888b6 --- /dev/null +++ b/src-tauri/src/platform_browser.rs @@ -0,0 +1,1006 @@ +use crate::browser::{create_browser, BrowserType}; +use crate::profile::BrowserProfile; +use std::ffi::OsString; +use std::path::Path; +use std::process::Command; + +// Platform-specific modules +#[cfg(target_os = "macos")] +pub mod macos { + use super::*; + + pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool { + match browser_type { + "mullvad-browser" => { + let has_mullvad_in_exe = exe_name.contains("mullvad"); + let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); + let has_mullvad_in_cmd = cmd.iter().any(|arg| { + let arg_str = arg.to_str().unwrap_or(""); + arg_str.contains("Mullvad Browser.app") + || arg_str.contains("mullvad") + || arg_str.contains("Mullvad") + || arg_str.contains("/Applications/Mullvad Browser.app/") + || arg_str.contains("MullvadBrowser") + }); + + has_mullvad_in_exe || (has_firefox_exe && has_mullvad_in_cmd) + } + "tor-browser" => { + let has_tor_in_exe = exe_name.contains("tor"); + let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin"); + let has_tor_in_cmd = cmd.iter().any(|arg| { + let arg_str = arg.to_str().unwrap_or(""); + arg_str.contains("Tor Browser.app") + || arg_str.contains("tor-browser") + || arg_str.contains("TorBrowser") + || arg_str.contains("/Applications/Tor Browser.app/") + || arg_str.contains("TorBrowser-Data") + }); + + has_tor_in_exe || (has_firefox_exe && has_tor_in_cmd) + } + _ => false, + } + } + + pub async fn launch_browser_process( + executable_path: &std::path::Path, + args: &[String], + ) -> Result> { + println!("Launching browser on macOS: {executable_path:?} with args: {args:?}"); + Ok(Command::new(executable_path).args(args).spawn()?) + } + + pub async fn open_url_in_existing_browser_firefox_like( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + let pid = profile.process_id.unwrap(); + let profile_data_path = profile.get_profile_data_path(profiles_dir); + + // First try: Use Firefox remote command + println!("Trying Firefox remote command for PID: {pid}"); + let browser = create_browser(browser_type); + if let Ok(executable_path) = browser.get_executable_path(browser_dir) { + let remote_args = vec![ + "-profile".to_string(), + profile_data_path.to_string_lossy().to_string(), + "-new-tab".to_string(), + url.to_string(), + ]; + + let remote_output = Command::new(executable_path).args(&remote_args).output(); + + match remote_output { + Ok(output) if output.status.success() => { + println!("Firefox remote command succeeded"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!( + "Firefox remote command failed with stderr: {stderr}, trying AppleScript fallback" + ); + } + Err(e) => { + println!("Firefox remote command error: {e}, trying AppleScript fallback"); + } + } + } + + // Fallback: Use AppleScript + let escaped_url = url + .replace("\"", "\\\"") + .replace("\\", "\\\\") + .replace("'", "\\'"); + + let script = format!( + r#" +try + tell application "System Events" + -- Find the exact process by PID + set targetProcess to (first application process whose unix id is {pid}) + + -- Verify the process exists + if not (exists targetProcess) then + error "No process found with PID {pid}" + end if + + -- Get the process name for verification + set processName to name of targetProcess + + -- Bring the process to the front first + set frontmost of targetProcess to true + delay 1.0 + + -- Check if the process has any visible windows + set windowList to windows of targetProcess + set hasVisibleWindow to false + repeat with w in windowList + if visible of w is true then + set hasVisibleWindow to true + exit repeat + end if + end repeat + + if not hasVisibleWindow then + -- No visible windows, create a new one + tell targetProcess + keystroke "n" using command down + delay 2.0 + end tell + end if + + -- Ensure the process is frontmost again + set frontmost of targetProcess to true + delay 0.5 + + -- Focus on the address bar and open URL + tell targetProcess + -- Open a new tab + keystroke "t" using command down + delay 1.5 + + -- Focus address bar (Cmd+L) + keystroke "l" using command down + delay 0.5 + + -- Type the URL + keystroke "{escaped_url}" + delay 0.5 + + -- Press Enter to navigate + keystroke return + end tell + + return "Successfully opened URL in " & processName & " (PID: {pid})" + end tell +on error errMsg number errNum + return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" +end try + "# + ); + + println!("Executing AppleScript fallback for Firefox-based browser (PID: {pid})..."); + let output = Command::new("osascript").args(["-e", &script]).output()?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + println!("AppleScript failed: {error_msg}"); + return Err( + format!( + "Both Firefox remote command and AppleScript failed. AppleScript error: {error_msg}" + ) + .into(), + ); + } else { + println!("AppleScript succeeded"); + } + + Ok(()) + } + + pub async fn open_url_in_existing_browser_tor_mullvad( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + _profiles_dir: &Path, + ) -> Result<(), Box> { + let pid = profile.process_id.unwrap(); + + println!("Opening URL in TOR/Mullvad browser using file-based approach (PID: {pid})"); + + // Method 1: Try using a temporary HTML file approach + println!("Attempting file-based URL opening for TOR/Mullvad browser"); + + let temp_dir = std::env::temp_dir(); + let temp_file_name = format!("donut_browser_url_{}.html", std::process::id()); + let temp_file_path = temp_dir.join(&temp_file_name); + + let html_content = format!( + r#" + + + + + Redirecting... + + + +

Redirecting to {url}...

+ +"# + ); + + match std::fs::write(&temp_file_path, html_content) { + Ok(()) => { + println!("Created temporary HTML file: {temp_file_path:?}"); + + let browser = create_browser(browser_type.clone()); + if let Ok(executable_path) = browser.get_executable_path(browser_dir) { + let open_result = Command::new("open") + .args([ + "-a", + executable_path.to_str().unwrap(), + temp_file_path.to_str().unwrap(), + ]) + .output(); + + // Clean up the temporary file after a short delay + let temp_file_path_clone = temp_file_path.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + let _ = std::fs::remove_file(temp_file_path_clone); + }); + + match open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using file-based approach"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("File-based approach failed: {stderr}"); + } + Err(e) => { + println!("File-based approach error: {e}"); + } + } + } + + let _ = std::fs::remove_file(&temp_file_path); + } + Err(e) => { + println!("Failed to create temporary HTML file: {e}"); + } + } + + // Method 2: Try using the 'open' command directly with the URL + println!("Attempting direct URL opening with 'open' command"); + + let browser = create_browser(browser_type.clone()); + if let Ok(executable_path) = browser.get_executable_path(browser_dir) { + let direct_open_result = Command::new("open") + .args(["-a", executable_path.to_str().unwrap(), url]) + .output(); + + match direct_open_result { + Ok(output) if output.status.success() => { + println!("Successfully opened URL using direct 'open' command"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("Direct 'open' command failed: {stderr}"); + } + Err(e) => { + println!("Direct 'open' command error: {e}"); + } + } + } + + // If all methods fail, return a helpful error message + Err( + format!( + "Failed to open URL in existing TOR/Mullvad browser (PID: {pid}). All methods failed:\n\ + 1. File-based approach failed\n\ + 2. Direct 'open' command failed\n\ + \n\ + This may be due to browser security restrictions or the browser process may have changed.\n\ + Try closing and reopening the browser, or manually paste the URL: {url}" + ) + .into(), + ) + } + + pub async fn open_url_in_existing_browser_chromium( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + _profiles_dir: &Path, + ) -> Result<(), Box> { + let pid = profile.process_id.unwrap(); + + // First, try using the browser's built-in URL opening capability + println!("Trying Chromium URL opening for PID: {pid}"); + + let browser = create_browser(browser_type); + if let Ok(executable_path) = browser.get_executable_path(browser_dir) { + let profile_data_path = profile.get_profile_data_path(_profiles_dir); + let remote_output = Command::new(executable_path) + .args([ + &format!("--user-data-dir={}", profile_data_path.to_string_lossy()), + url, + ]) + .output(); + + match remote_output { + Ok(output) if output.status.success() => { + println!("Chromium URL opening succeeded"); + return Ok(()); + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("Chromium URL opening failed: {stderr}, trying AppleScript"); + } + Err(e) => { + println!("Chromium URL opening error: {e}, trying AppleScript"); + } + } + } + + // Fallback to AppleScript + let escaped_url = url + .replace("\"", "\\\"") + .replace("\\", "\\\\") + .replace("'", "\\'"); + + let script = format!( + r#" +try + tell application "System Events" + -- Find the exact process by PID + set targetProcess to (first application process whose unix id is {pid}) + + -- Verify the process exists + if not (exists targetProcess) then + error "No process found with PID {pid}" + end if + + -- Get the process name for verification + set processName to name of targetProcess + + -- Bring the process to the front first + set frontmost of targetProcess to true + delay 1.0 + + -- Check if the process has any visible windows + set windowList to windows of targetProcess + set hasVisibleWindow to false + repeat with w in windowList + if visible of w is true then + set hasVisibleWindow to true + exit repeat + end if + end repeat + + if not hasVisibleWindow then + -- No visible windows, create a new one + tell targetProcess + keystroke "n" using command down + delay 2.0 + end tell + end if + + -- Ensure the process is frontmost again + set frontmost of targetProcess to true + delay 0.5 + + -- Focus on the address bar and open URL + tell targetProcess + -- Open a new tab + keystroke "t" using command down + delay 1.5 + + -- Focus address bar (Cmd+L) + keystroke "l" using command down + delay 0.5 + + -- Type the URL + keystroke "{escaped_url}" + delay 0.5 + + -- Press Enter to navigate + keystroke return + end tell + + return "Successfully opened URL in " & processName & " (PID: {pid})" + end tell +on error errMsg number errNum + return "AppleScript failed: " & errMsg & " (Error " & errNum & ")" +end try + "# + ); + + println!("Executing AppleScript for Chromium-based browser (PID: {pid})..."); + let output = Command::new("osascript").args(["-e", &script]).output()?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + println!("AppleScript failed: {error_msg}"); + return Err( + format!("Failed to open URL in existing Chromium-based browser: {error_msg}").into(), + ); + } else { + println!("AppleScript succeeded"); + } + + Ok(()) + } + + pub async fn kill_browser_process_impl( + pid: u32, + ) -> Result<(), Box> { + println!("Attempting to kill browser process with PID: {pid}"); + + // First try SIGTERM (graceful shutdown) + let output = Command::new("kill") + .args(["-TERM", &pid.to_string()]) + .output() + .map_err(|e| format!("Failed to execute kill command: {e}"))?; + + if !output.status.success() { + // If SIGTERM fails, try SIGKILL (force kill) + let output = Command::new("kill") + .args(["-KILL", &pid.to_string()]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to kill process {}: {}", + pid, + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + } + + println!("Successfully killed browser process with PID: {pid}"); + Ok(()) + } +} + +#[cfg(target_os = "windows")] +pub mod windows { + use super::*; + + pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool { + let exe_lower = exe_name.to_lowercase(); + + // Check for Firefox-based browsers first by executable name + let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe"); + + if !is_firefox_family { + return false; + } + + // Check command arguments for profile paths and browser-specific indicators + let cmd_line = cmd + .iter() + .map(|s| s.to_string_lossy().to_lowercase()) + .collect::>() + .join(" "); + + match browser_type { + "tor-browser" => { + // Check for TOR browser specific paths and arguments + cmd_line.contains("tor") + || cmd_line.contains("browser\\torbrowser") + || cmd_line.contains("tor-browser") + || cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb")) + } + "mullvad-browser" => { + // Check for Mullvad browser specific paths and arguments + cmd_line.contains("mullvad") + || cmd_line.contains("browser\\mullvadbrowser") + || cmd_line.contains("mullvad-browser") + || cmd_line.contains("profile") && cmd_line.contains("mullvad") + } + _ => false, + } + } + + pub async fn launch_browser_process( + executable_path: &std::path::Path, + args: &[String], + ) -> Result> { + println!( + "Launching browser on Windows: {:?} with args: {:?}", + executable_path, args + ); + + // Check if the executable exists + if !executable_path.exists() { + return Err(format!("Browser executable not found: {:?}", executable_path).into()); + } + + // On Windows, set up the command with proper working directory + let mut cmd = Command::new(executable_path); + cmd.args(args); + + // Set working directory to the executable's directory for better compatibility + if let Some(parent_dir) = executable_path.parent() { + cmd.current_dir(parent_dir); + } + + // For Windows 7 compatibility, set some environment variables + cmd.env( + "PROCESSOR_ARCHITECTURE", + std::env::var("PROCESSOR_ARCHITECTURE").unwrap_or_else(|_| "x86".to_string()), + ); + + // Ensure proper PATH for DLL loading + if let Some(exe_dir) = executable_path.parent() { + let mut path_var = std::env::var("PATH").unwrap_or_default(); + if !path_var.is_empty() { + path_var = format!("{};{}", exe_dir.display(), path_var); + } else { + path_var = exe_dir.display().to_string(); + } + cmd.env("PATH", path_var); + } + + // Launch the process + let child = cmd + .spawn() + .map_err(|e| format!("Failed to launch browser process: {}", e))?; + + println!( + "Successfully launched browser process with PID: {}", + child.id() + ); + Ok(child) + } + + pub async fn open_url_in_existing_browser_firefox_like( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + let browser = create_browser(browser_type); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let profile_data_path = profile.get_profile_data_path(profiles_dir); + + // For Windows, try using the -requestPending approach for Firefox + let mut cmd = Command::new(executable_path); + cmd.args([ + "-profile", + &profile_data_path.to_string_lossy(), + "-requestPending", + "-new-tab", + url, + ]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; + + if !output.status.success() { + // Fallback: try without -requestPending + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + let mut fallback_cmd = Command::new(executable_path); + let profile_data_path = profile.get_profile_data_path(profiles_dir); + fallback_cmd.args([ + "-profile", + &profile_data_path.to_string_lossy(), + "-new-tab", + url, + ]); + + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + fallback_cmd.current_dir(parent_dir); + } + + let fallback_output = fallback_cmd.output()?; + + if !fallback_output.status.success() { + return Err( + format!( + "Failed to open URL in existing browser: {}", + String::from_utf8_lossy(&fallback_output.stderr) + ) + .into(), + ); + } + } + + Ok(()) + } + + pub async fn open_url_in_existing_browser_tor_mullvad( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + // On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line + // even with -no-remote, by launching a new instance that hands off to existing one + let browser = create_browser(browser_type.clone()); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let mut cmd = Command::new(&executable_path); + let profile_data_path = profile.get_profile_data_path(profiles_dir); + cmd.args(["-profile", &profile_data_path.to_string_lossy(), url]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.", + browser_type.as_str(), + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + Ok(()) + } + + pub async fn open_url_in_existing_browser_chromium( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + let browser = create_browser(browser_type.clone()); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let mut cmd = Command::new(&executable_path); + cmd.args([ + &format!( + "--user-data-dir={}", + profile + .get_profile_data_path(profiles_dir) + .to_string_lossy() + ), + "--new-window", + url, + ]); + + // Set working directory + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + cmd.current_dir(parent_dir); + } + + let output = cmd.output()?; + + if !output.status.success() { + // Try fallback without --new-window + let mut fallback_cmd = Command::new(&executable_path); + fallback_cmd.args([ + &format!( + "--user-data-dir={}", + profile + .get_profile_data_path(profiles_dir) + .to_string_lossy() + ), + url, + ]); + + if let Some(parent_dir) = browser_dir + .parent() + .or_else(|| browser_dir.ancestors().nth(1)) + { + fallback_cmd.current_dir(parent_dir); + } + + let fallback_output = fallback_cmd.output()?; + + if !fallback_output.status.success() { + return Err( + format!( + "Failed to open URL in existing Chromium-based browser: {}", + String::from_utf8_lossy(&fallback_output.stderr) + ) + .into(), + ); + } + } + + Ok(()) + } + + pub async fn kill_browser_process_impl( + pid: u32, + ) -> Result<(), Box> { + // First try using sysinfo (cross-platform approach) + use sysinfo::{Pid, System}; + let system = System::new_all(); + if let Some(process) = system.process(Pid::from(pid as usize)) { + if process.kill() { + println!("Successfully killed browser process with PID: {pid}"); + return Ok(()); + } + } + + // Fallback to Windows-specific process termination + use std::process::Command; + + // Try taskkill command as fallback + let output = Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) + .output(); + + match output { + Ok(result) => { + if result.status.success() { + println!("Successfully killed browser process with PID: {pid} using taskkill"); + Ok(()) + } else { + Err( + format!( + "Failed to kill process {} with taskkill: {}", + pid, + String::from_utf8_lossy(&result.stderr) + ) + .into(), + ) + } + } + Err(e) => Err(format!("Failed to execute taskkill for process {}: {}", pid, e).into()), + } + } +} + +#[cfg(target_os = "linux")] +pub mod linux { + use super::*; + + pub fn is_tor_or_mullvad_browser( + _exe_name: &str, + _cmd: &[OsString], + _browser_type: &str, + ) -> bool { + // Linux implementation would go here + false + } + + pub async fn launch_browser_process( + executable_path: &std::path::Path, + args: &[String], + ) -> Result> { + println!( + "Launching browser on Linux: {:?} with args: {:?}", + executable_path, args + ); + + // Check if the executable exists and is executable + if !executable_path.exists() { + return Err(format!("Browser executable not found: {:?}", executable_path).into()); + } + + // Check if we can read the executable to detect architecture issues early + if let Err(e) = std::fs::File::open(executable_path) { + return Err(format!("Cannot access browser executable: {}", e).into()); + } + + // Ensure the executable has proper permissions + if let Err(e) = std::fs::metadata(executable_path) { + return Err(format!("Cannot get executable metadata: {}", e).into()); + } + + // On Linux, we might need to set LD_LIBRARY_PATH for some browsers + let mut cmd = Command::new(executable_path); + cmd.args(args); + + // For Firefox-based browsers, ensure library path includes the installation directory + if let Some(install_dir) = executable_path.parent() { + let mut ld_library_path = Vec::new(); + + // Add multiple potential library directories + let lib_dirs = [ + install_dir.join("lib"), + install_dir.join("../lib"), // Parent directory lib + install_dir.join("../../lib"), // Grandparent directory lib + install_dir.to_path_buf(), // Installation directory itself + ]; + + for lib_dir in &lib_dirs { + if lib_dir.exists() { + ld_library_path.push(lib_dir.to_string_lossy().to_string()); + } + } + + // For Firefox specifically, add common system library paths that might be needed + let firefox_lib_paths = [ + "/usr/lib/firefox", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + "/lib/x86_64-linux-gnu", + "/lib/aarch64-linux-gnu", + ]; + + for lib_path in &firefox_lib_paths { + let path = std::path::Path::new(lib_path); + if path.exists() { + ld_library_path.push(lib_path.to_string()); + } + } + + // Preserve existing LD_LIBRARY_PATH + if let Ok(existing_path) = std::env::var("LD_LIBRARY_PATH") { + ld_library_path.push(existing_path); + } + + // Set the combined LD_LIBRARY_PATH + if !ld_library_path.is_empty() { + cmd.env("LD_LIBRARY_PATH", ld_library_path.join(":")); + println!("Set LD_LIBRARY_PATH to: {}", ld_library_path.join(":")); + } + } + + // Additional Linux-specific environment variables for better compatibility + cmd.env( + "DISPLAY", + std::env::var("DISPLAY").unwrap_or(":0".to_string()), + ); + + // Set MOZ_ENABLE_WAYLAND for better Wayland support + if std::env::var("WAYLAND_DISPLAY").is_ok() { + cmd.env("MOZ_ENABLE_WAYLAND", "1"); + } + + // Disable GPU acceleration if running in headless environments + if std::env::var("DISPLAY").is_err() || std::env::var("WAYLAND_DISPLAY").is_err() { + println!("No display detected, browser may fail to start"); + } + + // Attempt to spawn with better error handling for architecture issues + match cmd.spawn() { + Ok(child) => Ok(child), + Err(e) => { + // Detect architecture mismatch errors + if e.kind() == std::io::ErrorKind::Other { + let error_msg = e.to_string(); + if error_msg.contains("Exec format error") { + return Err(format!( + "Architecture mismatch: The browser executable is not compatible with your system architecture ({}). \ + This typically happens when trying to run x86_64 binaries on ARM64 systems. \ + Please use a browser that supports your architecture, such as Zen Browser or Brave. \ + Executable: {:?}", + std::env::consts::ARCH, + executable_path + ).into()); + } else if error_msg.contains("No such file or directory") { + return Err(format!( + "Executable or required library not found. This might be due to missing dependencies or incorrect executable path. \ + Try installing missing libraries or verify the browser installation. \ + Executable: {:?}, Error: {}", + executable_path, error_msg + ).into()); + } + } + Err(format!("Failed to launch browser: {}", e).into()) + } + } + } + + pub async fn open_url_in_existing_browser_firefox_like( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + let browser = create_browser(browser_type); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let profile_data_path = profile.get_profile_data_path(profiles_dir); + let output = Command::new(executable_path) + .args([ + "-profile", + &profile_data_path.to_string_lossy(), + "-new-tab", + url, + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to open URL in existing browser: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + Ok(()) + } + + pub async fn open_url_in_existing_browser_tor_mullvad( + _profile: &BrowserProfile, + _url: &str, + _browser_type: BrowserType, + _browser_dir: &Path, + _profiles_dir: &Path, + ) -> Result<(), Box> { + Err("Opening URLs in existing Firefox-based browsers is not supported on Linux when using -no-remote".into()) + } + + pub async fn open_url_in_existing_browser_chromium( + profile: &BrowserProfile, + url: &str, + browser_type: BrowserType, + browser_dir: &Path, + profiles_dir: &Path, + ) -> Result<(), Box> { + let browser = create_browser(browser_type); + let executable_path = browser + .get_executable_path(browser_dir) + .map_err(|e| format!("Failed to get executable path: {}", e))?; + + let profile_data_path = profile.get_profile_data_path(profiles_dir); + let output = Command::new(executable_path) + .args([ + &format!("--user-data-dir={}", profile_data_path.to_string_lossy()), + url, + ]) + .output()?; + + if !output.status.success() { + return Err( + format!( + "Failed to open URL in existing Chromium-based browser: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into(), + ); + } + + Ok(()) + } + + pub async fn kill_browser_process_impl( + pid: u32, + ) -> Result<(), Box> { + use sysinfo::{Pid, System}; + let system = System::new_all(); + if let Some(process) = system.process(Pid::from(pid as usize)) { + if !process.kill() { + return Err(format!("Failed to kill process {}", pid).into()); + } + } else { + return Err(format!("Process {} not found", pid).into()); + } + + println!("Successfully killed browser process with PID: {pid}"); + Ok(()) + } +} diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs new file mode 100644 index 0000000..0bd8910 --- /dev/null +++ b/src-tauri/src/profile/manager.rs @@ -0,0 +1,1302 @@ +use crate::browser::{create_browser, BrowserType, ProxySettings}; +use crate::camoufox::CamoufoxConfig; +use crate::profile::types::BrowserProfile; +use crate::proxy_manager::PROXY_MANAGER; +use directories::BaseDirs; +use std::fs::{self, create_dir_all}; +use std::path::{Path, PathBuf}; +use sysinfo::{Pid, System}; +use tauri::Emitter; + +pub struct ProfileManager { + base_dirs: BaseDirs, +} + +impl ProfileManager { + pub fn new() -> Self { + Self { + base_dirs: BaseDirs::new().expect("Failed to get base directories"), + } + } + + pub fn get_profiles_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("profiles"); + path + } + + pub fn create_profile( + &self, + name: &str, + browser: &str, + version: &str, + release_type: &str, + proxy_id: Option, + camoufox_config: Option, + ) -> Result> { + self.create_profile_with_group( + name, + browser, + version, + release_type, + proxy_id, + camoufox_config, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_profile_with_group( + &self, + name: &str, + browser: &str, + version: &str, + release_type: &str, + proxy_id: Option, + camoufox_config: Option, + group_id: Option, + ) -> Result> { + println!("Attempting to create profile: {name}"); + + // Check if a profile with this name already exists (case insensitive) + let existing_profiles = self.list_profiles()?; + if existing_profiles + .iter() + .any(|p| p.name.to_lowercase() == name.to_lowercase()) + { + return Err(format!("Profile with name '{name}' already exists").into()); + } + + // Generate a new UUID for this profile + let profile_id = uuid::Uuid::new_v4(); + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile_id.to_string()); + let profile_data_dir = profile_uuid_dir.join("profile"); + let profile_file = profile_uuid_dir.join("metadata.json"); + + // Create profile directory with UUID and profile subdirectory + create_dir_all(&profile_uuid_dir)?; + create_dir_all(&profile_data_dir)?; + + let profile = BrowserProfile { + id: profile_id, + name: name.to_string(), + browser: browser.to_string(), + version: version.to_string(), + proxy_id: proxy_id.clone(), + process_id: None, + last_launch: None, + release_type: release_type.to_string(), + camoufox_config: camoufox_config.clone(), + group_id: group_id.clone(), + }; + + // Save profile info + self.save_profile(&profile)?; + + // Verify the profile was saved correctly + if !profile_file.exists() { + return Err(format!("Failed to create profile file for '{name}'").into()); + } + + println!("Profile '{name}' created successfully with ID: {profile_id}"); + + // Create user.js with common Firefox preferences and apply proxy settings if provided + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?; + } else { + // Proxy ID provided but not found, disable proxy + self.disable_proxy_settings_in_profile(&profile_data_dir)?; + } + } else { + // Create user.js with common Firefox preferences but no proxy + self.disable_proxy_settings_in_profile(&profile_data_dir)?; + } + + Ok(profile) + } + + pub fn save_profile(&self, profile: &BrowserProfile) -> Result<(), Box> { + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); + let profile_file = profile_uuid_dir.join("metadata.json"); + + // Ensure the UUID directory exists + create_dir_all(&profile_uuid_dir)?; + + let json = serde_json::to_string_pretty(profile)?; + fs::write(profile_file, json)?; + + Ok(()) + } + + pub fn list_profiles(&self) -> Result, Box> { + let profiles_dir = self.get_profiles_dir(); + if !profiles_dir.exists() { + return Ok(vec![]); + } + + let mut profiles = Vec::new(); + for entry in fs::read_dir(profiles_dir)? { + let entry = entry?; + let path = entry.path(); + + // Look for UUID directories containing metadata.json + if path.is_dir() { + let metadata_file = path.join("metadata.json"); + if metadata_file.exists() { + let content = fs::read_to_string(metadata_file)?; + let profile: BrowserProfile = serde_json::from_str(&content)?; + profiles.push(profile); + } + } + } + + Ok(profiles) + } + + pub fn rename_profile( + &self, + old_name: &str, + new_name: &str, + ) -> Result> { + // Check if new name already exists (case insensitive) + let existing_profiles = self.list_profiles()?; + if existing_profiles + .iter() + .any(|p| p.name.to_lowercase() == new_name.to_lowercase()) + { + return Err(format!("Profile with name '{new_name}' already exists").into()); + } + + // Find the profile by old name + let mut profile = existing_profiles + .into_iter() + .find(|p| p.name == old_name) + .ok_or_else(|| format!("Profile '{old_name}' not found"))?; + + // Update profile name (no need to move directories since we use UUID) + profile.name = new_name.to_string(); + + // Save profile with new name + self.save_profile(&profile)?; + + Ok(profile) + } + + pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { + println!("Attempting to delete profile: {profile_name}"); + + // Find the profile by name + let profiles = self.list_profiles()?; + let profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; + + // Check if browser is running + if profile.process_id.is_some() { + return Err( + "Cannot delete profile while browser is running. Please stop the browser first.".into(), + ); + } + + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); + + // Delete the entire UUID directory (contains both metadata.json and profile data) + if profile_uuid_dir.exists() { + println!("Deleting profile directory: {}", profile_uuid_dir.display()); + fs::remove_dir_all(&profile_uuid_dir)?; + println!("Profile directory deleted successfully"); + } + + // Verify deletion was successful + if profile_uuid_dir.exists() { + return Err(format!("Failed to completely delete profile '{profile_name}'").into()); + } + + println!("Profile '{profile_name}' deleted successfully"); + + Ok(()) + } + + pub fn update_profile_version( + &self, + profile_name: &str, + version: &str, + ) -> Result> { + // Find the profile by name + let profiles = self.list_profiles()?; + let mut profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile {profile_name} not found"))?; + + // Check if the browser is currently running + if profile.process_id.is_some() { + return Err( + "Cannot update version while browser is running. Please stop the browser first.".into(), + ); + } + + // Verify the new version is downloaded + let browser_type = BrowserType::from_str(&profile.browser) + .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; + let browser = create_browser(browser_type.clone()); + let binaries_dir = self.get_binaries_dir(); + + if !browser.is_version_downloaded(version, &binaries_dir) { + return Err(format!("Browser version {version} is not downloaded").into()); + } + + // Update version + profile.version = version.to_string(); + + // Update the release_type based on the version and browser + profile.release_type = + if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) { + "nightly".to_string() + } else { + "stable".to_string() + }; + + // Save the updated profile + self.save_profile(&profile)?; + + Ok(profile) + } + + pub fn assign_profiles_to_group( + &self, + profile_names: Vec, + group_id: Option, + ) -> Result<(), Box> { + let profiles = self.list_profiles()?; + + for profile_name in profile_names { + let mut profile = profiles + .iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile '{profile_name}' not found"))? + .clone(); + + // Check if browser is running + if profile.process_id.is_some() { + return Err(format!( + "Cannot modify group for profile '{profile_name}' while browser is running. Please stop the browser first." + ).into()); + } + + profile.group_id = group_id.clone(); + self.save_profile(&profile)?; + } + + Ok(()) + } + + pub fn delete_multiple_profiles( + &self, + profile_names: Vec, + ) -> Result<(), Box> { + let profiles = self.list_profiles()?; + + for profile_name in profile_names { + let profile = profiles + .iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; + + // Check if browser is running + if profile.process_id.is_some() { + return Err( + format!( + "Cannot delete profile '{profile_name}' while browser is running. Please stop the browser first." + ) + .into(), + ); + } + + // Delete the profile + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); + + if profile_uuid_dir.exists() { + std::fs::remove_dir_all(&profile_uuid_dir)?; + } + } + + Ok(()) + } + + pub fn update_camoufox_config( + &self, + profile_name: &str, + config: CamoufoxConfig, + ) -> Result<(), Box> { + // Find the profile by name + let profiles = self.list_profiles()?; + let mut profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile {profile_name} not found"))?; + + // Check if the browser is currently running + if profile.process_id.is_some() { + return Err( + "Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(), + ); + } + + // Update the Camoufox configuration + profile.camoufox_config = Some(config); + + // Save the updated profile + self.save_profile(&profile)?; + + println!("Camoufox configuration updated for profile '{profile_name}'."); + + Ok(()) + } + + pub async fn update_profile_proxy( + &self, + app_handle: tauri::AppHandle, + profile_name: &str, + proxy_id: Option, + ) -> Result> { + // Find the profile by name + let profiles = + self + .list_profiles() + .map_err(|e| -> Box { + format!("Failed to list profiles: {e}").into() + })?; + + let mut profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| -> Box { + format!("Profile {profile_name} not found").into() + })?; + + // Check if browser is running to manage proxy accordingly + let browser_is_running = profile.process_id.is_some() + && self + .check_browser_status(app_handle.clone(), &profile) + .await?; + + // If browser is running, stop existing proxy + if browser_is_running && profile.proxy_id.is_some() { + if let Some(pid) = profile.process_id { + let _ = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await; + } + } + + // Update proxy settings + profile.proxy_id = proxy_id.clone(); + + // Save the updated profile + self + .save_profile(&profile) + .map_err(|e| -> Box { + format!("Failed to save profile: {e}").into() + })?; + + // Handle proxy startup/configuration + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + if browser_is_running { + // Browser is running and proxy is enabled, start new proxy + if let Some(pid) = profile.process_id { + match PROXY_MANAGER + .start_proxy(app_handle.clone(), &proxy_settings, pid, Some(profile_name)) + .await + { + Ok(internal_proxy_settings) => { + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + + // Apply the proxy settings with the internal proxy to the profile directory + self + .apply_proxy_settings_to_profile( + &profile_path, + &proxy_settings, + Some(&internal_proxy_settings), + ) + .map_err(|e| format!("Failed to update profile proxy: {e}"))?; + + println!("Successfully started proxy for profile: {}", profile.name); + + // Give the proxy a moment to fully start up + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + Err(e) => { + eprintln!("Failed to start proxy: {e}"); + // Apply proxy settings without internal proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; + } + } + } else { + // No PID available, apply proxy settings without internal proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; + } + } else { + // Proxy disabled or browser not running, just apply settings + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; + } + } else { + // Proxy ID provided but proxy not found, disable proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .disable_proxy_settings_in_profile(&profile_path) + .map_err(|e| -> Box { + format!("Failed to disable proxy settings: {e}").into() + })?; + } + } else { + // No proxy ID provided, disable proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .disable_proxy_settings_in_profile(&profile_path) + .map_err(|e| -> Box { + format!("Failed to disable proxy settings: {e}").into() + })?; + } + + Ok(profile) + } + + pub async fn check_browser_status( + &self, + app_handle: tauri::AppHandle, + profile: &BrowserProfile, + ) -> Result> { + // Handle camoufox profiles using the same fast approach as other browsers + // No special handling needed - camoufox uses the same process checking logic + + // For non-camoufox browsers, use the existing logic + let mut inner_profile = profile.clone(); + let system = System::new_all(); + let mut is_running = false; + let mut found_pid: Option = None; + + // First check if the stored PID is still valid + if let Some(pid) = profile.process_id { + if let Some(process) = system.process(Pid::from(pid as usize)) { + let cmd = process.cmd(); + // Verify this process is actually our browser with the correct profile + let profiles_dir = self.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path_str = profile_data_path.to_string_lossy(); + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + // For Firefox-based browsers (including camoufox), check for exact profile path match + if profile.browser == "camoufox" { + // Camoufox uses user_data_dir like Chromium browsers + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } else if profile.browser == "tor-browser" + || profile.browser == "firefox" + || profile.browser == "firefox-developer" + || profile.browser == "mullvad-browser" + || profile.browser == "zen" + { + arg == profile_data_path_str + || arg == format!("-profile={profile_data_path_str}") + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) + } else { + // For Chromium-based browsers, check for user-data-dir + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } + }); + + if profile_path_match { + is_running = true; + found_pid = Some(pid); + println!( + "Found existing browser process with PID: {} for profile: {}", + pid, profile.name + ); + } else { + println!("PID {pid} exists but doesn't match our profile path exactly, searching for correct process..."); + } + } else { + println!("Stored PID {pid} no longer exists, searching for browser process..."); + } + } + + // If we didn't find the browser with the stored PID, search all processes + if !is_running { + for (pid, process) in system.processes() { + let cmd = process.cmd(); + if cmd.len() >= 2 { + // Check if this is the right browser executable first + let exe_name = process.name().to_string_lossy().to_lowercase(); + let is_correct_browser = match profile.browser.as_str() { + "firefox" => { + exe_name.contains("firefox") + && !exe_name.contains("developer") + && !exe_name.contains("tor") + && !exe_name.contains("mullvad") + && !exe_name.contains("camoufox") + } + "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), + "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"), + "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"), + "zen" => exe_name.contains("zen"), + "chromium" => exe_name.contains("chromium"), + "brave" => exe_name.contains("brave"), + "camoufox" => { + exe_name.contains("camoufox") + || (exe_name.contains("firefox") + && cmd + .iter() + .any(|arg| arg.to_str().unwrap_or("").contains("camoufox"))) + } + _ => false, + }; + + if !is_correct_browser { + continue; + } + + // Check for profile path match + let profiles_dir = self.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path_str = profile_data_path.to_string_lossy(); + let profile_path_match = cmd.iter().any(|s| { + let arg = s.to_str().unwrap_or(""); + // For Firefox-based browsers, check for exact profile path match + if profile.browser == "camoufox" { + // Camoufox uses user_data_dir like Chromium browsers + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } else if profile.browser == "tor-browser" + || profile.browser == "firefox" + || profile.browser == "firefox-developer" + || profile.browser == "mullvad-browser" + || profile.browser == "zen" + { + arg == profile_data_path_str + || arg == format!("-profile={profile_data_path_str}") + || (arg == "-profile" + && cmd + .iter() + .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) + } else { + // For Chromium-based browsers, check for user-data-dir + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } + }); + + if profile_path_match { + // Found a matching process + found_pid = Some(pid.as_u32()); + is_running = true; + println!( + "Found browser process with PID: {} for profile: {}", + pid.as_u32(), + profile.name + ); + break; + } + } + } + } + + // Update the process ID if we found a different one + if let Some(pid) = found_pid { + if inner_profile.process_id != Some(pid) { + inner_profile.process_id = Some(pid); + if let Err(e) = self.save_profile(&inner_profile) { + println!("Warning: Failed to update profile with new PID: {e}"); + } + } + } else if inner_profile.process_id.is_some() { + // Clear the PID if no process found + inner_profile.process_id = None; + if let Err(e) = self.save_profile(&inner_profile) { + println!("Warning: Failed to clear profile PID: {e}"); + } + } + + // Emit profile update event to frontend + if let Err(e) = app_handle.emit("profile-updated", &inner_profile) { + println!("Warning: Failed to emit profile update event: {e}"); + } + + Ok(is_running) + } + + // Helper function to check if a process matches TOR/Mullvad browser + fn is_tor_or_mullvad_browser( + &self, + exe_name: &str, + cmd: &[std::ffi::OsString], + browser_type: &str, + ) -> bool { + #[cfg(target_os = "macos")] + return crate::platform_browser::macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); + + #[cfg(target_os = "windows")] + return crate::platform_browser::windows::is_tor_or_mullvad_browser( + exe_name, + cmd, + browser_type, + ); + + #[cfg(target_os = "linux")] + return crate::platform_browser::linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type); + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + let _ = (exe_name, cmd, browser_type); + false + } + } + + fn get_binaries_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } + + fn get_common_firefox_preferences(&self) -> Vec { + vec![ + // Disable default browser check + "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), + "user_pref(\"app.update.enabled\", false);".to_string(), + "user_pref(\"app.update.auto\", false);".to_string(), + "user_pref(\"app.update.mode\", 2);".to_string(), + "user_pref(\"app.update.promptWaitTime\", 0);".to_string(), + "user_pref(\"app.update.service.enabled\", false);".to_string(), + "user_pref(\"app.update.silent\", true);".to_string(), + "user_pref(\"app.update.checkInstallTime\", false);".to_string(), + "user_pref(\"app.update.url\", \"\");".to_string(), + "user_pref(\"app.update.url.manual\", \"\");".to_string(), + "user_pref(\"app.update.url.details\", \"\");".to_string(), + "user_pref(\"app.update.url.override\", \"\");".to_string(), + "user_pref(\"app.update.interval\", 9999999999);".to_string(), + "user_pref(\"app.update.background.interval\", 9999999999);".to_string(), + "user_pref(\"app.update.download.attemptOnce\", false);".to_string(), + "user_pref(\"app.update.idletime\", -1);".to_string(), + ] + } + + pub fn apply_proxy_settings_to_profile( + &self, + profile_data_path: &Path, + proxy: &ProxySettings, + internal_proxy: Option<&ProxySettings>, + ) -> Result<(), Box> { + let user_js_path = profile_data_path.join("user.js"); + let mut preferences = Vec::new(); + + // Get the UUID directory (parent of profile data directory) + let uuid_dir = profile_data_path + .parent() + .ok_or("Invalid profile path - cannot find UUID directory")?; + + // Add common Firefox preferences (like disabling default browser check) + preferences.extend(self.get_common_firefox_preferences()); + + // Use embedded PAC template instead of reading from file + const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) { + return "{{proxy_url}}"; +}"#; + + // Format proxy URL based on type and whether we have an internal proxy + let proxy_url = if let Some(internal) = internal_proxy { + // Use internal proxy as the primary proxy + format!("HTTP {}:{}", internal.host, internal.port) + } else { + // Use user-configured proxy directly + match proxy.proxy_type.as_str() { + "http" => format!("HTTP {}:{}", proxy.host, proxy.port), + "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), + "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), + "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), + _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), + } + }; + + // Replace placeholders in PAC file + let pac_content = PAC_TEMPLATE + .replace("{{proxy_url}}", &proxy_url) + .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file + + // Save PAC file in UUID directory + let pac_path = uuid_dir.join("proxy.pac"); + fs::write(&pac_path, pac_content)?; + + // Configure Firefox to use the PAC file + preferences.extend([ + "user_pref(\"network.proxy.type\", 2);".to_string(), + format!( + "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", + pac_path.to_string_lossy() + ), + "user_pref(\"network.proxy.failover_direct\", false);".to_string(), + "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), + "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), + "user_pref(\"signon.autologin.proxy\", true);".to_string(), + "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), + "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), + "user_pref(\"network.auth-use-sspi\", false);".to_string(), + ]); + + // Write settings to user.js file + fs::write(user_js_path, preferences.join("\n"))?; + + Ok(()) + } + + pub fn disable_proxy_settings_in_profile( + &self, + profile_data_path: &Path, + ) -> Result<(), Box> { + let user_js_path = profile_data_path.join("user.js"); + let mut preferences = Vec::new(); + + // Get the UUID directory (parent of profile data directory) + let uuid_dir = profile_data_path + .parent() + .ok_or("Invalid profile path - cannot find UUID directory")?; + + // Add common Firefox preferences (like disabling default browser check) + preferences.extend(self.get_common_firefox_preferences()); + + preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); + preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); + + // Create a direct proxy PAC file in UUID directory + let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }"; + let pac_path = uuid_dir.join("proxy.pac"); + fs::write(&pac_path, pac_content)?; + preferences.push(format!( + "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", + pac_path.to_string_lossy() + )); + + fs::write(user_js_path, preferences.join("\n"))?; + + Ok(()) + } + + // Migrate old profile structure to new UUID-based structure + pub async fn migrate_profiles_to_uuid(&self) -> Result, Box> { + let profiles_dir = self.get_profiles_dir(); + if !profiles_dir.exists() { + return Ok(vec![]); + } + + let mut migrated_profiles = Vec::new(); + + // Scan for old-format profile files (*.json files directly in profiles directory) + for entry in fs::read_dir(&profiles_dir)? { + let entry = entry?; + let path = entry.path(); + + // Look for .json files that are NOT in UUID directories + if path.is_file() && path.extension().is_some_and(|ext| ext == "json") { + let content = fs::read_to_string(&path)?; + + // Try to parse as old profile format (without UUID) + let mut old_profile: serde_json::Value = serde_json::from_str(&content)?; + + // Skip if it already has an id field (already migrated) + if old_profile.get("id").is_some() { + continue; + } + + // Generate new UUID for this profile + let profile_id = uuid::Uuid::new_v4(); + + // Extract profile name before mutating + let profile_name = old_profile["name"] + .as_str() + .unwrap_or("unknown") + .to_string(); + + // Check if there's a running browser process for this profile and kill it + if let Some(process_id_value) = old_profile.get("process_id") { + if let Some(pid) = process_id_value.as_u64() { + let pid = pid as u32; + println!("Found running browser process (PID: {pid}) for profile '{profile_name}' during migration"); + + // Kill the process before migration + if let Err(e) = Self::kill_browser_process_by_pid(pid).await { + println!( + "Warning: Failed to kill browser process (PID: {pid}) during migration: {e}" + ); + // Continue with migration even if kill fails - the process might already be dead + } else { + println!( + "Successfully killed browser process (PID: {pid}) for profile '{profile_name}'" + ); + } + + // Clear the process_id since we killed it + old_profile["process_id"] = serde_json::Value::Null; + } + } + + let snake_case_name = profile_name.to_lowercase().replace(" ", "_"); + let old_profile_dir = profiles_dir.join(&snake_case_name); + + // Create new UUID directory and profile subdirectory + let new_profile_dir = profiles_dir.join(profile_id.to_string()); + let new_profile_data_dir = new_profile_dir.join("profile"); + create_dir_all(&new_profile_dir)?; + create_dir_all(&new_profile_data_dir)?; + + // Now update the profile with UUID (no need to store profile_path anymore) + old_profile["id"] = serde_json::Value::String(profile_id.to_string()); + + // Handle proxy migration - extract proxy to separate storage if it exists + if let Some(proxy_value) = old_profile.get("proxy").cloned() { + if !proxy_value.is_null() { + // Try to deserialize the proxy settings + if let Ok(proxy_settings) = serde_json::from_value::(proxy_value) { + // Create a stored proxy with the profile name (all proxies are now enabled by default) + let proxy_name = format!("{profile_name} Proxy"); + match PROXY_MANAGER.create_stored_proxy(proxy_name.clone(), proxy_settings.clone()) { + Ok(stored_proxy) => { + // Update profile to reference the stored proxy + old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); + println!( + "Migrated proxy for profile '{}' to stored proxy '{}'", + profile_name, stored_proxy.name + ); + } + Err(e) => { + println!("Warning: Failed to migrate proxy for profile '{profile_name}': {e}"); + // If creation fails (e.g., name collision), try to find existing proxy with same settings + let existing_proxies = PROXY_MANAGER.get_stored_proxies(); + if let Some(existing_proxy) = existing_proxies.iter().find(|p| { + p.proxy_settings.proxy_type == proxy_settings.proxy_type + && p.proxy_settings.host == proxy_settings.host + && p.proxy_settings.port == proxy_settings.port + && p.proxy_settings.username == proxy_settings.username + && p.proxy_settings.password == proxy_settings.password + }) { + old_profile["proxy_id"] = serde_json::Value::String(existing_proxy.id.clone()); + println!( + "Reused existing proxy '{}' for profile '{}'", + existing_proxy.name, profile_name + ); + } else { + // Try with a different name if the original failed due to name collision + let alt_proxy_name = format!( + "{profile_name} Proxy {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); + match PROXY_MANAGER + .create_stored_proxy(alt_proxy_name.clone(), proxy_settings.clone()) + { + Ok(stored_proxy) => { + old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); + println!( + "Migrated proxy for profile '{}' to stored proxy '{}' with fallback name", + profile_name, stored_proxy.name + ); + } + Err(e2) => { + println!("Error: Could not migrate proxy for profile '{profile_name}' even with fallback name: {e2}"); + } + } + } + } + } + } else { + println!( + "Warning: Could not deserialize proxy settings for profile '{profile_name}'" + ); + } + } + } + + // Always remove the old proxy field after migration attempt, whether successful or not + if old_profile + .as_object_mut() + .unwrap() + .remove("proxy") + .is_some() + { + println!("Removed legacy proxy field from profile '{profile_name}'"); + } + + // Move old profile directory contents to new UUID/profile directory if it exists + if old_profile_dir.exists() && old_profile_dir.is_dir() { + // Copy all contents from old directory to new profile subdirectory + for entry in fs::read_dir(&old_profile_dir)? { + let entry = entry?; + let source_path = entry.path(); + let dest_path = new_profile_data_dir.join(entry.file_name()); + + if source_path.is_dir() { + Self::copy_directory_recursive(&source_path, &dest_path)?; + } else { + fs::copy(&source_path, &dest_path)?; + } + } + + // Remove old profile directory + fs::remove_dir_all(&old_profile_dir)?; + println!( + "Migrated profile directory: {} -> {}", + old_profile_dir.display(), + new_profile_data_dir.display() + ); + } + + // Save migrated profile as metadata.json in UUID directory + let metadata_file = new_profile_dir.join("metadata.json"); + let json = serde_json::to_string_pretty(&old_profile)?; + fs::write(&metadata_file, json)?; + + // Remove old profile JSON file + fs::remove_file(&path)?; + + migrated_profiles.push(profile_name.clone()); + println!("Migrated profile '{profile_name}' to UUID: {profile_id}"); + } + } + + if !migrated_profiles.is_empty() { + println!( + "Successfully migrated {} profiles to UUID format", + migrated_profiles.len() + ); + } + + Ok(migrated_profiles) + } + + /// Helper function to kill a browser process by PID only (used during migration) + async fn kill_browser_process_by_pid( + pid: u32, + ) -> Result<(), Box> { + println!("Attempting to kill browser process with PID: {pid} during migration"); + + // Kill the process using platform-specific implementation + #[cfg(target_os = "macos")] + crate::platform_browser::macos::kill_browser_process_impl(pid).await?; + + #[cfg(target_os = "windows")] + crate::platform_browser::windows::kill_browser_process_impl(pid).await?; + + #[cfg(target_os = "linux")] + crate::platform_browser::linux::kill_browser_process_impl(pid).await?; + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + return Err("Unsupported platform".into()); + + println!("Successfully killed browser process with PID: {pid} during migration"); + Ok(()) + } + + /// Recursively copy directory contents + fn copy_directory_recursive( + source: &Path, + destination: &Path, + ) -> Result<(), Box> { + if !destination.exists() { + create_dir_all(destination)?; + } + + for entry in fs::read_dir(source)? { + let entry = entry?; + let source_path = entry.path(); + let dest_path = destination.join(entry.file_name()); + + if source_path.is_dir() { + Self::copy_directory_recursive(&source_path, &dest_path)?; + } else { + fs::copy(&source_path, &dest_path)?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::browser::ProxySettings; + use tempfile::TempDir; + + fn create_test_profile_manager() -> (ProfileManager, TempDir) { + let temp_dir = TempDir::new().unwrap(); + + // Mock the base directories by setting environment variables + std::env::set_var("HOME", temp_dir.path()); + + let profile_manager = ProfileManager::new(); + (profile_manager, temp_dir) + } + + #[test] + fn test_profile_manager_creation() { + let (_manager, _temp_dir) = create_test_profile_manager(); + // If we get here without panicking, the test passes + } + + #[test] + fn test_get_profiles_dir() { + let (manager, _temp_dir) = create_test_profile_manager(); + let profiles_dir = manager.get_profiles_dir(); + + assert!(profiles_dir.to_string_lossy().contains("DonutBrowser")); + assert!(profiles_dir.to_string_lossy().contains("profiles")); + } + + #[test] + fn test_create_profile() { + let (manager, _temp_dir) = create_test_profile_manager(); + + let profile = manager + .create_profile("Test Profile", "firefox", "139.0", "stable", None, None) + .unwrap(); + + assert_eq!(profile.name, "Test Profile"); + assert_eq!(profile.browser, "firefox"); + assert_eq!(profile.version, "139.0"); + assert!(profile.proxy_id.is_none()); + assert!(profile.process_id.is_none()); + } + + #[test] + fn test_save_and_load_profile() { + let (manager, _temp_dir) = create_test_profile_manager(); + + let profile = manager + .create_profile("Test Save Load", "firefox", "139.0", "stable", None, None) + .unwrap(); + + // Save the profile + manager.save_profile(&profile).unwrap(); + + // Load profiles and verify + let profiles = manager.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "Test Save Load"); + assert_eq!(profiles[0].browser, "firefox"); + assert_eq!(profiles[0].version, "139.0"); + } + + #[test] + fn test_rename_profile() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Create profile + let _ = manager + .create_profile("Original Name", "firefox", "139.0", "stable", None, None) + .unwrap(); + + // Rename profile + let renamed_profile = manager.rename_profile("Original Name", "New Name").unwrap(); + + assert_eq!(renamed_profile.name, "New Name"); + + // Verify old profile is gone and new one exists + let profiles = manager.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].name, "New Name"); + } + + #[test] + fn test_delete_profile() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Create profile + let _ = manager + .create_profile("To Delete", "firefox", "139.0", "stable", None, None) + .unwrap(); + + // Verify profile exists + let profiles = manager.list_profiles().unwrap(); + assert_eq!(profiles.len(), 1); + + // Delete profile + manager.delete_profile("To Delete").unwrap(); + + // Verify profile is gone + let profiles = manager.list_profiles().unwrap(); + assert_eq!(profiles.len(), 0); + } + + #[test] + fn test_profile_name_sanitization() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Create profile with spaces and special characters + let profile = manager + .create_profile( + "Test Profile With Spaces", + "firefox", + "139.0", + "stable", + None, + None, + ) + .unwrap(); + + // Profile path should contain UUID and end with /profile + let profiles_dir = manager.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + assert!(profile_data_path + .to_string_lossy() + .contains(&profile.id.to_string())); + assert!(profile_data_path.to_string_lossy().ends_with("/profile")); + // Profile name should remain unchanged + assert_eq!(profile.name, "Test Profile With Spaces"); + // Profile should have a valid UUID + assert!(uuid::Uuid::parse_str(&profile.id.to_string()).is_ok()); + } + + #[test] + fn test_multiple_profiles() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Create multiple profiles + let _ = manager + .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) + .unwrap(); + let _ = manager + .create_profile("Profile 2", "chromium", "1465660", "stable", None, None) + .unwrap(); + let _ = manager + .create_profile("Profile 3", "brave", "v1.81.9", "stable", None, None) + .unwrap(); + + // List profiles + let profiles = manager.list_profiles().unwrap(); + assert_eq!(profiles.len(), 3); + + let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); + assert!(profile_names.contains(&"Profile 1")); + assert!(profile_names.contains(&"Profile 2")); + assert!(profile_names.contains(&"Profile 3")); + } + + #[test] + fn test_profile_validation() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Test that we can't rename to an existing profile name + let _ = manager + .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) + .unwrap(); + let _ = manager + .create_profile("Profile 2", "firefox", "139.0", "stable", None, None) + .unwrap(); + + // Try to rename profile2 to profile1's name (should fail) + let result = manager.rename_profile("Profile 2", "Profile 1"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("already exists")); + } + + #[test] + fn test_firefox_default_browser_preferences() { + let (manager, _temp_dir) = create_test_profile_manager(); + + // Create profile without proxy + let profile = manager + .create_profile( + "Test Firefox Preferences", + "firefox", + "139.0", + "stable", + None, + None, + ) + .unwrap(); + + // Check that user.js file was created with default browser preference + let profiles_dir = manager.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let user_js_path = profile_data_path.join("user.js"); + assert!(user_js_path.exists()); + + let user_js_content = std::fs::read_to_string(user_js_path).unwrap(); + assert!(user_js_content.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content.contains("false")); + + // Verify automatic update disabling preferences are present + assert!(user_js_content.contains("app.update.enabled")); + assert!(user_js_content.contains("app.update.auto")); + + // Create profile with proxy (proxy object unused in new architecture) + let _proxy = ProxySettings { + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: 8080, + username: None, + password: None, + }; + + let profile_with_proxy = manager + .create_profile( + "Test Firefox Preferences Proxy", + "firefox", + "139.0", + "stable", + None, // Tests now use separate proxy storage system + None, // No camoufox config for this test + ) + .unwrap(); + + // Check that user.js file contains both proxy settings and default browser preference + let profile_with_proxy_data_path = profile_with_proxy.get_profile_data_path(&profiles_dir); + let user_js_path_proxy = profile_with_proxy_data_path.join("user.js"); + assert!(user_js_path_proxy.exists()); + + let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap(); + assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser")); + assert!(user_js_content_proxy.contains("network.proxy.type")); + + // Verify automatic update disabling preferences are present even with proxy + assert!(user_js_content_proxy.contains("app.update.enabled")); + assert!(user_js_content_proxy.contains("app.update.auto")); + } +} diff --git a/src-tauri/src/profile/mod.rs b/src-tauri/src/profile/mod.rs new file mode 100644 index 0000000..7e4259f --- /dev/null +++ b/src-tauri/src/profile/mod.rs @@ -0,0 +1,5 @@ +pub mod manager; +pub mod types; + +pub use manager::ProfileManager; +pub use types::BrowserProfile; diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs new file mode 100644 index 0000000..815a5f3 --- /dev/null +++ b/src-tauri/src/profile/types.rs @@ -0,0 +1,34 @@ +use crate::camoufox::CamoufoxConfig; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserProfile { + pub id: uuid::Uuid, + pub name: String, + pub browser: String, + pub version: String, + #[serde(default)] + pub proxy_id: Option, // Reference to stored proxy + #[serde(default)] + pub process_id: Option, + #[serde(default)] + pub last_launch: Option, + #[serde(default = "default_release_type")] + pub release_type: String, // "stable" or "nightly" + #[serde(default)] + pub camoufox_config: Option, // Camoufox configuration + #[serde(default)] + pub group_id: Option, // Reference to profile group +} + +pub fn default_release_type() -> String { + "stable".to_string() +} + +impl BrowserProfile { + /// Get the path to the profile data directory (profiles/{uuid}/profile) + pub fn get_profile_data_path(&self, profiles_dir: &Path) -> PathBuf { + profiles_dir.join(self.id.to_string()).join("profile") + } +} diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 524cdea..0cd0280 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -680,7 +680,7 @@ impl ProfileImporter { // We need to find a suitable version for this browser type let available_versions = self.get_default_version_for_browser(browser_type)?; - let profile = crate::browser_runner::BrowserProfile { + let profile = crate::profile::BrowserProfile { id: profile_id, name: new_profile_name.to_string(), browser: browser_type.to_string(),