use crate::database::core::with_connection; use crate::database::error::DatabaseError; use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview}; use crate::extension::core::types::{copy_directory, Extension, ExtensionSource}; use crate::extension::core::ExtensionPermissions; use crate::extension::crypto::ExtensionCrypto; use crate::extension::database::executor::SqlExecutor; use crate::extension::error::ExtensionError; use crate::extension::permissions::manager::PermissionManager; use crate::extension::permissions::types::ExtensionPermission; use crate::table_names::{TABLE_EXTENSIONS, TABLE_EXTENSION_PERMISSIONS}; use crate::AppState; use serde_json::Value as JsonValue; use std::collections::{HashMap, HashSet}; use std::fs; use std::io::Cursor; use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, SystemTime}; use tauri::{AppHandle, Manager, State}; use zip::ZipArchive; #[derive(Debug, Clone)] pub struct CachedPermission { pub permissions: Vec, pub cached_at: SystemTime, pub ttl: Duration, } #[derive(Debug, Clone)] pub struct MissingExtension { pub id: String, pub public_key: String, pub name: String, pub version: String, } struct ExtensionDataFromDb { id: String, manifest: ExtensionManifest, enabled: bool, } #[derive(Default)] pub struct ExtensionManager { pub production_extensions: Mutex>, pub dev_extensions: Mutex>, pub permission_cache: Mutex>, pub missing_extensions: Mutex>, } struct ExtractedExtension { temp_dir: PathBuf, manifest: ExtensionManifest, content_hash: String, } impl Drop for ExtractedExtension { fn drop(&mut self) { std::fs::remove_dir_all(&self.temp_dir).ok(); } } impl ExtensionManager { pub fn new() -> Self { Self::default() } /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest fn extract_and_validate_extension( bytes: Vec, temp_prefix: &str, ) -> Result { let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4())); fs::create_dir_all(&temp) .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?; let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { ExtensionError::InstallationFailed { reason: format!("Invalid ZIP: {}", e), } })?; archive .extract(&temp) .map_err(|e| ExtensionError::InstallationFailed { reason: format!("Cannot extract ZIP: {}", e), })?; // Read haextension_dir from config if it exists, otherwise use default let config_path = temp.join("haextension.config.json"); let haextension_dir = if config_path.exists() { let config_content = std::fs::read_to_string(&config_path) .map_err(|e| ExtensionError::ManifestError { reason: format!("Cannot read haextension.config.json: {}", e), })?; let config: serde_json::Value = serde_json::from_str(&config_content) .map_err(|e| ExtensionError::ManifestError { reason: format!("Invalid haextension.config.json: {}", e), })?; let dir = config .get("dev") .and_then(|dev| dev.get("haextension_dir")) .and_then(|dir| dir.as_str()) .unwrap_or("haextension") .to_string(); // Security: Validate that haextension_dir doesn't contain ".." for path traversal if dir.contains("..") { return Err(ExtensionError::ManifestError { reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(), }); } dir } else { "haextension".to_string() }; // Build the manifest path let manifest_path = temp.join(&haextension_dir).join("manifest.json"); // Ensure the resolved path is still within temp directory (safety check against path traversal) let canonical_temp = temp.canonicalize() .map_err(|e| ExtensionError::Filesystem { source: e })?; // Only check if manifest_path parent exists to avoid errors if let Some(parent) = manifest_path.parent() { if let Ok(canonical_manifest_dir) = parent.canonicalize() { if !canonical_manifest_dir.starts_with(&canonical_temp) { return Err(ExtensionError::ManifestError { reason: "Security violation: manifest path outside extension directory".to_string(), }); } } } // Check if manifest exists if !manifest_path.exists() { return Err(ExtensionError::ManifestError { reason: format!("manifest.json not found at {}/manifest.json", haextension_dir), }); } let actual_dir = temp.clone(); let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError { reason: format!("Cannot read manifest: {}", e), })?; let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| { ExtensionError::SignatureVerificationFailed { reason: e.to_string(), } })?; Ok(ExtractedExtension { temp_dir: actual_dir, manifest, content_hash, }) } pub fn get_base_extension_dir( &self, app_handle: &AppHandle, ) -> Result { let path = app_handle .path() .app_local_data_dir() .map_err(|e| ExtensionError::Filesystem { source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()), })? .join("extensions"); // Sicherstellen, dass das Basisverzeichnis existiert if !path.exists() { fs::create_dir_all(&path) .map_err(|e| ExtensionError::filesystem_with_path(path.display().to_string(), e))?; } Ok(path) } pub fn get_extension_dir( &self, app_handle: &AppHandle, public_key: &str, extension_name: &str, extension_version: &str, ) -> Result { let specific_extension_dir = self .get_base_extension_dir(app_handle)? .join(public_key) .join(extension_name) .join(extension_version); Ok(specific_extension_dir) } pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> { if extension.id.is_empty() { return Err(ExtensionError::ValidationError { reason: "Extension ID cannot be empty".to_string(), }); } match &extension.source { ExtensionSource::Production { .. } => { let mut extensions = self.production_extensions.lock().unwrap(); extensions.insert(extension.id.clone(), extension); Ok(()) } _ => Err(ExtensionError::ValidationError { reason: "Expected Production source".to_string(), }), } } pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> { if extension.id.is_empty() { return Err(ExtensionError::ValidationError { reason: "Extension ID cannot be empty".to_string(), }); } match &extension.source { ExtensionSource::Development { .. } => { let mut extensions = self.dev_extensions.lock().unwrap(); extensions.insert(extension.id.clone(), extension); Ok(()) } _ => Err(ExtensionError::ValidationError { reason: "Expected Development source".to_string(), }), } } pub fn get_extension(&self, extension_id: &str) -> Option { let dev_extensions = self.dev_extensions.lock().unwrap(); if let Some(extension) = dev_extensions.get(extension_id) { return Some(extension.clone()); } let prod_extensions = self.production_extensions.lock().unwrap(); prod_extensions.get(extension_id).cloned() } /// Find extension ID by public_key and name (checks dev extensions first, then production) fn find_extension_id_by_public_key_and_name( &self, public_key: &str, name: &str, ) -> Result, ExtensionError> { // 1. Check dev extensions first (higher priority) let dev_extensions = self.dev_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })?; for (id, ext) in dev_extensions.iter() { if ext.manifest.public_key == public_key && ext.manifest.name == name { return Ok(Some((id.clone(), ext.clone()))); } } // 2. Check production extensions let prod_extensions = self.production_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })?; for (id, ext) in prod_extensions.iter() { if ext.manifest.public_key == public_key && ext.manifest.name == name { return Ok(Some((id.clone(), ext.clone()))); } } Ok(None) } /// Get extension by public_key and name (used by frontend) pub fn get_extension_by_public_key_and_name( &self, public_key: &str, name: &str, ) -> Result, ExtensionError> { Ok(self .find_extension_id_by_public_key_and_name(public_key, name)? .map(|(_, ext)| ext)) } pub fn remove_extension(&self, public_key: &str, name: &str) -> Result<(), ExtensionError> { let (id, _) = self .find_extension_id_by_public_key_and_name(public_key, name)? .ok_or_else(|| ExtensionError::NotFound { public_key: public_key.to_string(), name: name.to_string(), })?; // Remove from dev extensions first { let mut dev_extensions = self.dev_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })?; if dev_extensions.remove(&id).is_some() { return Ok(()); } } // Remove from production extensions { let mut prod_extensions = self.production_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })?; prod_extensions.remove(&id); } Ok(()) } pub async fn remove_extension_internal( &self, app_handle: &AppHandle, public_key: &str, extension_name: &str, extension_version: &str, state: &State<'_, AppState>, ) -> Result<(), ExtensionError> { // Get the extension from memory to get its ID let extension = self .get_extension_by_public_key_and_name(public_key, extension_name)? .ok_or_else(|| ExtensionError::NotFound { public_key: public_key.to_string(), name: extension_name.to_string(), })?; eprintln!("DEBUG: Removing extension with ID: {}", extension.id); eprintln!( "DEBUG: Extension name: {}, version: {}", extension_name, extension_version ); // Lösche Permissions und Extension-Eintrag in einer Transaktion with_connection(&state.db, |conn| { let tx = conn.transaction().map_err(DatabaseError::from)?; let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned { reason: "Failed to lock HLC service".to_string(), })?; // Lösche alle Permissions mit extension_id eprintln!( "DEBUG: Deleting permissions for extension_id: {}", extension.id ); PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, &extension.id)?; // Lösche Extension-Eintrag mit extension_id let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS); eprintln!("DEBUG: Executing SQL: {} with id = {}", sql, extension.id); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &sql, rusqlite::params![&extension.id], )?; eprintln!("DEBUG: Committing transaction"); tx.commit().map_err(DatabaseError::from) })?; eprintln!("DEBUG: Transaction committed successfully"); // Entferne aus dem In-Memory-Manager self.remove_extension(public_key, extension_name)?; // Lösche nur den spezifischen Versions-Ordner: public_key/name/version let extension_dir = self.get_extension_dir(app_handle, public_key, extension_name, extension_version)?; if extension_dir.exists() { std::fs::remove_dir_all(&extension_dir).map_err(|e| { ExtensionError::filesystem_with_path(extension_dir.display().to_string(), e) })?; // Versuche, leere Parent-Ordner zu löschen // 1. Extension-Name-Ordner (key_hash/name) if let Some(name_dir) = extension_dir.parent() { if name_dir.exists() { if let Ok(entries) = std::fs::read_dir(name_dir) { if entries.count() == 0 { let _ = std::fs::remove_dir(name_dir); // 2. Key-Hash-Ordner (key_hash) - nur wenn auch leer if let Some(key_hash_dir) = name_dir.parent() { if key_hash_dir.exists() { if let Ok(entries) = std::fs::read_dir(key_hash_dir) { if entries.count() == 0 { let _ = std::fs::remove_dir(key_hash_dir); } } } } } } } } } Ok(()) } pub async fn preview_extension_internal( &self, file_bytes: Vec, ) -> Result { let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?; let is_valid_signature = ExtensionCrypto::verify_signature( &extracted.manifest.public_key, &extracted.content_hash, &extracted.manifest.signature, ) .is_ok(); let editable_permissions = extracted.manifest.to_editable_permissions(); Ok(ExtensionPreview { manifest: extracted.manifest.clone(), is_valid_signature, editable_permissions, }) } pub async fn install_extension_with_permissions_internal( &self, app_handle: AppHandle, file_bytes: Vec, custom_permissions: EditablePermissions, state: &State<'_, AppState>, ) -> Result { let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?; // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) ExtensionCrypto::verify_signature( &extracted.manifest.public_key, &extracted.content_hash, &extracted.manifest.signature, ) .map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?; let extensions_dir = self.get_extension_dir( &app_handle, &extracted.manifest.public_key, &extracted.manifest.name, &extracted.manifest.version, )?; // If extension version already exists, remove it completely before installing if extensions_dir.exists() { eprintln!( "Extension version already exists at {}, removing old version", extensions_dir.display() ); std::fs::remove_dir_all(&extensions_dir).map_err(|e| { ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e) })?; } std::fs::create_dir_all(&extensions_dir).map_err(|e| { ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e) })?; // Copy contents of extracted.temp_dir to extensions_dir // Note: extracted.temp_dir already points to the correct directory with manifest.json for entry in fs::read_dir(&extracted.temp_dir).map_err(|e| { ExtensionError::filesystem_with_path(extracted.temp_dir.display().to_string(), e) })? { let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?; let path = entry.path(); let file_name = entry.file_name(); let dest_path = extensions_dir.join(&file_name); if path.is_dir() { copy_directory( path.to_string_lossy().to_string(), dest_path.to_string_lossy().to_string(), )?; } else { fs::copy(&path, &dest_path).map_err(|e| { ExtensionError::filesystem_with_path(path.display().to_string(), e) })?; } } // Generate UUID for extension (Drizzle's $defaultFn only works from JS, not raw SQL) let extension_id = uuid::Uuid::new_v4().to_string(); let permissions = custom_permissions.to_internal_permissions(&extension_id); // Extension-Eintrag und Permissions in einer Transaktion speichern let actual_extension_id = with_connection(&state.db, |conn| { let tx = conn.transaction().map_err(DatabaseError::from)?; let hlc_service_guard = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned { reason: "Failed to lock HLC service".to_string(), })?; // Klonen, um den MutexGuard freizugeben, bevor potenziell lange DB-Operationen stattfinden let hlc_service = hlc_service_guard.clone(); drop(hlc_service_guard); // 1. Extension-Eintrag erstellen mit generierter UUID let insert_ext_sql = format!( "INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSIONS ); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &insert_ext_sql, rusqlite::params![ extension_id, extracted.manifest.name, extracted.manifest.version, extracted.manifest.author, extracted.manifest.entry, extracted.manifest.icon, extracted.manifest.public_key, extracted.manifest.signature, extracted.manifest.homepage, extracted.manifest.description, true, // enabled ], )?; // 2. Permissions speichern let insert_perm_sql = format!( "INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSION_PERMISSIONS ); for perm in &permissions { use crate::database::generated::HaexExtensionPermissions; let db_perm: HaexExtensionPermissions = perm.into(); SqlExecutor::execute_internal_typed( &tx, &hlc_service, &insert_perm_sql, rusqlite::params![ db_perm.id, db_perm.extension_id, db_perm.resource_type, db_perm.action, db_perm.target, db_perm.constraints, db_perm.status, ], )?; } tx.commit().map_err(DatabaseError::from)?; Ok(extension_id.clone()) })?; let extension = Extension { id: extension_id.clone(), source: ExtensionSource::Production { path: extensions_dir.clone(), version: extracted.manifest.version.clone(), }, manifest: extracted.manifest.clone(), enabled: true, last_accessed: SystemTime::now(), }; self.add_production_extension(extension)?; Ok(actual_extension_id) // Gebe die actual_extension_id an den Caller zurück } /// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen. pub async fn load_installed_extensions( &self, app_handle: &AppHandle, state: &State<'_, AppState>, ) -> Result, ExtensionError> { // Clear existing data self.production_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })? .clear(); self.permission_cache .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })? .clear(); self.missing_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })? .clear(); // Lade alle Daten aus der Datenbank let extensions = with_connection(&state.db, |conn| { let sql = format!( "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}", TABLE_EXTENSIONS ); eprintln!("DEBUG: SQL Query before transformation: {}", sql); let results = SqlExecutor::query_select(conn, &sql, &[])?; eprintln!("DEBUG: Query returned {} results", results.len()); let mut data = Vec::new(); for row in results { // Wir erwarten die Werte in der Reihenfolge der SELECT-Anweisung let id = row[0] .as_str() .ok_or_else(|| DatabaseError::SerializationError { reason: "Missing id field".to_string(), })? .to_string(); let manifest = ExtensionManifest { name: row[1] .as_str() .ok_or_else(|| DatabaseError::SerializationError { reason: "Missing name field".to_string(), })? .to_string(), version: row[2] .as_str() .ok_or_else(|| DatabaseError::SerializationError { reason: "Missing version field".to_string(), })? .to_string(), author: row[3].as_str().map(String::from), entry: row[4].as_str().unwrap_or("index.html").to_string(), icon: row[5].as_str().map(String::from), public_key: row[6].as_str().unwrap_or("").to_string(), signature: row[7].as_str().unwrap_or("").to_string(), permissions: ExtensionPermissions::default(), homepage: row[8].as_str().map(String::from), description: row[9].as_str().map(String::from), }; let enabled = row[10] .as_bool() .or_else(|| row[10].as_i64().map(|v| v != 0)) .unwrap_or(false); data.push(ExtensionDataFromDb { id, manifest, enabled, }); } Ok(data) })?; // Schritt 2: Die gesammelten Daten verarbeiten (Dateisystem, State-Mutationen). let mut loaded_extension_ids = Vec::new(); eprintln!("DEBUG: Found {} extensions in database", extensions.len()); for extension_data in extensions { let extension_id = extension_data.id; eprintln!("DEBUG: Processing extension: {}", extension_id); // Use public_key/name/version path structure let extension_path = self.get_extension_dir( app_handle, &extension_data.manifest.public_key, &extension_data.manifest.name, &extension_data.manifest.version, )?; if !extension_path.exists() || !extension_path.join("manifest.json").exists() { eprintln!( "DEBUG: Extension files missing for: {} at {:?}", extension_id, extension_path ); self.missing_extensions .lock() .map_err(|e| ExtensionError::MutexPoisoned { reason: e.to_string(), })? .push(MissingExtension { id: extension_id.clone(), public_key: extension_data.manifest.public_key.clone(), name: extension_data.manifest.name.clone(), version: extension_data.manifest.version.clone(), }); continue; } eprintln!("DEBUG: Extension loaded successfully: {}", extension_id); let extension = Extension { id: extension_id.clone(), source: ExtensionSource::Production { path: extension_path, version: extension_data.manifest.version.clone(), }, manifest: extension_data.manifest, enabled: extension_data.enabled, last_accessed: SystemTime::now(), }; loaded_extension_ids.push(extension_id.clone()); self.add_production_extension(extension)?; } Ok(loaded_extension_ids) } }