/****************************************************************************** * @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 "models/osint_dns_query.h" #include "models/osint_dns_proposal.h" #include "models/osint_execution_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 "core/tool_task.h" #include "core/osint_dns_integration.h" #include "core/osint_execution_integrity.h" #include "core/evidence_reclassification.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 "dao/relation_type_dao.h" #include "models/relation_type.h" #include "core/relation_type_service.h" #include "views/evidence_import_dialog.h" #include "views/evidence_metadata_dialog.h" #include "views/create_relation_dialog.h" #include "views/osint_dns_review_dialog.h" #include "views/osint_execution_history_dialog.h" #include "dao/evidence_dao.h" #include "dao/evidence_entity_dao.h" #include "dao/entity_dao.h" #include "dao/relation_dao.h" #include "dao/relation_evidence_dao.h" #include "dao/osint_execution_dao.h" #include "dao/extraction_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 "core/relation_service.h" #include "core/social_account_service.h" #include "core/person_entity_service.h" #include "core/person_creation_coordinator.h" #include "core/person_creation_task.h" #include "core/person_creation_guard.h" #include "core/eml_processing.h" #include "core/eml_integration.h" #include "core/eml_pipeline_task.h" #include "core/file_hash.h" #include "database/database.h" #include "database/transaction.h" #include "views/create_social_account_dialog.h" #include "views/create_person_dialog.h" #include "views/eml_analysis_dialog.h" #include "views/manage_entity_evidence_dialog.h" #include "views/rib_ocr_review_dialog.h" #include "core/rib_ocr.h" #include "core/iban_analyzer.h" #include "core/exiftool_metadata.h" #include "views/metadata_analysis_dialog.h" #include "core/pdf_password_recovery.h" #include "core/extraction_drop_service.h" #include "views/pdf_password_dialog.h" #include #include #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; guint64 session_generation; gboolean graph_viewport_restored; guint graph_viewport_save_source_id; char *selected_evidence_identifier; char *pending_relation_selection_identifier; char *pending_entity_selection_identifier; }; typedef struct { Application *application; PersonCreationGuard *guard; } ApplicationPersonDialogContext; typedef struct { Application *application; guint64 generation; char *root_path; char *database_path; EvidenceStaging *staging; } ApplicationPersonCreationTaskContext; static void application_person_dialog_context_free( ApplicationPersonDialogContext *context) { if (context == NULL) return; person_creation_guard_free(context->guard); g_free(context); } static void application_person_creation_task_context_free(gpointer data) { ApplicationPersonCreationTaskContext *context = data; if (context == NULL) return; g_free(context->root_path); g_free(context->database_path); evidence_staging_free(context->staging); g_free(context); } static void application_on_evidence_selected( const char *evidence_identifier, gpointer user_data); typedef struct { Application *application; char *observation_identifier; } ApplicationObservationRemovalContext; static void application_observation_removal_context_free( ApplicationObservationRemovalContext *context) { if (context == NULL) return; g_free(context->observation_identifier); g_free(context); } /** * @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import. */ typedef struct { Application *application; char *investigation_root_path; gpointer batch_context; } ApplicationEvidenceImportContext; /** @brief État d'un import groupé traité séquentiellement. */ typedef struct { Application *application; GPtrArray *file_paths; GPtrArray *evidence_types; GPtrArray *failures; guint current_index; guint success_count; guint skipped_count; } ApplicationEvidenceBatchContext; /** * @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 possédé pendant l'édition d'une preuve. */ typedef struct { Application *application; char *evidence_identifier; } ApplicationEvidenceMetadataContext; /** @brief Données d'actualisation différée après reclassement. */ typedef struct { Application *application; char *root_path; } ApplicationEvidenceRefreshContext; /** * @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 Contexte conservé jusqu'à la fin d'une action OSINT. */ typedef struct { Application *application; char *action_identifier; char *target_identifier; char *target_value; char *started_at; } ApplicationOsintActionContext; typedef struct { Application *application; ExtractionDropInfo *info; char *target_entity_identifier; } ApplicationExtractionDropContext; /** @brief Données possédées par l'étape de révision OSINT. */ typedef struct { Application *application; char *source_entity_identifier; char *execution_identifier; GPtrArray *proposals; } ApplicationOsintReviewContext; static char *application_osint_output_to_utf8(GBytes *bytes); static char *application_osint_create_timestamp(void); /** @brief Contexte possédé pendant la révision d'une analyse EML. */ typedef struct { Application *application; InvestigationSession *expected_session; char *evidence_identifier; char *evidence_name; char *relative_path; char *source_sha256; char *staging_directory; } ApplicationEmlReviewContext; static void application_start_graph_loading( Application *application, const char *database_path ); static void application_save_graph_viewport(Application *application) { GraphNodePositionDao *dao = NULL; GError *error = NULL; double zoom = 0.0; double offset_x = 0.0; double offset_y = 0.0; if (application == NULL || application->session == NULL || application->main_window == NULL || !main_window_get_graph_view_transform(application->main_window, &zoom, &offset_x, &offset_y)) return; dao = graph_node_position_dao_new( investigation_session_get_database(application->session), &error); if (dao == NULL || !graph_node_position_dao_upsert_viewport( dao, zoom, offset_x, offset_y, &error)) g_warning("Impossible d'enregistrer le cadrage du graphe : %s", error != NULL ? error->message : "erreur inconnue"); graph_node_position_dao_free(dao); g_clear_error(&error); } static gboolean application_save_graph_viewport_timeout(gpointer user_data) { Application *application = user_data; if (application == NULL) return G_SOURCE_REMOVE; application->graph_viewport_save_source_id = 0U; application_save_graph_viewport(application); return G_SOURCE_REMOVE; } static void application_on_graph_transform_changed(gpointer user_data) { Application *application = user_data; if (application == NULL) return; if (application->graph_viewport_save_source_id != 0U) g_source_remove(application->graph_viewport_save_source_id); application->graph_viewport_save_source_id = g_timeout_add( 250U, application_save_graph_viewport_timeout, application); } static void application_restore_graph_viewport(Application *application) { GraphNodePositionDao *dao = NULL; GError *error = NULL; gboolean found = FALSE; double zoom = 0.0; double offset_x = 0.0; double offset_y = 0.0; if (application == NULL || application->graph_viewport_restored || application->session == NULL || application->main_window == NULL) return; dao = graph_node_position_dao_new( investigation_session_get_database(application->session), &error); if (dao != NULL && graph_node_position_dao_load_viewport( dao, &zoom, &offset_x, &offset_y, &found, &error) && found) main_window_set_graph_view_transform( application->main_window, zoom, offset_x, offset_y); else if (error != NULL) g_warning("Impossible de restaurer le cadrage du graphe : %s", error->message); application->graph_viewport_restored = TRUE; graph_node_position_dao_free(dao); g_clear_error(&error); } static void application_present_error( Application *application, const char *title, const char *message ); static void application_extraction_drop_context_free( ApplicationExtractionDropContext *context) { if (context == NULL) return; extraction_drop_info_free(context->info); g_free(context->target_entity_identifier); g_free(context); } static void application_on_extraction_choice_finished( GObject *source_object, GAsyncResult *result, gpointer user_data) { ApplicationExtractionDropContext *context = user_data; Application *application = context != NULL ? context->application : NULL; GtkAlertDialog *dialog = GTK_ALERT_DIALOG(source_object); const InvestigationProject *project = NULL; Database *database = NULL; GError *error = NULL; int choice = gtk_alert_dialog_choose_finish(dialog, result, &error); gboolean success = FALSE; if (application == NULL || application->session == NULL) goto cleanup; if (error != NULL || choice == 0) { main_window_set_status(application->main_window, "Dépôt de l'extraction annulé."); goto cleanup; } database = investigation_session_get_database(application->session); if (choice == 1) success = extraction_drop_service_create_entity(database, context->info, NULL, &error); else if (choice == 2 && context->target_entity_identifier != NULL) success = extraction_drop_service_attach(database, context->info, context->target_entity_identifier, &error); else g_set_error_literal(&error, EXTRACTION_DROP_SERVICE_ERROR, EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT, "Déposez l'extraction directement sur une entité pour la rattacher."); if (!success) { application_present_error(application, "Intégration de l'extraction impossible", error != NULL ? error->message : "L'association n'a pas été créée."); goto cleanup; } main_window_set_status(application->main_window, choice == 1 ? "Entité créée depuis l'extraction." : "Extraction rattachée à l'entité."); project = investigation_session_get_project(application->session); if (project != NULL) application_start_graph_loading(application, investigation_project_get_database_path(project)); cleanup: g_clear_error(&error); application_extraction_drop_context_free(context); } static void application_on_extraction_dropped( const char *file_path, const char *target_entity_identifier, gpointer user_data) { Application *application = user_data; ApplicationExtractionDropContext *context = NULL; const InvestigationProject *project = NULL; GtkAlertDialog *dialog = NULL; const char *buttons[] = { "Annuler", "Créer une entité", "Rattacher à l'entité", NULL }; char *details = NULL; char *content_preview = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; project = investigation_session_get_project(application->session); context = g_new0(ApplicationExtractionDropContext, 1); context->application = application; context->target_entity_identifier = g_strdup(target_entity_identifier); context->info = extraction_drop_service_prepare( investigation_project_get_root_path(project), file_path, &error); if (context->info == NULL) { application_present_error(application, "Dépôt refusé", error != NULL ? error->message : "Extraction invalide."); g_clear_error(&error); application_extraction_drop_context_free(context); return; } { const char *content = extraction_drop_info_get_content(context->info); const char *preview_end = content; guint line_count = 0U; while (preview_end != NULL && *preview_end != '\0' && (gsize) (preview_end - content) < 800U && line_count < 12U) { if (*preview_end == '\n') line_count++; preview_end = g_utf8_next_char(preview_end); } content_preview = g_strndup(content, preview_end != NULL ? (gsize) (preview_end - content) : 0U); if (preview_end != NULL && *preview_end != '\0') { char *truncated_preview = g_strconcat(content_preview, "\n\n[… aperçu tronqué, le fichier complet sera conservé]", NULL); g_free(content_preview); content_preview = truncated_preview; } } details = g_strdup_printf("Fichier : %s\nSource : %s\n\n%s", extraction_drop_info_get_file_name(context->info), extraction_drop_info_get_source(context->info), content_preview != NULL ? content_preview : ""); dialog = gtk_alert_dialog_new("Intégrer cette extraction au graphe ?"); gtk_alert_dialog_set_detail(dialog, details); gtk_alert_dialog_set_buttons(dialog, buttons); gtk_alert_dialog_set_cancel_button(dialog, 0); gtk_alert_dialog_set_default_button(dialog, 0); gtk_alert_dialog_choose(dialog, main_window_get_window(application->main_window), NULL, application_on_extraction_choice_finished, context); g_object_unref(dialog); g_free(details); g_free(content_preview); } static void application_present_next_batch_metadata( ApplicationEvidenceBatchContext *context ); static void application_evidence_batch_context_free( ApplicationEvidenceBatchContext *context ); /** @brief Libère les données de révision possédées par le dialogue. */ static void application_osint_review_context_free(gpointer user_data) { ApplicationOsintReviewContext *context = user_data; if (context == NULL) return; g_clear_pointer(&context->proposals, g_ptr_array_unref); g_free(context->execution_identifier); g_free(context->source_entity_identifier); g_free(context); } /** @brief Démarre ou abandonne la préparation après confirmation globale. */ static void application_on_batch_selection_confirmed( gboolean confirmed, gpointer user_data ) { ApplicationEvidenceBatchContext *context = user_data; if (context == NULL) return; if (confirmed) application_present_next_batch_metadata(context); else { main_window_set_import_evidence_enabled( context->application->main_window, TRUE); main_window_set_status(context->application->main_window, "Import groupé annulé avant toute copie."); application_evidence_batch_context_free(context); } } /** @brief Intègre les propositions confirmées puis recharge le graphe. */ static void application_on_osint_integration_confirmed( GPtrArray *selected_proposals, gpointer user_data ) { ApplicationOsintReviewContext *review_context = user_data; Application *application = review_context != NULL ? review_context->application : NULL; const InvestigationProject *project = NULL; Database *database = NULL; const char *database_path = NULL; guint inserted_count = 0U; guint skipped_count = 0U; guint inserted_relation_count = 0U; guint skipped_relation_count = 0U; char *message = NULL; GError *error = NULL; if (application == NULL || review_context->source_entity_identifier == NULL || application->session == NULL || application->main_window == NULL) { return; } database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); database_path = project != NULL ? investigation_project_get_database_path(project) : NULL; if (!osint_dns_integration_apply( database, review_context->source_entity_identifier, review_context->execution_identifier, selected_proposals, &inserted_count, &skipped_count, &inserted_relation_count, &skipped_relation_count, &error )) { application_present_error( application, "Intégration DNS impossible", error != NULL ? error->message : "La transaction DNS a échoué." ); g_clear_error(&error); return; } message = g_strdup_printf( "%u entité(s) créée(s), %u entité(s) réutilisée(s) ou ignorée(s).\n" "%u relation(s) créée(s), %u relation(s) ignorée(s).\n\n" "Les objets créés ou réutilisés sont liés à l'exécution OSINT %s.", inserted_count, skipped_count, inserted_relation_count, skipped_relation_count, review_context->execution_identifier ); application_message_dialog_present( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Intégration DNS terminée", message ); g_free(message); if (database_path != NULL && database_path[0] != '\0') application_start_graph_loading(application, database_path); } /** @brief Affiche les propositions et demande une sélection explicite. */ static void application_on_osint_prepare_integration(gpointer user_data) { ApplicationOsintReviewContext *context = user_data; if (context == NULL || context->application == NULL || context->application->main_window == NULL || context->proposals == NULL) return; osint_dns_review_dialog_present( main_window_get_window(context->application->main_window), context->proposals, application_on_osint_integration_confirmed, context ); } /** * @brief Libère le contexte d'une action OSINT. */ static void application_osint_action_context_free( gpointer user_data ) { ApplicationOsintActionContext *context = user_data; if (context == NULL) { return; } g_free(context->target_value); g_free(context->target_identifier); g_free(context->action_identifier); g_free(context->started_at); g_free(context); } /** @brief Extrait un pseudo final depuis une URL de profil social. */ static char *application_osint_search_target(const char *action_identifier, const char *target_value) { char *target = g_strdup(target_value != NULL ? target_value : ""); char *query = NULL; char *slash = NULL; if (target == NULL) return NULL; if (g_str_has_prefix(action_identifier, "social-")) { query = strchr(target, '?'); if (query != NULL) *query = '\0'; slash = strrchr(target, '/'); if (slash != NULL && slash[1] != '\0') { char *username = g_strdup(slash + 1); g_free(target); target = username; } } return target; } /** @brief Présente et archive la sortie d'une recherche sociale/email. */ static void application_on_osint_search_completed(BackgroundTask *task, gpointer user_data) { ApplicationOsintActionContext *context = user_data; const ToolTaskResult *task_result = NULL; const ToolProcessResult *process_result = NULL; GBytes *stdout_bytes = NULL; GBytes *stderr_bytes = NULL; char *stdout_text = NULL; char *stderr_text = NULL; char *details = NULL; char *output_directory = NULL; char *output_name = NULL; char *output_path = NULL; guint output_suffix = 0; const InvestigationProject *project = NULL; GError *error = NULL; if (context == NULL || context->application == NULL) return; if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED) { main_window_set_status(context->application->main_window, "Recherche OSINT annulée."); return; } if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) { error = background_task_dup_error(task); application_present_error(context->application, "Recherche OSINT impossible", error != NULL ? error->message : "L'outil OSINT a échoué."); g_clear_error(&error); return; } task_result = tool_task_result_from_background_task(task); process_result = tool_task_result_get_process_result(task_result); stdout_bytes = tool_process_result_ref_stdout(process_result); stderr_bytes = tool_process_result_ref_stderr(process_result); stdout_text = application_osint_output_to_utf8(stdout_bytes); stderr_text = application_osint_output_to_utf8(stderr_bytes); project = investigation_session_get_project(context->application->session); output_directory = project != NULL ? g_build_filename( investigation_project_get_root_path(project), "02_Preuves_Traitees", "Extractions", "OSINT", NULL) : NULL; if (output_directory == NULL || g_mkdir_with_parents(output_directory, 0750) != 0) goto failure; output_name = g_strdup_printf("%s-%s.txt", context->target_identifier, context->action_identifier); output_path = g_build_filename(output_directory, output_name, NULL); while (g_file_test(output_path, G_FILE_TEST_EXISTS)) { char *unique_name = g_strdup_printf("%s-%s (%u).txt", context->target_identifier, context->action_identifier, ++output_suffix); g_free(output_name); g_free(output_path); output_name = unique_name; output_path = g_build_filename(output_directory, output_name, NULL); } details = g_strdup_printf("Cible : %s\nAction : %s\n\n%s%s%s", context->target_value, context->action_identifier, stdout_text != NULL ? stdout_text : "", stderr_text != NULL && stderr_text[0] != '\0' ? "\n\nErreur :\n" : "", stderr_text != NULL ? stderr_text : ""); if (!g_file_set_contents(output_path, details, -1, &error)) goto failure; { ExtractionDao *extraction_dao = extraction_dao_new( investigation_session_get_database(context->application->session), &error); char *extraction_id = g_uuid_string_random(); char *created_at = application_osint_create_timestamp(); if (extraction_dao == NULL || extraction_id == NULL || created_at == NULL || !extraction_dao_insert(extraction_dao, extraction_id, NULL, "entity", context->target_identifier, context->action_identifier, created_at, &error)) { extraction_dao_free(extraction_dao); g_free(extraction_id); g_free(created_at); goto failure; } extraction_dao_free(extraction_dao); g_free(extraction_id); g_free(created_at); } application_message_dialog_present_details_action( main_window_get_window(context->application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Résultat de la recherche OSINT", "La sortie publique a été archivée dans les preuves traitées.", details, NULL, NULL, NULL, NULL); goto cleanup; failure: application_present_error(context->application, "Archivage OSINT impossible", error != NULL ? error->message : "La sortie n'a pas pu être conservée."); cleanup: g_clear_error(&error); g_clear_pointer(&stdout_bytes, g_bytes_unref); g_clear_pointer(&stderr_bytes, g_bytes_unref); g_free(stdout_text); g_free(stderr_text); g_free(details); g_free(output_directory); g_free(output_name); g_free(output_path); } /** @brief Lance Sherlock, Maigret ou Holehe selon l'action choisie. */ static void application_start_osint_search(Application *application, const char *action_identifier, const char *target_identifier, const char *target_value) { const char *tool_identifier = NULL; const char *arguments[4] = { NULL, NULL, NULL, NULL }; char *search_target = NULL; ToolTask *tool_task = NULL; BackgroundTask *background_task = NULL; ApplicationOsintActionContext *context = NULL; GError *error = NULL; if (g_strcmp0(action_identifier, "social-username-sherlock") == 0) { tool_identifier = "osint.sherlock"; arguments[0] = "--print-found"; arguments[1] = "--no-color"; } else if (g_strcmp0(action_identifier, "social-username-maigret") == 0) { tool_identifier = "osint.maigret"; arguments[0] = "--json"; arguments[1] = "simple"; } else if (g_strcmp0(action_identifier, "email-account-holehe") == 0) { tool_identifier = "osint.holehe"; arguments[0] = "--no-color"; } else return; search_target = application_osint_search_target(action_identifier, target_value); arguments[2] = search_target; tool_task = tool_task_new(tool_initializer_get_registry(application->tool_initializer), tool_identifier, "Recherche OSINT", arguments, NULL, &error); if (tool_task == NULL) goto failure; context = g_new0(ApplicationOsintActionContext, 1); context->application = application; context->action_identifier = g_strdup(action_identifier); context->target_identifier = g_strdup(target_identifier); context->target_value = g_strdup(search_target); context->started_at = application_osint_create_timestamp(); background_task = tool_task_get_background_task(tool_task); if (context->action_identifier == NULL || context->target_identifier == NULL || context->target_value == NULL || context->started_at == NULL || !task_manager_add(application->task_manager, background_task, &error) || !tool_task_start(tool_task, application_on_osint_search_completed, context, application_osint_action_context_free, &error)) goto failure; context = NULL; main_window_set_status(application->main_window, "Recherche OSINT lancée ; progression disponible dans les tâches."); goto cleanup; failure: application_osint_action_context_free(context); application_present_error(application, "Recherche OSINT impossible", error != NULL ? error->message : "Impossible de démarrer l'outil."); cleanup: g_clear_error(&error); tool_task_free(tool_task); g_free(search_target); } /** * @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 l'état possédé d'un import groupé. */ static void application_evidence_batch_context_free( ApplicationEvidenceBatchContext *context ) { if (context == NULL) return; g_clear_pointer(&context->file_paths, g_ptr_array_unref); g_clear_pointer(&context->evidence_types, g_ptr_array_unref); g_clear_pointer(&context->failures, g_ptr_array_unref); g_free(context); } /** @brief Affiche le bilan final puis libère l'import groupé. */ static void application_finish_evidence_batch( ApplicationEvidenceBatchContext *context ) { GString *details = NULL; char *message = NULL; if (context == NULL) return; details = g_string_new(NULL); for (guint index = 0U; index < context->failures->len; index++) g_string_append_printf(details, "- %s\n", (const char *) g_ptr_array_index(context->failures, index)); message = g_strdup_printf( "%u preuve(s) importée(s), %u ignorée(s), %u échec(s).", context->success_count, context->skipped_count, context->failures->len); application_message_dialog_present_details( main_window_get_window(context->application->main_window), context->failures->len == 0U ? APPLICATION_MESSAGE_DIALOG_INFORMATION : APPLICATION_MESSAGE_DIALOG_WARNING, "Import groupé terminé", message, details->len > 0U ? details->str : "Tous les fichiers ont été importés."); main_window_set_import_evidence_enabled( context->application->main_window, TRUE); g_free(message); g_string_free(details, TRUE); application_evidence_batch_context_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 ); application_restore_graph_viewport(application); if (application->pending_relation_selection_identifier != NULL) { main_window_select_graph_relation(application->main_window, application->pending_relation_selection_identifier); g_clear_pointer(&application->pending_relation_selection_identifier, g_free); } if (application->pending_entity_selection_identifier != NULL) { main_window_select_graph_entity(application->main_window, application->pending_entity_selection_identifier); g_clear_pointer(&application->pending_entity_selection_identifier, g_free); } 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 Publie dans l'interface les états du registre d'outils. */ static void application_publish_osint_tool_states( Application *application ) { ToolRegistry *tool_registry = NULL; gsize tool_index = 0; if (application == NULL || application->main_window == NULL || application->tool_initializer == NULL) { return; } tool_registry = tool_initializer_get_registry(application->tool_initializer); for (tool_index = 0; tool_registry != NULL && tool_index < tool_registry_get_count(tool_registry); tool_index++) { const ToolInfo *tool_info = tool_registry_get_tool(tool_registry, tool_index); OsintActionToolState state = OSINT_ACTION_TOOL_STATE_UNKNOWN; if (tool_info_get_availability(tool_info) == TOOL_AVAILABILITY_AVAILABLE) state = OSINT_ACTION_TOOL_STATE_AVAILABLE; else if (tool_info_get_availability(tool_info) == TOOL_AVAILABILITY_MISSING) state = OSINT_ACTION_TOOL_STATE_MISSING; main_window_set_osint_tool_state( application->main_window, tool_info_get_identifier(tool_info), state, tool_info_get_detected_version(tool_info) ); } } /** * @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; } application_publish_osint_tool_states(application); } /** * @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 Convertit une sortie binaire d'outil en texte UTF-8 affichable. * * @param bytes Octets empruntés, ou NULL. * * @return Nouvelle chaîne UTF-8, jamais NULL. */ static char *application_osint_output_to_utf8( GBytes *bytes ) { gconstpointer data = NULL; gsize data_size = 0; if (bytes == NULL) { return g_strdup(""); } data = g_bytes_get_data(bytes, &data_size); if (data == NULL || data_size == 0U) { return g_strdup(""); } return g_utf8_make_valid(data, (gssize) data_size); } /** @brief Crée une date UTC au format persistant du projet. */ static char *application_osint_create_timestamp(void) { GDateTime *now = g_date_time_new_now_utc(); char *timestamp = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL; g_clear_pointer(&now, g_date_time_unref); return timestamp; } /** @brief Persiste une exécution DNS terminée et retourne son UUID. */ static char *application_persist_dns_execution( ApplicationOsintActionContext *context, const char *final_state, gboolean has_exit_code, gint exit_code, GBytes *stdout_raw, GBytes *stderr_raw, GError **error ) { Application *application = context != NULL ? context->application : NULL; ToolRegistry *registry = NULL; const ToolInfo *tool_info = NULL; Database *database = NULL; OsintExecutionDao *dao = NULL; OsintExecutionRecord *record = NULL; char *identifier = NULL; char *finished_at = NULL; char *arguments = NULL; char *sha256 = NULL; gboolean inserted = FALSE; if (application == NULL || application->session == NULL) return NULL; database = investigation_session_get_database(application->session); registry = tool_initializer_get_registry(application->tool_initializer); tool_info = tool_registry_find(registry, "dns.dig"); identifier = g_uuid_string_random(); finished_at = application_osint_create_timestamp(); arguments = g_strdup_printf( "[\"+noall\",\"+answer\",\"%s\"]", context->target_value ); sha256 = osint_execution_integrity_calculate(stdout_raw, stderr_raw); record = osint_execution_record_new( identifier, "dns.dig", tool_info != NULL ? tool_info_get_detected_version(tool_info) : NULL, "dns-preview", context->target_identifier, "entity", context->target_value, arguments, context->started_at, finished_at, has_exit_code, exit_code, final_state, stdout_raw, stderr_raw, sha256, error ); dao = osint_execution_dao_new(database, error); if (record != NULL && dao != NULL) inserted = osint_execution_dao_insert(dao, record, error); osint_execution_dao_free(dao); osint_execution_record_free(record); g_free(finished_at); g_free(arguments); g_free(sha256); if (!inserted) g_clear_pointer(&identifier, g_free); return identifier; } /** * @brief Présente le résultat final d'une résolution DNS. */ static void application_on_dns_lookup_completed( BackgroundTask *task, gpointer user_data ) { ApplicationOsintActionContext *context = user_data; Application *application = NULL; const ToolTaskResult *task_result = NULL; const ToolProcessResult *process_result = NULL; GBytes *stdout_bytes = NULL; GBytes *stderr_bytes = NULL; char *stdout_text = NULL; char *stderr_text = NULL; char *details = NULL; char *message = NULL; char *execution_identifier = NULL; GPtrArray *proposals = NULL; ApplicationOsintReviewContext *review_context = NULL; GError *error = NULL; const ToolInfo *dns_tool_info = NULL; if (task == NULL || context == NULL || context->application == NULL) { return; } application = context->application; if (application->main_window == NULL) { return; } dns_tool_info = tool_registry_find( tool_initializer_get_registry(application->tool_initializer), "dns.dig" ); if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED) { GBytes *empty_output = g_bytes_new_static("", 0U); execution_identifier = application_persist_dns_execution( context, "cancelled", FALSE, -1, empty_output, empty_output, &error ); g_bytes_unref(empty_output); application_message_dialog_present( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Résolution DNS annulée", execution_identifier != NULL ? "La tâche a été annulée et sa provenance a été enregistrée." : "La tâche a été annulée, mais sa provenance n'a pas pu être enregistrée." ); g_free(execution_identifier); g_clear_error(&error); return; } if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) { GBytes *empty_output = g_bytes_new_static("", 0U); execution_identifier = application_persist_dns_execution( context, "failed", FALSE, -1, empty_output, empty_output, NULL ); g_bytes_unref(empty_output); error = background_task_dup_error(task); application_present_error( application, "Résolution DNS impossible", error != NULL ? error->message : "L'exécution de dig a été annulée ou a échoué." ); g_free(execution_identifier); g_clear_error(&error); return; } task_result = tool_task_result_from_background_task(task); process_result = tool_task_result_get_process_result(task_result); if (process_result == NULL) { application_present_error( application, "Résolution DNS incomplète", "La tâche s'est terminée sans résultat exploitable." ); return; } stdout_bytes = tool_process_result_ref_stdout(process_result); stderr_bytes = tool_process_result_ref_stderr(process_result); execution_identifier = application_persist_dns_execution( context, "completed", TRUE, tool_process_result_get_exit_status(process_result), stdout_bytes, stderr_bytes, &error ); if (execution_identifier == NULL) { application_present_error( application, "Conservation OSINT impossible", error != NULL ? error->message : "La sortie brute n'a pas pu être enregistrée." ); g_clear_error(&error); g_clear_pointer(&stdout_bytes, g_bytes_unref); return; } stdout_text = application_osint_output_to_utf8(stdout_bytes); stderr_text = application_osint_output_to_utf8(stderr_bytes); if (stdout_text != NULL && stdout_text[0] != '\0' && stderr_text != NULL && stderr_text[0] != '\0') { details = g_strdup_printf( "Sortie standard :\n%s\n\nSortie d'erreur :\n%s", stdout_text, stderr_text ); } else if (stdout_text != NULL && stdout_text[0] != '\0') { details = g_strdup(stdout_text); } else if (stderr_text != NULL && stderr_text[0] != '\0') { details = g_strdup(stderr_text); } else { details = g_strdup("Aucun enregistrement DNS retourné."); } proposals = osint_dns_proposal_parse(context->target_value, stdout_text); if (tool_process_result_is_success(process_result) && proposals != NULL && proposals->len > 0U) { review_context = g_try_new0(ApplicationOsintReviewContext, 1); if (review_context != NULL) { review_context->application = application; review_context->source_entity_identifier = g_strdup( context->target_identifier ); review_context->execution_identifier = g_strdup( execution_identifier ); review_context->proposals = g_ptr_array_ref(proposals); } if (review_context == NULL || review_context->proposals == NULL || review_context->source_entity_identifier == NULL || review_context->execution_identifier == NULL) { application_osint_review_context_free(review_context); review_context = NULL; } } message = g_strdup_printf( "Cible : %s\nOutil : dns.dig%s%s\nDébut : %s\nCode de sortie : %d\n" "Exécution : %s\nSortie brute enregistrée avec son empreinte SHA-256.", context->target_value, dns_tool_info != NULL && tool_info_get_detected_version(dns_tool_info) != NULL ? " — " : "", dns_tool_info != NULL && tool_info_get_detected_version(dns_tool_info) != NULL ? tool_info_get_detected_version(dns_tool_info) : "", context->started_at, tool_process_result_get_exit_status(process_result), execution_identifier ); application_message_dialog_present_details_action( main_window_get_window(application->main_window), tool_process_result_is_success(process_result) ? APPLICATION_MESSAGE_DIALOG_INFORMATION : APPLICATION_MESSAGE_DIALOG_WARNING, "Résultat de la résolution DNS", message, details, review_context != NULL ? "Préparer l'intégration" : NULL, review_context != NULL ? application_on_osint_prepare_integration : NULL, review_context, application_osint_review_context_free ); g_clear_pointer(&stdout_bytes, g_bytes_unref); g_clear_pointer(&stderr_bytes, g_bytes_unref); g_free(stdout_text); g_free(stderr_text); g_free(details); g_free(message); g_free(execution_identifier); g_clear_pointer(&proposals, g_ptr_array_unref); } /** * @brief Prépare et démarre une résolution DNS avec dig. */ static void application_start_dns_lookup( Application *application, const char *target_identifier, const char *target_value ) { ToolRegistry *tool_registry = NULL; ToolTask *tool_task = NULL; BackgroundTask *background_task = NULL; ApplicationOsintActionContext *context = NULL; const char *arguments[] = {"+noall", "+answer", target_value, NULL}; GError *error = NULL; if (application == NULL || application->task_manager == NULL || application->tool_initializer == NULL || target_value == NULL || target_identifier == NULL || !g_uuid_string_is_valid(target_identifier) || !osint_dns_query_is_valid_target(target_value)) { if (application != NULL) { application_present_error( application, "Résolution DNS impossible", "Le nom de domaine sélectionné est invalide." ); } return; } tool_registry = tool_initializer_get_registry(application->tool_initializer); tool_task = tool_task_new( tool_registry, "dns.dig", "Résolution DNS", arguments, NULL, &error ); if (tool_task == NULL) { application_present_error( application, "Résolution DNS impossible", error != NULL ? error->message : "Impossible de préparer dig." ); g_clear_error(&error); return; } context = g_try_new0(ApplicationOsintActionContext, 1); if (context != NULL) { context->application = application; context->target_identifier = g_strdup(target_identifier); context->target_value = g_strdup(target_value); context->started_at = application_osint_create_timestamp(); } if (context == NULL || context->target_identifier == NULL || context->target_value == NULL || context->started_at == NULL) { application_osint_action_context_free(context); tool_task_free(tool_task); application_present_error( application, "Résolution DNS impossible", "Impossible de conserver le contexte de l'action." ); return; } background_task = tool_task_get_background_task(tool_task); if (!task_manager_add(application->task_manager, background_task, &error) || !tool_task_start( tool_task, application_on_dns_lookup_completed, context, application_osint_action_context_free, &error )) { task_manager_remove(application->task_manager, background_task); application_osint_action_context_free(context); application_present_error( application, "Résolution DNS impossible", error != NULL ? error->message : "Impossible de démarrer dig." ); g_clear_error(&error); tool_task_free(tool_task); return; } tool_task_free(tool_task); } /** @brief Charge et présente l'historique OSINT d'une sélection. */ static void application_present_osint_history( Application *application, const char *selection_kind, const char *selection_identifier ) { Database *database = NULL; OsintExecutionDao *dao = NULL; GPtrArray *records = NULL; GHashTable *linked_objects = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || application->main_window == NULL) return; database = investigation_session_get_database(application->session); dao = osint_execution_dao_new(database, &error); if (dao != NULL) records = osint_execution_dao_list_by_selection( dao, selection_kind, selection_identifier, &error); linked_objects = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); for (guint index = 0U; records != NULL && index < records->len; index++) { const OsintExecutionRecord *record = g_ptr_array_index(records, index); GPtrArray *objects = osint_execution_dao_list_linked_objects( dao, osint_execution_record_get_identifier(record), &error); if (objects == NULL) break; g_hash_table_insert(linked_objects, g_strdup(osint_execution_record_get_identifier(record)), objects); } osint_execution_dao_free(dao); if (error != NULL) { application_present_error(application, "Historique OSINT indisponible", error->message); g_clear_error(&error); } else if (records == NULL || records->len == 0U) { application_message_dialog_present( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Historique OSINT", "Aucune exécution OSINT n'est enregistrée pour cette sélection." ); } else { osint_execution_history_dialog_present( main_window_get_window(application->main_window), records, linked_objects); } g_clear_pointer(&records, g_ptr_array_unref); g_hash_table_unref(linked_objects); } /** * @brief Traite une action OSINT relayée par la fenêtre principale. */ static void application_on_osint_action_requested( const char *action_identifier, const char *target_identifier, const char *target_value, gpointer user_data ) { Application *application = user_data; if (g_strcmp0(action_identifier, "history-entity") == 0 || g_strcmp0(action_identifier, "history-relation") == 0) { application_present_osint_history( application, g_strcmp0(action_identifier, "history-relation") == 0 ? "relation" : "entity", target_identifier ); return; } if (g_strcmp0(action_identifier, "dns-preview") == 0) { application_start_dns_lookup( application, target_identifier, target_value ); return; } if (g_strcmp0(action_identifier, "social-username-sherlock") == 0 || g_strcmp0(action_identifier, "social-username-maigret") == 0 || g_strcmp0(action_identifier, "email-account-holehe") == 0) { application_start_osint_search(application, action_identifier, target_identifier, target_value); return; } application_present_error( application, "Action OSINT inconnue", "Cette action n'est pas encore prise en charge." ); } /** * @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 ); if (application->graph_viewport_save_source_id != 0U) { g_source_remove(application->graph_viewport_save_source_id); application->graph_viewport_save_source_id = 0U; } application_save_graph_viewport(application); task_manager_cancel_all(application->task_manager); investigation_session_close( application->session ); application->tree_model = new_tree_model; application->session = new_session; application->session_generation++; application->graph_viewport_restored = FALSE; 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 ); main_window_set_add_social_account_enabled( application->main_window, TRUE ); main_window_set_add_person_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; ApplicationEvidenceBatchContext *batch_context = NULL; if (task == NULL || context == NULL) { return; } application = context->application; batch_context = context->batch_context; if (application == NULL || application->main_window == NULL) { return; } if (batch_context == NULL) 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) { if (batch_context != NULL) { g_ptr_array_add(batch_context->failures, g_strdup( "Tâche terminée sans preuve exploitable.")); batch_context->current_index++; application_present_next_batch_metadata(batch_context); return; } 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 ); if (batch_context != NULL) { batch_context->success_count++; batch_context->current_index++; application_present_next_batch_metadata(batch_context); } return; } if (state == BACKGROUND_TASK_STATE_CANCELLED) { if (batch_context != NULL) { batch_context->skipped_count++; batch_context->current_index++; application_present_next_batch_metadata(batch_context); return; } 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" ); if (batch_context != NULL) { const char *path = batch_context->current_index < batch_context->file_paths->len ? g_ptr_array_index(batch_context->file_paths, batch_context->current_index) : "fichier inconnu"; char *failure = g_strdup_printf("%s : %s", path, error != NULL ? error->message : "échec de l'import"); g_ptr_array_add(batch_context->failures, failure); batch_context->current_index++; application_present_next_batch_metadata(batch_context); } else 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 Libère le contexte d'édition des métadonnées. */ static void application_evidence_metadata_context_free( ApplicationEvidenceMetadataContext *context ) { if (context == NULL) return; g_free(context->evidence_identifier); g_free(context); } /** @brief Actualise les vues après la destruction du dialogue d'édition. */ static gboolean application_refresh_after_evidence_reclassification( gpointer user_data ) { ApplicationEvidenceRefreshContext *context = user_data; GError *error = NULL; if (context != NULL && context->application != NULL && context->application->main_window != NULL) { application_refresh_investigation_tree( context->application, context->root_path); application_refresh_evidence_models(context->application, &error); main_window_set_selected_evidence( context->application->main_window, NULL); application_message_dialog_present( main_window_get_window(context->application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Preuve reclassée", "Les métadonnées et le dossier ont été mis à jour. Le SHA-256 est inchangé."); } g_clear_error(&error); if (context != NULL) { g_free(context->root_path); g_free(context); } return G_SOURCE_REMOVE; } /** @brief Applique le reclassement confirmé puis actualise les vues. */ static void application_on_evidence_metadata_edited( EvidenceMetadataDialogResult *result, gpointer user_data ) { ApplicationEvidenceMetadataContext *context = user_data; Application *application = context != NULL ? context->application : NULL; const InvestigationProject *project = NULL; const char *root_path = NULL; const char *subdirectory = NULL; char *relative_directory = NULL; GError *error = NULL; if (result == NULL) { application_evidence_metadata_context_free(context); return; } if (application == NULL || application->session == NULL) goto cleanup; project = investigation_session_get_project(application->session); root_path = project != NULL ? investigation_project_get_root_path(project) : NULL; subdirectory = application_get_evidence_subdirectory( evidence_metadata_dialog_result_get_type_identifier(result)); relative_directory = g_build_filename( APPLICATION_EVIDENCE_ROOT_DIRECTORY, subdirectory, NULL); if (!evidence_reclassification_apply( investigation_session_get_database(application->session), root_path, context->evidence_identifier, evidence_metadata_dialog_result_get_type_identifier(result), relative_directory, evidence_metadata_dialog_result_get_source(result), evidence_metadata_dialog_result_get_description(result), &error)) { application_present_error(application, "Modification impossible", error != NULL ? error->message : "Le reclassement a échoué."); g_clear_error(&error); goto cleanup; } { ApplicationEvidenceRefreshContext *refresh_context = g_new0(ApplicationEvidenceRefreshContext, 1); refresh_context->application = application; refresh_context->root_path = g_strdup(root_path); g_timeout_add(50U, application_refresh_after_evidence_reclassification, refresh_context); } cleanup: g_free(relative_directory); evidence_metadata_dialog_result_free(result); application_evidence_metadata_context_free(context); } /** @brief Charge et présente les métadonnées éditables d'une preuve. */ static void application_on_edit_evidence_requested( const char *evidence_identifier, gpointer user_data ) { Application *application = user_data; Database *database = NULL; EvidenceDao *evidence_dao = NULL; EvidenceTypeDao *type_dao = NULL; EvidenceRecord *record = NULL; GPtrArray *types = NULL; ApplicationEvidenceMetadataContext *context = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || evidence_identifier == NULL) return; database = investigation_session_get_database(application->session); evidence_dao = evidence_dao_new(database, &error); type_dao = evidence_type_dao_new(database, &error); if (evidence_dao != NULL) record = evidence_dao_find_by_identifier( evidence_dao, evidence_identifier, &error); if (type_dao != NULL) types = evidence_type_dao_list_all(type_dao, &error); if (record == NULL || types == NULL || types->len == 0U) { application_present_error(application, "Modification impossible", error != NULL ? error->message : "Les métadonnées sont indisponibles."); goto cleanup; } context = g_new0(ApplicationEvidenceMetadataContext, 1); context->application = application; context->evidence_identifier = g_strdup(evidence_identifier); if (!evidence_metadata_dialog_present( main_window_get_window(application->main_window), record, types, application_on_evidence_metadata_edited, context)) { application_evidence_metadata_context_free(context); context = NULL; application_present_error(application, "Modification impossible", "Le dialogue de métadonnées n'a pas pu être créé."); } cleanup: g_clear_error(&error); g_clear_pointer(&types, g_ptr_array_unref); evidence_record_free(record); evidence_type_dao_free(type_dao); evidence_dao_free(evidence_dao); } /** @brief Libère le contexte de révision EML. */ static void application_eml_review_context_free(ApplicationEmlReviewContext *context) { if (context == NULL) return; g_free(context->evidence_identifier); g_free(context->evidence_name); g_free(context->relative_path); g_free(context->source_sha256); g_free(context->staging_directory); g_free(context); } /** @brief Supprime récursivement une zone de staging créée par l'application. */ static void application_remove_staging_directory(const char *path) { if (path == NULL) return; GFile *directory = g_file_new_for_path(path); GError *error = NULL; GFileEnumerator *enumerator = g_file_enumerate_children(directory, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error); if (enumerator != NULL) { GFileInfo *info = NULL; while ((info = g_file_enumerator_next_file( enumerator, NULL, &error)) != NULL) { GFile *child = g_file_get_child(directory, g_file_info_get_name(info)); char *child_path = g_file_get_path(child); if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) application_remove_staging_directory(child_path); else (void) g_file_delete(child, NULL, NULL); g_free(child_path); g_object_unref(child); g_object_unref(info); } g_object_unref(enumerator); } g_clear_error(&error); (void) g_file_delete(directory, NULL, NULL); g_object_unref(directory); } /** @brief Intègre la sélection EML puis actualise le graphe. */ static void application_on_eml_selection_completed( GPtrArray *proposals, gpointer user_data) { ApplicationEmlReviewContext *context = user_data; Application *application = context != NULL ? context->application : NULL; const InvestigationProject *project = NULL; GError *error = NULL; guint observations = 0, created = 0, reused = 0; char *message = NULL; if (proposals == NULL) { application_remove_staging_directory(context != NULL ? context->staging_directory : NULL); application_eml_review_context_free(context); return; } if (application == NULL || application->session == NULL || !eml_integration_apply(investigation_session_get_database(application->session), context->evidence_identifier, proposals, &observations, &created, &reused, &error)) application_present_error(application, "Intégration EML impossible", error != NULL ? error->message : "La transaction a échoué."); else { message = g_strdup_printf( "%u observation(s) conservée(s), %u entité(s) créée(s), " "%u réutilisée(s).", observations, created, reused); application_message_dialog_present(main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message); if (created + reused > 0) { project = investigation_session_get_project(application->session); application_start_graph_loading(application, investigation_project_get_database_path(project)); } (void) application_refresh_evidence_models(application, NULL); application_on_evidence_selected(context->evidence_identifier, application); } g_free(message); g_clear_error(&error); g_ptr_array_unref(proposals); application_remove_staging_directory(context != NULL ? context->staging_directory : NULL); application_eml_review_context_free(context); } /** @brief Termine le pipeline EML sur le contexte GTK principal. */ static void application_on_eml_pipeline_completed( BackgroundTask *task, gpointer user_data) { ApplicationEmlReviewContext *context = user_data; Application *application = context != NULL ? context->application : NULL; BackgroundTaskState state = background_task_get_state(task); if (application == NULL || application->main_window == NULL || application->session == NULL || application->session != context->expected_session) { application_remove_staging_directory(context != NULL ? context->staging_directory : NULL); application_eml_review_context_free(context); return; } if (state == BACKGROUND_TASK_STATE_CANCELLED) { main_window_set_status(application->main_window, "Analyse de l’e-mail annulée."); application_remove_staging_directory(context->staging_directory); application_eml_review_context_free(context); return; } if (state != BACKGROUND_TASK_STATE_COMPLETED) { GError *error = background_task_dup_error(task); application_present_error(application, "Analyse EML impossible", error != NULL ? error->message : "Le pipeline EML a échoué."); g_clear_error(&error); application_remove_staging_directory(context->staging_directory); application_eml_review_context_free(context); return; } EmlPipelineResult *result = background_task_get_result(task); if (result == NULL) { application_present_error(application, "Analyse EML impossible", "Le pipeline n’a produit aucun résultat."); application_remove_staging_directory(context->staging_directory); application_eml_review_context_free(context); return; } eml_analysis_dialog_present_pipeline( main_window_get_window(application->main_window), result, context->evidence_name, context->relative_path, context->source_sha256, application_on_eml_selection_completed, context); } /** @brief Vérifie la preuve puis lance le pipeline EML en arrière-plan. */ static void application_on_analyze_eml_requested( const char *evidence_identifier, gpointer user_data) { Application *application = user_data; const InvestigationProject *project = NULL; EvidenceDao *dao = NULL; EvidenceRecord *record = NULL; ApplicationEmlReviewContext *context = NULL; BackgroundTask *task = NULL; char *root = NULL; char *candidate = NULL; char *source_path = NULL; char *current_sha256 = NULL; guint64 current_size = 0; char *staging = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || evidence_identifier == NULL) return; dao = evidence_dao_new( investigation_session_get_database(application->session), &error); if (dao != NULL) record = evidence_dao_find_by_identifier( dao, evidence_identifier, &error); project = investigation_session_get_project(application->session); if (record == NULL || project == NULL) goto failure; root = g_canonicalize_filename( investigation_project_get_root_path(project), NULL); candidate = g_build_filename(root, evidence_record_get_relative_path(record), NULL); source_path = g_canonicalize_filename(candidate, NULL); const char *name = evidence_record_get_original_name(record); char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL; gboolean is_eml = lower != NULL && g_str_has_suffix(lower, ".eml"); g_free(lower); if (!is_eml || source_path == NULL || !g_str_has_prefix(source_path, root) || (source_path[strlen(root)] != G_DIR_SEPARATOR && source_path[strlen(root)] != '\0') || !g_file_test(source_path, G_FILE_TEST_IS_REGULAR)) { g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "La preuve sélectionnée n’est pas un fichier EML exploitable."); goto failure; } if (!file_hash_compute_sha256(source_path, NULL, ¤t_sha256, ¤t_size, &error) || g_strcmp0(current_sha256, evidence_record_get_sha256(record)) != 0) { if (error == NULL) g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "L’empreinte SHA-256 de la preuve diverge de " "l’empreinte enregistrée. L’analyse est bloquée."); goto failure; } staging = g_dir_make_tmp("labfy-eml-staging-XXXXXX", &error); if (staging == NULL) goto failure; context = g_new0(ApplicationEmlReviewContext, 1); context->application = application; context->expected_session = application->session; context->evidence_identifier = g_strdup(evidence_identifier); context->evidence_name = g_strdup(name); context->relative_path = g_strdup( evidence_record_get_relative_path(record)); context->source_sha256 = g_strdup(current_sha256); context->staging_directory = staging; staging = NULL; DocumentAnalysisTools tools = { .exiftool = "exiftool", .tesseract = "tesseract", .pdfinfo = "pdfinfo", .pdftotext = "pdftotext", .pdftoppm = "pdftoppm" }; task = eml_pipeline_task_start(source_path, context->staging_directory, evidence_identifier, &tools, application_on_eml_pipeline_completed, context, NULL); if (task == NULL) goto failure; context = NULL; if (!task_manager_add(application->task_manager, task, &error)) { background_task_cancel(task); goto cleanup; } main_window_set_status(application->main_window, "Analyse de l’e-mail lancée en arrière-plan."); background_task_unref(task); task = NULL; goto cleanup; failure: if (task != NULL) background_task_cancel(task); if (context != NULL) application_remove_staging_directory(context->staging_directory); else application_remove_staging_directory(staging); if (context != NULL) application_eml_review_context_free(context); if (application != NULL) application_present_error(application, "Analyse EML impossible", error != NULL ? error->message : "La preuve n’a pas pu être analysée."); cleanup: background_task_unref(task); g_free(staging); g_free(current_sha256); g_free(source_path); g_free(candidate); g_free(root); evidence_record_free(record); evidence_dao_free(dao); g_clear_error(&error); } /** @brief Contexte possédé pendant la révision OCR d'un RIB. */ typedef struct { Application *application; char *evidence_identifier; char *ocr_text; char *tool_version; } ApplicationRibOcrContext; /** @brief Libère le contexte OCR d'un RIB. */ static void application_rib_ocr_context_free(ApplicationRibOcrContext *context) { if (context == NULL) return; g_free(context->evidence_identifier); g_free(context->ocr_text); g_free(context->tool_version); g_free(context); } /** @brief Crée ou réutilise l'entité IBAN confirmée et la lie à la preuve. */ static void application_on_rib_ocr_confirmed(const char *iban, gpointer user_data) { ApplicationRibOcrContext *context = user_data; Application *application = context != NULL ? context->application : NULL; Database *database = NULL; EntityDao *entity_dao = NULL; EvidenceEntityDao *link_dao = NULL; GPtrArray *entities = NULL; EntityRecord *record = NULL; const char *entity_identifier = NULL; char *new_identifier = NULL; char *timestamp = NULL; char *description = NULL; GDateTime *now = NULL; GError *error = NULL; gboolean active = FALSE; const InvestigationProject *project = NULL; if (iban == NULL || application == NULL || application->session == NULL) goto cleanup; database = investigation_session_get_database(application->session); entity_dao = entity_dao_new(database, &error); if (entity_dao == NULL) goto failure; entities = entity_dao_list_all(entity_dao, &error); if (entities == NULL) goto failure; for (guint index = 0; index < entities->len; index++) { EntityRecord *candidate = g_ptr_array_index(entities, index); if (g_strcmp0(entity_record_get_type_identifier(candidate), "iban") == 0 && g_strcmp0(entity_record_get_value(candidate), iban) == 0) entity_identifier = entity_record_get_identifier(candidate); } if (!database_transaction_begin(database)) goto failure; active = TRUE; if (entity_identifier == NULL) { new_identifier = g_uuid_string_random(); now = g_date_time_new_now_utc(); timestamp = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL; description = g_strdup_printf("IBAN extrait localement par OCR (%s).", context->tool_version != NULL ? context->tool_version : "Tesseract"); record = entity_record_new(new_identifier, "iban", iban, "IBAN extrait du RIB", description, 70, timestamp, timestamp, ENTITY_STATUS_ACTIVE, &error); if (record == NULL || !entity_dao_insert(entity_dao, record, &error)) goto failure; entity_identifier = new_identifier; } link_dao = evidence_entity_dao_new(database, &error); if (link_dao == NULL) goto failure; if (!evidence_entity_dao_link(link_dao, context->evidence_identifier, entity_identifier, &error)) goto failure; if (!database_transaction_commit(database)) goto failure; active = FALSE; project = investigation_session_get_project(application->session); g_free(application->pending_entity_selection_identifier); application->pending_entity_selection_identifier = g_strdup(entity_identifier); main_window_set_status(application->main_window, "Entité IBAN créée et reliée à la preuve."); application_start_graph_loading(application, investigation_project_get_database_path(project)); goto cleanup; failure: if (active) database_transaction_rollback(database); application_present_error(application, "Intégration du RIB impossible", error != NULL ? error->message : "La transaction a échoué."); cleanup: g_clear_error(&error); entity_record_free(record); g_clear_pointer(&entities, g_ptr_array_unref); entity_dao_free(entity_dao); evidence_entity_dao_free(link_dao); g_free(new_identifier); g_free(timestamp); g_free(description); g_clear_pointer(&now, g_date_time_unref); application_rib_ocr_context_free(context); } /** @brief Exécute localement Tesseract sur la preuve image sélectionnée. */ static void application_on_analyze_rib_requested( const char *evidence_identifier, gpointer user_data) { Application *application = user_data; const InvestigationProject *project = NULL; EvidenceDao *dao = NULL; EvidenceRecord *record = NULL; ApplicationRibOcrContext *context = NULL; GPtrArray *ibans = NULL; char *candidate_path = NULL; char *canonical_root = NULL; char *path = NULL; char *output_path = NULL; GError *error = NULL; char *work_copy_path = NULL; GFile *source_file = NULL; GFile *copy_file = NULL; if (application == NULL || application->session == NULL) return; dao = evidence_dao_new(investigation_session_get_database(application->session), &error); if (dao != NULL) record = evidence_dao_find_by_identifier(dao, evidence_identifier, &error); project = investigation_session_get_project(application->session); if (record == NULL || project == NULL) goto failure; canonical_root = g_canonicalize_filename( investigation_project_get_root_path(project), NULL); candidate_path = g_build_filename(canonical_root, evidence_record_get_relative_path(record), NULL); path = g_canonicalize_filename(candidate_path, NULL); if (path == NULL || !g_str_has_prefix(path, canonical_root) || (path[strlen(canonical_root)] != G_DIR_SEPARATOR && path[strlen(canonical_root)] != '\0')) goto failure; work_copy_path = g_strdup_printf("%s/02_Preuves_Traitees/OCR/%s-%s", canonical_root, evidence_identifier, evidence_record_get_original_name(record)); source_file = g_file_new_for_path(path); copy_file = g_file_new_for_path(work_copy_path); if (!g_file_copy(source_file, copy_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) goto failure; context = g_new0(ApplicationRibOcrContext, 1); context->application = application; context->evidence_identifier = g_strdup(evidence_identifier); if (!rib_ocr_extract_text(work_copy_path, &context->ocr_text, &context->tool_version, &error)) goto failure; output_path = g_strdup_printf("%s/02_Preuves_Traitees/OCR/%s-ocr.txt", canonical_root, evidence_identifier); if (!g_file_set_contents(output_path, context->ocr_text, -1, &error)) goto failure; ibans = iban_analyzer_extract(context->ocr_text); rib_ocr_review_dialog_present(main_window_get_window(application->main_window), context->ocr_text, ibans->len > 0 ? g_ptr_array_index(ibans, 0) : NULL, application_on_rib_ocr_confirmed, context); context = NULL; goto cleanup; failure: application_rib_ocr_context_free(context); context = NULL; application_present_error(application, "Analyse OCR impossible", error != NULL ? error->message : "La preuve n'a pas pu être analysée par Tesseract."); cleanup: g_clear_error(&error); g_clear_pointer(&ibans, g_ptr_array_unref); g_free(output_path); g_free(path); g_free(candidate_path); g_free(canonical_root); g_free(work_copy_path); g_clear_object(&source_file); g_clear_object(©_file); evidence_record_free(record); evidence_dao_free(dao); } /** @brief Analyse avec ExifTool une copie de travail de la preuve. */ static void application_on_extract_metadata_requested( const char *evidence_identifier, gpointer user_data) { Application *application = user_data; const InvestigationProject *project = NULL; EvidenceDao *dao = NULL; EvidenceRecord *record = NULL; char *canonical_root = NULL; char *candidate_path = NULL; char *source_path = NULL; char *output_directory = NULL; char *copy_name = NULL; char *copy_path = NULL; char *json_name = NULL; char *json_path = NULL; char *summary = NULL; char *json = NULL; char *version = NULL; GFile *source_file = NULL; GFile *copy_file = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || evidence_identifier == NULL) return; dao = evidence_dao_new( investigation_session_get_database(application->session), &error); if (dao != NULL) record = evidence_dao_find_by_identifier(dao, evidence_identifier, &error); project = investigation_session_get_project(application->session); if (record == NULL || project == NULL) goto failure; canonical_root = g_canonicalize_filename( investigation_project_get_root_path(project), NULL); candidate_path = g_build_filename(canonical_root, evidence_record_get_relative_path(record), NULL); source_path = g_canonicalize_filename(candidate_path, NULL); if (source_path == NULL || !g_str_has_prefix(source_path, canonical_root) || (source_path[strlen(canonical_root)] != G_DIR_SEPARATOR && source_path[strlen(canonical_root)] != '\0')) { g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "Le chemin de la preuve sort du dossier de l'enquête."); goto failure; } output_directory = g_build_filename(canonical_root, "02_Preuves_Traitees", "Extractions", "Metadata", NULL); if (g_mkdir_with_parents(output_directory, 0750) != 0) { g_set_error(&error, G_IO_ERROR, g_io_error_from_errno(errno), "Impossible de créer le dossier des métadonnées : %s", g_strerror(errno)); goto failure; } copy_name = g_strdup_printf("%s-%s", evidence_identifier, evidence_record_get_original_name(record)); copy_path = g_build_filename(output_directory, copy_name, NULL); source_file = g_file_new_for_path(source_path); copy_file = g_file_new_for_path(copy_path); if (!g_file_copy(source_file, copy_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) goto failure; if (!exiftool_metadata_extract(copy_path, &summary, &json, &version, &error)) goto failure; json_name = g_strdup_printf("%s-exiftool.json", evidence_identifier); json_path = g_build_filename(output_directory, json_name, NULL); if (!g_file_set_contents(json_path, json, -1, &error)) goto failure; metadata_analysis_dialog_present( main_window_get_window(application->main_window), summary, json_path, version); main_window_set_status(application->main_window, "Métadonnées extraites localement et sortie JSON conservée."); goto cleanup; failure: application_present_error(application, "Extraction impossible", error != NULL ? error->message : "La preuve n'a pas pu être analysée avec ExifTool."); cleanup: g_clear_error(&error); g_clear_object(&source_file); g_clear_object(©_file); g_free(canonical_root); g_free(candidate_path); g_free(source_path); g_free(output_directory); g_free(copy_name); g_free(copy_path); g_free(json_name); g_free(json_path); g_free(summary); g_free(json); g_free(version); evidence_record_free(record); evidence_dao_free(dao); } /** @brief Contexte conservé pendant la tâche de récupération PDF. */ typedef struct { Application *application; char *copy_path; char *output_directory; char *evidence_identifier; } ApplicationPdfRecoveryContext; /** @brief Libère le contexte d'une récupération PDF. */ static void application_pdf_recovery_context_free(gpointer data) { ApplicationPdfRecoveryContext *context = data; if (context == NULL) return; g_free(context->copy_path); g_free(context->output_directory); g_free(context->evidence_identifier); g_free(context); } /** @brief Contexte secret utilisé pour créer la copie PDF déverrouillée. */ typedef struct { Application *application; char *password; char *copy_path; char *unlocked_path; } ApplicationPdfDecryptContext; /** @brief Efface le mot de passe puis libère le contexte de déchiffrement. */ static void application_pdf_decrypt_context_free(gpointer data) { ApplicationPdfDecryptContext *context = data; if (context == NULL) return; if (context->password != NULL) memset(context->password, 0, strlen(context->password)); g_free(context->password); g_free(context->copy_path); g_free(context->unlocked_path); g_free(context); } /** @brief Crée avec qpdf la copie déverrouillée demandée explicitement. */ static void application_on_pdf_decrypt_requested(gpointer user_data) { ApplicationPdfDecryptContext *context = user_data; GSubprocess *process = NULL; char *password_path = NULL; char *password_argument = NULL; char *stderr_text = NULL; int descriptor = -1; GError *error = NULL; const char *arguments[6] = { "qpdf", NULL, "--decrypt", NULL, NULL, NULL }; gssize password_length = 0; if (context == NULL || context->application == NULL) return; descriptor = g_file_open_tmp("labfy-qpdf-password-XXXXXX", &password_path, &error); password_length = context->password != NULL ? (gssize) strlen(context->password) : 0; if (descriptor < 0 || password_length == 0 || write(descriptor, context->password, (size_t) password_length) != password_length) goto failure; close(descriptor); descriptor = -1; password_argument = g_strdup_printf("--password-file=%s", password_path); arguments[1] = password_argument; arguments[3] = context->copy_path; arguments[4] = context->unlocked_path; process = g_subprocess_newv(arguments, G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_PIPE, &error); if (process == NULL || !g_subprocess_communicate_utf8(process, NULL, NULL, NULL, &stderr_text, &error) || !g_subprocess_get_successful(process)) { if (error == NULL) g_set_error(&error, G_IO_ERROR, G_IO_ERROR_FAILED, "qpdf a échoué : %s", stderr_text != NULL ? stderr_text : "aucun détail disponible"); goto failure; } application_message_dialog_present( main_window_get_window(context->application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "PDF déverrouillé", "La copie déverrouillée a été créée dans les preuves traitées."); main_window_set_status(context->application->main_window, "Copie PDF déverrouillée créée."); goto cleanup; failure: application_present_error(context->application, "Déverrouillage impossible", error != NULL ? error->message : "qpdf n'a pas pu créer la copie déverrouillée."); cleanup: if (descriptor >= 0) close(descriptor); if (password_path != NULL) g_unlink(password_path); g_clear_error(&error); g_clear_object(&process); g_free(password_path); g_free(password_argument); g_free(stderr_text); } /** @brief Présente le résultat final d'une récupération PDF. */ static void application_on_pdf_recovery_completed(BackgroundTask *task, gpointer user_data) { ApplicationPdfRecoveryContext *context = user_data; const PdfPasswordRecoveryResult *result = NULL; const char *password = NULL; ApplicationPdfDecryptContext *decrypt_context = NULL; char *details = NULL; char *unlocked_name = NULL; GError *error = NULL; if (context == NULL || context->application == NULL) return; if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED) { main_window_set_status(context->application->main_window, "Récupération du mot de passe PDF annulée."); return; } if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) { error = background_task_dup_error(task); application_present_error(context->application, "Récupération PDF impossible", error != NULL ? error->message : "John the Ripper a échoué."); g_clear_error(&error); return; } result = pdf_password_recovery_get_result(task); if (!pdf_password_recovery_result_is_recovered(result)) { application_message_dialog_present( main_window_get_window(context->application->main_window), APPLICATION_MESSAGE_DIALOG_WARNING, "Mot de passe non trouvé", "Aucun candidat n'a correspondu. Essayez un dictionnaire enrichi " "ou un masque plus précis."); return; } password = pdf_password_recovery_result_get_password(result); decrypt_context = g_new0(ApplicationPdfDecryptContext, 1); decrypt_context->application = context->application; decrypt_context->password = g_strdup(password); decrypt_context->copy_path = g_strdup(context->copy_path); unlocked_name = g_strdup_printf("%s-unlocked.pdf", context->evidence_identifier); decrypt_context->unlocked_path = g_build_filename( context->output_directory, unlocked_name, NULL); details = g_strdup_printf("Mot de passe retrouvé : %s\n\n" "Il est uniquement conservé en mémoire. Utilisez l'action ci-dessous " "pour créer une copie déverrouillée.", password); application_message_dialog_present_details_action( main_window_get_window(context->application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Mot de passe PDF retrouvé", "La recherche locale a réussi.", details, "Créer la copie déverrouillée", application_on_pdf_decrypt_requested, decrypt_context, application_pdf_decrypt_context_free); g_free(details); g_free(unlocked_name); } /** @brief Prépare la copie PDF puis démarre la tâche choisie. */ static void application_on_pdf_recovery_configured( PdfPasswordRecoveryMethod method, const char *parameter, gpointer user_data) { Application *application = user_data; const InvestigationProject *project = NULL; EvidenceDao *dao = NULL; EvidenceRecord *record = NULL; ApplicationPdfRecoveryContext *context = NULL; char *root = NULL; char *candidate = NULL; char *source_path = NULL; char *copy_name = NULL; GFile *source_file = NULL; GFile *copy_file = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || application->selected_evidence_identifier == NULL) return; dao = evidence_dao_new( investigation_session_get_database(application->session), &error); if (dao != NULL) record = evidence_dao_find_by_identifier(dao, application->selected_evidence_identifier, &error); project = investigation_session_get_project(application->session); if (record == NULL || project == NULL) goto failure; root = g_canonicalize_filename(investigation_project_get_root_path(project), NULL); candidate = g_build_filename(root, evidence_record_get_relative_path(record), NULL); source_path = g_canonicalize_filename(candidate, NULL); if (source_path == NULL || !g_str_has_prefix(source_path, root) || (source_path[strlen(root)] != G_DIR_SEPARATOR && source_path[strlen(root)] != '\0')) goto failure; context = g_new0(ApplicationPdfRecoveryContext, 1); context->application = application; context->evidence_identifier = g_strdup( application->selected_evidence_identifier); context->output_directory = g_build_filename(root, "02_Preuves_Traitees", "Extractions", "PDF", NULL); if (g_mkdir_with_parents(context->output_directory, 0750) != 0) goto failure; copy_name = g_strdup_printf("%s-locked.pdf", context->evidence_identifier); context->copy_path = g_build_filename(context->output_directory, copy_name, NULL); source_file = g_file_new_for_path(source_path); copy_file = g_file_new_for_path(context->copy_path); if (!g_file_copy(source_file, copy_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error)) goto failure; if (pdf_password_recovery_start(application->task_manager, context->copy_path, context->output_directory, context->evidence_identifier, method, parameter, application_on_pdf_recovery_completed, context, application_pdf_recovery_context_free, &error) == NULL) goto failure; context = NULL; main_window_set_status(application->main_window, "Récupération PDF lancée ; progression disponible dans les tâches."); goto cleanup; failure: application_pdf_recovery_context_free(context); application_present_error(application, "Récupération PDF impossible", error != NULL ? error->message : "La copie de travail n'a pas pu être préparée."); cleanup: g_clear_error(&error); g_clear_object(&source_file); g_clear_object(©_file); g_free(root); g_free(candidate); g_free(source_path); g_free(copy_name); evidence_record_free(record); evidence_dao_free(dao); } /** @brief Ouvre la configuration de récupération pour le PDF sélectionné. */ static void application_on_recover_pdf_password_requested( const char *evidence_identifier, gpointer user_data) { Application *application = user_data; if (application == NULL || evidence_identifier == NULL) return; g_free(application->selected_evidence_identifier); application->selected_evidence_identifier = g_strdup(evidence_identifier); pdf_password_dialog_present( main_window_get_window(application->main_window), application_on_pdf_recovery_configured, application); } /** * @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, ApplicationEvidenceBatchContext *batch_context ) { 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->batch_context = batch_context; 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 ), NULL ); 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 les métadonnées du fichier courant d'un import groupé. */ static void application_on_batch_metadata_completed( EvidenceImportDialogResult *result, gpointer user_data ) { ApplicationEvidenceBatchContext *context = user_data; const char *file_path = NULL; if (context == NULL) { evidence_import_dialog_result_free(result); return; } if (result == NULL) { context->skipped_count++; context->current_index++; application_present_next_batch_metadata(context); return; } file_path = g_ptr_array_index(context->file_paths, context->current_index); application_start_evidence_import( context->application, 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), context); evidence_import_dialog_result_free(result); } /** @brief Présente les métadonnées du prochain fichier, ou le bilan final. */ static void application_present_next_batch_metadata( ApplicationEvidenceBatchContext *context ) { GError *error = NULL; const char *file_path = NULL; char *status = NULL; if (context == NULL) return; if (context->current_index >= context->file_paths->len) { application_finish_evidence_batch(context); return; } file_path = g_ptr_array_index(context->file_paths, context->current_index); status = g_strdup_printf("Préparation %u/%u : %s", context->current_index + 1U, context->file_paths->len, file_path); main_window_set_status(context->application->main_window, status); g_free(status); if (!evidence_import_dialog_present( main_window_get_window(context->application->main_window), file_path, context->evidence_types, application_on_batch_metadata_completed, context, &error)) { char *failure = g_strdup_printf("%s : %s", file_path, error != NULL ? error->message : "dialogue indisponible"); g_ptr_array_add(context->failures, failure); g_clear_error(&error); context->current_index++; application_present_next_batch_metadata(context); } } /** @brief Prépare un import groupé depuis les chemins sélectionnés. */ static void application_on_evidence_files_selected( const GPtrArray *paths, const GError *error, gpointer user_data ) { Application *application = user_data; ApplicationEvidenceBatchContext *context = NULL; EvidenceTypeDao *dao = NULL; GPtrArray *types = NULL; GError *local_error = NULL; if (application == NULL || application->main_window == NULL) return; if (error != NULL) { application_present_error(application, "Sélection impossible", error->message); return; } if (paths == NULL || paths->len == 0U) { main_window_set_status(application->main_window, "Import de preuve annulé."); return; } if (paths->len == 1U) { application_on_evidence_file_selected( g_ptr_array_index((GPtrArray *) paths, 0U), NULL, application); return; } dao = evidence_type_dao_new( investigation_session_get_database(application->session), &local_error); if (dao != NULL) types = evidence_type_dao_list_all(dao, &local_error); evidence_type_dao_free(dao); if (types == NULL || types->len == 0U) { application_present_error(application, "Types de preuve indisponibles", local_error != NULL ? local_error->message : "Aucun type de preuve n'est disponible."); g_clear_error(&local_error); g_clear_pointer(&types, g_ptr_array_unref); return; } context = g_new0(ApplicationEvidenceBatchContext, 1); context->application = application; context->file_paths = g_ptr_array_new_with_free_func(g_free); context->evidence_types = types; context->failures = g_ptr_array_new_with_free_func(g_free); for (guint index = 0U; index < paths->len; index++) g_ptr_array_add(context->file_paths, g_strdup(g_ptr_array_index((GPtrArray *) paths, index))); main_window_set_import_evidence_enabled(application->main_window, FALSE); { char *message = g_strdup_printf( "%u fichiers ont été sélectionnés. Chaque fichier sera révisé " "puis importé séparément avec sa propre empreinte SHA-256.", paths->len); application_message_dialog_present_confirmation( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Préparer l'import groupé", message, "Continuer", application_on_batch_selection_confirmed, context); g_free(message); } } /** * @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_files( main_window_get_window( application->main_window ), application_on_evidence_files_selected, application ); } /** @brief Persiste le résultat validé du dialogue de compte social. */ static void application_on_social_account_completed( CreateSocialAccountDialogResult *result, gpointer user_data) { Application *application = user_data; SocialAccountInput input = {0}; const InvestigationProject *project = NULL; Database *database = NULL; GError *error = NULL; char *identifier = NULL; if (result == NULL || application == NULL || application->session == NULL) { create_social_account_dialog_result_free(result); return; } input.platform = create_social_account_dialog_result_get_platform(result); input.profile_url = create_social_account_dialog_result_get_profile_url(result); input.username = create_social_account_dialog_result_get_username(result); input.platform_identifier = create_social_account_dialog_result_get_platform_identifier(result); input.first_observed_at = create_social_account_dialog_result_get_first_observed_at(result); input.account_state = create_social_account_dialog_result_get_account_state(result); input.notes = create_social_account_dialog_result_get_notes(result); input.evidence_identifier = create_social_account_dialog_result_get_evidence_identifier(result); database = investigation_session_get_database(application->session); if (!social_account_service_create(database, &input, &identifier, &error)) { application_present_error(application, "Compte social non créé", error != NULL ? error->message : "L'écriture dans l'enquête a échoué."); } else { project = investigation_session_get_project(application->session); application_message_dialog_present( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Compte social ajouté", "Le profil et son éventuelle preuve associée ont été ajoutés au graphe."); application_start_graph_loading(application, investigation_project_get_database_path(project)); } g_clear_error(&error); g_free(identifier); create_social_account_dialog_result_free(result); } /** @brief Charge les preuves puis ouvre le formulaire de compte social. */ static void application_on_add_social_account_requested(gpointer user_data) { Application *application = user_data; Database *database = NULL; EvidenceDao *dao = NULL; GPtrArray *evidence_records = NULL; GError *error = NULL; if (application == NULL || application->main_window == NULL || application->session == NULL) return; database = investigation_session_get_database(application->session); dao = evidence_dao_new(database, &error); if (dao != NULL) evidence_records = evidence_dao_list_all(dao, &error); if (evidence_records == NULL) { application_present_error(application, "Formulaire indisponible", error != NULL ? error->message : "Impossible de charger les preuves."); } else { create_social_account_dialog_present( main_window_get_window(application->main_window), evidence_records, application_on_social_account_completed, application); } g_clear_pointer(&evidence_records, g_ptr_array_unref); evidence_dao_free(dao); g_clear_error(&error); } /** @brief Persiste une personne validée puis recharge le graphe. */ static void application_on_person_creation_task_completed( BackgroundTask *task, gpointer user_data) { ApplicationPersonCreationTaskContext *context = user_data; Application *application = context != NULL ? context->application : NULL; const InvestigationProject *project = application != NULL && application->session != NULL ? investigation_session_get_project(application->session) : NULL; gboolean session_matches = project != NULL && application->session_generation == context->generation && g_strcmp0(investigation_project_get_root_path(project), context->root_path) == 0 && g_strcmp0(investigation_project_get_database_path(project), context->database_path) == 0; if (!session_matches) return; if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) { GError *error = background_task_dup_error(task); application_present_error(application, "Personne non créée", error != NULL ? error->message : "L’écriture a échoué."); g_clear_error(&error); return; } main_window_set_status(application->main_window, "Personne et preuves ajoutées. Actualisation du graphe…"); application_start_graph_loading(application, context->database_path); } static void application_on_person_completed( CreatePersonDialogResult *result, gpointer user_data) { ApplicationPersonDialogContext *context = user_data; Application *application = context != NULL ? context->application : NULL; const PersonEntityInput *input = NULL; const InvestigationProject *project = NULL; GError *error = NULL; PersonCreationTaskRequest *request = NULL; BackgroundTask *task = NULL; ApplicationPersonCreationTaskContext *task_context = NULL; const char *active_root = NULL, *active_database = NULL; if (result == NULL) { return; } input = create_person_dialog_result_get_input(result); if (application == NULL || application->session == NULL) goto changed_session; project = investigation_session_get_project(application->session); active_root = investigation_project_get_root_path(project); active_database = investigation_project_get_database_path(project); if (!person_creation_guard_matches(context->guard, TRUE, application->session_generation, active_root, active_database)) goto changed_session; request = person_creation_task_request_new(active_database, active_root, input, create_person_dialog_result_get_evidence_selection(result)); task_context = g_new0(ApplicationPersonCreationTaskContext, 1); task_context->application = application; task_context->generation = application->session_generation; task_context->root_path = g_strdup(active_root); task_context->database_path = g_strdup(active_database); task_context->staging = create_person_dialog_result_steal_staging(result); task = person_creation_task_start(application->task_manager, request, application_on_person_creation_task_completed, task_context, application_person_creation_task_context_free, &error); if (task == NULL) { application_person_creation_task_context_free(task_context); application_present_error(application, "Personne non créée", error != NULL ? error->message : "L'écriture a échoué."); } else { main_window_set_status(application->main_window, "Création et import des preuves en cours…"); background_task_unref(task); } person_creation_task_request_free(request); g_clear_error(&error); create_person_dialog_result_free(result); return; changed_session: application_present_error(application, "Personne non créée", "L'enquête active a changé depuis l'ouverture de l'assistant."); create_person_dialog_result_free(result); } static gboolean application_person_dialog_session_matches(gpointer user_data) { ApplicationPersonDialogContext *context = user_data; Application *application = context != NULL ? context->application : NULL; const InvestigationProject *project; if (application == NULL || application->session == NULL) return FALSE; project = investigation_session_get_project(application->session); return person_creation_guard_matches(context->guard, TRUE, application->session_generation, investigation_project_get_root_path(project), investigation_project_get_database_path(project)); } /** @brief Charge les preuves puis ouvre le formulaire d'une personne. */ static void application_on_add_person_requested(gpointer user_data) { Application *application = user_data; EvidenceDao *dao = NULL; GPtrArray *records = NULL; ApplicationPersonDialogContext *context = NULL; const InvestigationProject *project = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; dao = evidence_dao_new( investigation_session_get_database(application->session), &error); if (dao != NULL) records = evidence_dao_list_all(dao, &error); if (records == NULL) application_present_error(application, "Formulaire indisponible", error != NULL ? error->message : "Impossible de charger les preuves."); else { project = investigation_session_get_project(application->session); context = g_new0(ApplicationPersonDialogContext, 1); context->application = application; context->guard = person_creation_guard_new( application->session_generation, investigation_project_get_root_path(project), investigation_project_get_database_path(project)); create_person_dialog_present(main_window_get_window(application->main_window), records, investigation_project_get_root_path(project), application->task_manager, application_person_dialog_session_matches, application_on_person_completed, context, (GDestroyNotify) application_person_dialog_context_free); } g_clear_pointer(&records, g_ptr_array_unref); evidence_dao_free(dao); g_clear_error(&error); } /** * @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"); main_window_set_evidence_preview( application->main_window, investigation_node_get_path(node), node_name); } } /** * @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; const InvestigationProject *project = NULL; const char *investigation_root = 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 ); { EvidenceEntityDao *observation_dao = evidence_entity_dao_new(database, &error); GPtrArray *observations = observation_dao != NULL ? evidence_entity_dao_list_observations(observation_dao, evidence_identifier, &error) : NULL; if (error == NULL) main_window_set_evidence_observations(application->main_window, observations); else g_warning("Observations EML indisponibles : %s", error->message); g_clear_error(&error); g_clear_pointer(&observations, g_ptr_array_unref); evidence_entity_dao_free(observation_dao); } project = investigation_session_get_project(application->session); investigation_root = project != NULL ? investigation_project_get_root_path(project) : NULL; gboolean eml_analysis_available = FALSE; if (investigation_root != NULL) { const char *selected_name = evidence_record_get_original_name(evidence_record); char *lower_name = selected_name != NULL ? g_ascii_strdown(selected_name, -1) : NULL; eml_analysis_available = lower_name != NULL && g_str_has_suffix(lower_name, ".eml"); g_free(lower_name); main_window_show_evidence_preview(application->main_window, investigation_root, evidence_record); } main_window_set_eml_analysis_available( application->main_window, eml_analysis_available); 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 InvestigationProject *project = NULL; Database *database = NULL; RelationService *relation_service = NULL; RelationRecord *relation_record = NULL; GDateTime *current_date_time = NULL; const char *source_identifier = NULL; const char *target_identifier = NULL; const char *relation_type = NULL; const char *database_path = NULL; char *relation_identifier = NULL; char *timestamp = NULL; GError *error = NULL; gboolean relation_created = FALSE; gint64 relation_type_identifier = 0; 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; } if (application->session == NULL) { application_present_error( application, "Ajout de relation impossible", "Aucune enquête n'est actuellement ouverte." ); goto cleanup; } 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 ); database = investigation_session_get_database( application->session ); project = investigation_session_get_project( application->session ); if (database == NULL || project == NULL) { application_present_error( application, "Ajout de relation impossible", "La session d'enquête est invalide." ); goto cleanup; } database_path = investigation_project_get_database_path( project ); if (database_path == NULL || database_path[0] == '\0') { application_present_error( application, "Ajout de relation impossible", "Le chemin de la base SQLite est invalide." ); goto cleanup; } relation_identifier = g_uuid_string_random(); current_date_time = g_date_time_new_now_utc(); if (current_date_time != NULL) { timestamp = g_date_time_format( current_date_time, "%Y-%m-%dT%H:%M:%SZ" ); } if (relation_identifier == NULL || timestamp == NULL) { application_present_error( application, "Ajout de relation impossible", "Impossible de préparer l'identifiant ou la date " "de la nouvelle relation." ); goto cleanup; } relation_type_identifier = create_relation_dialog_result_get_relation_type_identifier(result); if (relation_type_identifier == 0) { RelationTypeService *type_service = relation_type_service_new(database, &error); if (type_service == NULL || !relation_type_service_create_custom(type_service, relation_type, NULL, &relation_type_identifier, &error)) { relation_type_service_free(type_service); application_present_error(application, "Création du type impossible", error != NULL ? error->message : "Le nouveau type n'a pas pu être créé."); goto cleanup; } relation_type_service_free(type_service); } relation_record = relation_record_new_canonical( relation_identifier, source_identifier, target_identifier, relation_type_identifier, create_relation_dialog_result_get_relation_type_code(result), relation_type, create_relation_dialog_result_get_label( result ), create_relation_dialog_result_get_justification( result ), create_relation_dialog_result_get_confidence( result ), timestamp, timestamp, RELATION_STATUS_ACTIVE, &error ); if (relation_record == NULL) { application_present_error( application, "Relation invalide", error != NULL ? error->message : "Les informations de la relation sont invalides." ); g_clear_error( &error ); goto cleanup; } relation_service = relation_service_new( database, &error ); if (relation_service == NULL) { application_present_error( application, "Ajout de relation impossible", error != NULL ? error->message : "Impossible de préparer le service des relations." ); g_clear_error( &error ); goto cleanup; } /* * Aucune preuve n'est associée pendant ce premier parcours. * Le service accepte donc un tableau NULL. */ if (!relation_service_create( relation_service, relation_record, create_relation_dialog_result_get_evidence_identifiers(result), &error )) { g_warning( "Impossible d'enregistrer la relation '%s' : %s", relation_identifier, error != NULL ? error->message : "erreur inconnue" ); application_present_error( application, "Enregistrement de la relation impossible", error != NULL ? error->message : "La relation n'a pas pu être enregistrée." ); g_clear_error( &error ); goto cleanup; } relation_created = TRUE; g_message( "Relation enregistrée : %s -> %s (%s), identifiant %s.", source_identifier, target_identifier, relation_type, relation_identifier ); main_window_set_status( application->main_window, "Relation enregistrée. Actualisation du graphe…" ); /* * Le graphe n'est rechargé qu'après le COMMIT réussi. * En cas d'échec SQLite, l'affichage courant reste intact. */ application_start_graph_loading( application, database_path ); cleanup: if (!relation_created && application->main_window != NULL) { main_window_set_status( application->main_window, "La relation n'a pas été enregistrée." ); } relation_service_free( relation_service ); relation_record_free( relation_record ); if (current_date_time != NULL) { g_date_time_unref( current_date_time ); } g_free( timestamp ); g_free( relation_identifier ); create_relation_dialog_result_free( result ); } /** * @brief Ouvre le dialogue de création depuis la fiche d'une entité. */ /** @brief Contexte conservé pendant l'édition d'une relation. */ typedef struct { Application *application; char *relation_identifier; } ApplicationEditRelationContext; /** @brief Contexte possédé pendant la confirmation de suppression. */ typedef struct { Application *application; char *relation_identifier; } ApplicationDeleteRelationContext; /** @brief Libère un contexte de suppression de relation. */ static void application_delete_relation_context_free( ApplicationDeleteRelationContext *context) { if (context == NULL) return; g_free(context->relation_identifier); g_free(context); } /** @brief Supprime la relation après confirmation explicite. */ static void application_on_delete_relation_confirmed(gboolean confirmed, gpointer user_data) { ApplicationDeleteRelationContext *context = user_data; Application *application = context != NULL ? context->application : NULL; Database *database = NULL; RelationDao *relation_dao = NULL; const InvestigationProject *project = NULL; GError *error = NULL; gboolean transaction_active = FALSE; if (!confirmed || application == NULL || application->session == NULL) goto cleanup; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); relation_dao = relation_dao_new(database, &error); if (relation_dao == NULL || !database_transaction_begin(database)) goto failure; transaction_active = TRUE; if (!relation_dao_delete(relation_dao, context->relation_identifier, &error) || !database_transaction_commit(database)) goto failure; transaction_active = FALSE; main_window_set_status(application->main_window, "Relation supprimée. Actualisation du graphe…"); application_start_graph_loading(application, investigation_project_get_database_path(project)); goto cleanup; failure: if (transaction_active) database_transaction_rollback(database); application_present_error(application, "Suppression impossible", error != NULL ? error->message : "La relation n'a pas pu être supprimée."); cleanup: g_clear_error(&error); relation_dao_free(relation_dao); application_delete_relation_context_free(context); } /** @brief Prépare la confirmation de suppression d'une relation. */ static void application_on_delete_relation_requested( const char *relation_identifier, gpointer user_data) { Application *application = user_data; Database *database = NULL; RelationDao *relation_dao = NULL; EntityDao *entity_dao = NULL; RelationRecord *relation = NULL; EntityRecord *source = NULL; EntityRecord *target = NULL; ApplicationDeleteRelationContext *context = NULL; char *message = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || relation_identifier == NULL) return; database = investigation_session_get_database(application->session); relation_dao = relation_dao_new(database, &error); if (relation_dao != NULL) relation = relation_dao_find_by_identifier( relation_dao, relation_identifier, &error); entity_dao = entity_dao_new(database, &error); if (relation != NULL && entity_dao != NULL) { source = entity_dao_find_by_identifier(entity_dao, relation_record_get_source_entity_identifier(relation), &error); target = entity_dao_find_by_identifier(entity_dao, relation_record_get_target_entity_identifier(relation), &error); } if (relation == NULL || source == NULL || target == NULL) goto failure; context = g_new0(ApplicationDeleteRelationContext, 1); context->application = application; context->relation_identifier = g_strdup(relation_identifier); message = g_strdup_printf( "Supprimer définitivement la relation :\n\n%s → %s\n\n" "Les entités et les preuves resteront dans l’enquête.", entity_record_get_label(source) != NULL ? entity_record_get_label(source) : entity_record_get_value(source), entity_record_get_label(target) != NULL ? entity_record_get_label(target) : entity_record_get_value(target)); if (context->relation_identifier == NULL || message == NULL) goto failure; application_message_dialog_present_confirmation( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_WARNING, "Supprimer la relation", message, "Supprimer", application_on_delete_relation_confirmed, context); context = NULL; goto cleanup; failure: application_delete_relation_context_free(context); context = NULL; application_present_error(application, "Suppression impossible", error != NULL ? error->message : "La relation sélectionnée n'a pas pu être chargée."); cleanup: g_free(message); g_clear_error(&error); entity_record_free(source); entity_record_free(target); relation_record_free(relation); entity_dao_free(entity_dao); relation_dao_free(relation_dao); } /** @brief Enregistre la catégorie choisie depuis la fiche personne. */ static void application_on_person_role_changed(const char *entity_identifier, PersonRole role, gpointer user_data) { Application *application = user_data; Database *database = NULL; const InvestigationProject *project = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); if (!person_entity_service_update_role(database, entity_identifier, role, &error)) { application_present_error(application, "Catégorie non enregistrée", error != NULL ? error->message : "Impossible de catégoriser cette personne."); g_clear_error(&error); return; } g_free(application->pending_entity_selection_identifier); application->pending_entity_selection_identifier = g_strdup(entity_identifier); main_window_set_status(application->main_window, "Catégorie enregistrée. Actualisation du graphe…"); application_start_graph_loading(application, investigation_project_get_database_path(project)); } /** @brief Enregistre la confiance choisie dans la fiche personne. */ static void application_on_person_confidence_changed( const char *entity_identifier, gint confidence, gpointer user_data) { Application *application = user_data; Database *database = NULL; const InvestigationProject *project = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); if (!person_entity_service_update_confidence(database, entity_identifier, confidence, &error)) { application_present_error(application, "Confiance non enregistrée", error != NULL ? error->message : "Impossible de modifier la confiance de cette personne."); g_clear_error(&error); return; } g_free(application->pending_entity_selection_identifier); application->pending_entity_selection_identifier = g_strdup(entity_identifier); main_window_set_status(application->main_window, "Confiance enregistrée. Actualisation du graphe…"); application_start_graph_loading(application, investigation_project_get_database_path(project)); } /** @brief Enregistre le nom affiché choisi dans la fiche personne. */ static void application_on_person_name_changed(const char *entity_identifier, const char *display_name, gpointer user_data) { Application *application = user_data; Database *database = NULL; const InvestigationProject *project = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); if (!person_entity_service_update_display_name(database, entity_identifier, display_name, &error)) { application_present_error(application, "Nom non enregistré", error != NULL ? error->message : "Impossible de modifier le nom affiché de cette personne."); g_clear_error(&error); return; } g_free(application->pending_entity_selection_identifier); application->pending_entity_selection_identifier = g_strdup(entity_identifier); main_window_set_status(application->main_window, "Nom enregistré. Actualisation du graphe…"); application_start_graph_loading(application, investigation_project_get_database_path(project)); } /** @brief Charge et affiche les preuves liées à l'entité sélectionnée. */ static void application_on_entity_selected(const char *entity_identifier, gpointer user_data) { Application *application = user_data; Database *database = NULL; EvidenceEntityDao *link_dao = NULL; EvidenceDao *evidence_dao = NULL; GPtrArray *identifiers = NULL; GPtrArray *records = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || entity_identifier == NULL) return; database = investigation_session_get_database(application->session); link_dao = evidence_entity_dao_new(database, &error); if (link_dao == NULL) goto cleanup; evidence_dao = evidence_dao_new(database, &error); if (evidence_dao == NULL) goto cleanup; identifiers = evidence_entity_dao_list_evidence_identifiers(link_dao, entity_identifier, &error); if (identifiers == NULL) goto cleanup; records = g_ptr_array_new_with_free_func((GDestroyNotify) evidence_record_free); for (guint index = 0; index < identifiers->len; index++) { EvidenceRecord *record = evidence_dao_find_by_identifier(evidence_dao, g_ptr_array_index(identifiers, index), &error); if (record == NULL) goto cleanup; g_ptr_array_add(records, record); } main_window_set_person_evidences(application->main_window, records); cleanup: if (error != NULL) main_window_set_person_evidences(application->main_window, NULL); g_clear_error(&error); g_clear_pointer(&records, g_ptr_array_unref); g_clear_pointer(&identifiers, g_ptr_array_unref); evidence_dao_free(evidence_dao); evidence_entity_dao_free(link_dao); } /** @brief Contexte possédé par la gestion des preuves d'une personne. */ typedef struct { Application *application; char *entity_identifier; } ApplicationPersonEvidenceContext; /** @brief Libère le contexte de gestion des preuves. */ static void application_person_evidence_context_free( ApplicationPersonEvidenceContext *context) { if (context == NULL) return; g_free(context->entity_identifier); g_free(context); } /** @brief Synchronise atomiquement les preuves choisies. */ static void application_on_person_evidences_selected(GPtrArray *selected, gpointer user_data) { ApplicationPersonEvidenceContext *context = user_data; Application *application = context != NULL ? context->application : NULL; Database *database = NULL; EvidenceEntityDao *dao = NULL; GPtrArray *existing = NULL; GError *error = NULL; gboolean active = FALSE; if (selected == NULL || application == NULL || application->session == NULL) goto cleanup; database = investigation_session_get_database(application->session); dao = evidence_entity_dao_new(database, &error); if (dao == NULL || !database_transaction_begin(database)) goto failure; active = TRUE; existing = evidence_entity_dao_list_evidence_identifiers(dao, context->entity_identifier, &error); if (existing == NULL) goto failure; for (guint index = 0; index < existing->len; index++) { const char *identifier = g_ptr_array_index(existing, index); gboolean retained = FALSE; for (guint wanted = 0; wanted < selected->len; wanted++) if (g_strcmp0(identifier, g_ptr_array_index(selected, wanted)) == 0) retained = TRUE; if (!retained && !evidence_entity_dao_unlink(dao, identifier, context->entity_identifier, &error)) goto failure; } for (guint index = 0; index < selected->len; index++) { const char *identifier = g_ptr_array_index(selected, index); if (!evidence_entity_dao_link(dao, identifier, context->entity_identifier, &error)) goto failure; } if (!database_transaction_commit(database)) goto failure; active = FALSE; main_window_set_status(application->main_window, "Pièces jointes de la personne enregistrées."); application_on_entity_selected(context->entity_identifier, application); goto cleanup; failure: if (active) database_transaction_rollback(database); application_present_error(application, "Pièces jointes non enregistrées", error != NULL ? error->message : "La sélection n'a pas pu être enregistrée."); cleanup: g_clear_error(&error); g_clear_pointer(&existing, g_ptr_array_unref); g_clear_pointer(&selected, g_ptr_array_unref); evidence_entity_dao_free(dao); application_person_evidence_context_free(context); } /** @brief Ouvre le gestionnaire des preuves de la personne sélectionnée. */ static void application_on_person_evidence_requested( const char *entity_identifier, gpointer user_data) { Application *application = user_data; Database *database = NULL; EvidenceDao *evidence_dao = NULL; EvidenceEntityDao *link_dao = NULL; GPtrArray *records = NULL; GPtrArray *selected = NULL; ApplicationPersonEvidenceContext *context = NULL; const InvestigationProject *project = NULL; GError *error = NULL; if (application == NULL || application->session == NULL) return; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); evidence_dao = evidence_dao_new(database, &error); if (evidence_dao != NULL) records = evidence_dao_list_all(evidence_dao, &error); link_dao = evidence_entity_dao_new(database, &error); if (link_dao != NULL) selected = evidence_entity_dao_list_evidence_identifiers(link_dao, entity_identifier, &error); if (records == NULL || selected == NULL || project == NULL) goto failure; context = g_new0(ApplicationPersonEvidenceContext, 1); context->application = application; context->entity_identifier = g_strdup(entity_identifier); if (context->entity_identifier == NULL || !manage_entity_evidence_dialog_present(main_window_get_window( application->main_window), records, selected, investigation_project_get_root_path(project), application_on_person_evidences_selected, context, &error)) goto failure; context = NULL; goto cleanup; failure: application_person_evidence_context_free(context); context = NULL; application_present_error(application, "Pièces jointes indisponibles", error != NULL ? error->message : "Le gestionnaire n'a pas pu être ouvert."); cleanup: g_clear_error(&error); g_clear_pointer(&records, g_ptr_array_unref); g_clear_pointer(&selected, g_ptr_array_unref); evidence_dao_free(evidence_dao); evidence_entity_dao_free(link_dao); } /** @brief Libère le contexte d'édition d'une relation. */ static void application_edit_relation_context_free( ApplicationEditRelationContext *context) { if (context == NULL) return; g_free(context->relation_identifier); g_free(context); } /** @brief Persiste les valeurs validées du dialogue d'édition. */ static void application_on_edit_relation_completed( CreateRelationDialogResult *result, gpointer user_data) { ApplicationEditRelationContext *context = user_data; Application *application = context != NULL ? context->application : NULL; Database *database = NULL; RelationDao *relation_dao = NULL; RelationEvidenceDao *relation_evidence_dao = NULL; RelationRecord *existing = NULL; RelationRecord *updated = NULL; const InvestigationProject *project = NULL; GDateTime *now = NULL; char *timestamp = NULL; GError *error = NULL; gboolean transaction_active = FALSE; gint64 relation_type_identifier = 0; if (result == NULL || application == NULL || application->session == NULL) goto cleanup; database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); relation_dao = relation_dao_new(database, &error); relation_evidence_dao = relation_evidence_dao_new(database, &error); if (relation_dao == NULL || relation_evidence_dao == NULL) goto failure; existing = relation_dao_find_by_identifier(relation_dao, context->relation_identifier, &error); if (existing == NULL) goto failure; now = g_date_time_new_now_utc(); timestamp = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL; relation_type_identifier = create_relation_dialog_result_get_relation_type_identifier(result); if (relation_type_identifier == 0) { RelationTypeService *type_service = relation_type_service_new(database, &error); if (type_service == NULL || !relation_type_service_create_custom(type_service, create_relation_dialog_result_get_relation_type(result), NULL, &relation_type_identifier, &error)) { relation_type_service_free(type_service); goto failure; } relation_type_service_free(type_service); } updated = relation_record_new_canonical( relation_record_get_identifier(existing), relation_record_get_source_entity_identifier(existing), relation_record_get_target_entity_identifier(existing), relation_type_identifier, create_relation_dialog_result_get_relation_type_code(result), create_relation_dialog_result_get_relation_type(result), create_relation_dialog_result_get_label(result), create_relation_dialog_result_get_justification(result), create_relation_dialog_result_get_confidence(result), relation_record_get_created_at(existing), timestamp, relation_record_get_status(existing), &error); if (updated == NULL || !database_transaction_begin(database)) goto failure; transaction_active = TRUE; if (!relation_dao_update(relation_dao, updated, &error)) goto failure; { GPtrArray *existing_evidence = relation_evidence_dao_list_evidence_identifiers( relation_evidence_dao, context->relation_identifier, &error); const GPtrArray *selected_evidence = create_relation_dialog_result_get_evidence_identifiers(result); if (existing_evidence == NULL) goto failure; for (guint index = 0; index < existing_evidence->len; index++) { const char *identifier = g_ptr_array_index(existing_evidence, index); gboolean retained = FALSE; for (guint selected_index = 0; selected_index < selected_evidence->len; selected_index++) if (g_strcmp0(identifier, g_ptr_array_index(selected_evidence, selected_index)) == 0) retained = TRUE; if (!retained && !relation_evidence_dao_unlink( relation_evidence_dao, context->relation_identifier, identifier, &error)) { g_ptr_array_unref(existing_evidence); goto failure; } } for (guint index = 0; index < selected_evidence->len; index++) { const char *identifier = g_ptr_array_index(selected_evidence, index); gboolean exists = FALSE; if (!relation_evidence_dao_exists(relation_evidence_dao, context->relation_identifier, identifier, &exists, &error) || (!exists && !relation_evidence_dao_link(relation_evidence_dao, context->relation_identifier, identifier, &error))) { g_ptr_array_unref(existing_evidence); goto failure; } } g_ptr_array_unref(existing_evidence); } if (!database_transaction_commit(database)) goto failure; transaction_active = FALSE; main_window_set_status(application->main_window, "Relation modifiée. Actualisation du graphe…"); g_free(application->pending_relation_selection_identifier); application->pending_relation_selection_identifier = g_strdup( context->relation_identifier); application_start_graph_loading(application, investigation_project_get_database_path(project)); goto cleanup; failure: if (transaction_active) database_transaction_rollback(database); application_present_error(application, "Modification impossible", error != NULL ? error->message : "La relation n'a pas pu être modifiée."); cleanup: g_clear_error(&error); g_free(timestamp); if (now != NULL) g_date_time_unref(now); relation_record_free(updated); relation_record_free(existing); relation_dao_free(relation_dao); relation_evidence_dao_free(relation_evidence_dao); create_relation_dialog_result_free(result); application_edit_relation_context_free(context); } /** @brief Ouvre le formulaire prérempli de la relation sélectionnée. */ static void application_on_edit_relation_requested( const char *relation_identifier, gpointer user_data) { Application *application = user_data; Database *database = NULL; RelationDao *relation_dao = NULL; EntityDao *entity_dao = NULL; EvidenceDao *evidence_dao = NULL; RelationEvidenceDao *relation_evidence_dao = NULL; RelationRecord *relation = NULL; GPtrArray *entities = NULL; GPtrArray *evidences = NULL; GPtrArray *selected_evidences = NULL; GPtrArray *relation_types = NULL; RelationTypeDao *relation_type_dao = NULL; const InvestigationProject *project = NULL; ApplicationEditRelationContext *context = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || relation_identifier == NULL) return; database = investigation_session_get_database(application->session); relation_dao = relation_dao_new(database, &error); entity_dao = entity_dao_new(database, &error); evidence_dao = evidence_dao_new(database, &error); relation_evidence_dao = relation_evidence_dao_new(database, &error); relation_type_dao = relation_type_dao_new(database, &error); project = investigation_session_get_project(application->session); if (relation_dao != NULL) relation = relation_dao_find_by_identifier(relation_dao, relation_identifier, &error); if (entity_dao != NULL) entities = entity_dao_list_all(entity_dao, &error); if (evidence_dao != NULL) evidences = evidence_dao_list_all(evidence_dao, &error); if (relation_evidence_dao != NULL) selected_evidences = relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao, relation_identifier, &error); if (relation_type_dao != NULL) relation_types = relation_type_dao_list_all(relation_type_dao, &error); if (relation == NULL || entities == NULL || evidences == NULL || selected_evidences == NULL || relation_types == NULL || project == NULL) goto failure; context = g_new0(ApplicationEditRelationContext, 1); context->application = application; context->relation_identifier = g_strdup(relation_identifier); if (context->relation_identifier == NULL || !edit_relation_dialog_present( main_window_get_window(application->main_window), relation, entities, relation_types, evidences, selected_evidences, investigation_project_get_root_path(project), application_on_edit_relation_completed, context, &error)) goto failure; context = NULL; goto cleanup; failure: application_present_error(application, "Modification impossible", error != NULL ? error->message : "Le formulaire de modification n'a pas pu être ouvert."); cleanup: application_edit_relation_context_free(context); g_clear_error(&error); g_clear_pointer(&entities, g_ptr_array_unref); g_clear_pointer(&evidences, g_ptr_array_unref); g_clear_pointer(&selected_evidences, g_ptr_array_unref); relation_record_free(relation); entity_dao_free(entity_dao); evidence_dao_free(evidence_dao); relation_evidence_dao_free(relation_evidence_dao); relation_type_dao_free(relation_type_dao); g_clear_pointer(&relation_types, g_ptr_array_unref); relation_dao_free(relation_dao); } /** @brief Charge les preuves liées à la relation affichée dans le volet. */ static void application_on_relation_selected( const char *relation_identifier, gpointer user_data) { Application *application = user_data; Database *database = NULL; RelationEvidenceDao *relation_evidence_dao = NULL; EvidenceDao *evidence_dao = NULL; GPtrArray *identifiers = NULL; GPtrArray *records = NULL; GError *error = NULL; if (application == NULL || application->session == NULL || relation_identifier == NULL) return; database = investigation_session_get_database(application->session); relation_evidence_dao = relation_evidence_dao_new(database, &error); if (relation_evidence_dao == NULL) goto cleanup; evidence_dao = evidence_dao_new(database, &error); if (evidence_dao == NULL) goto cleanup; identifiers = relation_evidence_dao_list_evidence_identifiers( relation_evidence_dao, relation_identifier, &error); if (identifiers == NULL) goto cleanup; records = g_ptr_array_new_with_free_func((GDestroyNotify) evidence_record_free); if (records == NULL) goto cleanup; for (guint index = 0; index < identifiers->len; index++) { EvidenceRecord *record = evidence_dao_find_by_identifier(evidence_dao, g_ptr_array_index(identifiers, index), &error); if (record == NULL) goto cleanup; g_ptr_array_add(records, record); } main_window_set_relation_evidences(application->main_window, records); cleanup: if (error != NULL) { g_warning("Impossible de charger les preuves de la relation : %s", error->message); main_window_set_relation_evidences(application->main_window, NULL); } g_clear_error(&error); g_clear_pointer(&records, g_ptr_array_unref); g_clear_pointer(&identifiers, g_ptr_array_unref); evidence_dao_free(evidence_dao); relation_evidence_dao_free(relation_evidence_dao); } 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; EvidenceDao *evidence_dao = NULL; RelationTypeDao *relation_type_dao = NULL; const InvestigationProject *project = NULL; GPtrArray *entities = NULL; GPtrArray *evidences = NULL; GPtrArray *relation_types = 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 ); project = investigation_session_get_project(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 ); evidence_dao = evidence_dao_new(database, &error); if (evidence_dao != NULL) evidences = evidence_dao_list_all(evidence_dao, &error); evidence_dao_free(evidence_dao); relation_type_dao = relation_type_dao_new(database, &error); if (relation_type_dao != NULL) relation_types = relation_type_dao_list_all(relation_type_dao, &error); relation_type_dao_free(relation_type_dao); entity_dao_free( entity_dao ); if (entities == NULL || evidences == NULL || relation_types == NULL || project == NULL) { application_present_error( application, "Relations indisponibles", error != NULL ? error->message : "Impossible de charger les entités." ); g_clear_error( &error ); g_clear_pointer(&entities, g_ptr_array_unref); g_clear_pointer(&evidences, g_ptr_array_unref); g_clear_pointer(&relation_types, g_ptr_array_unref); return; } if (!create_relation_dialog_present( main_window_get_window( application->main_window ), source_entity_identifier, entities, relation_types, evidences, investigation_project_get_root_path(project), 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 ); g_ptr_array_unref(evidences); g_ptr_array_unref(relation_types); } /** * @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 ); } static void application_on_observation_remove_confirmed( gboolean confirmed, gpointer user_data) { ApplicationObservationRemovalContext *context = user_data; Application *application = context != NULL ? context->application : NULL; GError *error = NULL; gboolean deleted = FALSE, shared = FALSE; if (!confirmed || application == NULL || application->session == NULL) { application_observation_removal_context_free(context); return; } if (!eml_integration_remove_promotion( investigation_session_get_database(application->session), context->observation_identifier, &deleted, &shared, &error)) application_present_error(application, "Retrait du graphe impossible", error != NULL ? error->message : "La transaction a échoué."); else { if (deleted) { const InvestigationProject *project = investigation_session_get_project(application->session); application_start_graph_loading(application, investigation_project_get_database_path(project)); } char *selected_evidence = g_strdup( application->selected_evidence_identifier); application_on_evidence_selected(selected_evidence, application); g_free(selected_evidence); main_window_set_status(application->main_window, deleted ? "Observation conservée et nœud orphelin supprimé." : shared ? "Observation détachée ; entité partagée conservée." : "Observation retirée du graphe."); } g_clear_error(&error); application_observation_removal_context_free(context); } static void application_on_observation_remove_requested( const char *observation_identifier, gpointer user_data) { Application *application = user_data; if (application == NULL || !g_uuid_string_is_valid(observation_identifier)) return; ApplicationObservationRemovalContext *context = g_new0(ApplicationObservationRemovalContext, 1); context->application = application; context->observation_identifier = g_strdup(observation_identifier); application_message_dialog_present_confirmation( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_WARNING, "Retirer du graphe", "L’observation restera dans la fiche. Le nœud sera supprimé " "uniquement s’il n’est utilisé par aucune autre preuve, observation " "ou relation.", "Retirer du graphe", application_on_observation_remove_confirmed, context); } /** * @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_observation_remove_callback(application->main_window, application_on_observation_remove_requested, application); main_window_set_verify_evidence_callback( application->main_window, application_on_verify_evidence_requested, application ); main_window_set_edit_evidence_callback( application->main_window, application_on_edit_evidence_requested, application ); main_window_set_analyze_eml_callback(application->main_window, application_on_analyze_eml_requested, application); main_window_set_analyze_rib_callback(application->main_window, application_on_analyze_rib_requested, application); main_window_set_extract_metadata_callback(application->main_window, application_on_extract_metadata_requested, application); main_window_set_recover_pdf_password_callback(application->main_window, application_on_recover_pdf_password_requested, application); main_window_set_graph_node_moved_callback( application->main_window, application_on_graph_node_moved, application ); main_window_set_graph_transform_changed_callback( application->main_window, application_on_graph_transform_changed, application); main_window_set_extraction_drop_callback( application->main_window, application_on_extraction_dropped, 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_edit_relation_callback(application->main_window, application_on_edit_relation_requested, application); main_window_set_delete_relation_callback(application->main_window, application_on_delete_relation_requested, application); main_window_set_relation_selected_callback(application->main_window, application_on_relation_selected, application); main_window_set_person_role_callback(application->main_window, application_on_person_role_changed, application); main_window_set_person_confidence_callback(application->main_window, application_on_person_confidence_changed, application); main_window_set_person_name_callback(application->main_window, application_on_person_name_changed, application); main_window_set_person_evidence_callback(application->main_window, application_on_person_evidence_requested, application); main_window_set_entity_selected_callback(application->main_window, application_on_entity_selected, application); main_window_set_osint_action_callback( application->main_window, application_on_osint_action_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_add_social_account_callback( application->main_window, application_on_add_social_account_requested, application ); main_window_set_add_person_callback(application->main_window, application_on_add_person_requested, application); main_window_set_import_evidence_enabled( application->main_window, application->session != NULL ); main_window_set_add_social_account_enabled( application->main_window, application->session != NULL ); main_window_set_add_person_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 ); } /** @brief Ajoute le répertoire privé des outils OSINT au PATH du processus. */ static void application_prepare_osint_tool_path(void) { const char *current_path = g_getenv("PATH"); const char *home_directory = g_get_home_dir(); char *osint_directory = NULL; char *new_path = NULL; if (home_directory == NULL || home_directory[0] == '\0') return; osint_directory = g_build_filename(home_directory, ".local", "share", "labfy-osint", "bin", NULL); if (!g_file_test(osint_directory, G_FILE_TEST_IS_DIR)) { g_free(osint_directory); return; } if (current_path != NULL && g_strstr_len(current_path, -1, osint_directory) != NULL) { g_free(osint_directory); return; } new_path = g_strdup_printf("%s%s%s", osint_directory, current_path != NULL && current_path[0] != '\0' ? G_SEARCHPATH_SEPARATOR_S : "", current_path != NULL ? current_path : ""); g_setenv("PATH", new_path, TRUE); g_free(new_path); g_free(osint_directory); } Application *application_new(void) { Application *application = NULL; GError *error = NULL; application = g_new0( Application, 1 ); application_prepare_osint_tool_path(); 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 ); if (application->graph_viewport_save_source_id != 0U) { g_source_remove(application->graph_viewport_save_source_id); application->graph_viewport_save_source_id = 0U; } application_save_graph_viewport(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_clear_pointer(&application->pending_relation_selection_identifier, g_free); g_clear_pointer(&application->pending_entity_selection_identifier, g_free); g_free( application ); }