/****************************************************************************** * @file application.c * @brief Implémentation du cycle de vie de l'application GTK. ******************************************************************************/ #include "core/application.h" #include "core/investigation_node.h" #include "core/investigation_project.h" #include "core/investigation_session.h" #include "core/investigation_tree_model.h" #include "core/investigation_graph_load_task.h" #include "models/investigation_graph_layout.h" #include "models/investigation_graph_model.h" #include "models/investigation_record.h" #include "core/investigation_tree_builder.h" #include "views/create_investigation_dialog.h" #include "views/folder_dialog.h" #include "views/main_window.h" #include "views/application_message_dialog.h" #include "core/task_manager.h" #include "core/background_task.h" #include "core/tool_initializer.h" #include "views/file_dialog.h" #include "core/evidence_import_task.h" #include "models/evidence_record.h" #include "dao/evidence_type_dao.h" #include "dao/graph_node_position_dao.h" #include "views/evidence_import_dialog.h" #include "views/create_relation_dialog.h" #include "dao/evidence_dao.h" #include "dao/entity_dao.h" #include "models/evidence_type.h" #include "widgets/evidence_list_model.h" #include "widgets/evidence_category_model.h" #include "core/evidence_integrity_task.h" #include "core/evidence_integrity_verifier.h" #include "database/database.h" #include #include /** * @brief Identifiant unique utilisé par GLib pour l'application. */ #define APPLICATION_ID "com.labfytools.investigation" /** * @brief Erreurs produites lors du chargement d'une enquête. */ typedef enum { APPLICATION_OPEN_ERROR_INVALID_ARGUMENT, APPLICATION_OPEN_ERROR_INVALID_PROJECT, APPLICATION_OPEN_ERROR_TREE_BUILD, APPLICATION_OPEN_ERROR_INSTALL, APPLICATION_OPEN_ERROR_EVIDENCE } ApplicationOpenError; /** * @brief Domaine d'erreur du chargement d'une enquête. */ #define APPLICATION_OPEN_ERROR \ application_open_error_quark() /** * @brief Dossier racine contenant les preuves originales. */ #define APPLICATION_EVIDENCE_ROOT_DIRECTORY \ "01_Preuves_Originales" /** * @brief Retourne le domaine d'erreur du chargement d'une enquête. * * @return Quark GLib du domaine d'erreur. */ static GQuark application_open_error_quark(void) { return g_quark_from_static_string( "labfy-investigation-application-open-error" ); } /** * @brief État interne de l'application. * * Cette structure reste privée au module afin d'empêcher les autres * composants de manipuler directement les objets GTK. */ struct Application { GtkApplication *gtk_application; MainWindow *main_window; InvestigationSession *session; InvestigationTreeModel *tree_model; TaskManager *task_manager; ToolInitializer *tool_initializer; EvidenceListModel *evidence_list_model; EvidenceCategoryModel *evidence_category_model; InvestigationGraphLoadTask *graph_load_task; InvestigationGraphModel *graph_model; InvestigationGraphLayout *graph_layout; guint64 graph_load_generation; char *selected_evidence_identifier; }; /** * @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import. */ typedef struct { Application *application; char *investigation_root_path; } ApplicationEvidenceImportContext; /** * @brief Contexte conservé jusqu'à la fin d'un chargement de graphe. */ typedef struct { Application *application; guint64 generation; } ApplicationGraphLoadContext; /** * @brief Contexte conservé pendant le dialogue de métadonnées. * * Le chemin fourni par FileDialog n'est valide que pendant son callback. * Il doit donc être copié avant l'ouverture du second dialogue. */ typedef struct { Application *application; char *file_path; } ApplicationEvidenceDialogContext; /** * @brief Contexte conservé jusqu'à la fin d'une vérification. */ typedef struct { Application *application; char *investigation_root_path; char *database_path; char *evidence_identifier; char *original_name; } ApplicationEvidenceIntegrityContext; /** * @brief Libère le contexte du dialogue de métadonnées. */ static void application_evidence_dialog_context_free( gpointer user_data ) { ApplicationEvidenceDialogContext *context = user_data; if (context == NULL) { return; } g_free( context->file_path ); g_free( context ); } /** * @brief Libère le contexte d’un import asynchrone. */ static void application_evidence_import_context_free( gpointer user_data ) { ApplicationEvidenceImportContext *context = user_data; if (context == NULL) { return; } g_free( context->investigation_root_path ); g_free( context ); } /** * @brief Libère le contexte d'une vérification d'intégrité. */ static void application_evidence_integrity_context_free( gpointer user_data ) { ApplicationEvidenceIntegrityContext *context = user_data; if (context == NULL) { return; } g_free( context->original_name ); g_free( context->evidence_identifier ); g_free( context->database_path ); g_free( context->investigation_root_path ); g_free( context ); } /** * @brief Libère le contexte d'un chargement de graphe. */ static void application_graph_load_context_free( gpointer user_data ) { g_free( user_data ); } /** * @brief Annule et détache le chargement de graphe courant. * * Chaque appel invalide les résultats appartenant à la génération * précédente, même lorsqu'aucune tâche n'est encore active. */ static void application_cancel_graph_loading( Application *application ) { InvestigationGraphLoadTask *load_task = NULL; if (application == NULL) { return; } application->graph_load_generation++; load_task = application->graph_load_task; application->graph_load_task = NULL; if (load_task == NULL) { return; } investigation_graph_load_task_cancel( load_task ); investigation_graph_load_task_free( load_task ); } /** * @brief Détache puis libère le graphe possédé par Application. */ static void application_clear_graph( Application *application ) { InvestigationGraphModel *graph_model = NULL; InvestigationGraphLayout *graph_layout = NULL; if (application == NULL) { return; } if (application->main_window != NULL) { main_window_clear_graph( application->main_window ); } graph_model = application->graph_model; graph_layout = application->graph_layout; application->graph_model = NULL; application->graph_layout = NULL; investigation_graph_model_free( graph_model ); investigation_graph_layout_free( graph_layout ); } /** * @brief Traite le résultat d'un chargement asynchrone de graphe. * * Le callback est exécuté sur le contexte principal GLib. */ static void application_on_graph_loaded( InvestigationGraphLoadTask *load_task, InvestigationGraphModel *graph_model, const GError *error, gpointer user_data ) { ApplicationGraphLoadContext *context = user_data; Application *application = NULL; InvestigationGraphLayout *graph_layout = NULL; char *status_message = NULL; gboolean is_current_result = FALSE; if (context != NULL) { application = context->application; } graph_layout = investigation_graph_load_task_take_layout( load_task ); if (application != NULL) { is_current_result = load_task == application->graph_load_task && context->generation == application->graph_load_generation; } /* * Un résultat appartenant à une ancienne enquête ne doit jamais * modifier l'interface actuelle. */ if (!is_current_result) { investigation_graph_model_free( graph_model ); investigation_graph_layout_free( graph_layout ); investigation_graph_load_task_free( load_task ); return; } application->graph_load_task = NULL; if (error != NULL) { investigation_graph_model_free( graph_model ); investigation_graph_layout_free( graph_layout ); main_window_set_graph_error( application->main_window, error->message ); main_window_set_status( application->main_window, "Le graphe de l'enquête n'a pas pu être chargé." ); investigation_graph_load_task_free( load_task ); return; } if (graph_model == NULL || graph_layout == NULL) { investigation_graph_model_free( graph_model ); investigation_graph_layout_free( graph_layout ); main_window_set_graph_error( application->main_window, "Le chargement s'est terminé sans fournir " "un graphe et une disposition valides." ); main_window_set_status( application->main_window, "Le graphe de l'enquête est indisponible." ); investigation_graph_load_task_free( load_task ); return; } /* * MainWindow et Workspace doivent être détachés de l'ancien modèle * avant sa libération. */ application_clear_graph( application ); application->graph_model = graph_model; application->graph_layout = graph_layout; main_window_set_graph( application->main_window, application->graph_model, application->graph_layout ); status_message = g_strdup_printf( "Graphe chargé : %" G_GUINT64_FORMAT " entité(s), %" G_GUINT64_FORMAT " relation(s), %u position(s) restaurée(s).", investigation_graph_model_get_entity_count( application->graph_model ), investigation_graph_model_get_relation_count( application->graph_model ), investigation_graph_layout_get_count( application->graph_layout ) ); main_window_set_status( application->main_window, status_message != NULL ? status_message : "Graphe chargé." ); g_free( status_message ); investigation_graph_load_task_free( load_task ); } /** * @brief Démarre le chargement asynchrone du graphe d'une enquête. */ static void application_start_graph_loading( Application *application, const char *database_path ) { InvestigationGraphLoadTask *load_task = NULL; ApplicationGraphLoadContext *context = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL) { return; } /* * Cette opération invalide l'ancienne génération et libère * l'ancien chargement avant d'en créer un nouveau. */ application_cancel_graph_loading( application ); application_clear_graph( application ); if (database_path == NULL || database_path[0] == '\0') { main_window_set_graph_error( application->main_window, "Le chemin de la base SQLite est invalide." ); return; } main_window_set_graph_loading( application->main_window ); load_task = investigation_graph_load_task_new( database_path, &error ); if (load_task == NULL) { main_window_set_graph_error( application->main_window, error != NULL ? error->message : "Impossible de préparer le chargement du graphe." ); g_clear_error( &error ); return; } context = g_try_new0( ApplicationGraphLoadContext, 1 ); if (context == NULL) { investigation_graph_load_task_free( load_task ); main_window_set_graph_error( application->main_window, "Impossible d'allouer le contexte de chargement." ); return; } context->application = application; context->generation = application->graph_load_generation; application->graph_load_task = load_task; if (!investigation_graph_load_task_start( load_task, application_on_graph_loaded, context, application_graph_load_context_free, &error )) { /* * En cas d'échec immédiat, la tâche n'a pas pris possession * du contexte fourni. */ application->graph_load_task = NULL; application_graph_load_context_free( context ); investigation_graph_load_task_free( load_task ); main_window_set_graph_error( application->main_window, error != NULL ? error->message : "Impossible de démarrer le chargement du graphe." ); g_clear_error( &error ); } } /** * @brief Vérifie que l’enquête active correspond à un chemin racine. */ static gboolean application_session_matches_root( const Application *application, const char *expected_root_path ) { const InvestigationProject *project = NULL; const char *current_root_path = NULL; if (application == NULL || application->session == NULL || expected_root_path == NULL) { return FALSE; } project = investigation_session_get_project( application->session ); if (project == NULL) { return FALSE; } current_root_path = investigation_project_get_root_path( project ); return g_strcmp0( current_root_path, expected_root_path ) == 0; } /** * @brief Mémorise l'identifiant de la preuve actuellement affichée. */ static void application_set_selected_evidence_identifier( Application *application, const char *evidence_identifier ) { char *identifier_copy = NULL; if (application == NULL) { return; } if (evidence_identifier != NULL && g_uuid_string_is_valid( evidence_identifier )) { identifier_copy = g_strdup( evidence_identifier ); } g_free( application->selected_evidence_identifier ); application->selected_evidence_identifier = identifier_copy; } static void application_present_error( Application *application, const char *title, const char *message ); /** * @brief Traite la fin de l’initialisation des outils externes. * * Ce callback est exécuté sur le contexte principal GLib. * * @param tool_initializer Initialiseur ayant terminé. * @param final_state État final de la tâche. * @param summary Résumé emprunté en cas de réussite. * @param error Erreur empruntée en cas d’échec. * @param user_data Pointeur emprunté vers Application. */ static void application_on_tool_initialization_completed( ToolInitializer *tool_initializer, BackgroundTaskState final_state, const ToolInitializationSummary *summary, const GError *error, gpointer user_data ) { Application *application = user_data; if (application == NULL || tool_initializer == NULL) { return; } if (tool_initializer != application->tool_initializer) { g_warning( "Notification reçue depuis un initialiseur inconnu." ); return; } switch (final_state) { case BACKGROUND_TASK_STATE_COMPLETED: if (summary == NULL) { g_warning( "L’initialisation des outils est terminée " "sans résumé valide." ); return; } g_message( "Outils externes initialisés : " "%" G_GSIZE_FORMAT " disponibles, " "%" G_GSIZE_FORMAT " absents, " "%" G_GSIZE_FORMAT " versions détectées, " "%" G_GSIZE_FORMAT " versions non détectées.", summary->available_count, summary->missing_count, summary->version_detected_count, summary->version_failed_count ); break; case BACKGROUND_TASK_STATE_CANCELLED: g_message( "L’initialisation des outils externes a été annulée." ); break; case BACKGROUND_TASK_STATE_FAILED: g_warning( "L’initialisation des outils externes a échoué : %s", error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Initialisation des outils impossible", error != NULL ? error->message : "Les outils externes n’ont pas pu être initialisés." ); break; case BACKGROUND_TASK_STATE_PENDING: case BACKGROUND_TASK_STATE_RUNNING: default: g_warning( "Notification de fin reçue avec un état invalide : %d.", (int) final_state ); break; } } /** * @brief Démarre la détection asynchrone des outils externes. * * Une erreur empêche seulement l’initialisation des outils. * Elle ne doit pas fermer l’application. * * @param application Application concernée. */ static void application_start_tool_initialization( Application *application ) { GError *error = NULL; if (application == NULL || application->tool_initializer == NULL) { return; } if (!tool_initializer_start( application->tool_initializer, &error )) { g_warning( "Impossible d’initialiser les outils externes : %s", error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Initialisation des outils impossible", error != NULL ? error->message : "Les outils externes n’ont pas pu être initialisés." ); g_clear_error( &error ); } } /** * @brief Affiche une erreur dans une fenêtre GTK. * * @param application Application propriétaire de la fenêtre principale. * @param title Titre compréhensible par l'utilisateur. * @param message Description de l'erreur. */ static void application_present_error( Application *application, const char *title, const char *message ) { GtkWindow *parent_window = NULL; if (application != NULL && application->main_window != NULL) { parent_window = main_window_get_window( application->main_window ); } application_message_dialog_present( parent_window, APPLICATION_MESSAGE_DIALOG_ERROR, title, message ); } static void application_on_tool_initialization_completed( ToolInitializer *tool_initializer, BackgroundTaskState final_state, const ToolInitializationSummary *summary, const GError *error, gpointer user_data ); static void application_present_error( Application *application, const char *title, const char *message ); /** * @brief Charge les preuves de la session et construit leurs modèles GTK. * * Les nouveaux modèles sont entièrement préparés avant de remplacer * ceux actuellement affichés. * * En cas d'échec, les anciens modèles restent la propriété de * l'application et ne sont pas modifiés. */ static gboolean application_refresh_evidence_models( Application *application, GError **error ) { Database *database = NULL; EvidenceDao *evidence_dao = NULL; EvidenceTypeDao *evidence_type_dao = NULL; GPtrArray *evidence_records = NULL; GPtrArray *evidence_types = NULL; GHashTable *type_labels = NULL; EvidenceListModel *new_evidence_list_model = NULL; EvidenceCategoryModel *new_evidence_category_model = NULL; GError *local_error = NULL; guint type_index = 0; gboolean refreshed = FALSE; g_return_val_if_fail( error == NULL || *error == NULL, FALSE ); if (application == NULL || application->main_window == NULL || application->session == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "L'application ou la session d'enquête est invalide." ); return FALSE; } database = investigation_session_get_database( application->session ); if (database == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "La session ne fournit aucune connexion SQLite." ); return FALSE; } evidence_dao = evidence_dao_new( database, &local_error ); if (evidence_dao == NULL) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de créer le DAO des preuves : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } else { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible de créer le DAO des preuves." ); } goto cleanup; } evidence_records = evidence_dao_list_all( evidence_dao, &local_error ); if (evidence_records == NULL) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de charger les preuves : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } else { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible de charger les preuves." ); } goto cleanup; } evidence_type_dao = evidence_type_dao_new( database, &local_error ); if (evidence_type_dao == NULL) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de créer le DAO des types de preuve : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } else { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible de créer le DAO des types de preuve." ); } goto cleanup; } evidence_types = evidence_type_dao_list_all( evidence_type_dao, &local_error ); if (evidence_types == NULL) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de charger les types de preuve : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } else { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible de charger les types de preuve." ); } goto cleanup; } type_labels = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, g_free ); if (type_labels == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible d'allouer la table des libellés." ); goto cleanup; } for (type_index = 0; type_index < evidence_types->len; type_index++) { const EvidenceType *evidence_type = NULL; const char *type_code = NULL; const char *type_label = NULL; evidence_type = g_ptr_array_index( evidence_types, type_index ); if (evidence_type == NULL) { continue; } type_code = evidence_type_get_code( evidence_type ); type_label = evidence_type_get_label( evidence_type ); if (type_code == NULL || type_code[0] == '\0' || type_label == NULL || type_label[0] == '\0') { continue; } g_hash_table_replace( type_labels, g_strdup( type_code ), g_strdup( type_label ) ); } new_evidence_list_model = evidence_list_model_new(); new_evidence_category_model = evidence_category_model_new(); if (new_evidence_list_model == NULL || new_evidence_category_model == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible d'allouer les modèles d'affichage des preuves." ); goto cleanup; } if (!evidence_list_model_replace( new_evidence_list_model, evidence_records, &local_error )) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de préparer la liste des preuves : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } goto cleanup; } if (!evidence_category_model_replace( new_evidence_category_model, evidence_list_model_get_list_model( new_evidence_list_model ), type_labels, &local_error )) { if (local_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, local_error, "Impossible de regrouper les preuves : " ); local_error = NULL; } else { g_clear_error( &local_error ); } } goto cleanup; } /* * La vue doit être détachée avant la libération de son ancien modèle. */ main_window_set_evidence_model( application->main_window, NULL ); evidence_category_model_free( application->evidence_category_model ); evidence_list_model_free( application->evidence_list_model ); application->evidence_list_model = new_evidence_list_model; application->evidence_category_model = new_evidence_category_model; new_evidence_list_model = NULL; new_evidence_category_model = NULL; main_window_set_evidence_model( application->main_window, application->evidence_category_model ); refreshed = TRUE; cleanup: g_clear_error( &local_error ); evidence_category_model_free( new_evidence_category_model ); evidence_list_model_free( new_evidence_list_model ); if (type_labels != NULL) { g_hash_table_unref( type_labels ); } if (evidence_types != NULL) { g_ptr_array_unref( evidence_types ); } if (evidence_records != NULL) { g_ptr_array_unref( evidence_records ); } evidence_type_dao_free( evidence_type_dao ); evidence_dao_free( evidence_dao ); return refreshed; } /** * @brief Installe une nouvelle session et son arbre dans l'application. * * En cas de succès, Application devient propriétaire de new_session * et de new_tree_model. * * En cas d'échec, le code appelant conserve la propriété des deux objets. * * @param application Application à mettre à jour. * @param new_session Nouvelle session valide. * @param new_tree_model Nouvel arbre valide. * * @return TRUE si l'installation réussit, sinon FALSE. */ static gboolean application_install_session( Application *application, InvestigationSession *new_session, InvestigationTreeModel *new_tree_model ) { const InvestigationProject *project = NULL; const InvestigationRecord *record = NULL; const char *root_path = NULL; const char *database_path = NULL; const char *investigation_name = NULL; GError *evidence_error = NULL; if (application == NULL || application->main_window == NULL || new_session == NULL || new_tree_model == NULL) { return FALSE; } project = investigation_session_get_project( new_session ); record = investigation_session_get_record( new_session ); if (project == NULL || record == NULL) { return FALSE; } root_path = investigation_project_get_root_path( project ); database_path = investigation_project_get_database_path( project ); investigation_name = investigation_record_get_name( record ); if (root_path == NULL || root_path[0] == '\0' || database_path == NULL || database_path[0] == '\0' || investigation_name == NULL || investigation_name[0] == '\0') { return FALSE; } application_set_selected_evidence_identifier( application, NULL ); main_window_set_selected_evidence( application->main_window, NULL ); /* * Les nouveaux objets sont entièrement valides. * Les anciens peuvent maintenant être libérés. */ investigation_tree_model_free( application->tree_model ); investigation_session_close( application->session ); application->tree_model = new_tree_model; application->session = new_session; main_window_set_tree_model( application->main_window, application->tree_model ); main_window_set_investigation( application->main_window, investigation_name, root_path ); main_window_set_import_evidence_enabled( application->main_window, TRUE ); application_start_graph_loading( application, database_path ); /* * L'ancienne liste ne doit jamais rester affichée après * un changement d'enquête. */ main_window_set_evidence_model( application->main_window, NULL ); evidence_category_model_free( application->evidence_category_model ); evidence_list_model_free( application->evidence_list_model ); application->evidence_category_model = NULL; application->evidence_list_model = NULL; if (!application_refresh_evidence_models( application, &evidence_error )) { g_warning( "Impossible de charger les preuves de l'enquête : %s", evidence_error != NULL ? evidence_error->message : "erreur inconnue" ); application_present_error( application, "Preuves indisponibles", evidence_error != NULL ? evidence_error->message : "Les preuves enregistrées n'ont pas pu être chargées." ); g_clear_error( &evidence_error ); } return TRUE; } /** * @brief Ouvre une enquête et l'installe dans l'application. * * En cas de succès, Application devient propriétaire de la nouvelle * session et du nouvel arbre. * * En cas d'échec, l'état précédent de l'application reste inchangé. * * @param application Application à mettre à jour. * @param root_path Chemin racine de l'enquête. * @param error Emplacement facultatif pour l'erreur. * * @return TRUE si l'enquête est ouverte et installée, sinon FALSE. */ static gboolean application_open_and_install_investigation( Application *application, const char *root_path, GError **error ) { InvestigationSession *new_session = NULL; InvestigationTreeModel *new_tree_model = NULL; const InvestigationProject *project = NULL; const char *canonical_root_path = NULL; GError *session_error = NULL; g_return_val_if_fail( error == NULL || *error == NULL, FALSE ); if (application == NULL || application->main_window == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INVALID_ARGUMENT, "L'application ou sa fenêtre principale est invalide." ); return FALSE; } if (root_path == NULL || root_path[0] == '\0') { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INVALID_ARGUMENT, "Le chemin racine de l'enquête est invalide." ); return FALSE; } new_session = investigation_session_open( root_path, &session_error ); if (new_session == NULL) { if (session_error != NULL) { if (error != NULL) { g_propagate_prefixed_error( error, session_error, "Impossible d'ouvrir la session : " ); } else { g_clear_error( &session_error ); } } else { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INVALID_PROJECT, "Impossible d'ouvrir la session de l'enquête." ); } return FALSE; } project = investigation_session_get_project( new_session ); if (project == NULL) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INVALID_PROJECT, "La session ne fournit aucun projet valide." ); investigation_session_close( new_session ); return FALSE; } canonical_root_path = investigation_project_get_root_path( project ); if (canonical_root_path == NULL || canonical_root_path[0] == '\0') { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INVALID_PROJECT, "Le projet ne fournit aucun chemin racine valide." ); investigation_session_close( new_session ); return FALSE; } new_tree_model = investigation_tree_builder_build( canonical_root_path ); if (new_tree_model == NULL) { g_set_error( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_TREE_BUILD, "Impossible de construire l'arborescence de l'enquête " "située dans '%s'.", canonical_root_path ); investigation_session_close( new_session ); return FALSE; } if (!application_install_session( application, new_session, new_tree_model )) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_INSTALL, "Impossible d'installer la session dans l'application." ); investigation_tree_model_free( new_tree_model ); investigation_session_close( new_session ); return FALSE; } /* * La propriété de new_session et new_tree_model a été * transférée à Application. */ return TRUE; } /** * @brief Traite le dossier d'enquête sélectionné par l'utilisateur. * * @param folder_path Chemin sélectionné, ou NULL en cas d'annulation. * @param user_data Pointeur vers Application. */ static void application_on_folder_selected( const char *folder_path, gpointer user_data ) { Application *application = user_data; GError *error = NULL; if (application == NULL) { return; } /* * L'annulation du sélecteur ne constitue pas une erreur. */ if (folder_path == NULL) { return; } if (!application_open_and_install_investigation( application, folder_path, &error )) { g_warning( "Impossible d'ouvrir l'enquête '%s' : %s", folder_path, error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Ouverture impossible", error != NULL ? error->message : "L'enquête sélectionnée n'a pas pu être ouverte." ); g_clear_error( &error ); } } /** * @brief Traite la demande de création d'une nouvelle enquête. * * @param parent_directory Dossier dans lequel créer l'enquête. * @param investigation_name Nom nettoyé de l'enquête. * @param user_data Pointeur vers Application. */ static void application_on_create_investigation( const char *parent_directory, const char *investigation_name, gpointer user_data ) { Application *application = user_data; char *created_root_path = NULL; char *user_message = NULL; GError *error = NULL; if (application == NULL) { return; } /* * Une annulation du dialogue transmet deux pointeurs NULL. */ if (parent_directory == NULL || investigation_name == NULL) { return; } created_root_path = investigation_project_create( parent_directory, investigation_name ); if (created_root_path == NULL) { g_warning( "Impossible de créer l'enquête '%s' dans '%s'.", investigation_name, parent_directory ); user_message = g_strdup_printf( "L'enquête « %s » n'a pas pu être créée dans :\n" "%s\n\n" "Vérifiez que le dossier existe et que vous disposez " "des droits d'écriture nécessaires.", investigation_name, parent_directory ); application_present_error( application, "Création impossible", user_message ); g_free( user_message ); return; } if (!application_open_and_install_investigation( application, created_root_path, &error )) { g_warning( "L'enquête a été créée dans '%s', mais son " "ouverture a échoué : %s", created_root_path, error != NULL ? error->message : "erreur inconnue" ); user_message = g_strdup_printf( "L'enquête a bien été créée sur le disque dans :\n" "%s\n\n" "Labfy Investigation n'a cependant pas pu l'ouvrir :\n" "%s", created_root_path, error != NULL ? error->message : "Aucun détail supplémentaire n'est disponible." ); application_present_error( application, "Enquête créée mais non ouverte", user_message ); g_free( user_message ); g_clear_error( &error ); } g_free( created_root_path ); } /** * @brief Ouvre le dialogue de création d'une enquête. * * @param user_data Pointeur vers Application. */ static void application_on_new_investigation_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL) { return; } create_investigation_dialog_present( main_window_get_window( application->main_window ), application_on_create_investigation, application ); } /** * @brief Ouvre le sélecteur d'une enquête existante. * * @param user_data Pointeur vers Application. */ static void application_on_open_investigation_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL) { return; } folder_dialog_select_folder( main_window_get_window( application->main_window ), application_on_folder_selected, application ); } /** * @brief Reconstruit et remplace l’arborescence active. * * Le nouveau modèle est entièrement construit avant que l’ancien * soit détaché et libéré. * * @param application Application concernée. * @param investigation_root_path Racine à parcourir. * * @return TRUE si le remplacement a réussi. */ static gboolean application_refresh_investigation_tree( Application *application, const char *investigation_root_path ) { InvestigationTreeModel *new_tree_model = NULL; InvestigationTreeModel *old_tree_model = NULL; if (application == NULL || application->main_window == NULL || investigation_root_path == NULL || investigation_root_path[0] == '\0') { return FALSE; } new_tree_model = investigation_tree_builder_build( investigation_root_path ); if (new_tree_model == NULL) { return FALSE; } old_tree_model = application->tree_model; /* * MainWindow transmet le nouveau modèle à la Sidebar. * * InvestigationTreeView détache d’abord son ancien modèle GTK, * puis construit le nouveau. L’ancien modèle métier reste vivant * pendant toute cette opération. */ main_window_set_tree_model( application->main_window, new_tree_model ); application->tree_model = new_tree_model; /* * La vue ne référence plus les anciens nœuds. */ investigation_tree_model_free( old_tree_model ); return TRUE; } /** * @brief Traite la fin d’une tâche d’import. * * @param task Tâche d’import terminée. * @param user_data Contexte ApplicationEvidenceImportContext. */ static void application_on_evidence_import_completed( BackgroundTask *task, gpointer user_data ) { ApplicationEvidenceImportContext *context = user_data; Application *application = NULL; BackgroundTaskState state; EvidenceRecord *evidence_record = NULL; const char *original_name = NULL; char *status_message = NULL; GError *error = NULL; GError *evidence_refresh_error = NULL; gboolean tree_refreshed = FALSE; gboolean evidence_refreshed = FALSE; if (task == NULL || context == NULL) { return; } application = context->application; if (application == NULL || application->main_window == NULL) { return; } main_window_set_import_evidence_enabled( application->main_window, application->session != NULL ); state = background_task_get_state( task ); if (state == BACKGROUND_TASK_STATE_COMPLETED) { evidence_record = background_task_get_result( task ); if (evidence_record == NULL) { application_present_error( application, "Import incomplet", "La tâche est terminée sans fournir de preuve." ); return; } original_name = evidence_record_get_original_name( evidence_record ); /* * L'utilisateur peut avoir changé d'enquête pendant * l'exécution asynchrone de l'import. * * Dans ce cas, la preuve reste correctement importée dans * l'ancienne enquête, mais l'interface actuelle ne doit pas * être rafraîchie avec ses données. */ if (application_session_matches_root( application, context->investigation_root_path )) { tree_refreshed = application_refresh_investigation_tree( application, context->investigation_root_path ); evidence_refreshed = application_refresh_evidence_models( application, &evidence_refresh_error ); if (!tree_refreshed) { g_warning( "La preuve a été importée, mais l'arborescence de '%s' " "n'a pas pu être reconstruite.", context->investigation_root_path ); } if (!evidence_refreshed) { g_warning( "La preuve a été importée, mais la liste des preuves " "n'a pas pu être actualisée : %s", evidence_refresh_error != NULL ? evidence_refresh_error->message : "erreur inconnue" ); } if (tree_refreshed && evidence_refreshed) { status_message = g_strdup_printf( "Preuve importée : %s", original_name != NULL && original_name[0] != '\0' ? original_name : "(nom indisponible)" ); } else if (!tree_refreshed && evidence_refreshed) { status_message = g_strdup_printf( "Preuve importée : %s — " "arborescence non actualisée", original_name != NULL && original_name[0] != '\0' ? original_name : "(nom indisponible)" ); } else if (tree_refreshed && !evidence_refreshed) { status_message = g_strdup_printf( "Preuve importée : %s — " "liste des preuves non actualisée", original_name != NULL && original_name[0] != '\0' ? original_name : "(nom indisponible)" ); } else { status_message = g_strdup_printf( "Preuve importée : %s — " "interface non actualisée", original_name != NULL && original_name[0] != '\0' ? original_name : "(nom indisponible)" ); } main_window_set_status( application->main_window, status_message ); g_clear_error( &evidence_refresh_error ); } g_message( "Preuve importée dans '%s' : %s", context->investigation_root_path, original_name != NULL && original_name[0] != '\0' ? original_name : "(nom indisponible)" ); g_free( status_message ); return; } if (state == BACKGROUND_TASK_STATE_CANCELLED) { if (application_session_matches_root( application, context->investigation_root_path )) { main_window_set_status( application->main_window, "Import de preuve annulé." ); } return; } error = background_task_dup_error( task ); g_warning( "L’import de la preuve a échoué : %s", error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Import impossible", error != NULL ? error->message : "La preuve n’a pas pu être importée." ); g_clear_error( &error ); } /** * @brief Retourne le dossier de stockage associé à un type de preuve. * * La chaîne retournée est statique et ne doit pas être libérée. */ static const char *application_get_evidence_subdirectory( const char *type_identifier ) { if (g_strcmp0( type_identifier, "screenshot" ) == 0) { return "Captures_Ecran"; } if (g_strcmp0( type_identifier, "photo" ) == 0) { return "Photos"; } if (g_strcmp0( type_identifier, "video" ) == 0) { return "Videos"; } if (g_strcmp0( type_identifier, "email" ) == 0) { return "Emails"; } if (g_strcmp0( type_identifier, "audio" ) == 0) { return "Audios"; } if (g_strcmp0( type_identifier, "archive" ) == 0) { return "Archives"; } if (g_strcmp0( type_identifier, "conversation" ) == 0) { return "Conversations"; } /* * Documents, textes, autres types et types inconnus. */ return "Documents"; } /** * @brief Prépare et démarre l’import asynchrone d’un fichier. */ static void application_start_evidence_import( Application *application, const char *file_path, const char *type_identifier, const char *collected_at, const char *source, const char *description ) { const InvestigationProject *project = NULL; const char *root_path = NULL; const char *database_path = NULL; const char *evidence_subdirectory = NULL; char *relative_directory = NULL; EvidenceImportTaskRequest request = {0}; ApplicationEvidenceImportContext *context = NULL; BackgroundTask *task = NULL; char *destination_directory = NULL; char *source_name = NULL; char *status_message = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL || application->task_manager == NULL || file_path == NULL || file_path[0] == '\0' || type_identifier == NULL || type_identifier[0] == '\0' || collected_at == NULL || collected_at[0] == '\0') { return; } project = investigation_session_get_project( application->session ); if (project == NULL) { application_present_error( application, "Import impossible", "La session ne fournit aucun projet d’enquête." ); return; } root_path = investigation_project_get_root_path( project ); database_path = investigation_project_get_database_path( project ); if (root_path == NULL || root_path[0] == '\0' || database_path == NULL || database_path[0] == '\0') { application_present_error( application, "Import impossible", "Les chemins de l’enquête sont invalides." ); return; } evidence_subdirectory = application_get_evidence_subdirectory( type_identifier ); relative_directory = g_build_filename( APPLICATION_EVIDENCE_ROOT_DIRECTORY, evidence_subdirectory, NULL ); if (relative_directory == NULL) { application_present_error( application, "Import impossible", "Impossible de construire le chemin relatif de la preuve." ); return; } destination_directory = g_build_filename( root_path, relative_directory, NULL ); if (destination_directory == NULL) { application_present_error( application, "Import impossible", "Impossible de construire le dossier de destination." ); g_free( relative_directory ); return; } /* * Le dossier devrait exister dans une enquête normale. * Cette création rend néanmoins l’import robuste face à un dossier * supprimé accidentellement. */ if (g_mkdir_with_parents( destination_directory, 0700 ) != 0) { status_message = g_strdup_printf( "Impossible de préparer le dossier '%s' : %s", destination_directory, g_strerror(errno) ); application_present_error( application, "Import impossible", status_message ); g_free( status_message ); g_free( destination_directory ); return; } context = g_try_new0( ApplicationEvidenceImportContext, 1 ); if (context == NULL) { application_present_error( application, "Import impossible", "Impossible d’allouer le contexte de la tâche." ); g_free( destination_directory ); return; } context->application = application; context->investigation_root_path = g_strdup( root_path ); if (context->investigation_root_path == NULL) { application_evidence_import_context_free( context ); g_free( destination_directory ); application_present_error( application, "Import impossible", "Impossible de conserver le chemin de l’enquête." ); return; } request.database_path = database_path; request.source_path = file_path; request.destination_directory = destination_directory; request.relative_directory = relative_directory; request.type_identifier = type_identifier; request.collected_at = collected_at; request.source = source; request.description = description; task = evidence_import_task_start( application->task_manager, &request, application_on_evidence_import_completed, context, application_evidence_import_context_free, &error ); if (task == NULL) { /* * La tâche n’a pas pris possession du contexte. */ application_evidence_import_context_free( context ); application_present_error( application, "Import impossible", error != NULL ? error->message : "La tâche d’import n’a pas pu être démarrée." ); g_clear_error( &error ); g_free( destination_directory ); g_free( relative_directory ); return; } /* * Le TaskManager et le worker conservent désormais leurs références. */ background_task_unref( task ); main_window_set_import_evidence_enabled( application->main_window, FALSE ); source_name = g_path_get_basename( file_path ); status_message = g_strdup_printf( "Import en cours : %s", source_name != NULL ? source_name : file_path ); main_window_set_status( application->main_window, status_message ); g_free( status_message ); g_free( source_name ); g_free( destination_directory ); g_free( relative_directory ); } /** * @brief Traite la validation ou l'annulation des métadonnées. * * @param result Résultat possédé par ce callback, ou NULL. * @param user_data Contexte ApplicationEvidenceDialogContext. */ static void application_on_evidence_metadata_completed( EvidenceImportDialogResult *result, gpointer user_data ) { ApplicationEvidenceDialogContext *context = user_data; Application *application = NULL; if (context == NULL) { evidence_import_dialog_result_free( result ); return; } application = context->application; if (application == NULL || application->main_window == NULL) { evidence_import_dialog_result_free( result ); application_evidence_dialog_context_free( context ); return; } /* * Une annulation du dialogue ne doit lancer aucune tâche. */ if (result == NULL) { main_window_set_status( application->main_window, "Import de preuve annulé." ); application_evidence_dialog_context_free( context ); return; } application_start_evidence_import( application, context->file_path, evidence_import_dialog_result_get_type_identifier( result ), evidence_import_dialog_result_get_collected_at( result ), evidence_import_dialog_result_get_source( result ), evidence_import_dialog_result_get_description( result ) ); evidence_import_dialog_result_free( result ); application_evidence_dialog_context_free( context ); } /** * @brief Traite le fichier de preuve sélectionné. * * @param file_path Chemin local sélectionné, ou NULL. * @param error Erreur de sélection, ou NULL. * @param user_data Pointeur vers Application. */ static void application_on_evidence_file_selected( const char *file_path, const GError *error, gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL) { return; } /* * Une annulation ne constitue pas une erreur. */ if (file_path == NULL && error == NULL) { main_window_set_status( application->main_window, "Import de preuve annulé." ); return; } if (error != NULL) { g_warning( "Impossible de sélectionner le fichier : %s", error->message ); application_present_error( application, "Sélection impossible", error->message ); return; } if (file_path == NULL || file_path[0] == '\0') { application_present_error( application, "Sélection impossible", "Le chemin du fichier sélectionné est invalide." ); return; } Database *database = NULL; EvidenceTypeDao *evidence_type_dao = NULL; GPtrArray *evidence_types = NULL; ApplicationEvidenceDialogContext *dialog_context = NULL; GError *dialog_error = NULL; database = investigation_session_get_database( application->session ); if (database == NULL) { application_present_error( application, "Import impossible", "La session ne fournit aucune connexion SQLite." ); return; } evidence_type_dao = evidence_type_dao_new( database, &dialog_error ); if (evidence_type_dao == NULL) { application_present_error( application, "Types de preuve indisponibles", dialog_error != NULL ? dialog_error->message : "Impossible de créer le DAO des types de preuve." ); g_clear_error( &dialog_error ); return; } evidence_types = evidence_type_dao_list_all( evidence_type_dao, &dialog_error ); evidence_type_dao_free( evidence_type_dao ); if (evidence_types == NULL) { application_present_error( application, "Types de preuve indisponibles", dialog_error != NULL ? dialog_error->message : "Impossible de lire les types de preuve." ); g_clear_error( &dialog_error ); return; } if (evidence_types->len == 0) { application_present_error( application, "Types de preuve indisponibles", "Aucun type de preuve n'est enregistré dans la base." ); g_ptr_array_unref( evidence_types ); return; } dialog_context = g_try_new0( ApplicationEvidenceDialogContext, 1 ); if (dialog_context == NULL) { application_present_error( application, "Import impossible", "Impossible d'allouer le contexte du dialogue." ); g_ptr_array_unref( evidence_types ); return; } dialog_context->application = application; dialog_context->file_path = g_strdup( file_path ); if (dialog_context->file_path == NULL) { application_present_error( application, "Import impossible", "Impossible de conserver le chemin du fichier." ); application_evidence_dialog_context_free( dialog_context ); g_ptr_array_unref( evidence_types ); return; } if (!evidence_import_dialog_present( main_window_get_window( application->main_window ), dialog_context->file_path, evidence_types, application_on_evidence_metadata_completed, dialog_context, &dialog_error )) { application_present_error( application, "Dialogue d'import impossible", dialog_error != NULL ? dialog_error->message : "Le dialogue de métadonnées n'a pas pu être ouvert." ); g_clear_error( &dialog_error ); application_evidence_dialog_context_free( dialog_context ); } g_ptr_array_unref( evidence_types ); } /** * @brief Traite la demande d'import d'une preuve. * * Le sélecteur de fichier sera ajouté à l'étape suivante. * * @param user_data Pointeur vers Application. */ static void application_on_import_evidence_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL) { return; } if (application->session == NULL) { main_window_set_import_evidence_enabled( application->main_window, FALSE ); return; } file_dialog_select_file( main_window_get_window( application->main_window ), application_on_evidence_file_selected, application ); } /** * @brief Traite la sélection d'un nœud dans l'arborescence. * * @param node Nœud sélectionné, ou NULL si aucune sélection. * @param user_data Pointeur vers Application. */ static void application_on_tree_node_selected( const InvestigationNode *node, gpointer user_data ) { Application *application = user_data; const char *node_name = NULL; InvestigationNodeType node_type; if (application == NULL) { return; } main_window_set_selected_node( application->main_window, node ); if (node == NULL) { g_print("Aucun nœud sélectionné.\n"); return; } node_name = investigation_node_get_name( node ); node_type = investigation_node_get_type( node ); g_print( "Nœud sélectionné : %s\n", node_name != NULL ? node_name : "(sans nom)" ); if (node_type == INVESTIGATION_NODE_DIRECTORY) { g_print("Type : dossier\n"); } else { g_print("Type : fichier\n"); } } /** * @brief Traite la sélection d'une preuve dans la Sidebar. * * L'identifiant est emprunté uniquement pendant cet appel. * La preuve complète est relue depuis SQLite avant d'être transmise * au Workspace. * * @param evidence_identifier UUID de la preuve, ou NULL. * @param user_data Pointeur vers Application. */ static void application_on_evidence_selected( const char *evidence_identifier, gpointer user_data ) { Application *application = user_data; Database *database = NULL; EvidenceDao *evidence_dao = NULL; EvidenceRecord *evidence_record = NULL; const char *original_name = NULL; char *status_message = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL) { return; } /* * Une catégorie ou une sélection vide transmet NULL. */ if (evidence_identifier == NULL || evidence_identifier[0] == '\0') { application_set_selected_evidence_identifier( application, NULL ); main_window_set_selected_evidence( application->main_window, NULL ); application_set_selected_evidence_identifier( application, NULL ); return; } if (application->session == NULL) { main_window_set_selected_evidence( application->main_window, NULL ); return; } database = investigation_session_get_database( application->session ); if (database == NULL) { main_window_set_selected_evidence( application->main_window, NULL ); application_present_error( application, "Preuve indisponible", "La session ne fournit aucune connexion SQLite." ); return; } evidence_dao = evidence_dao_new( database, &error ); if (evidence_dao == NULL) { g_warning( "Impossible de créer le DAO des preuves : %s", error != NULL ? error->message : "erreur inconnue" ); main_window_set_selected_evidence( application->main_window, NULL ); application_present_error( application, "Preuve indisponible", error != NULL ? error->message : "Impossible d'accéder aux preuves enregistrées." ); g_clear_error( &error ); return; } evidence_record = evidence_dao_find_by_identifier( evidence_dao, evidence_identifier, &error ); evidence_dao_free( evidence_dao ); if (error != NULL) { g_warning( "Impossible de charger la preuve '%s' : %s", evidence_identifier, error->message ); main_window_set_selected_evidence( application->main_window, NULL ); application_present_error( application, "Preuve indisponible", error->message ); g_clear_error( &error ); return; } /* * Une preuve absente retourne NULL sans erreur. */ if (evidence_record == NULL) { g_warning( "La preuve '%s' n'existe plus dans la base.", evidence_identifier ); main_window_set_selected_evidence( application->main_window, NULL ); main_window_set_status( application->main_window, "La preuve sélectionnée est introuvable." ); return; } application_set_selected_evidence_identifier( application, evidence_identifier ); /* * Workspace copie immédiatement les valeurs dans ses widgets GTK. */ main_window_set_selected_evidence( application->main_window, evidence_record ); original_name = evidence_record_get_original_name( evidence_record ); status_message = g_strdup_printf( "Preuve sélectionnée : %s", original_name != NULL && original_name[0] != '\0' ? original_name : "(sans nom)" ); main_window_set_status( application->main_window, status_message ); g_free( status_message ); evidence_record_free( evidence_record ); } /** * @brief Traite la validation ou l'annulation du dialogue de relation. * * Cette première étape valide le parcours complet de l'interface. * L'écriture SQLite sera branchée après validation de la compilation. */ static void application_on_create_relation_completed( CreateRelationDialogResult *result, gpointer user_data ) { Application *application = user_data; const char *source_identifier = NULL; const char *target_identifier = NULL; const char *relation_type = NULL; char *status_message = NULL; if (application == NULL || application->main_window == NULL) { create_relation_dialog_result_free( result ); return; } if (result == NULL) { main_window_set_status( application->main_window, "Ajout de relation annulé." ); return; } source_identifier = create_relation_dialog_result_get_source_identifier( result ); target_identifier = create_relation_dialog_result_get_target_identifier( result ); relation_type = create_relation_dialog_result_get_relation_type( result ); g_message( "Relation préparée : %s -> %s (%s), confiance %d %%.", source_identifier != NULL ? source_identifier : "(source absente)", target_identifier != NULL ? target_identifier : "(cible absente)", relation_type != NULL ? relation_type : "(type absent)", create_relation_dialog_result_get_confidence( result ) ); status_message = g_strdup_printf( "Relation préparée : %s. " "L'enregistrement SQLite sera branché à l'étape suivante.", relation_type != NULL && relation_type[0] != '\0' ? relation_type : "(type inconnu)" ); main_window_set_status( application->main_window, status_message != NULL ? status_message : "Relation préparée." ); g_free( status_message ); create_relation_dialog_result_free( result ); } /** * @brief Ouvre le dialogue de création depuis la fiche d'une entité. */ static void application_on_add_relation_requested( const char *source_entity_identifier, gpointer user_data ) { Application *application = user_data; Database *database = NULL; EntityDao *entity_dao = NULL; GPtrArray *entities = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL || source_entity_identifier == NULL || !g_uuid_string_is_valid( source_entity_identifier )) { return; } database = investigation_session_get_database( application->session ); if (database == NULL) { application_present_error( application, "Relations indisponibles", "La session ne fournit aucune connexion SQLite." ); return; } entity_dao = entity_dao_new( database, &error ); if (entity_dao == NULL) { application_present_error( application, "Relations indisponibles", error != NULL ? error->message : "Impossible d'accéder aux entités." ); g_clear_error( &error ); return; } entities = entity_dao_list_all( entity_dao, &error ); entity_dao_free( entity_dao ); if (entities == NULL) { application_present_error( application, "Relations indisponibles", error != NULL ? error->message : "Impossible de charger les entités." ); g_clear_error( &error ); return; } if (!create_relation_dialog_present( main_window_get_window( application->main_window ), source_entity_identifier, entities, application_on_create_relation_completed, application, &error )) { application_present_error( application, "Ajout de relation impossible", error != NULL ? error->message : "Le dialogue de relation n'a pas pu être ouvert." ); g_clear_error( &error ); } g_ptr_array_unref( entities ); } /** * @brief Enregistre le statut d'intégrité dans l'enquête d'origine. * * Une connexion indépendante est utilisée afin que la bonne base soit * mise à jour même si l'utilisateur a changé d'enquête entre-temps. */ static gboolean application_persist_evidence_integrity_status( const ApplicationEvidenceIntegrityContext *context, EvidenceIntegrityStatus integrity_status, GError **error ) { Database *database = NULL; EvidenceDao *evidence_dao = NULL; gboolean updated = FALSE; g_return_val_if_fail( error == NULL || *error == NULL, FALSE ); if (context == NULL || context->database_path == NULL || context->database_path[0] == '\0' || context->evidence_identifier == NULL || !g_uuid_string_is_valid( context->evidence_identifier )) { g_set_error_literal( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Le contexte de mise à jour de l'intégrité est invalide." ); return FALSE; } database = database_open( context->database_path ); if (database == NULL) { g_set_error( error, APPLICATION_OPEN_ERROR, APPLICATION_OPEN_ERROR_EVIDENCE, "Impossible d'ouvrir la base SQLite : %s", context->database_path ); return FALSE; } evidence_dao = evidence_dao_new( database, error ); if (evidence_dao == NULL) { database_close( database ); return FALSE; } updated = evidence_dao_update_integrity_status( evidence_dao, context->evidence_identifier, integrity_status, error ); evidence_dao_free( evidence_dao ); database_close( database ); return updated; } /** * @brief Traite la fin d'une vérification d'intégrité. */ static void application_on_evidence_integrity_completed( BackgroundTask *task, gpointer user_data ) { ApplicationEvidenceIntegrityContext *context = user_data; Application *application = NULL; const EvidenceIntegrityVerificationResult *result = NULL; EvidenceIntegrityStatus integrity_status = EVIDENCE_INTEGRITY_STATUS_ERROR; BackgroundTaskState task_state; const char *diagnostic = NULL; const char *display_name = NULL; char *status_message = NULL; GError *error = NULL; GError *refresh_error = NULL; gboolean session_matches = FALSE; gboolean evidence_was_selected = FALSE; if (task == NULL || context == NULL) { return; } application = context->application; if (application == NULL) { return; } task_state = background_task_get_state( task ); session_matches = application_session_matches_root( application, context->investigation_root_path ); display_name = context->original_name != NULL && context->original_name[0] != '\0' ? context->original_name : context->evidence_identifier; if (task_state == BACKGROUND_TASK_STATE_COMPLETED) { result = background_task_get_result( task ); if (result == NULL) { if (session_matches) { application_present_error( application, "Vérification incomplète", "La tâche s'est terminée sans fournir de résultat." ); } return; } integrity_status = evidence_integrity_verification_result_get_status( result ); /* * UNKNOWN après une vérification complète serait incohérent. */ if (integrity_status == EVIDENCE_INTEGRITY_STATUS_UNKNOWN) { integrity_status = EVIDENCE_INTEGRITY_STATUS_ERROR; } diagnostic = evidence_integrity_verification_result_get_diagnostic( result ); if (!application_persist_evidence_integrity_status( context, integrity_status, &error )) { g_warning( "Impossible d'enregistrer le statut de la preuve '%s' : %s", context->evidence_identifier, error != NULL ? error->message : "erreur inconnue" ); if (session_matches) { application_present_error( application, "Statut non enregistré", error != NULL ? error->message : "Le résultat n'a pas pu être enregistré." ); } g_clear_error( &error ); return; } if (session_matches) { /* * La reconstruction du modèle peut provoquer temporairement * une sélection vide. On mémorise donc l'état avant celle-ci. */ evidence_was_selected = g_strcmp0( application->selected_evidence_identifier, context->evidence_identifier ) == 0; if (!application_refresh_evidence_models( application, &refresh_error )) { g_warning( "Le statut a été enregistré, mais la liste " "des preuves n'a pas été actualisée : %s", refresh_error != NULL ? refresh_error->message : "erreur inconnue" ); } g_clear_error( &refresh_error ); /* * On ne force pas l'affichage d'une ancienne preuve lorsque * l'utilisateur en a sélectionné une autre entre-temps. */ if (evidence_was_selected) { application_on_evidence_selected( context->evidence_identifier, application ); } switch (integrity_status) { case EVIDENCE_INTEGRITY_STATUS_VALID: status_message = g_strdup_printf( "Intégrité valide : %s", display_name ); break; case EVIDENCE_INTEGRITY_STATUS_MODIFIED: status_message = g_strdup_printf( "Alerte d'intégrité — fichier modifié : %s", display_name ); break; case EVIDENCE_INTEGRITY_STATUS_MISSING: status_message = g_strdup_printf( "Preuve absente du disque : %s", display_name ); break; case EVIDENCE_INTEGRITY_STATUS_ERROR: case EVIDENCE_INTEGRITY_STATUS_UNKNOWN: default: status_message = g_strdup_printf( "Erreur de vérification : %s", display_name ); break; } main_window_set_status( application->main_window, status_message ); if (integrity_status == EVIDENCE_INTEGRITY_STATUS_ERROR && diagnostic != NULL && diagnostic[0] != '\0') { application_present_error( application, "Vérification impossible", diagnostic ); } } g_message( "Vérification de '%s' terminée avec le statut %d.", context->evidence_identifier, (int) integrity_status ); g_free( status_message ); return; } if (task_state == BACKGROUND_TASK_STATE_CANCELLED) { if (session_matches) { main_window_set_status( application->main_window, "Vérification d'intégrité annulée." ); } return; } error = background_task_dup_error( task ); g_warning( "La vérification de la preuve '%s' a échoué : %s", context->evidence_identifier, error != NULL ? error->message : "erreur inconnue" ); if (session_matches) { application_present_error( application, "Vérification impossible", error != NULL ? error->message : "La vérification n'a pas pu être exécutée." ); } g_clear_error( &error ); } /** * @brief Charge la preuve et démarre sa vérification asynchrone. */ static void application_start_evidence_integrity_verification( Application *application, const char *evidence_identifier ) { const InvestigationProject *project = NULL; Database *database = NULL; EvidenceDao *evidence_dao = NULL; EvidenceRecord *evidence_record = NULL; ApplicationEvidenceIntegrityContext *context = NULL; EvidenceIntegrityTaskRequest request = {0}; BackgroundTask *task = NULL; const char *root_path = NULL; const char *database_path = NULL; const char *relative_path = NULL; const char *expected_sha256 = NULL; const char *original_name = NULL; char *status_message = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL || application->task_manager == NULL || evidence_identifier == NULL || !g_uuid_string_is_valid( evidence_identifier )) { return; } project = investigation_session_get_project( application->session ); database = investigation_session_get_database( application->session ); if (project == NULL || database == NULL) { application_present_error( application, "Vérification impossible", "La session d'enquête est invalide." ); return; } root_path = investigation_project_get_root_path( project ); database_path = investigation_project_get_database_path( project ); if (root_path == NULL || root_path[0] == '\0' || database_path == NULL || database_path[0] == '\0') { application_present_error( application, "Vérification impossible", "Les chemins de l'enquête sont invalides." ); return; } evidence_dao = evidence_dao_new( database, &error ); if (evidence_dao == NULL) { application_present_error( application, "Vérification impossible", error != NULL ? error->message : "Impossible d'accéder aux preuves." ); g_clear_error( &error ); return; } evidence_record = evidence_dao_find_by_identifier( evidence_dao, evidence_identifier, &error ); evidence_dao_free( evidence_dao ); evidence_dao = NULL; if (error != NULL) { application_present_error( application, "Vérification impossible", error->message ); g_clear_error( &error ); return; } if (evidence_record == NULL) { application_present_error( application, "Preuve introuvable", "La preuve n'existe plus dans la base SQLite." ); return; } relative_path = evidence_record_get_relative_path( evidence_record ); expected_sha256 = evidence_record_get_sha256( evidence_record ); original_name = evidence_record_get_original_name( evidence_record ); if (relative_path == NULL || relative_path[0] == '\0' || expected_sha256 == NULL || expected_sha256[0] == '\0') { application_present_error( application, "Vérification impossible", "La preuve ne contient pas les données nécessaires." ); evidence_record_free( evidence_record ); return; } context = g_try_new0( ApplicationEvidenceIntegrityContext, 1 ); if (context == NULL) { application_present_error( application, "Vérification impossible", "Impossible d'allouer le contexte de la tâche." ); evidence_record_free( evidence_record ); return; } context->application = application; context->investigation_root_path = g_strdup( root_path ); context->database_path = g_strdup( database_path ); context->evidence_identifier = g_strdup( evidence_identifier ); context->original_name = g_strdup( original_name ); if (context->investigation_root_path == NULL || context->database_path == NULL || context->evidence_identifier == NULL) { application_evidence_integrity_context_free( context ); evidence_record_free( evidence_record ); application_present_error( application, "Vérification impossible", "Impossible de copier les paramètres de la tâche." ); return; } request.investigation_root_path = root_path; request.relative_path = relative_path; request.expected_sha256 = expected_sha256; task = evidence_integrity_task_start( application->task_manager, &request, application_on_evidence_integrity_completed, context, application_evidence_integrity_context_free, &error ); if (task == NULL) { /* * La propriété du contexte n'a pas été transférée. */ application_evidence_integrity_context_free( context ); application_present_error( application, "Vérification impossible", error != NULL ? error->message : "La tâche n'a pas pu être démarrée." ); g_clear_error( &error ); evidence_record_free( evidence_record ); return; } /* * BackgroundTask et TaskManager possèdent désormais leurs références. */ background_task_unref( task ); status_message = g_strdup_printf( "Vérification en cours : %s", original_name != NULL && original_name[0] != '\0' ? original_name : evidence_identifier ); main_window_set_status( application->main_window, status_message ); g_free( status_message ); evidence_record_free( evidence_record ); } /** * @brief Reçoit la demande de vérification d'une preuve. * * Cette première étape valide uniquement le relais de l'interface. * Le lancement de la tâche sera ajouté ensuite. * * @param evidence_identifier UUID de la preuve. * @param user_data Pointeur vers Application. */ /** * @brief Traite la demande de vérification provenant du Workspace. */ static void application_on_verify_evidence_requested( const char *evidence_identifier, gpointer user_data ) { Application *application = user_data; application_start_evidence_integrity_verification( application, evidence_identifier ); } /** * @brief Ferme proprement l'application. * * @param user_data Pointeur vers Application. */ /** * @brief Enregistre la nouvelle position logique d'un nœud. * * Le modèle en mémoire est mis à jour avant l'UPSERT SQLite. En cas d'échec * SQLite, le déplacement reste visible pendant la session, mais un message * explicite indique qu'il ne survivra pas à la réouverture de l'enquête. */ static void application_on_graph_node_moved( const char *entity_identifier, double x, double y, gpointer user_data ) { Application *application = user_data; Database *database = NULL; GraphNodePositionDao *position_dao = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL || application->graph_layout == NULL || entity_identifier == NULL || !g_uuid_string_is_valid( entity_identifier )) { return; } if (!investigation_graph_layout_set_position( application->graph_layout, entity_identifier, x, y, &error )) { g_warning( "Impossible de mettre à jour la position du nœud '%s' : %s", entity_identifier, error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Position invalide", error != NULL ? error->message : "La position du nœud ne peut pas être conservée." ); g_clear_error( &error ); return; } database = investigation_session_get_database( application->session ); if (database == NULL) { main_window_set_status( application->main_window, "Nœud déplacé, mais position non enregistrée." ); application_present_error( application, "Enregistrement impossible", "La session ne fournit aucune connexion SQLite." ); return; } position_dao = graph_node_position_dao_new( database, &error ); if (position_dao == NULL) { g_warning( "Impossible de créer le DAO des positions : %s", error != NULL ? error->message : "erreur inconnue" ); main_window_set_status( application->main_window, "Nœud déplacé, mais position non enregistrée." ); application_present_error( application, "Enregistrement impossible", error != NULL ? error->message : "Impossible d'accéder aux positions du graphe." ); g_clear_error( &error ); return; } if (!graph_node_position_dao_upsert( position_dao, entity_identifier, x, y, &error )) { g_warning( "Impossible d'enregistrer la position du nœud '%s' : %s", entity_identifier, error != NULL ? error->message : "erreur inconnue" ); main_window_set_status( application->main_window, "Nœud déplacé, mais position non enregistrée." ); application_present_error( application, "Enregistrement impossible", error != NULL ? error->message : "La position du nœud n'a pas pu être enregistrée." ); g_clear_error( &error ); graph_node_position_dao_free( position_dao ); return; } graph_node_position_dao_free( position_dao ); main_window_set_status( application->main_window, "Position du nœud enregistrée." ); } /** * @brief Supprime les positions persistées et restaure la grille automatique. * * Cette fonction est appelée uniquement après une confirmation explicite. * L'affichage et le modèle en mémoire restent inchangés en cas d'échec SQLite. */ static void application_reset_graph_layout( Application *application ) { Database *database = NULL; GraphNodePositionDao *position_dao = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL || application->graph_model == NULL || application->graph_layout == NULL) { return; } database = investigation_session_get_database( application->session ); if (database == NULL) { application_present_error( application, "Réinitialisation impossible", "La session ne fournit aucune connexion SQLite." ); return; } position_dao = graph_node_position_dao_new( database, &error ); if (position_dao == NULL) { g_warning( "Impossible de créer le DAO des positions : %s", error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Réinitialisation impossible", error != NULL ? error->message : "Impossible d'accéder aux positions enregistrées." ); g_clear_error( &error ); return; } if (!graph_node_position_dao_delete_all( position_dao, &error )) { g_warning( "Impossible de supprimer les positions du graphe : %s", error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Réinitialisation impossible", error != NULL ? error->message : "Les positions enregistrées n'ont pas pu être supprimées." ); g_clear_error( &error ); graph_node_position_dao_free( position_dao ); return; } graph_node_position_dao_free( position_dao ); investigation_graph_layout_clear( application->graph_layout ); main_window_reset_graph_layout( application->main_window ); main_window_set_status( application->main_window, "Disposition du graphe réinitialisée." ); } /** * @brief Traite la réponse du dialogue de confirmation. */ static void application_on_reset_graph_layout_confirmed( gboolean confirmed, gpointer user_data ) { Application *application = user_data; if (!confirmed) { return; } application_reset_graph_layout( application ); } /** * @brief Demande confirmation avant toute suppression de position. */ static void application_on_show_graph_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL || application->graph_model == NULL) { return; } application_set_selected_evidence_identifier( application, NULL ); /* * Passer NULL restaure l'état principal du Workspace. Quand le graphe * est prêt, workspace_show_default() affiche automatiquement le canvas. */ main_window_set_selected_evidence( application->main_window, NULL ); main_window_set_status( application->main_window, "Graphe de l'enquête affiché." ); } /** * @brief Demande confirmation avant toute suppression de position. */ static void application_on_reset_graph_layout_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->main_window == NULL || application->session == NULL || application->graph_model == NULL || application->graph_layout == NULL) { return; } application_message_dialog_present_confirmation( main_window_get_window( application->main_window ), APPLICATION_MESSAGE_DIALOG_WARNING, "Réinitialiser la disposition du graphe", "Toutes les positions personnalisées des nœuds seront supprimées. " "Le graphe retrouvera sa disposition automatique.\n\n" "Cette action ne peut pas être annulée.", "Réinitialiser", application_on_reset_graph_layout_confirmed, application ); } /** * @brief Ferme proprement l'application. * * @param user_data Pointeur vers Application. */ static void application_on_quit_requested( gpointer user_data ) { Application *application = user_data; if (application == NULL || application->gtk_application == NULL) { return; } task_manager_cancel_all( application->task_manager ); g_application_quit( G_APPLICATION( application->gtk_application ) ); } /** * @brief Crée la fenêtre principale lors de l'activation. * * Le signal activate peut être reçu plusieurs fois. Si la fenêtre existe * déjà, elle est simplement présentée. * * @param gtk_application Application GTK ayant reçu le signal. * @param user_data Pointeur vers Application. */ static void application_on_activate( GtkApplication *gtk_application, gpointer user_data ) { Application *application = user_data; if (application == NULL) { return; } if (application->main_window != NULL) { main_window_present( application->main_window ); return; } application->main_window = main_window_new( gtk_application, application->task_manager ); if (application->main_window == NULL) { g_warning( "Impossible de créer la fenêtre principale." ); return; } main_window_set_tree_selection_callback( application->main_window, application_on_tree_node_selected, application ); main_window_set_evidence_selection_callback( application->main_window, application_on_evidence_selected, application ); main_window_set_verify_evidence_callback( application->main_window, application_on_verify_evidence_requested, application ); main_window_set_graph_node_moved_callback( application->main_window, application_on_graph_node_moved, application ); main_window_set_reset_graph_layout_callback( application->main_window, application_on_reset_graph_layout_requested, application ); main_window_set_show_graph_callback( application->main_window, application_on_show_graph_requested, application ); main_window_set_add_relation_callback( application->main_window, application_on_add_relation_requested, application ); main_window_set_new_investigation_callback( application->main_window, application_on_new_investigation_requested, application ); main_window_set_open_investigation_callback( application->main_window, application_on_open_investigation_requested, application ); main_window_set_import_evidence_callback( application->main_window, application_on_import_evidence_requested, application ); main_window_set_import_evidence_enabled( application->main_window, application->session != NULL ); main_window_set_quit_callback( application->main_window, application_on_quit_requested, application ); main_window_present( application->main_window ); /* * Le démarrage est non bloquant. Le worker s’exécute dans un * thread secondaire et sa progression apparaît dans TaskPanel. */ application_start_tool_initialization( application ); } Application *application_new(void) { Application *application = NULL; GError *error = NULL; application = g_new0( Application, 1 ); application->task_manager = task_manager_new(); if (application->task_manager == NULL) { g_free( application ); return NULL; } application->tool_initializer = tool_initializer_new( application->task_manager, &error ); if (application->tool_initializer == NULL) { g_warning( "Impossible de créer l’initialiseur des outils : %s", error != NULL ? error->message : "erreur inconnue" ); g_clear_error( &error ); task_manager_free( application->task_manager ); g_free( application ); return NULL; } tool_initializer_set_completed_callback( application->tool_initializer, application_on_tool_initialization_completed, application, NULL ); application->gtk_application = gtk_application_new( APPLICATION_ID, G_APPLICATION_DEFAULT_FLAGS ); if (application->gtk_application == NULL) { tool_initializer_free( application->tool_initializer ); task_manager_free( application->task_manager ); g_free( application ); return NULL; } g_signal_connect( application->gtk_application, "activate", G_CALLBACK(application_on_activate), application ); return application; } int application_run( Application *application, int argc, char **argv ) { if (application == NULL || application->gtk_application == NULL) { return 1; } return g_application_run( G_APPLICATION( application->gtk_application ), argc, argv ); } const InvestigationSession *application_get_session( const Application *application ) { if (application == NULL) { return NULL; } return application->session; } void application_free( Application *application ) { if (application == NULL) { return; } if (application->tool_initializer != NULL) { tool_initializer_set_completed_callback( application->tool_initializer, NULL, NULL, NULL ); } task_manager_cancel_all( application->task_manager ); application_cancel_graph_loading( application ); application_clear_graph( application ); investigation_tree_model_free( application->tree_model ); investigation_session_close( application->session ); if (application->main_window != NULL) { main_window_set_evidence_model( application->main_window, NULL ); } evidence_category_model_free( application->evidence_category_model ); evidence_list_model_free( application->evidence_list_model ); /* * MainWindow contient TaskPanel. * Il doit être détruit avant TaskManager. */ main_window_free( application->main_window ); /* * ToolInitializer emprunte TaskManager. * Il doit être libéré avant celui-ci. */ tool_initializer_free( application->tool_initializer ); task_manager_free( application->task_manager ); if (application->gtk_application != NULL) { g_object_unref( application->gtk_application ); } g_clear_pointer( &application->selected_evidence_identifier, g_free ); g_free( application ); }