From a6ec67f38d1d203f99baebb18b093516eac6566d Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 22:29:59 +0200 Subject: [PATCH] feat(persons): identify impersonated identities --- database/schema_current.sql | 2 +- database/schema_v6.sql | 38 +++++++++++++++++++ docs/database/DATABASE_ARCHITECTURE.md | 6 +-- include/core/person_entity_service.h | 10 +++++ include/database/schema.h | 3 ++ include/models/entity_record.h | 3 +- include/views/main_window.h | 6 +++ include/widgets/entity_details_panel.h | 14 +++++++ include/widgets/workspace.h | 6 +++ src/core/application.c | 31 +++++++++++++++ src/core/person_entity_service.c | 52 ++++++++++++++++++++++++++ src/database/database.c | 33 +++++++++++++++- src/database/schema.c | 6 +++ src/models/entity_record.c | 5 +++ src/views/main_window.c | 22 +++++++++++ src/widgets/entity_details_panel.c | 49 +++++++++++++++++++++++- src/widgets/investigation_graph_view.c | 2 + src/widgets/workspace.c | 23 ++++++++++++ tests/test_database.c | 4 +- tests/test_person_entity_service.c | 17 +++++++++ 20 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 database/schema_v6.sql diff --git a/database/schema_current.sql b/database/schema_current.sql index c46f352..d5f6f1b 100644 --- a/database/schema_current.sql +++ b/database/schema_current.sql @@ -1,7 +1,7 @@ /****************************************************************************** * Labfy Investigation * - * Extensions idempotentes du schéma SQLite courant V5 + * Extensions idempotentes du schéma SQLite courant V6 ******************************************************************************/ /* diff --git a/database/schema_v6.sql b/database/schema_v6.sql new file mode 100644 index 0000000..5ed61e3 --- /dev/null +++ b/database/schema_v6.sql @@ -0,0 +1,38 @@ +/****************************************************************************** + * Labfy Investigation + * + * Migration du schéma SQLite V5 vers V6 : catégorie identité usurpée + ******************************************************************************/ + +ALTER TABLE person_roles RENAME TO person_roles_v5; + +CREATE TABLE person_roles +( + entity_id TEXT PRIMARY KEY, + role TEXT NOT NULL DEFAULT 'uncategorized', + updated_at TEXT NOT NULL, + + FOREIGN KEY (entity_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + CHECK (role IN + ( + 'uncategorized', + 'alleged_scammer', + 'victim', + 'witness', + 'suspect', + 'related_person', + 'impersonated_identity' + )), + CHECK (length(updated_at) = 20) +); + +INSERT INTO person_roles(entity_id, role, updated_at) +SELECT entity_id, role, updated_at FROM person_roles_v5; + +DROP TABLE person_roles_v5; + +CREATE INDEX idx_person_roles_role ON person_roles(role); diff --git a/docs/database/DATABASE_ARCHITECTURE.md b/docs/database/DATABASE_ARCHITECTURE.md index 5511649..4dd778a 100644 --- a/docs/database/DATABASE_ARCHITECTURE.md +++ b/docs/database/DATABASE_ARCHITECTURE.md @@ -1161,11 +1161,11 @@ compte via `preuve_entites`. La création des deux lignes et de cette liaison est transactionnelle : aucun nœud incomplet n'est conservé si une étape échoue. -### Rôles d'enquête des personnes — schéma V5 +### Rôles d'enquête des personnes — schémas V5 et V6 La table `person_roles` associe une entité de type personne à une catégorie -d'enquête contrôlée : scammer présumé, victime, témoin, suspect, personne liée -ou non catégorisée. La clé étrangère avec suppression en cascade évite les +d'enquête contrôlée : scammer présumé, victime, témoin, suspect, personne liée, +identité usurpée ou non catégorisée. La clé étrangère avec suppression en cascade évite les rôles orphelins. Le code stable est persisté ; le libellé et la couleur restent des choix de présentation. diff --git a/include/core/person_entity_service.h b/include/core/person_entity_service.h index 28fa4e9..df560e4 100644 --- a/include/core/person_entity_service.h +++ b/include/core/person_entity_service.h @@ -42,5 +42,15 @@ gboolean person_entity_service_update_role(Database *database, */ gboolean person_entity_service_update_confidence(Database *database, const char *entity_identifier, gint confidence, GError **error); +/** + * @brief Enregistre le nom affiché d'une personne existante. + * @param database Connexion ouverte empruntée. + * @param entity_identifier UUID de la personne. + * @param display_name Nouveau nom non vide. + * @param error Destination facultative d'une erreur. + * @return TRUE lorsque la modification est validée. + */ +gboolean person_entity_service_update_display_name(Database *database, + const char *entity_identifier, const char *display_name, GError **error); G_END_DECLS #endif diff --git a/include/database/schema.h b/include/database/schema.h index ee6b0bc..6ec52ef 100644 --- a/include/database/schema.h +++ b/include/database/schema.h @@ -84,6 +84,9 @@ bool schema_install_v4( /** @brief Installe la migration des rôles de personnes du schéma V5. */ bool schema_install_v5(Database *database); +/** @brief Installe la catégorie d'identité usurpée du schéma V6. */ +bool schema_install_v6(Database *database); + /** * @brief Garantit la présence des extensions du schéma courant V2. * diff --git a/include/models/entity_record.h b/include/models/entity_record.h index f8546e3..cba79f3 100644 --- a/include/models/entity_record.h +++ b/include/models/entity_record.h @@ -37,7 +37,8 @@ typedef enum PERSON_ROLE_VICTIM, PERSON_ROLE_WITNESS, PERSON_ROLE_SUSPECT, - PERSON_ROLE_RELATED_PERSON + PERSON_ROLE_RELATED_PERSON, + PERSON_ROLE_IMPERSONATED_IDENTITY } PersonRole; /** diff --git a/include/views/main_window.h b/include/views/main_window.h index 1f46e80..f7a9b8e 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -170,6 +170,9 @@ typedef void (*MainWindowOsintActionCallback)( const char *target_value, gpointer user_data ); +/** @brief Callback appelé pour modifier le nom affiché d'une personne. */ +typedef void (*MainWindowPersonNameCallback)(const char *entity_identifier, + const char *display_name, gpointer user_data); /** * @brief Définit le callback de vérification d'une preuve. @@ -287,6 +290,9 @@ void main_window_set_osint_action_callback( MainWindowOsintActionCallback callback, gpointer user_data ); +/** @brief Définit le callback de modification du nom affiché. */ +void main_window_set_person_name_callback(MainWindow *main_window, + MainWindowPersonNameCallback callback, gpointer user_data); /** * @brief Définit le callback du bouton « Nouvelle enquête ». diff --git a/include/widgets/entity_details_panel.h b/include/widgets/entity_details_panel.h index 699dfc0..e7ba9ce 100644 --- a/include/widgets/entity_details_panel.h +++ b/include/widgets/entity_details_panel.h @@ -60,6 +60,13 @@ typedef void (*EntityDetailsPanelPersonConfidenceCallback)( gpointer user_data ); +/** @brief Callback appelé pour enregistrer le nom affiché d'une personne. */ +typedef void (*EntityDetailsPanelPersonNameCallback)( + const char *entity_identifier, + const char *display_name, + gpointer user_data +); + /** * @brief Crée un volet de détails fermé. * @@ -155,6 +162,13 @@ void entity_details_panel_set_person_confidence_callback( gpointer user_data ); +/** @brief Définit le callback de modification du nom affiché. */ +void entity_details_panel_set_person_name_callback( + EntityDetailsPanel *details_panel, + EntityDetailsPanelPersonNameCallback callback, + gpointer user_data +); + /** * @brief Indique si le volet est actuellement ouvert. * diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 4fad120..fc91777 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -114,6 +114,9 @@ typedef void (*WorkspaceOsintActionCallback)( const char *target_value, gpointer user_data ); +/** @brief Callback appelé pour modifier le nom affiché d'une personne. */ +typedef void (*WorkspacePersonNameCallback)(const char *entity_identifier, + const char *display_name, gpointer user_data); /** @brief Callback appelé pour modifier les métadonnées d'une preuve. */ typedef void (*WorkspaceEditEvidenceCallback)( @@ -365,6 +368,9 @@ void workspace_set_person_confidence_callback(Workspace *workspace, void workspace_set_graph_loading( Workspace *workspace ); +/** @brief Définit le callback de modification du nom affiché. */ +void workspace_set_person_name_callback(Workspace *workspace, + WorkspacePersonNameCallback callback, gpointer user_data); /** * @brief Affiche le graphe chargé avec sa disposition persistée. diff --git a/src/core/application.c b/src/core/application.c index f60e09e..94324fe 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -5033,6 +5033,35 @@ static void application_on_person_confidence_changed( 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 Libère le contexte d'édition d'une relation. */ static void application_edit_relation_context_free( ApplicationEditRelationContext *context) @@ -6705,6 +6734,8 @@ static void application_on_activate( 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_osint_action_callback( application->main_window, diff --git a/src/core/person_entity_service.c b/src/core/person_entity_service.c index ebc9f36..f60ca09 100644 --- a/src/core/person_entity_service.c +++ b/src/core/person_entity_service.c @@ -17,6 +17,58 @@ static gboolean person_entity_service_status_valid(const char *status) g_strcmp0(status, "confirmed") == 0; } +gboolean person_entity_service_update_display_name(Database *database, + const char *entity_identifier, const char *display_name, GError **error) +{ + static const char sql[] = + "UPDATE entites SET label=trim(?), updated_at=? WHERE id=? AND " + "type_id=(SELECT id FROM types_entite WHERE code='person');"; + DatabaseStatement *statement = NULL; + GDateTime *now = NULL; + char *timestamp = NULL; + char *normalized = NULL; + gboolean active = FALSE, success = FALSE; + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + normalized = display_name != NULL ? g_strdup(display_name) : NULL; + if (normalized != NULL) g_strstrip(normalized); + if (database == NULL || entity_identifier == NULL || + !g_uuid_string_is_valid(entity_identifier) || normalized == NULL || + normalized[0] == '\0') + { + g_set_error_literal(error, g_quark_from_static_string( + "person-entity-service-error"), 6, + "Le nom affiché de la personne est invalide."); + g_free(normalized); + return FALSE; + } + now = g_date_time_new_now_utc(); + timestamp = now != NULL ? g_date_time_format(now, + "%Y-%m-%dT%H:%M:%SZ") : NULL; + if (timestamp == NULL || !database_transaction_begin(database)) + goto cleanup; + active = TRUE; + statement = database_statement_prepare(database, sql); + if (statement == NULL || !database_statement_bind_text(statement, 1, + normalized) || !database_statement_bind_text(statement, 2, + timestamp) || !database_statement_bind_text(statement, 3, + entity_identifier) || database_statement_step(statement) != + DATABASE_STATEMENT_STEP_DONE) + goto cleanup; + database_statement_finalize(statement); statement = NULL; + if (!database_transaction_commit(database)) goto cleanup; + active = FALSE; success = TRUE; +cleanup: + database_statement_finalize(statement); + if (!success && active) database_transaction_rollback(database); + if (!success && error != NULL && *error == NULL) + g_set_error_literal(error, g_quark_from_static_string( + "person-entity-service-error"), 7, + "Le nom affiché n'a pas pu être enregistré."); + g_free(normalized); g_free(timestamp); + g_clear_pointer(&now, g_date_time_unref); + return success; +} + gboolean person_entity_service_update_confidence(Database *database, const char *entity_identifier, gint confidence, GError **error) { diff --git a/src/database/database.c b/src/database/database.c index fa9ff4b..3a61455 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -16,12 +16,12 @@ /** * @brief Version actuelle du schéma SQLite. */ -#define DATABASE_SCHEMA_VERSION_CURRENT 5 +#define DATABASE_SCHEMA_VERSION_CURRENT 6 /** * @brief Version actuelle sous forme textuelle pour metadata. */ -#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "5" +#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "6" /** * @brief Nom de l'application enregistré dans les métadonnées. @@ -730,6 +730,24 @@ rollback: return false; } +/** @brief Applique atomiquement la migration du schéma V5 vers V6. */ +static bool database_migrate_v5_to_v6(Database *database) +{ + bool transaction_started = false; + if (database == NULL || !database_transaction_begin(database)) + return false; + transaction_started = true; + if (!schema_install_v6(database) || + !database_update_schema_version(database, "6") || + !database_transaction_commit(database)) + goto rollback; + return true; +rollback: + if (transaction_started && !database_transaction_rollback(database)) + g_warning("Impossible d’annuler la migration SQLite V5 vers V6."); + return false; +} + /** * @brief Garantit atomiquement la présence des extensions du schéma courant. */ @@ -934,6 +952,12 @@ bool database_migrate_to_latest( schema_version = 5; break; + case 5: + if (!database_migrate_v5_to_v6(database)) + return false; + schema_version = 6; + break; + default: database_set_error( database, @@ -1060,6 +1084,11 @@ bool database_initialize( goto rollback; } + if (!schema_install_v6(database)) + { + goto rollback; + } + if (!schema_ensure_current( database )) diff --git a/src/database/schema.c b/src/database/schema.c index 0c72013..e56ef90 100644 --- a/src/database/schema.c +++ b/src/database/schema.c @@ -221,6 +221,12 @@ bool schema_install_v5(Database *database) "la migration SQLite V5"); } +bool schema_install_v6(Database *database) +{ + return schema_execute_file(database, "database/schema_v6.sql", + "la migration SQLite V6"); +} + bool schema_ensure_current( Database *database ) diff --git a/src/models/entity_record.c b/src/models/entity_record.c index 8e2fda1..c939277 100644 --- a/src/models/entity_record.c +++ b/src/models/entity_record.c @@ -613,6 +613,8 @@ const char *person_role_to_code(PersonRole role) case PERSON_ROLE_WITNESS: return "witness"; case PERSON_ROLE_SUSPECT: return "suspect"; case PERSON_ROLE_RELATED_PERSON: return "related_person"; + case PERSON_ROLE_IMPERSONATED_IDENTITY: + return "impersonated_identity"; default: return NULL; } } @@ -626,6 +628,8 @@ PersonRole person_role_from_code(const char *code) if (g_strcmp0(code, "suspect") == 0) return PERSON_ROLE_SUSPECT; if (g_strcmp0(code, "related_person") == 0) return PERSON_ROLE_RELATED_PERSON; + if (g_strcmp0(code, "impersonated_identity") == 0) + return PERSON_ROLE_IMPERSONATED_IDENTITY; return PERSON_ROLE_UNCATEGORIZED; } @@ -638,6 +642,7 @@ const char *person_role_get_label(PersonRole role) case PERSON_ROLE_WITNESS: return "Témoin"; case PERSON_ROLE_SUSPECT: return "Suspect"; case PERSON_ROLE_RELATED_PERSON: return "Personne liée"; + case PERSON_ROLE_IMPERSONATED_IDENTITY: return "Identité usurpée"; case PERSON_ROLE_UNCATEGORIZED: default: return "Non catégorisé"; } diff --git a/src/views/main_window.c b/src/views/main_window.c index 1b6cbe4..f102f96 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -135,6 +135,8 @@ struct MainWindow gpointer person_role_user_data; MainWindowPersonConfidenceCallback person_confidence_callback; gpointer person_confidence_user_data; + MainWindowPersonNameCallback person_name_callback; + gpointer person_name_user_data; MainWindowOsintActionCallback osint_action_callback; @@ -551,6 +553,16 @@ static void main_window_on_person_confidence_changed( main_window->person_confidence_user_data); } +/** @brief Relaie la modification du nom affiché d'une personne. */ +static void main_window_on_person_name_changed(const char *entity_identifier, + const char *display_name, gpointer user_data) +{ + MainWindow *main_window = user_data; + if (main_window != NULL && main_window->person_name_callback != NULL) + main_window->person_name_callback(entity_identifier, display_name, + main_window->person_name_user_data); +} + /** * @brief Relaie la demande de vérification provenant du Workspace. */ @@ -895,6 +907,8 @@ MainWindow *main_window_new( main_window_on_person_role_changed, main_window); workspace_set_person_confidence_callback(main_window->workspace, main_window_on_person_confidence_changed, main_window); + workspace_set_person_name_callback(main_window->workspace, + main_window_on_person_name_changed, main_window); workspace_set_osint_action_callback( main_window->workspace, @@ -1718,6 +1732,14 @@ void main_window_set_person_confidence_callback(MainWindow *main_window, main_window->person_confidence_user_data = user_data; } +void main_window_set_person_name_callback(MainWindow *main_window, + MainWindowPersonNameCallback callback, gpointer user_data) +{ + if (main_window == NULL) return; + main_window->person_name_callback = callback; + main_window->person_name_user_data = user_data; +} + void main_window_set_quit_callback( MainWindow *main_window, MainWindowQuitCallback callback, diff --git a/src/widgets/entity_details_panel.c b/src/widgets/entity_details_panel.c index f36233f..0797b23 100644 --- a/src/widgets/entity_details_panel.c +++ b/src/widgets/entity_details_panel.c @@ -33,6 +33,8 @@ struct EntityDetailsPanel GtkDropDown *person_role_dropdown; GtkSpinButton *person_confidence_spin; GtkWidget *person_confidence_button; + GtkEntry *person_name_entry; + GtkWidget *person_name_button; char *selected_entity_identifier; @@ -50,8 +52,26 @@ struct EntityDetailsPanel gboolean updating_person_role; EntityDetailsPanelPersonConfidenceCallback person_confidence_callback; gpointer person_confidence_user_data; + EntityDetailsPanelPersonNameCallback person_name_callback; + gpointer person_name_user_data; }; +/** @brief Relaie le nouveau nom affiché explicitement enregistré. */ +static void entity_details_panel_on_person_name_clicked(GtkButton *button, + gpointer user_data) +{ + EntityDetailsPanel *details_panel = user_data; + const char *name = NULL; + (void) button; + if (details_panel == NULL || details_panel->person_name_callback == NULL || + details_panel->selected_entity_identifier == NULL) return; + name = gtk_editable_get_text(GTK_EDITABLE(details_panel->person_name_entry)); + if (name == NULL || name[0] == '\0') return; + details_panel->person_name_callback( + details_panel->selected_entity_identifier, name, + details_panel->person_name_user_data); +} + /** @brief Relaie la confiance explicitement enregistrée par l'utilisateur. */ static void entity_details_panel_on_person_confidence_clicked( GtkButton *button, gpointer user_data) @@ -80,7 +100,7 @@ static void entity_details_panel_on_person_role_changed(GObject *object, details_panel->selected_entity_identifier == NULL) return; selected = gtk_drop_down_get_selected(details_panel->person_role_dropdown); - if (selected > PERSON_ROLE_RELATED_PERSON) return; + if (selected > PERSON_ROLE_IMPERSONATED_IDENTITY) return; details_panel->person_role_callback( details_panel->selected_entity_identifier, (PersonRole) selected, details_panel->person_role_user_data); @@ -744,7 +764,7 @@ EntityDetailsPanel *entity_details_panel_new(void) details_panel->person_role_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); person_role_labels = gtk_string_list_new(NULL); for (guint role = PERSON_ROLE_UNCATEGORIZED; - role <= PERSON_ROLE_RELATED_PERSON; role++) + role <= PERSON_ROLE_IMPERSONATED_IDENTITY; role++) gtk_string_list_append(person_role_labels, person_role_get_label((PersonRole) role)); details_panel->person_role_dropdown = GTK_DROP_DOWN( @@ -764,12 +784,25 @@ EntityDetailsPanel *entity_details_panel_new(void) GTK_WIDGET(details_panel->person_confidence_spin)); gtk_box_append(GTK_BOX(details_panel->person_role_box), details_panel->person_confidence_button); + details_panel->person_name_entry = GTK_ENTRY(gtk_entry_new()); + gtk_entry_set_placeholder_text(details_panel->person_name_entry, + "Nom réel ou nom affiché"); + details_panel->person_name_button = gtk_button_new_with_label( + "Enregistrer le nom"); + gtk_box_append(GTK_BOX(details_panel->person_role_box), + gtk_label_new("Nom affiché dans le graphe")); + gtk_box_append(GTK_BOX(details_panel->person_role_box), + GTK_WIDGET(details_panel->person_name_entry)); + gtk_box_append(GTK_BOX(details_panel->person_role_box), + details_panel->person_name_button); gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box); g_signal_connect(details_panel->person_role_dropdown, "notify::selected", G_CALLBACK(entity_details_panel_on_person_role_changed), details_panel); g_signal_connect(details_panel->person_confidence_button, "clicked", G_CALLBACK(entity_details_panel_on_person_confidence_clicked), details_panel); + g_signal_connect(details_panel->person_name_button, "clicked", + G_CALLBACK(entity_details_panel_on_person_name_clicked), details_panel); if (details_panel->entity_value_label == NULL || details_panel->entity_type_label == NULL || @@ -896,6 +929,9 @@ void entity_details_panel_set_entity( (guint) entity_record_get_person_role(entity_record)); gtk_spin_button_set_value(details_panel->person_confidence_spin, entity_record_get_confidence(entity_record)); + gtk_editable_set_text(GTK_EDITABLE(details_panel->person_name_entry), + entity_record_get_label(entity_record) != NULL + ? entity_record_get_label(entity_record) : ""); details_panel->updating_person_role = FALSE; entity_details_panel_update_add_relation_button( @@ -1161,6 +1197,15 @@ void entity_details_panel_set_person_confidence_callback( details_panel->person_confidence_user_data = user_data; } +void entity_details_panel_set_person_name_callback( + EntityDetailsPanel *details_panel, + EntityDetailsPanelPersonNameCallback callback, gpointer user_data) +{ + if (details_panel == NULL) return; + details_panel->person_name_callback = callback; + details_panel->person_name_user_data = user_data; +} + gboolean entity_details_panel_is_open( const EntityDetailsPanel *details_panel ) diff --git a/src/widgets/investigation_graph_view.c b/src/widgets/investigation_graph_view.c index ed4d9aa..97af47f 100644 --- a/src/widgets/investigation_graph_view.c +++ b/src/widgets/investigation_graph_view.c @@ -3343,6 +3343,8 @@ static void investigation_graph_view_draw_entity( cairo_set_source_rgb(cairo_context, 0.62, 0.32, 0.08); break; case PERSON_ROLE_RELATED_PERSON: cairo_set_source_rgb(cairo_context, 0.38, 0.20, 0.52); break; + case PERSON_ROLE_IMPERSONATED_IDENTITY: + cairo_set_source_rgb(cairo_context, 0.61, 0.35, 0.71); break; case PERSON_ROLE_UNCATEGORIZED: default: cairo_set_source_rgb(cairo_context, 0.25, 0.27, 0.31); break; diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 68cef8a..2a25fbc 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -138,6 +138,8 @@ struct Workspace gpointer person_role_user_data; WorkspacePersonConfidenceCallback person_confidence_callback; gpointer person_confidence_user_data; + WorkspacePersonNameCallback person_name_callback; + gpointer person_name_user_data; WorkspaceOsintActionCallback osint_action_callback; @@ -894,6 +896,16 @@ static void workspace_on_person_confidence_changed( workspace->person_confidence_user_data); } +/** @brief Relaie la modification du nom affiché d'une personne. */ +static void workspace_on_person_name_changed(const char *entity_identifier, + const char *display_name, gpointer user_data) +{ + Workspace *workspace = user_data; + if (workspace != NULL && workspace->person_name_callback != NULL) + workspace->person_name_callback(entity_identifier, display_name, + workspace->person_name_user_data); +} + /** * @brief Désélectionne le nœud lorsque le volet est fermé manuellement. */ @@ -1816,6 +1828,9 @@ Workspace *workspace_new(void) entity_details_panel_set_person_confidence_callback( workspace->entity_details_panel, workspace_on_person_confidence_changed, workspace); + entity_details_panel_set_person_name_callback( + workspace->entity_details_panel, workspace_on_person_name_changed, + workspace); gtk_overlay_set_child( GTK_OVERLAY( @@ -3281,6 +3296,14 @@ void workspace_set_person_confidence_callback(Workspace *workspace, workspace->person_confidence_user_data = user_data; } +void workspace_set_person_name_callback(Workspace *workspace, + WorkspacePersonNameCallback callback, gpointer user_data) +{ + if (workspace == NULL) return; + workspace->person_name_callback = callback; + workspace->person_name_user_data = user_data; +} + void workspace_reset_graph_layout( Workspace *workspace ) diff --git a/tests/test_database.c b/tests/test_database.c index ccc333c..e5859c6 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void) "FROM investigation;" ); - assert(strcmp(schema_version, "5") == 0); + assert(strcmp(schema_version, "6") == 0); test_database_assert_table_exists(database, "osint_executions"); test_database_assert_table_exists(database, "osint_execution_entities"); test_database_assert_table_exists(database, "osint_execution_relations"); @@ -989,7 +989,7 @@ static void test_database_migrate_v1_to_v2(void) assert( strcmp( schema_version, - "5" + "6" ) == 0 ); diff --git a/tests/test_person_entity_service.c b/tests/test_person_entity_service.c index 52aec3d..3b4fb63 100644 --- a/tests/test_person_entity_service.c +++ b/tests/test_person_entity_service.c @@ -44,6 +44,23 @@ static void test_person_entity_create(void) record = entity_dao_find_by_identifier(dao, identifier, &error); assert(record != NULL && error == NULL); assert(entity_record_get_person_role(record) == PERSON_ROLE_VICTIM); + entity_record_free(record); record = NULL; + assert(person_entity_service_update_role(database, identifier, + PERSON_ROLE_IMPERSONATED_IDENTITY, &error)); + record = entity_dao_find_by_identifier(dao, identifier, &error); + assert(record != NULL && error == NULL); + assert(entity_record_get_person_role(record) == + PERSON_ROLE_IMPERSONATED_IDENTITY); + assert(strcmp(person_role_to_code(PERSON_ROLE_IMPERSONATED_IDENTITY), + "impersonated_identity") == 0); + assert(strcmp(person_role_get_label(PERSON_ROLE_IMPERSONATED_IDENTITY), + "Identité usurpée") == 0); + assert(person_entity_service_update_display_name(database, identifier, + "Vraie identité", &error)); + entity_record_free(record); record = NULL; + record = entity_dao_find_by_identifier(dao, identifier, &error); + assert(record != NULL && error == NULL); + assert(strcmp(entity_record_get_label(record), "Vraie identité") == 0); entity_record_free(record); entity_dao_free(dao); database_close(database); assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0); g_free(identifier); g_free(path); g_free(directory);