use crate::api_client::is_browser_version_nightly; use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::camoufox_manager::CamoufoxConfig; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::profile::types::BrowserProfile; use crate::proxy_manager::PROXY_MANAGER; use crate::wayfern_manager::WayfernConfig; 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, camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager, wayfern_manager: &'static crate::wayfern_manager::WayfernManager, } impl ProfileManager { fn new() -> Self { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(), wayfern_manager: crate::wayfern_manager::WayfernManager::instance(), } } pub fn instance() -> &'static ProfileManager { &PROFILE_MANAGER } 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 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 } #[allow(clippy::too_many_arguments)] pub async fn create_profile_with_group( &self, app_handle: &tauri::AppHandle, name: &str, browser: &str, version: &str, release_type: &str, proxy_id: Option, camoufox_config: Option, wayfern_config: Option, group_id: Option, ) -> Result> { log::info!("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)?; // For Camoufox profiles, generate fingerprint during creation let final_camoufox_config = if browser == "camoufox" { let mut config = camoufox_config.unwrap_or_else(|| { log::info!("Creating default Camoufox config for profile: {name}"); crate::camoufox_manager::CamoufoxConfig::default() }); // Always ensure executable_path is set to the user's binary location if config.executable_path.is_none() { let mut browser_dir = self.get_binaries_dir(); browser_dir.push(browser); browser_dir.push(version); #[cfg(target_os = "macos")] let binary_path = browser_dir .join("Camoufox.app") .join("Contents") .join("MacOS") .join("camoufox"); #[cfg(target_os = "windows")] let binary_path = browser_dir.join("camoufox.exe"); #[cfg(target_os = "linux")] let binary_path = browser_dir.join("camoufox"); config.executable_path = Some(binary_path.to_string_lossy().to_string()); log::info!("Set Camoufox executable path: {:?}", config.executable_path); } // Pass upstream proxy information to config for fingerprint generation if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { // For fingerprint generation, pass upstream proxy directly with credentials if present let proxy_url = if let (Some(username), Some(password)) = (&proxy_settings.username, &proxy_settings.password) { format!( "{}://{}:{}@{}:{}", proxy_settings.proxy_type.to_lowercase(), username, password, proxy_settings.host, proxy_settings.port ) } else { format!( "{}://{}:{}", proxy_settings.proxy_type.to_lowercase(), proxy_settings.host, proxy_settings.port ) }; config.proxy = Some(proxy_url); log::info!( "Using upstream proxy for Camoufox fingerprint generation: {}://{}:{}", proxy_settings.proxy_type.to_lowercase(), proxy_settings.host, proxy_settings.port ); } } // Generate fingerprint if not already provided if config.fingerprint.is_none() { log::info!("Generating fingerprint for Camoufox profile: {name}"); // Use the camoufox launcher to generate the config // Create a temporary profile for fingerprint generation let temp_profile = BrowserProfile { id: uuid::Uuid::new_v4(), 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: None, wayfern_config: None, group_id: group_id.clone(), tags: Vec::new(), note: None, sync_enabled: false, last_sync: None, }; match self .camoufox_manager .generate_fingerprint_config(app_handle, &temp_profile, &config) .await { Ok(generated_fingerprint) => { config.fingerprint = Some(generated_fingerprint); log::info!("Successfully generated fingerprint for profile: {name}"); } Err(e) => { return Err( format!("Failed to generate fingerprint for Camoufox profile '{name}': {e}").into(), ); } } } else { log::info!("Using provided fingerprint for Camoufox profile: {name}"); } // Clear the proxy from config after fingerprint generation // Browser launch should always use local proxy, never direct to upstream config.proxy = None; Some(config) } else { camoufox_config.clone() }; // For Wayfern profiles, generate fingerprint during creation let final_wayfern_config = if browser == "wayfern" { let mut config = wayfern_config.unwrap_or_else(|| { log::info!("Creating default Wayfern config for profile: {name}"); crate::wayfern_manager::WayfernConfig::default() }); // Always ensure executable_path is set to the user's binary location if config.executable_path.is_none() { let mut browser_dir = self.get_binaries_dir(); browser_dir.push(browser); browser_dir.push(version); #[cfg(target_os = "macos")] let binary_path = browser_dir .join("Chromium.app") .join("Contents") .join("MacOS") .join("Chromium"); #[cfg(target_os = "windows")] let binary_path = browser_dir.join("chrome.exe"); #[cfg(target_os = "linux")] let binary_path = browser_dir.join("chrome"); config.executable_path = Some(binary_path.to_string_lossy().to_string()); log::info!("Set Wayfern executable path: {:?}", config.executable_path); } // Pass upstream proxy information to config for fingerprint generation if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { let proxy_url = if let (Some(username), Some(password)) = (&proxy_settings.username, &proxy_settings.password) { format!( "{}://{}:{}@{}:{}", proxy_settings.proxy_type.to_lowercase(), username, password, proxy_settings.host, proxy_settings.port ) } else { format!( "{}://{}:{}", proxy_settings.proxy_type.to_lowercase(), proxy_settings.host, proxy_settings.port ) }; config.proxy = Some(proxy_url); log::info!( "Using upstream proxy for Wayfern fingerprint generation: {}://{}:{}", proxy_settings.proxy_type.to_lowercase(), proxy_settings.host, proxy_settings.port ); } } // Generate fingerprint if not already provided if config.fingerprint.is_none() { log::info!("Generating fingerprint for Wayfern profile: {name}"); // Create a temporary profile for fingerprint generation let temp_profile = BrowserProfile { id: uuid::Uuid::new_v4(), 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: None, wayfern_config: None, group_id: group_id.clone(), tags: Vec::new(), note: None, sync_enabled: false, last_sync: None, }; match self .wayfern_manager .generate_fingerprint_config(app_handle, &temp_profile, &config) .await { Ok(generated_fingerprint) => { config.fingerprint = Some(generated_fingerprint); log::info!("Successfully generated fingerprint for Wayfern profile: {name}"); } Err(e) => { return Err( format!("Failed to generate fingerprint for Wayfern profile '{name}': {e}").into(), ); } } } else { log::info!("Using provided fingerprint for Wayfern profile: {name}"); } // Clear the proxy from config after fingerprint generation config.proxy = None; Some(config) } else { wayfern_config.clone() }; 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: final_camoufox_config, wayfern_config: final_wayfern_config, group_id: group_id.clone(), tags: Vec::new(), note: None, sync_enabled: false, last_sync: None, }; // 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()); } log::info!("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)?; } // Emit profile creation event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } 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)?; // Update tag suggestions after any save let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); 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, app_handle: &tauri::AppHandle, profile_id: &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 ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let mut profile = existing_profiles .into_iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' 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)?; // Keep tag suggestions up to date after name change (rebuild from all profiles) let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); // Emit profile rename event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(profile) } pub fn delete_profile( &self, app_handle: &tauri::AppHandle, profile_id: &str, ) -> Result<(), Box> { log::info!("Attempting to delete profile with ID: {profile_id}"); // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let profiles = self.list_profiles()?; let profile = profiles .into_iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' 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(), ); } // Remember sync_enabled before deleting local files let was_sync_enabled = profile.sync_enabled; 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() { log::info!("Deleting profile directory: {}", profile_uuid_dir.display()); fs::remove_dir_all(&profile_uuid_dir)?; log::info!("Profile directory deleted successfully"); } // Verify deletion was successful if profile_uuid_dir.exists() { return Err(format!("Failed to completely delete profile '{}'", profile.name).into()); } log::info!( "Profile '{}' (ID: {}) deleted successfully", profile.name, profile_id ); // If sync was enabled, also delete from S3 if was_sync_enabled { let profile_id_owned = profile_id.to_string(); let app_handle_clone = app_handle.clone(); tauri::async_runtime::spawn(async move { match crate::sync::SyncEngine::create_from_settings(&app_handle_clone).await { Ok(engine) => { if let Err(e) = engine.delete_profile(&profile_id_owned).await { log::warn!( "Failed to delete profile {} from sync: {}", profile_id_owned, e ); } else { log::info!("Profile {} deleted from S3 sync storage", profile_id_owned); } } Err(e) => { log::debug!("Sync not configured, skipping remote deletion: {}", e); } } }); } // Rebuild tag suggestions after deletion let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); // Always perform cleanup after profile deletion to remove unused binaries if let Err(e) = DownloadedBrowsersRegistry::instance().cleanup_unused_binaries() { log::warn!("Warning: Failed to cleanup unused binaries after profile deletion: {e}"); } // Emit profile deletion event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(()) } pub fn update_profile_version( &self, app_handle: &tauri::AppHandle, profile_id: &str, version: &str, ) -> Result> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let profiles = self.list_profiles()?; let mut profile = profiles .into_iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' 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 is_browser_version_nightly(&profile.browser, version, None) { "nightly".to_string() } else { "stable".to_string() }; // Save the updated profile self.save_profile(&profile)?; // Emit profile update event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(profile) } pub fn assign_profiles_to_group( &self, app_handle: &tauri::AppHandle, profile_ids: Vec, group_id: Option, ) -> Result<(), Box> { let profiles = self.list_profiles()?; for profile_id in profile_ids { let profile_uuid = uuid::Uuid::parse_str(&profile_id) .map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let mut profile = profiles .iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))? .clone(); // Check if browser is running if profile.process_id.is_some() { return Err(format!( "Cannot modify group for profile '{}' while browser is running. Please stop the browser first.", profile.name ).into()); } profile.group_id = group_id.clone(); self.save_profile(&profile)?; // Auto-enable sync for new group if profile has sync enabled if profile.sync_enabled { if let Some(ref new_group_id) = group_id { let group_id_clone = new_group_id.clone(); let app_handle_clone = app_handle.clone(); tauri::async_runtime::spawn(async move { let _ = crate::sync::enable_group_sync_if_needed(&group_id_clone, &app_handle_clone).await; if let Some(scheduler) = crate::sync::get_global_scheduler() { scheduler.queue_group_sync(group_id_clone).await; } }); } } } // Rebuild tag suggestions after group changes just in case let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); // Emit profile group assignment event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(()) } pub fn update_profile_tags( &self, app_handle: &tauri::AppHandle, profile_id: &str, tags: Vec, ) -> Result> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let profiles = self.list_profiles()?; let mut profile = profiles .into_iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?; let mut seen = std::collections::HashSet::new(); let mut deduped: Vec = Vec::with_capacity(tags.len()); for t in tags.into_iter() { if seen.insert(t.clone()) { deduped.push(t); } } profile.tags = deduped; // Save profile self.save_profile(&profile)?; // Update global tag suggestions from all profiles let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { let _ = tm.rebuild_from_profiles(&self.list_profiles().unwrap_or_default()); }); // Emit profile tags update event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(profile) } pub fn update_profile_note( &self, app_handle: &tauri::AppHandle, profile_id: &str, note: Option, ) -> Result> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let profiles = self.list_profiles()?; let mut profile = profiles .into_iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?; // Update note (trim whitespace, set to None if empty) profile.note = note.map(|n| n.trim().to_string()).filter(|n| !n.is_empty()); // Save profile self.save_profile(&profile)?; // Emit profile note update event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(profile) } pub fn delete_multiple_profiles( &self, app_handle: &tauri::AppHandle, profile_ids: Vec, ) -> Result<(), Box> { let profiles = self.list_profiles()?; let mut sync_enabled_ids: Vec = Vec::new(); for profile_id in profile_ids { let profile_uuid = uuid::Uuid::parse_str(&profile_id) .map_err(|_| format!("Invalid profile ID: {profile_id}"))?; let profile = profiles .iter() .find(|p| p.id == profile_uuid) .ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?; // Check if browser is running if profile.process_id.is_some() { return Err( format!( "Cannot delete profile '{}' while browser is running. Please stop the browser first.", profile.name ) .into(), ); } // Track sync-enabled profiles for remote deletion if profile.sync_enabled { sync_enabled_ids.push(profile_id.clone()); } // 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)?; } } // Delete sync-enabled profiles from S3 if !sync_enabled_ids.is_empty() { let app_handle_clone = app_handle.clone(); tauri::async_runtime::spawn(async move { if let Ok(engine) = crate::sync::SyncEngine::create_from_settings(&app_handle_clone).await { for profile_id in sync_enabled_ids { if let Err(e) = engine.delete_profile(&profile_id).await { log::warn!("Failed to delete profile {} from sync: {}", profile_id, e); } } } }); } // Emit profile deletion event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(()) } pub async fn update_camoufox_config( &self, app_handle: tauri::AppHandle, profile_id: &str, config: CamoufoxConfig, ) -> Result<(), Box> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( |_| -> Box { format!("Invalid profile ID: {profile_id}").into() }, )?; 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.id == profile_uuid) .ok_or_else(|| -> Box { format!("Profile with ID '{profile_id}' not found").into() })?; // Check if the browser is currently running using the comprehensive status check let is_running = self .check_browser_status(app_handle.clone(), &profile) .await?; if is_running { 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) .map_err(|e| -> Box { format!("Failed to save profile: {e}").into() })?; log::info!( "Camoufox configuration updated for profile '{}' (ID: {}).", profile.name, profile_id ); // Emit profile config update event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(()) } pub async fn update_wayfern_config( &self, app_handle: tauri::AppHandle, profile_id: &str, config: WayfernConfig, ) -> Result<(), Box> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( |_| -> Box { format!("Invalid profile ID: {profile_id}").into() }, )?; 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.id == profile_uuid) .ok_or_else(|| -> Box { format!("Profile with ID '{profile_id}' not found").into() })?; // Check if the browser is currently running using the comprehensive status check let is_running = self .check_browser_status(app_handle.clone(), &profile) .await?; if is_running { return Err( "Cannot update Wayfern configuration while browser is running. Please stop the browser first.".into(), ); } // Update the Wayfern configuration profile.wayfern_config = Some(config); // Save the updated profile self .save_profile(&profile) .map_err(|e| -> Box { format!("Failed to save profile: {e}").into() })?; log::info!( "Wayfern configuration updated for profile '{}' (ID: {}).", profile.name, profile_id ); // Emit profile config update event if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(()) } pub async fn update_profile_proxy( &self, app_handle: tauri::AppHandle, profile_id: &str, proxy_id: Option, ) -> Result> { // Find the profile by ID let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( |_| -> Box { format!("Invalid profile ID: {profile_id}").into() }, )?; 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.id == profile_uuid) .ok_or_else(|| -> Box { format!("Profile with ID '{profile_id}' not found").into() })?; // Remember old proxy_id for cleanup (not used yet, but may be needed for cleanup) let _old_proxy_id = profile.proxy_id.clone(); // 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() })?; // Auto-enable sync for new proxy if profile has sync enabled if profile.sync_enabled { if let Some(ref new_proxy_id) = proxy_id { let _ = crate::sync::enable_proxy_sync_if_needed(new_proxy_id, &app_handle).await; if let Some(scheduler) = crate::sync::get_global_scheduler() { scheduler.queue_proxy_sync(new_proxy_id.clone()).await; } } } // Update on-disk browser profile config immediately if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { 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() })?; } // Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager) if let Err(e) = app_handle.emit("profile-updated", &profile) { log::warn!("Warning: Failed to emit profile update event: {e}"); } // Emit general profiles changed event for profile list updates if let Err(e) = app_handle.emit("profiles-changed", ()) { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); } Ok(profile) } pub async fn check_browser_status( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { // Handle Camoufox profiles using CamoufoxManager-based status checking if profile.browser == "camoufox" { return self.check_camoufox_status(&app_handle, profile).await; } // Handle Wayfern profiles using WayfernManager-based status checking if profile.browser == "wayfern" { return self.check_wayfern_status(&app_handle, profile).await; } // For non-camoufox browsers, use the existing PID-based logic let inner_profile = profile.clone(); let mut system = System::new(); 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, check for exact profile path match if profile.browser == "firefox" || profile.browser == "firefox-developer" || 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); // Found existing browser process } } } // If we didn't find the browser with the stored PID, search all processes if !is_running { // Refresh all processes only when we need to search (expensive but necessary) system.refresh_all(); 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("camoufox") } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "zen" => exe_name.contains("zen"), "chromium" => exe_name.contains("chromium"), "brave" => exe_name.contains("brave"), // Camoufox is handled via CamoufoxManager, not PID-based checking _ => 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 == "firefox" || profile.browser == "firefox-developer" || 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; log::info!( "Found browser process with PID: {} for profile: {}", pid.as_u32(), profile.name ); break; } } } } // Only persist status changes if the profile metadata still exists on disk let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { // Load the latest profile from disk to avoid overwriting fields like proxy_id let latest_profile: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => inner_profile.clone(), }; let mut merged = latest_profile.clone(); if let Some(pid) = found_pid { if merged.process_id != Some(pid) { merged.process_id = Some(pid); if let Err(e) = self.save_profile(&merged) { log::warn!("Warning: Failed to update profile with new PID: {e}"); } } } else if merged.process_id.is_some() { // Clear the PID if no process found merged.process_id = None; if let Err(e) = self.save_profile(&merged) { log::warn!("Warning: Failed to clear profile PID: {e}"); } } // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &merged) { log::warn!("Warning: Failed to emit profile update event: {e}"); } } Ok(is_running) } // Check Camoufox status using CamoufoxManager async fn check_camoufox_status( &self, app_handle: &tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { let launcher = self.camoufox_manager; let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if there's a running Camoufox instance for this profile match launcher.find_camoufox_by_profile(&profile_path_str).await { Ok(Some(camoufox_process)) => { // Found a running instance, update profile with process info if changed let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { // Load latest to avoid overwriting other fields let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => profile.clone(), }; if latest.process_id != camoufox_process.processId { latest.process_id = camoufox_process.processId; if let Err(e) = self.save_profile(&latest) { log::warn!("Warning: Failed to update Camoufox profile with process info: {e}"); } // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &latest) { log::warn!("Warning: Failed to emit profile update event: {e}"); } log::info!( "Camoufox process has started for profile '{}' with PID: {:?}", profile.name, camoufox_process.processId ); } } Ok(true) } Ok(None) => { // No running instance found, clear process ID if set and stop proxy let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => profile.clone(), }; if latest.process_id.is_some() { latest.process_id = None; if let Err(e) = self.save_profile(&latest) { log::warn!("Warning: Failed to clear Camoufox profile process info: {e}"); } if let Err(e) = app_handle.emit("profile-updated", &latest) { log::warn!("Warning: Failed to emit profile update event: {e}"); } } } Ok(false) } Err(e) => { // Error checking status, assume not running and clear process ID log::warn!("Warning: Failed to check Camoufox status: {e}"); let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => profile.clone(), }; if latest.process_id.is_some() { latest.process_id = None; if let Err(e2) = self.save_profile(&latest) { log::warn!( "Warning: Failed to clear Camoufox profile process info after error: {e2}" ); } // Emit profile update event to frontend if let Err(e3) = app_handle.emit("profile-updated", &latest) { log::warn!("Warning: Failed to emit profile update event: {e3}"); } } } Ok(false) } } } // Check Wayfern status using WayfernManager async fn check_wayfern_status( &self, app_handle: &tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { let manager = self.wayfern_manager; let profiles_dir = self.get_profiles_dir(); let profile_data_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if there's a running Wayfern instance for this profile match manager.find_wayfern_by_profile(&profile_path_str).await { Some(wayfern_process) => { // Found a running instance, update profile with process info if changed let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { // Load latest to avoid overwriting other fields let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => profile.clone(), }; if latest.process_id != wayfern_process.processId { latest.process_id = wayfern_process.processId; if let Err(e) = self.save_profile(&latest) { log::warn!("Warning: Failed to update Wayfern profile with process info: {e}"); } // Emit profile update event to frontend if let Err(e) = app_handle.emit("profile-updated", &latest) { log::warn!("Warning: Failed to emit profile update event: {e}"); } log::info!( "Wayfern process has started for profile '{}' with PID: {:?}", profile.name, wayfern_process.processId ); } } Ok(true) } None => { // No running instance found, clear process ID if set let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); let metadata_exists = metadata_file.exists(); if metadata_exists { let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) .ok() .and_then(|s| serde_json::from_str(&s).ok()) { Some(p) => p, None => profile.clone(), }; if latest.process_id.is_some() { latest.process_id = None; if let Err(e) = self.save_profile(&latest) { log::warn!("Warning: Failed to clear Wayfern profile process info: {e}"); } if let Err(e) = app_handle.emit("profile-updated", &latest) { log::warn!("Warning: Failed to emit profile update event: {e}"); } } } Ok(false) } } } fn get_common_firefox_preferences(&self) -> Vec { vec![ // Disable default browser check "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), "user_pref(\"browser.shell.skipDefaultBrowserCheckOnFirstRun\", true);".to_string(), "user_pref(\"browser.preferences.moreFromMozilla\", false);".to_string(), "user_pref(\"services.sync.prefs.sync.browser.startup.upgradeDialog.enabled\", false);" .to_string(), // Disable welcome / first-run screens "user_pref(\"browser.aboutwelcome.enabled\", false);".to_string(), "user_pref(\"browser.startup.homepage_override.mstone\", \"ignore\");".to_string(), "user_pref(\"startup.homepage_welcome_url\", \"\");".to_string(), "user_pref(\"startup.homepage_welcome_url.additional\", \"\");".to_string(), "user_pref(\"startup.homepage_override_url\", \"\");".to_string(), // Keep extension updates enabled "user_pref(\"extensions.update.enabled\", true);".to_string(), "user_pref(\"extensions.update.autoUpdateDefault\", true);".to_string(), // Completely disable browser update checking "user_pref(\"app.update.enabled\", false);".to_string(), "user_pref(\"app.update.auto\", false);".to_string(), "user_pref(\"app.update.mode\", 0);".to_string(), "user_pref(\"app.update.service.enabled\", false);".to_string(), "user_pref(\"app.update.staging.enabled\", false);".to_string(), "user_pref(\"app.update.silent\", true);".to_string(), "user_pref(\"app.update.disabledForTesting\", true);".to_string(), // Prevent update URL access entirely "user_pref(\"app.update.url\", \"\");".to_string(), "user_pref(\"app.update.url.manual\", \"\");".to_string(), "user_pref(\"app.update.url.details\", \"\");".to_string(), // Disable update timing/scheduling "user_pref(\"app.update.timerFirstInterval\", 999999999);".to_string(), "user_pref(\"app.update.interval\", 999999999);".to_string(), "user_pref(\"app.update.background.interval\", 999999999);".to_string(), "user_pref(\"app.update.idletime\", 999999999);".to_string(), "user_pref(\"app.update.promptWaitTime\", 999999999);".to_string(), // Disable update attempts "user_pref(\"app.update.download.maxAttempts\", 0);".to_string(), "user_pref(\"app.update.elevate.maxAttempts\", 0);".to_string(), "user_pref(\"app.update.checkInstallTime\", false);".to_string(), // Suppress update UI/prompts/notifications "user_pref(\"app.update.doorhanger\", false);".to_string(), "user_pref(\"app.update.badge\", false);".to_string(), "user_pref(\"app.update.notifyDuringDownload\", false);".to_string(), "user_pref(\"app.update.background.scheduling.enabled\", false);".to_string(), "user_pref(\"app.update.background.enabled\", false);".to_string(), // Disable BITS (Windows Background Intelligent Transfer Service) updates "user_pref(\"app.update.BITS.enabled\", false);".to_string(), // Disable language pack updates "user_pref(\"app.update.langpack.enabled\", false);".to_string(), // Suppress upgrade dialogs on startup "user_pref(\"browser.startup.upgradeDialog.enabled\", false);".to_string(), // Disable update ping telemetry "user_pref(\"toolkit.telemetry.updatePing.enabled\", false);".to_string(), // Zen browser specific - disable welcome screen and updates "user_pref(\"zen.welcome-screen.seen\", true);".to_string(), "user_pref(\"zen.updates.enabled\", false);".to_string(), "user_pref(\"zen.updates.check-for-updates\", false);".to_string(), // Additional first-run suppressions "user_pref(\"app.normandy.first_run\", false);".to_string(), "user_pref(\"trailhead.firstrun.didSeeAboutWelcome\", true);".to_string(), "user_pref(\"datareporting.policy.dataSubmissionPolicyBypassNotification\", true);" .to_string(), "user_pref(\"toolkit.telemetry.reportingpolicy.firstRun\", false);".to_string(), // Disable quit confirmation dialogs "user_pref(\"browser.warnOnQuit\", false);".to_string(), "user_pref(\"browser.showQuitWarning\", false);".to_string(), "user_pref(\"browser.tabs.warnOnClose\", false);".to_string(), "user_pref(\"browser.tabs.warnOnCloseOtherTabs\", false);".to_string(), "user_pref(\"browser.sessionstore.warnOnQuit\", false);".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 prefs_js_path = profile_data_path.join("prefs.js"); // Remove prefs.js if it exists to ensure Firefox reads user.js instead // Firefox may cache proxy settings in prefs.js, so we need to clear it if prefs_js_path.exists() { log::info!("Removing prefs.js to ensure Firefox reads updated user.js settings"); let _ = fs::remove_file(&prefs_js_path); } let mut preferences = Vec::new(); // Add common Firefox preferences (like disabling default browser check) preferences.extend(self.get_common_firefox_preferences()); // Determine which proxy settings to use let effective_proxy = internal_proxy.unwrap_or(proxy); let proxy_host = &effective_proxy.host; let proxy_port = effective_proxy.port; // Check if this is a SOCKS proxy (only possible when using upstream directly) let is_socks = internal_proxy.is_none() && (proxy.proxy_type == "socks4" || proxy.proxy_type == "socks5"); log::info!( "Applying manual proxy settings to Firefox profile: {}:{} (is_internal: {}, is_socks: {})", proxy_host, proxy_port, internal_proxy.is_some(), is_socks ); // Use MANUAL proxy configuration (type 1) instead of PAC file (type 2) // PAC files with file:// URLs are blocked by privacy-focused browsers like Zen // Manual proxy configuration works reliably across all Firefox variants preferences.push("user_pref(\"network.proxy.type\", 1);".to_string()); if is_socks { // SOCKS proxy configuration preferences.extend([ format!("user_pref(\"network.proxy.socks\", \"{}\");", proxy_host), format!("user_pref(\"network.proxy.socks_port\", {});", proxy_port), format!( "user_pref(\"network.proxy.socks_version\", {});", if proxy.proxy_type == "socks5" { 5 } else { 4 } ), "user_pref(\"network.proxy.http\", \"\");".to_string(), "user_pref(\"network.proxy.http_port\", 0);".to_string(), "user_pref(\"network.proxy.ssl\", \"\");".to_string(), "user_pref(\"network.proxy.ssl_port\", 0);".to_string(), ]); } else { // HTTP/HTTPS proxy configuration (including our internal local proxy) preferences.extend([ format!("user_pref(\"network.proxy.http\", \"{}\");", proxy_host), format!("user_pref(\"network.proxy.http_port\", {});", proxy_port), format!("user_pref(\"network.proxy.ssl\", \"{}\");", proxy_host), format!("user_pref(\"network.proxy.ssl_port\", {});", proxy_port), format!("user_pref(\"network.proxy.ftp\", \"{}\");", proxy_host), format!("user_pref(\"network.proxy.ftp_port\", {});", proxy_port), "user_pref(\"network.proxy.socks\", \"\");".to_string(), "user_pref(\"network.proxy.socks_port\", 0);".to_string(), ]); } // Common proxy settings - keep it simple like proxy-chain expected preferences.extend([ "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), "user_pref(\"network.proxy.autoconfig_url\", \"\");".to_string(), // Disable QUIC/HTTP3 - it bypasses HTTP proxy "user_pref(\"network.http.http3.enable\", false);".to_string(), "user_pref(\"network.http.http3.enabled\", false);".to_string(), ]); // Write settings to user.js file let user_js_content = preferences.join("\n"); fs::write(user_js_path, &user_js_content)?; log::info!( "Updated user.js with manual proxy settings: {}:{}", proxy_host, proxy_port ); 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(()) } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn create_test_profile_manager() -> (&'static ProfileManager, TempDir) { let temp_dir = TempDir::new().unwrap(); // Mock the base directories by setting environment variables unsafe { std::env::set_var("HOME", temp_dir.path()); } let profile_manager = ProfileManager::instance(); (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"), "Profiles dir should contain DonutBrowser" ); assert!( profiles_dir.to_string_lossy().contains("profiles"), "Profiles dir should contain profiles" ); } #[test] fn test_get_common_firefox_preferences() { let (manager, _temp_dir) = create_test_profile_manager(); let prefs = manager.get_common_firefox_preferences(); assert!(!prefs.is_empty(), "Should return non-empty preferences"); // Check for some expected preferences let prefs_string = prefs.join("\n"); assert!( prefs_string.contains("browser.shell.checkDefaultBrowser"), "Should contain default browser check preference" ); assert!( prefs_string.contains("app.update.enabled"), "Should contain update preference" ); } #[test] fn test_get_binaries_dir() { let (manager, _temp_dir) = create_test_profile_manager(); let binaries_dir = manager.get_binaries_dir(); let path_str = binaries_dir.to_string_lossy(); assert!( path_str.contains("DonutBrowser"), "Binaries dir should contain DonutBrowser" ); assert!( path_str.contains("binaries"), "Binaries dir should contain binaries" ); } #[test] fn test_disable_proxy_settings_in_profile() { let (manager, temp_dir) = create_test_profile_manager(); // Create a test profile directory let profile_dir = temp_dir.path().join("test_profile"); fs::create_dir_all(&profile_dir).expect("Should create profile directory"); let result = manager.disable_proxy_settings_in_profile(&profile_dir); assert!(result.is_ok(), "Should successfully disable proxy settings"); // Check that user.js was created let user_js_path = profile_dir.join("user.js"); assert!(user_js_path.exists(), "user.js should be created"); let content = fs::read_to_string(&user_js_path).expect("Should read user.js"); assert!( content.contains("network.proxy.type"), "Should contain proxy type setting" ); assert!( content.contains("0"), "Should set proxy type to 0 (no proxy)" ); } #[test] fn test_apply_proxy_settings_to_profile() { let (manager, temp_dir) = create_test_profile_manager(); // Create a test profile directory structure let uuid_dir = temp_dir.path().join("test_uuid"); let profile_dir = uuid_dir.join("profile"); fs::create_dir_all(&profile_dir).expect("Should create profile directory"); let proxy_settings = ProxySettings { proxy_type: "http".to_string(), host: "proxy.example.com".to_string(), port: 8080, username: Some("user".to_string()), password: Some("pass".to_string()), }; let result = manager.apply_proxy_settings_to_profile(&profile_dir, &proxy_settings, None); assert!(result.is_ok(), "Should successfully apply proxy settings"); // Check that user.js was created let user_js_path = profile_dir.join("user.js"); assert!(user_js_path.exists(), "user.js should be created"); let content = fs::read_to_string(&user_js_path).expect("Should read user.js"); // Check for manual proxy configuration (type 1) instead of PAC (type 2) // Manual proxy is used because PAC file:// URLs are blocked by privacy browsers like Zen assert!( content.contains("network.proxy.type\", 1"), "Should set proxy type to 1 (manual)" ); assert!( content.contains("network.proxy.http\", \"proxy.example.com\""), "Should set HTTP proxy host" ); assert!( content.contains("network.proxy.http_port\", 8080"), "Should set HTTP proxy port" ); assert!( content.contains("network.proxy.ssl\", \"proxy.example.com\""), "Should set SSL proxy host" ); assert!( content.contains("network.proxy.ssl_port\", 8080"), "Should set SSL proxy port" ); } } #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn create_browser_profile_with_group( app_handle: tauri::AppHandle, name: String, browser: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, wayfern_config: Option, group_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .create_profile_with_group( &app_handle, &name, &browser, &version, &release_type, proxy_id, camoufox_config, wayfern_config, group_id, ) .await .map_err(|e| format!("Failed to create profile: {e}")) } #[tauri::command] pub fn list_browser_profiles() -> Result, String> { let profile_manager = ProfileManager::instance(); profile_manager .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}")) } #[tauri::command] pub async fn update_profile_proxy( app_handle: tauri::AppHandle, profile_id: String, proxy_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_proxy(app_handle, &profile_id, proxy_id) .await .map_err(|e| format!("Failed to update profile: {e}")) } #[tauri::command] pub fn update_profile_tags( app_handle: tauri::AppHandle, profile_id: String, tags: Vec, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_tags(&app_handle, &profile_id, tags) .map_err(|e| format!("Failed to update profile tags: {e}")) } #[tauri::command] pub fn update_profile_note( app_handle: tauri::AppHandle, profile_id: String, note: Option, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .update_profile_note(&app_handle, &profile_id, note) .map_err(|e| format!("Failed to update profile note: {e}")) } #[tauri::command] pub async fn check_browser_status( app_handle: tauri::AppHandle, profile: BrowserProfile, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .check_browser_status(app_handle, &profile) .await .map_err(|e| format!("Failed to check browser status: {e}")) } #[tauri::command] pub fn rename_profile( app_handle: tauri::AppHandle, profile_id: String, new_name: String, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager .rename_profile(&app_handle, &profile_id, &new_name) .map_err(|e| format!("Failed to rename profile: {e}")) } #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn create_browser_profile_new( app_handle: tauri::AppHandle, name: String, browser_str: String, version: String, release_type: String, proxy_id: Option, camoufox_config: Option, wayfern_config: Option, group_id: Option, ) -> Result { let browser_type = BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; create_browser_profile_with_group( app_handle, name, browser_type.as_str().to_string(), version, release_type, proxy_id, camoufox_config, wayfern_config, group_id, ) .await } #[tauri::command] pub async fn update_camoufox_config( app_handle: tauri::AppHandle, profile_id: String, config: CamoufoxConfig, ) -> Result<(), String> { let profile_manager = ProfileManager::instance(); profile_manager .update_camoufox_config(app_handle, &profile_id, config) .await .map_err(|e| format!("Failed to update Camoufox config: {e}")) } #[tauri::command] pub async fn update_wayfern_config( app_handle: tauri::AppHandle, profile_id: String, config: WayfernConfig, ) -> Result<(), String> { let profile_manager = ProfileManager::instance(); profile_manager .update_wayfern_config(app_handle, &profile_id, config) .await .map_err(|e| format!("Failed to update Wayfern config: {e}")) } // Global singleton instance #[tauri::command] pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> { ProfileManager::instance() .delete_profile(&app_handle, &profile_id) .map_err(|e| format!("Failed to delete profile: {e}")) } lazy_static::lazy_static! { static ref PROFILE_MANAGER: ProfileManager = ProfileManager::new(); }