Compare commits
5 commits
8adfc3f106
...
6b43379e75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b43379e75 | ||
|
|
f54af89e3e | ||
|
|
1d8d01a2ed | ||
|
|
bdcbcce0aa | ||
|
|
33a4a8be10 |
26 changed files with 1511 additions and 65 deletions
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Glisser-déposer confirmé des extractions texte vers le graphe, avec
|
||||
persistance de leur association à une entité.
|
||||
|
||||
- Initialisation du cycle de vie de l'application.
|
||||
- Création du module `Application`.
|
||||
- Intégration de GTK4.
|
||||
|
|
|
|||
24
Makefile
24
Makefile
|
|
@ -128,6 +128,7 @@ TEST_EML_ANALYZER := tests/test_eml_analyzer
|
|||
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
||||
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
||||
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
||||
TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
|
|
@ -197,6 +198,22 @@ $(TEST_ERROR): \
|
|||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_EXTRACTION_DROP_SERVICE): \
|
||||
tests/test_extraction_drop_service.c \
|
||||
src/core/extraction_drop_service.c \
|
||||
src/core/file_hash.c \
|
||||
src/dao/evidence_dao.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/dao/evidence_entity_dao.c \
|
||||
src/models/evidence_record.c \
|
||||
src/models/entity_record.c \
|
||||
src/database/database.c \
|
||||
src/database/schema.c \
|
||||
src/database/statement.c \
|
||||
src/database/transaction.c \
|
||||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_INVESTIGATION_RECORD): \
|
||||
tests/test_investigation_record.c \
|
||||
src/models/investigation_record.c
|
||||
|
|
@ -746,7 +763,8 @@ test: \
|
|||
$(TEST_EML_ANALYZER) \
|
||||
$(TEST_IBAN_ANALYZER) \
|
||||
$(TEST_EXIFTOOL_METADATA) \
|
||||
$(TEST_PDF_PASSWORD_RECOVERY)
|
||||
$(TEST_PDF_PASSWORD_RECOVERY) \
|
||||
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
@echo "Exécution des tests..."
|
||||
@./$(TEST_NODE)
|
||||
@./$(TEST_TREE_MODEL)
|
||||
|
|
@ -809,6 +827,7 @@ test: \
|
|||
@$(TEST_IBAN_ANALYZER)
|
||||
@$(TEST_EXIFTOOL_METADATA)
|
||||
@$(TEST_PDF_PASSWORD_RECOVERY)
|
||||
@$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
@echo "Tous les tests sont valides."
|
||||
|
||||
%.o: %.c
|
||||
|
|
@ -874,7 +893,8 @@ clean:
|
|||
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
|
||||
$(TEST_SOCIAL_PLATFORM) \
|
||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||
$(TEST_EML_ANALYZER)
|
||||
$(TEST_EML_ANALYZER) \
|
||||
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
|
||||
-include $(DEP)
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ Le socle actuel comprend notamment :
|
|||
sorties standard et d'erreur, et objets créés ou réutilisés ;
|
||||
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans
|
||||
réécriture de l'empreinte ou des données contrôlées.
|
||||
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
||||
avec confirmation explicite, création d'entité ou rattachement à une entité
|
||||
existante, sans déplacement du fichier produit ;
|
||||
|
||||
Les outils actuellement présents dans le catalogue initial sont :
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
# Ticket 105 — Glisser-déposer des extractions vers le graphe
|
||||
|
||||
## Objectif
|
||||
|
||||
Permettre de prendre une extraction depuis `Dossier → Extractions` et de la
|
||||
déposer dans le graphe afin de créer ou compléter les liens de l’enquête.
|
||||
|
||||
## Fonctionnement attendu
|
||||
|
||||
1. Une extraction sélectionnée dans la Sidebar peut être déplacée.
|
||||
2. Le graphe accepte le dépôt uniquement sur une zone valide.
|
||||
3. Une fenêtre de confirmation affiche le fichier, sa source et son contenu
|
||||
textuel disponible.
|
||||
4. L’utilisateur choisit explicitement :
|
||||
- créer une nouvelle entité à partir de l’extraction ;
|
||||
- rattacher l’extraction à une entité existante ;
|
||||
- annuler.
|
||||
5. L’association est enregistrée en base et visible dans la fiche concernée.
|
||||
6. Une relation graphique peut être créée sans déplacer ni dupliquer le
|
||||
fichier original.
|
||||
|
||||
## Contraintes
|
||||
|
||||
- Ne jamais créer de relation silencieusement.
|
||||
- Refuser les fichiers hors du dossier de l’enquête.
|
||||
- Conserver les associations déjà présentes.
|
||||
- Utiliser les API GTK de glisser-déposer et les commentaires Doxygen prévus
|
||||
par le projet.
|
||||
|
||||
## Critères d’acceptation
|
||||
|
||||
- Un fichier `.txt` d’OSINT est déplaçable depuis la Sidebar.
|
||||
- Le dépôt sur le graphe ouvre le dialogue de choix.
|
||||
- L’entité ou l’association créée survit à la réouverture de l’enquête.
|
||||
- Les dépôts annulés ne modifient ni la base ni le graphe.
|
||||
- Tests unitaires des chemins invalides, du choix d’action et de la
|
||||
persistance.
|
||||
|
||||
## Vérification
|
||||
|
||||
```sh
|
||||
make -j2
|
||||
make test
|
||||
```
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/******************************************************************************
|
||||
* Labfy Investigation
|
||||
*
|
||||
* Extensions idempotentes du schéma SQLite courant V6
|
||||
* Extensions idempotentes du schéma SQLite courant V8
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
|
|
@ -93,6 +93,15 @@ CREATE TABLE IF NOT EXISTS graph_layout_positions
|
|||
CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS graph_viewport
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
zoom REAL NOT NULL CHECK (zoom = zoom AND zoom > 0),
|
||||
offset_x REAL NOT NULL CHECK (offset_x = offset_x),
|
||||
offset_y REAL NOT NULL CHECK (offset_y = offset_y),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
||||
/* Migration idempotente des positions d'entités déjà enregistrées. */
|
||||
INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at)
|
||||
SELECT entity_id, x, y, updated_at
|
||||
|
|
|
|||
8
database/schema_v8.sql
Normal file
8
database/schema_v8.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS graph_viewport
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
zoom REAL NOT NULL CHECK (zoom = zoom AND zoom > 0),
|
||||
offset_x REAL NOT NULL CHECK (offset_x = offset_x),
|
||||
offset_y REAL NOT NULL CHECK (offset_y = offset_y),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
|
@ -2269,6 +2269,12 @@ Cette stratégie préserve les positions existantes tout en permettant aux
|
|||
nœuds de relation de suivre exactement le même cycle de chargement,
|
||||
d'enregistrement et de réinitialisation que les nœuds d'entité.
|
||||
|
||||
Le cadrage global est stocké séparément dans la ligne unique de
|
||||
`graph_viewport`. Elle conserve le niveau de zoom et les décalages horizontal
|
||||
et vertical du canevas. Ces valeurs sont enregistrées à la fermeture de
|
||||
l'enquête et restaurées lors de son premier chargement, afin que la vue
|
||||
réapparaisse exactement à l'endroit où l'utilisateur l'avait laissée.
|
||||
|
||||
---
|
||||
|
||||
# 12. Conclusion
|
||||
|
|
|
|||
76
include/core/extraction_drop_service.h
Normal file
76
include/core/extraction_drop_service.h
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/******************************************************************************
|
||||
* @file extraction_drop_service.h
|
||||
* @brief Validation et intégration d'une extraction déposée sur le graphe.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_EXTRACTION_DROP_SERVICE_H
|
||||
#define LABFY_INVESTIGATION_EXTRACTION_DROP_SERVICE_H
|
||||
|
||||
#include "database/database.h"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct ExtractionDropInfo ExtractionDropInfo;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_READ,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_DATABASE
|
||||
} ExtractionDropServiceError;
|
||||
|
||||
#define EXTRACTION_DROP_SERVICE_ERROR extraction_drop_service_error_quark()
|
||||
|
||||
GQuark extraction_drop_service_error_quark(void);
|
||||
|
||||
/**
|
||||
* @brief Valide et lit une extraction texte appartenant à l'enquête.
|
||||
*
|
||||
* Le fichier doit être un `.txt` régulier situé sous
|
||||
* `02_Preuves_Traitees/Extractions`. Aucun fichier n'est déplacé ou modifié.
|
||||
*/
|
||||
ExtractionDropInfo *extraction_drop_service_prepare(
|
||||
const char *investigation_root,
|
||||
const char *file_path,
|
||||
GError **error
|
||||
);
|
||||
|
||||
void extraction_drop_info_free(ExtractionDropInfo *info);
|
||||
const char *extraction_drop_info_get_file_path(const ExtractionDropInfo *info);
|
||||
const char *extraction_drop_info_get_relative_path(
|
||||
const ExtractionDropInfo *info);
|
||||
const char *extraction_drop_info_get_file_name(const ExtractionDropInfo *info);
|
||||
const char *extraction_drop_info_get_source(const ExtractionDropInfo *info);
|
||||
const char *extraction_drop_info_get_content(const ExtractionDropInfo *info);
|
||||
|
||||
/**
|
||||
* @brief Enregistre l'extraction comme preuve traitée et la lie à une entité.
|
||||
*
|
||||
* Une preuve ou une association déjà présente est réutilisée.
|
||||
*/
|
||||
gboolean extraction_drop_service_attach(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
const char *entity_identifier,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une entité générique puis lui associe l'extraction.
|
||||
*
|
||||
* @return UUID nouvellement alloué dans out_entity_identifier, si demandé.
|
||||
*/
|
||||
gboolean extraction_drop_service_create_entity(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
char **out_entity_identifier,
|
||||
GError **error
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -143,6 +143,23 @@ gboolean graph_node_position_dao_delete_all(
|
|||
GError **error
|
||||
);
|
||||
|
||||
gboolean graph_node_position_dao_load_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y,
|
||||
gboolean *found,
|
||||
GError **error
|
||||
);
|
||||
|
||||
gboolean graph_node_position_dao_upsert_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y,
|
||||
GError **error
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ bool schema_install_v5(Database *database);
|
|||
bool schema_install_v6(Database *database);
|
||||
|
||||
bool schema_install_v7(Database *database);
|
||||
bool schema_install_v8(Database *database);
|
||||
|
||||
/**
|
||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ typedef void (*MainWindowGraphNodeMovedCallback)(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
typedef void (*MainWindowExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||
*
|
||||
|
|
@ -246,6 +252,12 @@ void main_window_set_graph_node_moved_callback(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
void main_window_set_extraction_drop_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||
*
|
||||
|
|
@ -514,6 +526,11 @@ void main_window_set_graph(
|
|||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean main_window_get_graph_view_transform(
|
||||
MainWindow *main_window, double *zoom, double *offset_x, double *offset_y);
|
||||
void main_window_set_graph_view_transform(
|
||||
MainWindow *main_window, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/** @brief Sélectionne une relation dans le graphe affiché. */
|
||||
gboolean main_window_select_graph_relation(
|
||||
MainWindow *main_window,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ typedef void (*EntityDetailsPanelPersonNameCallback)(
|
|||
/** @brief Callback appelé pour gérer les preuves d'une personne. */
|
||||
typedef void (*EntityDetailsPanelPersonEvidenceCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé lorsqu'une preuve associée est activée. */
|
||||
typedef void (*EntityDetailsPanelEvidenceActivatedCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé pour ouvrir les recherches OSINT compatibles. */
|
||||
typedef void (*EntityDetailsPanelOsintCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
|
|
@ -178,6 +181,10 @@ void entity_details_panel_set_person_name_callback(
|
|||
void entity_details_panel_set_person_evidence_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelPersonEvidenceCallback callback, gpointer user_data);
|
||||
void entity_details_panel_set_evidence_activated_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelEvidenceActivatedCallback callback,
|
||||
gpointer user_data);
|
||||
/** @brief Définit le callback du bouton de recherche OSINT. */
|
||||
void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelOsintCallback callback, gpointer user_data);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@ typedef void (*InvestigationGraphViewNodeMovedCallback)(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé lorsqu'une extraction est déposée sur le graphe. */
|
||||
typedef void (*InvestigationGraphViewExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle vue graphique vide.
|
||||
*
|
||||
|
|
@ -170,6 +177,12 @@ void investigation_graph_view_set_node_moved_callback(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
void investigation_graph_view_set_extraction_drop_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne l'entité actuellement sélectionnée.
|
||||
*
|
||||
|
|
@ -234,6 +247,26 @@ void investigation_graph_view_reset_view(
|
|||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Lit le zoom et le déplacement courants du canvas.
|
||||
*
|
||||
* @return TRUE lorsqu'un graphe est actuellement présenté.
|
||||
*/
|
||||
gboolean investigation_graph_view_get_view_transform(
|
||||
const InvestigationGraphView *graph_view,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y
|
||||
);
|
||||
|
||||
/** @brief Restaure un zoom et un déplacement précédemment lus. */
|
||||
void investigation_graph_view_set_view_transform(
|
||||
InvestigationGraphView *graph_view,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Restaure la disposition déterministe initiale des nœuds.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ typedef void (*WorkspaceGraphNodeMovedCallback)(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
typedef void (*WorkspaceExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||
*
|
||||
|
|
@ -123,6 +129,8 @@ typedef void (*WorkspacePersonNameCallback)(const char *entity_identifier,
|
|||
/** @brief Callback appelé pour gérer les preuves d'une personne. */
|
||||
typedef void (*WorkspacePersonEvidenceCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
typedef void (*WorkspaceEvidenceActivatedCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé quand une entité devient sélectionnée. */
|
||||
typedef void (*WorkspaceEntitySelectedCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
|
|
@ -317,6 +325,12 @@ void workspace_set_graph_node_moved_callback(
|
|||
gpointer user_data
|
||||
);
|
||||
|
||||
void workspace_set_extraction_drop_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||
*
|
||||
|
|
@ -404,6 +418,10 @@ void workspace_set_person_name_callback(Workspace *workspace,
|
|||
/** @brief Définit le callback de gestion des preuves d'une personne. */
|
||||
void workspace_set_person_evidence_callback(Workspace *workspace,
|
||||
WorkspacePersonEvidenceCallback callback, gpointer user_data);
|
||||
void workspace_set_evidence_activated_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceEvidenceActivatedCallback callback,
|
||||
gpointer user_data);
|
||||
/** @brief Définit le callback de sélection d'une entité. */
|
||||
void workspace_set_entity_selected_callback(Workspace *workspace,
|
||||
WorkspaceEntitySelectedCallback callback, gpointer user_data);
|
||||
|
|
@ -429,6 +447,11 @@ void workspace_set_graph(
|
|||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean workspace_get_graph_view_transform(
|
||||
Workspace *workspace, double *zoom, double *offset_x, double *offset_y);
|
||||
void workspace_set_graph_view_transform(
|
||||
Workspace *workspace, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/**
|
||||
* @brief Affiche une erreur de chargement du graphe.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
#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 <gtk/gtk.h>
|
||||
|
|
@ -136,6 +137,7 @@ struct Application
|
|||
InvestigationGraphModel *graph_model;
|
||||
InvestigationGraphLayout *graph_layout;
|
||||
guint64 graph_load_generation;
|
||||
gboolean graph_viewport_restored;
|
||||
|
||||
char *selected_evidence_identifier;
|
||||
char *pending_relation_selection_identifier;
|
||||
|
|
@ -225,6 +227,13 @@ typedef struct
|
|||
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
|
||||
{
|
||||
|
|
@ -244,12 +253,196 @@ static void application_start_graph_loading(
|
|||
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 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
|
||||
);
|
||||
|
|
@ -929,6 +1122,8 @@ static void application_on_graph_loaded(
|
|||
application->graph_layout
|
||||
);
|
||||
|
||||
application_restore_graph_viewport(application);
|
||||
|
||||
if (application->pending_relation_selection_identifier != NULL)
|
||||
{
|
||||
main_window_select_graph_relation(application->main_window,
|
||||
|
|
@ -2470,12 +2665,14 @@ static gboolean application_install_session(
|
|||
application->tree_model
|
||||
);
|
||||
|
||||
application_save_graph_viewport(application);
|
||||
investigation_session_close(
|
||||
application->session
|
||||
);
|
||||
|
||||
application->tree_model = new_tree_model;
|
||||
application->session = new_session;
|
||||
application->graph_viewport_restored = FALSE;
|
||||
|
||||
main_window_set_tree_model(
|
||||
application->main_window,
|
||||
|
|
@ -7644,6 +7841,9 @@ static void application_on_activate(
|
|||
application_on_graph_node_moved,
|
||||
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,
|
||||
|
|
@ -7926,6 +8126,8 @@ void application_free(
|
|||
application
|
||||
);
|
||||
|
||||
application_save_graph_viewport(application);
|
||||
|
||||
application_clear_graph(
|
||||
application
|
||||
);
|
||||
|
|
|
|||
361
src/core/extraction_drop_service.c
Normal file
361
src/core/extraction_drop_service.c
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
/******************************************************************************
|
||||
* @file extraction_drop_service.c
|
||||
* @brief Validation et intégration d'une extraction déposée sur le graphe.
|
||||
******************************************************************************/
|
||||
|
||||
#include "core/extraction_drop_service.h"
|
||||
|
||||
#include "core/file_hash.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/evidence_dao.h"
|
||||
#include "dao/evidence_entity_dao.h"
|
||||
#include "database/transaction.h"
|
||||
#include "models/entity_record.h"
|
||||
#include "models/evidence_record.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
struct ExtractionDropInfo
|
||||
{
|
||||
char *file_path;
|
||||
char *relative_path;
|
||||
char *file_name;
|
||||
char *source;
|
||||
char *content;
|
||||
};
|
||||
|
||||
GQuark extraction_drop_service_error_quark(void)
|
||||
{
|
||||
return g_quark_from_static_string("extraction-drop-service-error");
|
||||
}
|
||||
|
||||
static void extraction_drop_service_set_error(
|
||||
GError **error,
|
||||
ExtractionDropServiceError code,
|
||||
const char *message)
|
||||
{
|
||||
if (error != NULL && *error == NULL)
|
||||
{
|
||||
g_set_error_literal(error, EXTRACTION_DROP_SERVICE_ERROR, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
static char *extraction_drop_service_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;
|
||||
}
|
||||
|
||||
static char *extraction_drop_service_source_from_content(const char *content)
|
||||
{
|
||||
const char *line_end = NULL;
|
||||
if (content == NULL || content[0] == '\0')
|
||||
{
|
||||
return g_strdup("Extraction locale");
|
||||
}
|
||||
line_end = strchr(content, '\n');
|
||||
if (line_end != NULL && line_end > content)
|
||||
{
|
||||
return g_strndup(content, (gsize) (line_end - content));
|
||||
}
|
||||
return g_strdup(content);
|
||||
}
|
||||
|
||||
ExtractionDropInfo *extraction_drop_service_prepare(
|
||||
const char *investigation_root,
|
||||
const char *file_path,
|
||||
GError **error)
|
||||
{
|
||||
ExtractionDropInfo *info = NULL;
|
||||
char *canonical_root = NULL;
|
||||
char *extractions_root = NULL;
|
||||
char *canonical_extractions_root = NULL;
|
||||
char *canonical_file = NULL;
|
||||
char *content = NULL;
|
||||
gsize content_length = 0U;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||
if (investigation_root == NULL || investigation_root[0] == '\0' ||
|
||||
file_path == NULL || file_path[0] == '\0')
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"Le chemin de l'extraction est invalide.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
canonical_root = g_canonicalize_filename(investigation_root, NULL);
|
||||
extractions_root = g_build_filename(canonical_root,
|
||||
"02_Preuves_Traitees", "Extractions", NULL);
|
||||
canonical_extractions_root = g_canonicalize_filename(extractions_root, NULL);
|
||||
canonical_file = g_canonicalize_filename(file_path, NULL);
|
||||
if (canonical_root == NULL || canonical_extractions_root == NULL ||
|
||||
canonical_file == NULL)
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"Impossible de normaliser le chemin de l'extraction.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (!g_str_has_prefix(canonical_file, canonical_extractions_root) ||
|
||||
canonical_file[strlen(canonical_extractions_root)] != G_DIR_SEPARATOR)
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION,
|
||||
"Le fichier ne se trouve pas dans les extractions de l'enquête.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (!g_file_test(canonical_file, G_FILE_TEST_IS_REGULAR) ||
|
||||
g_file_test(canonical_file, G_FILE_TEST_IS_SYMLINK) ||
|
||||
!g_str_has_suffix(canonical_file, ".txt"))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||
"Seuls les fichiers texte réguliers d'extraction sont acceptés.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (!g_file_get_contents(canonical_file, &content, &content_length, error))
|
||||
{
|
||||
if (error != NULL && *error != NULL)
|
||||
{
|
||||
(*error)->domain = EXTRACTION_DROP_SERVICE_ERROR;
|
||||
(*error)->code = EXTRACTION_DROP_SERVICE_ERROR_READ;
|
||||
}
|
||||
goto cleanup;
|
||||
}
|
||||
if (!g_utf8_validate(content, (gssize) content_length, NULL))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||
"L'extraction texte n'est pas encodée en UTF-8.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
info = g_new0(ExtractionDropInfo, 1);
|
||||
info->file_path = g_strdup(canonical_file);
|
||||
info->relative_path = g_strdup(canonical_file + strlen(canonical_root) + 1U);
|
||||
info->file_name = g_path_get_basename(canonical_file);
|
||||
info->content = g_steal_pointer(&content);
|
||||
info->source = extraction_drop_service_source_from_content(info->content);
|
||||
if (info->file_path == NULL || info->relative_path == NULL ||
|
||||
info->file_name == NULL || info->content == NULL || info->source == NULL)
|
||||
{
|
||||
extraction_drop_info_free(info);
|
||||
info = NULL;
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"Impossible d'allouer les informations de l'extraction.");
|
||||
}
|
||||
|
||||
cleanup:
|
||||
g_free(content);
|
||||
g_free(canonical_file);
|
||||
g_free(canonical_extractions_root);
|
||||
g_free(extractions_root);
|
||||
g_free(canonical_root);
|
||||
return info;
|
||||
}
|
||||
|
||||
void extraction_drop_info_free(ExtractionDropInfo *info)
|
||||
{
|
||||
if (info == NULL) return;
|
||||
g_free(info->file_path);
|
||||
g_free(info->relative_path);
|
||||
g_free(info->file_name);
|
||||
g_free(info->source);
|
||||
g_free(info->content);
|
||||
g_free(info);
|
||||
}
|
||||
|
||||
#define EXTRACTION_DROP_GETTER(name, field) \
|
||||
const char *name(const ExtractionDropInfo *info) \
|
||||
{ return info != NULL ? info->field : NULL; }
|
||||
|
||||
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_path, file_path)
|
||||
EXTRACTION_DROP_GETTER(extraction_drop_info_get_relative_path, relative_path)
|
||||
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_name, file_name)
|
||||
EXTRACTION_DROP_GETTER(extraction_drop_info_get_source, source)
|
||||
EXTRACTION_DROP_GETTER(extraction_drop_info_get_content, content)
|
||||
|
||||
static char *extraction_drop_service_ensure_evidence(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
GError **error)
|
||||
{
|
||||
EvidenceDao *dao = NULL;
|
||||
EvidenceRecord *record = NULL;
|
||||
GPtrArray *records = NULL;
|
||||
char *identifier = NULL;
|
||||
char *sha256 = NULL;
|
||||
char *timestamp = NULL;
|
||||
guint64 size_bytes = 0U;
|
||||
|
||||
dao = evidence_dao_new(database, error);
|
||||
if (dao == NULL) goto cleanup;
|
||||
records = evidence_dao_list_all(dao, error);
|
||||
if (records == NULL) goto cleanup;
|
||||
for (guint index = 0U; index < records->len; index++)
|
||||
{
|
||||
const EvidenceRecord *candidate = g_ptr_array_index(records, index);
|
||||
if (g_strcmp0(evidence_record_get_relative_path(candidate),
|
||||
info->relative_path) == 0)
|
||||
{
|
||||
identifier = g_strdup(evidence_record_get_identifier(candidate));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
if (!file_hash_compute_sha256(info->file_path, NULL, &sha256,
|
||||
&size_bytes, error)) goto cleanup;
|
||||
identifier = g_uuid_string_random();
|
||||
timestamp = extraction_drop_service_timestamp();
|
||||
record = evidence_record_new(identifier, info->file_name, info->file_name,
|
||||
info->relative_path, "text", size_bytes, sha256, timestamp, NULL,
|
||||
info->source, "Extraction intégrée depuis le graphe.",
|
||||
EVIDENCE_INTEGRITY_STATUS_VALID, error);
|
||||
if (record == NULL || !evidence_dao_insert(dao, record, error))
|
||||
{
|
||||
g_clear_pointer(&identifier, g_free);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
evidence_record_free(record);
|
||||
g_clear_pointer(&records, g_ptr_array_unref);
|
||||
evidence_dao_free(dao);
|
||||
g_free(timestamp);
|
||||
g_free(sha256);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
static gboolean extraction_drop_service_attach_in_transaction(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
const char *entity_identifier,
|
||||
GError **error)
|
||||
{
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
char *evidence_identifier = NULL;
|
||||
gboolean exists = FALSE;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (database == NULL || info == NULL || entity_identifier == NULL ||
|
||||
!g_uuid_string_is_valid(entity_identifier))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"L'entité de destination est invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
evidence_identifier = extraction_drop_service_ensure_evidence(database,
|
||||
info, error);
|
||||
if (evidence_identifier == NULL) goto cleanup;
|
||||
link_dao = evidence_entity_dao_new(database, error);
|
||||
if (link_dao == NULL ||
|
||||
!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
||||
entity_identifier, &exists, error) ||
|
||||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||
entity_identifier, error))) goto cleanup;
|
||||
success = TRUE;
|
||||
cleanup:
|
||||
evidence_entity_dao_free(link_dao);
|
||||
g_free(evidence_identifier);
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean extraction_drop_service_attach(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
const char *entity_identifier,
|
||||
GError **error)
|
||||
{
|
||||
gboolean success = FALSE;
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (database == NULL || info == NULL || entity_identifier == NULL ||
|
||||
!g_uuid_string_is_valid(entity_identifier))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"L'entité de destination est invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!database_transaction_begin(database)) goto database_failure;
|
||||
if (!extraction_drop_service_attach_in_transaction(database, info,
|
||||
entity_identifier, error))
|
||||
{
|
||||
database_transaction_rollback(database);
|
||||
return FALSE;
|
||||
}
|
||||
if (!database_transaction_commit(database)) goto database_failure;
|
||||
success = TRUE;
|
||||
return success;
|
||||
|
||||
database_failure:
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||
"Impossible d'enregistrer l'association de l'extraction.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean extraction_drop_service_create_entity(
|
||||
Database *database,
|
||||
const ExtractionDropInfo *info,
|
||||
char **out_entity_identifier,
|
||||
GError **error)
|
||||
{
|
||||
EntityDao *entity_dao = NULL;
|
||||
EntityRecord *entity = NULL;
|
||||
char *identifier = NULL;
|
||||
char *timestamp = NULL;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (out_entity_identifier != NULL) *out_entity_identifier = NULL;
|
||||
if (database == NULL || info == NULL)
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||
"Les paramètres de création d'entité sont invalides.");
|
||||
return FALSE;
|
||||
}
|
||||
identifier = g_uuid_string_random();
|
||||
timestamp = extraction_drop_service_timestamp();
|
||||
entity = entity_record_new(identifier, "other", info->file_name,
|
||||
info->file_name, info->content, 50, timestamp, timestamp,
|
||||
ENTITY_STATUS_ACTIVE, error);
|
||||
if (!database_transaction_begin(database))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||
"Impossible de démarrer la création de l'entité.");
|
||||
goto cleanup;
|
||||
}
|
||||
entity_dao = entity_dao_new(database, error);
|
||||
if (entity == NULL || entity_dao == NULL ||
|
||||
!entity_dao_insert(entity_dao, entity, error) ||
|
||||
!extraction_drop_service_attach_in_transaction(database, info,
|
||||
identifier, error))
|
||||
{
|
||||
database_transaction_rollback(database);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!database_transaction_commit(database))
|
||||
{
|
||||
extraction_drop_service_set_error(error,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||
"Impossible de valider la création de l'entité.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (out_entity_identifier != NULL)
|
||||
*out_entity_identifier = g_strdup(identifier);
|
||||
success = TRUE;
|
||||
|
||||
cleanup:
|
||||
entity_dao_free(entity_dao);
|
||||
entity_record_free(entity);
|
||||
g_free(timestamp);
|
||||
g_free(identifier);
|
||||
return success;
|
||||
}
|
||||
|
|
@ -71,6 +71,16 @@ static const char *const graph_node_position_dao_delete_sql =
|
|||
static const char *const graph_node_position_dao_delete_all_sql =
|
||||
"DELETE FROM graph_layout_positions;";
|
||||
|
||||
static const char *const graph_node_position_dao_load_viewport_sql =
|
||||
"SELECT zoom, offset_x, offset_y FROM graph_viewport WHERE id = 1;";
|
||||
|
||||
static const char *const graph_node_position_dao_upsert_viewport_sql =
|
||||
"INSERT INTO graph_viewport(id, zoom, offset_x, offset_y, updated_at) "
|
||||
"VALUES(1, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET zoom = excluded.zoom, "
|
||||
"offset_x = excluded.offset_x, offset_y = excluded.offset_y, "
|
||||
"updated_at = excluded.updated_at;";
|
||||
|
||||
/**
|
||||
* @brief Enregistre une erreur littérale.
|
||||
*/
|
||||
|
|
@ -869,3 +879,125 @@ cleanup:
|
|||
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean graph_node_position_dao_load_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y,
|
||||
gboolean *found,
|
||||
GError **error
|
||||
)
|
||||
{
|
||||
DatabaseStatement *statement = NULL;
|
||||
DatabaseStatementStepResult step_result;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (position_dao == NULL || position_dao->database == NULL ||
|
||||
zoom == NULL || offset_x == NULL || offset_y == NULL || found == NULL)
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
|
||||
"Les paramètres de chargement du cadrage sont invalides.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*found = FALSE;
|
||||
statement = database_statement_prepare(position_dao->database,
|
||||
graph_node_position_dao_load_viewport_sql);
|
||||
if (statement == NULL)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
|
||||
"Impossible de préparer le chargement du cadrage");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
step_result = database_statement_step(statement);
|
||||
if (step_result == DATABASE_STATEMENT_STEP_ROW)
|
||||
{
|
||||
if (!database_statement_column_double(statement, 0, zoom) ||
|
||||
!database_statement_column_double(statement, 1, offset_x) ||
|
||||
!database_statement_column_double(statement, 2, offset_y) ||
|
||||
!isfinite(*zoom) || *zoom <= 0.0 ||
|
||||
!isfinite(*offset_x) || !isfinite(*offset_y))
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_READ,
|
||||
"Le cadrage enregistré est invalide.");
|
||||
goto cleanup;
|
||||
}
|
||||
*found = TRUE;
|
||||
}
|
||||
else if (step_result == DATABASE_STATEMENT_STEP_ERROR)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
|
||||
"Impossible de charger le cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
success = TRUE;
|
||||
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean graph_node_position_dao_upsert_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y,
|
||||
GError **error
|
||||
)
|
||||
{
|
||||
DatabaseStatement *statement = NULL;
|
||||
char *updated_at = NULL;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (position_dao == NULL || position_dao->database == NULL ||
|
||||
!isfinite(zoom) || zoom <= 0.0 ||
|
||||
!isfinite(offset_x) || !isfinite(offset_y))
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
|
||||
"Le cadrage du graphe est invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
updated_at = graph_node_position_dao_create_utc_timestamp();
|
||||
statement = database_statement_prepare(position_dao->database,
|
||||
graph_node_position_dao_upsert_viewport_sql);
|
||||
if (updated_at == NULL || statement == NULL)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
|
||||
"Impossible de préparer l'enregistrement du cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
if (!database_statement_bind_double(statement, 1, zoom) ||
|
||||
!database_statement_bind_double(statement, 2, offset_x) ||
|
||||
!database_statement_bind_double(statement, 3, offset_y) ||
|
||||
!database_statement_bind_text(statement, 4, updated_at))
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_BIND,
|
||||
"Impossible de lier les données du cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
if (database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
|
||||
"Impossible d'enregistrer le cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
success = TRUE;
|
||||
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
g_free(updated_at);
|
||||
return success;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @brief Version actuelle du schéma SQLite.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 7
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 8
|
||||
|
||||
/**
|
||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "7"
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "8"
|
||||
|
||||
/**
|
||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||
|
|
@ -748,6 +748,40 @@ rollback:
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v6_to_v7(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database))
|
||||
return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v7(database) ||
|
||||
!database_update_schema_version(database, "7") ||
|
||||
!database_transaction_commit(database))
|
||||
goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V6 vers V7.");
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v7_to_v8(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database))
|
||||
return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v8(database) ||
|
||||
!database_update_schema_version(database, "8") ||
|
||||
!database_transaction_commit(database))
|
||||
goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V7 vers V8.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||
*/
|
||||
|
|
@ -959,11 +993,17 @@ bool database_migrate_to_latest(
|
|||
break;
|
||||
|
||||
case 6:
|
||||
if (!schema_install_v7(database))
|
||||
if (!database_migrate_v6_to_v7(database))
|
||||
return false;
|
||||
schema_version = 7;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
if (!database_migrate_v7_to_v8(database))
|
||||
return false;
|
||||
schema_version = 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
database,
|
||||
|
|
|
|||
|
|
@ -233,6 +233,12 @@ bool schema_install_v7(Database *database)
|
|||
"la migration SQLite V7");
|
||||
}
|
||||
|
||||
bool schema_install_v8(Database *database)
|
||||
{
|
||||
return schema_execute_file(database, "database/schema_v8.sql",
|
||||
"la migration SQLite V8");
|
||||
}
|
||||
|
||||
bool schema_ensure_current(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ struct MainWindow
|
|||
gpointer
|
||||
verify_evidence_user_data;
|
||||
|
||||
MainWindowEvidenceSelectionCallback evidence_selection_callback;
|
||||
gpointer evidence_selection_user_data;
|
||||
|
||||
MainWindowEditEvidenceCallback edit_evidence_callback;
|
||||
gpointer edit_evidence_user_data;
|
||||
MainWindowAnalyzeEmlCallback analyze_eml_callback;
|
||||
|
|
@ -123,6 +126,9 @@ struct MainWindow
|
|||
gpointer
|
||||
graph_node_moved_user_data;
|
||||
|
||||
MainWindowExtractionDropCallback extraction_drop_callback;
|
||||
gpointer extraction_drop_user_data;
|
||||
|
||||
MainWindowResetGraphLayoutCallback
|
||||
reset_graph_layout_callback;
|
||||
|
||||
|
|
@ -508,6 +514,15 @@ static void main_window_on_graph_node_moved(
|
|||
);
|
||||
}
|
||||
|
||||
static void main_window_on_extraction_dropped(const char *file_path,
|
||||
const char *target_entity_identifier, gpointer user_data)
|
||||
{
|
||||
MainWindow *main_window = user_data;
|
||||
if (main_window != NULL && main_window->extraction_drop_callback != NULL)
|
||||
main_window->extraction_drop_callback(file_path,
|
||||
target_entity_identifier, main_window->extraction_drop_user_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Relaie la demande de vérification provenant du Workspace.
|
||||
*/
|
||||
|
|
@ -619,6 +634,15 @@ static void main_window_on_person_evidence_requested(const char *identifier,
|
|||
window->person_evidence_callback(identifier,
|
||||
window->person_evidence_user_data);
|
||||
}
|
||||
|
||||
static void main_window_on_evidence_activated(const char *identifier,
|
||||
gpointer user_data)
|
||||
{
|
||||
MainWindow *window = user_data;
|
||||
if (window != NULL && window->evidence_selection_callback != NULL)
|
||||
window->evidence_selection_callback(identifier,
|
||||
window->evidence_selection_user_data);
|
||||
}
|
||||
/** @brief Relaie la sélection d'une entité vers le contrôleur. */
|
||||
static void main_window_on_entity_selected(const char *identifier,
|
||||
gpointer user_data)
|
||||
|
|
@ -954,6 +978,8 @@ MainWindow *main_window_new(
|
|||
main_window_on_graph_node_moved,
|
||||
main_window
|
||||
);
|
||||
workspace_set_extraction_drop_callback(main_window->workspace,
|
||||
main_window_on_extraction_dropped, main_window);
|
||||
|
||||
workspace_set_reset_graph_layout_callback(
|
||||
main_window->workspace,
|
||||
|
|
@ -979,6 +1005,8 @@ MainWindow *main_window_new(
|
|||
main_window_on_person_name_changed, main_window);
|
||||
workspace_set_person_evidence_callback(main_window->workspace,
|
||||
main_window_on_person_evidence_requested, main_window);
|
||||
workspace_set_evidence_activated_callback(main_window->workspace,
|
||||
main_window_on_evidence_activated, main_window);
|
||||
workspace_set_entity_selected_callback(main_window->workspace,
|
||||
main_window_on_entity_selected, main_window);
|
||||
|
||||
|
|
@ -1393,6 +1421,22 @@ void main_window_set_graph(
|
|||
);
|
||||
}
|
||||
|
||||
gboolean main_window_get_graph_view_transform(
|
||||
MainWindow *main_window, double *zoom, double *offset_x, double *offset_y)
|
||||
{
|
||||
return main_window != NULL &&
|
||||
workspace_get_graph_view_transform(
|
||||
main_window->workspace, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void main_window_set_graph_view_transform(
|
||||
MainWindow *main_window, double zoom, double offset_x, double offset_y)
|
||||
{
|
||||
if (main_window != NULL)
|
||||
workspace_set_graph_view_transform(
|
||||
main_window->workspace, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
gboolean main_window_select_graph_relation(MainWindow *main_window,
|
||||
const char *relation_identifier)
|
||||
{
|
||||
|
|
@ -1600,6 +1644,9 @@ void main_window_set_evidence_selection_callback(
|
|||
return;
|
||||
}
|
||||
|
||||
main_window->evidence_selection_callback = callback;
|
||||
main_window->evidence_selection_user_data = user_data;
|
||||
|
||||
sidebar_set_evidence_selection_callback(
|
||||
main_window->sidebar,
|
||||
callback,
|
||||
|
|
@ -1756,6 +1803,16 @@ void main_window_set_graph_node_moved_callback(
|
|||
user_data;
|
||||
}
|
||||
|
||||
void main_window_set_extraction_drop_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowExtractionDropCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (main_window == NULL) return;
|
||||
main_window->extraction_drop_callback = callback;
|
||||
main_window->extraction_drop_user_data = user_data;
|
||||
}
|
||||
|
||||
void main_window_set_reset_graph_layout_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowResetGraphLayoutCallback callback,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ struct EntityDetailsPanel
|
|||
GtkWidget *person_name_button;
|
||||
GtkWidget *person_evidence_button;
|
||||
GtkWidget *person_evidence_summary_label;
|
||||
GtkWidget *entity_evidence_box;
|
||||
GtkWidget *evidence_buttons_box;
|
||||
|
||||
char *selected_entity_identifier;
|
||||
char *selected_entity_type;
|
||||
|
|
@ -61,6 +63,8 @@ struct EntityDetailsPanel
|
|||
gpointer person_name_user_data;
|
||||
EntityDetailsPanelPersonEvidenceCallback person_evidence_callback;
|
||||
gpointer person_evidence_user_data;
|
||||
EntityDetailsPanelEvidenceActivatedCallback evidence_activated_callback;
|
||||
gpointer evidence_activated_user_data;
|
||||
EntityDetailsPanelOsintCallback osint_callback;
|
||||
gpointer osint_user_data;
|
||||
};
|
||||
|
|
@ -98,6 +102,18 @@ static void entity_details_panel_on_person_evidence_clicked(GtkButton *button,
|
|||
panel->person_evidence_user_data);
|
||||
}
|
||||
|
||||
static void entity_details_panel_on_evidence_activated(GtkButton *button,
|
||||
gpointer user_data)
|
||||
{
|
||||
EntityDetailsPanel *panel = user_data;
|
||||
const char *identifier = g_object_get_data(G_OBJECT(button),
|
||||
"evidence-identifier");
|
||||
if (panel != NULL && panel->evidence_activated_callback != NULL &&
|
||||
identifier != NULL)
|
||||
panel->evidence_activated_callback(identifier,
|
||||
panel->evidence_activated_user_data);
|
||||
}
|
||||
|
||||
/** @brief Relaie le nouveau nom affiché explicitement enregistré. */
|
||||
static void entity_details_panel_on_person_name_clicked(GtkButton *button,
|
||||
gpointer user_data)
|
||||
|
|
@ -846,23 +862,30 @@ EntityDetailsPanel *entity_details_panel_new(void)
|
|||
GTK_WIDGET(details_panel->person_name_entry));
|
||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
||||
details_panel->person_name_button);
|
||||
details_panel->entity_evidence_box = gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL, 6);
|
||||
details_panel->person_evidence_button = gtk_button_new_with_label(
|
||||
"Gérer les pièces jointes");
|
||||
details_panel->person_evidence_summary_label = gtk_label_new(
|
||||
"Pièces jointes : chargement…");
|
||||
details_panel->evidence_buttons_box = gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_label_set_xalign(GTK_LABEL(
|
||||
details_panel->person_evidence_summary_label), 0.0F);
|
||||
gtk_label_set_wrap(GTK_LABEL(
|
||||
details_panel->person_evidence_summary_label), TRUE);
|
||||
gtk_label_set_selectable(GTK_LABEL(
|
||||
details_panel->person_evidence_summary_label), TRUE);
|
||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
||||
gtk_label_new("Preuves associées à cette personne"));
|
||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
||||
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||
gtk_label_new("Preuves associées à cette entité"));
|
||||
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||
details_panel->person_evidence_summary_label);
|
||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
||||
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||
details_panel->evidence_buttons_box);
|
||||
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||
details_panel->person_evidence_button);
|
||||
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
|
||||
gtk_box_append(GTK_BOX(details_box), details_panel->entity_evidence_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",
|
||||
|
|
@ -1295,6 +1318,15 @@ void entity_details_panel_set_person_evidence_callback(
|
|||
details_panel->person_evidence_callback = callback;
|
||||
details_panel->person_evidence_user_data = user_data;
|
||||
}
|
||||
void entity_details_panel_set_evidence_activated_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelEvidenceActivatedCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (details_panel == NULL) return;
|
||||
details_panel->evidence_activated_callback = callback;
|
||||
details_panel->evidence_activated_user_data = user_data;
|
||||
}
|
||||
void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelOsintCallback callback, gpointer user_data)
|
||||
{
|
||||
|
|
@ -1310,24 +1342,52 @@ void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
|
|||
void entity_details_panel_set_person_evidences(EntityDetailsPanel *panel,
|
||||
const GPtrArray *records)
|
||||
{
|
||||
GString *text = NULL;
|
||||
GtkWidget *child = NULL;
|
||||
if (panel == NULL || panel->person_evidence_summary_label == NULL) return;
|
||||
text = g_string_new(NULL);
|
||||
while (panel->evidence_buttons_box != NULL &&
|
||||
(child = gtk_widget_get_first_child(
|
||||
panel->evidence_buttons_box)) != NULL)
|
||||
gtk_box_remove(GTK_BOX(panel->evidence_buttons_box), child);
|
||||
if (records == NULL || records->len == 0)
|
||||
g_string_append(text, "Aucune pièce jointe");
|
||||
gtk_label_set_text(GTK_LABEL(panel->person_evidence_summary_label),
|
||||
"Aucune pièce jointe");
|
||||
else
|
||||
{
|
||||
char *summary = g_strdup_printf("%u pièce%s jointe%s",
|
||||
records->len, records->len > 1U ? "s" : "",
|
||||
records->len > 1U ? "s" : "");
|
||||
gtk_label_set_text(GTK_LABEL(panel->person_evidence_summary_label),
|
||||
summary);
|
||||
g_free(summary);
|
||||
for (guint index = 0; index < records->len; index++)
|
||||
{
|
||||
const EvidenceRecord *record = g_ptr_array_index(records, index);
|
||||
GtkWidget *button = NULL;
|
||||
char *label = NULL;
|
||||
if (record == NULL) continue;
|
||||
if (text->len > 0) g_string_append_c(text, '\n');
|
||||
g_string_append_printf(text, "• %s — %s",
|
||||
label = g_strdup_printf("%s — %s",
|
||||
evidence_record_get_original_name(record),
|
||||
evidence_record_get_type_identifier(record));
|
||||
button = gtk_button_new_with_label(label);
|
||||
gtk_widget_set_halign(button, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_tooltip_text(button, label);
|
||||
gtk_widget_add_css_class(button, "flat");
|
||||
if (GTK_IS_LABEL(gtk_button_get_child(GTK_BUTTON(button))))
|
||||
{
|
||||
GtkLabel *button_label = GTK_LABEL(
|
||||
gtk_button_get_child(GTK_BUTTON(button)));
|
||||
gtk_label_set_xalign(button_label, 0.0F);
|
||||
gtk_label_set_ellipsize(button_label, PANGO_ELLIPSIZE_END);
|
||||
gtk_label_set_max_width_chars(button_label, 28);
|
||||
}
|
||||
g_object_set_data_full(G_OBJECT(button), "evidence-identifier",
|
||||
g_strdup(evidence_record_get_identifier(record)), g_free);
|
||||
g_signal_connect(button, "clicked",
|
||||
G_CALLBACK(entity_details_panel_on_evidence_activated), panel);
|
||||
gtk_box_append(GTK_BOX(panel->evidence_buttons_box), button);
|
||||
g_free(label);
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(panel->person_evidence_summary_label),
|
||||
text->str);
|
||||
g_string_free(text, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
gboolean entity_details_panel_is_open(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "models/social_platform.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <math.h>
|
||||
#include <pango/pangocairo.h>
|
||||
|
||||
#define INVESTIGATION_GRAPH_VIEW_MARGIN 32.0
|
||||
|
|
@ -120,6 +121,8 @@ struct InvestigationGraphView
|
|||
|
||||
InvestigationGraphViewNodeMovedCallback node_moved_callback;
|
||||
gpointer node_moved_user_data;
|
||||
InvestigationGraphViewExtractionDropCallback extraction_drop_callback;
|
||||
gpointer extraction_drop_user_data;
|
||||
|
||||
double node_drag_pointer_offset_x;
|
||||
double node_drag_pointer_offset_y;
|
||||
|
|
@ -128,8 +131,46 @@ struct InvestigationGraphView
|
|||
gboolean dragging;
|
||||
gboolean node_dragging;
|
||||
gboolean click_suppressed;
|
||||
GtkDropTarget *extraction_drop_target;
|
||||
};
|
||||
|
||||
static gboolean investigation_graph_view_screen_to_logical(
|
||||
const InvestigationGraphView *graph_view, double screen_x, double screen_y,
|
||||
double *logical_x, double *logical_y);
|
||||
static InvestigationGraphNodeLayout *investigation_graph_view_find_node_at(
|
||||
InvestigationGraphView *graph_view, double logical_x, double logical_y);
|
||||
|
||||
/** @brief Relaie un chemin d'extraction et l'éventuelle entité visée. */
|
||||
static gboolean investigation_graph_view_on_extraction_drop(
|
||||
GtkDropTarget *target, const GValue *value, double x, double y,
|
||||
gpointer user_data)
|
||||
{
|
||||
InvestigationGraphView *graph_view = user_data;
|
||||
InvestigationGraphNodeLayout *node_layout = NULL;
|
||||
const char *path = NULL;
|
||||
const char *entity_identifier = NULL;
|
||||
double logical_x = 0.0;
|
||||
double logical_y = 0.0;
|
||||
(void) target;
|
||||
if (graph_view == NULL || value == NULL ||
|
||||
!G_VALUE_HOLDS_STRING(value)) return FALSE;
|
||||
path = g_value_get_string(value);
|
||||
if (path == NULL || path[0] == '\0') return FALSE;
|
||||
if (investigation_graph_view_screen_to_logical(graph_view, x, y,
|
||||
&logical_x, &logical_y))
|
||||
{
|
||||
node_layout = investigation_graph_view_find_node_at(graph_view,
|
||||
logical_x, logical_y);
|
||||
if (node_layout != NULL && node_layout->entity_record != NULL)
|
||||
entity_identifier = entity_record_get_identifier(
|
||||
node_layout->entity_record);
|
||||
}
|
||||
if (graph_view->extraction_drop_callback == NULL) return FALSE;
|
||||
graph_view->extraction_drop_callback(path, entity_identifier,
|
||||
graph_view->extraction_drop_user_data);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static const char *investigation_graph_view_get_node_identifier(
|
||||
const InvestigationGraphNodeLayout *node_layout
|
||||
);
|
||||
|
|
@ -3948,6 +3989,13 @@ InvestigationGraphView *investigation_graph_view_new(void)
|
|||
TRUE
|
||||
);
|
||||
|
||||
graph_view->extraction_drop_target = GTK_DROP_TARGET(
|
||||
gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_COPY));
|
||||
g_signal_connect(graph_view->extraction_drop_target, "drop",
|
||||
G_CALLBACK(investigation_graph_view_on_extraction_drop), graph_view);
|
||||
gtk_widget_add_controller(graph_view->drawing_area,
|
||||
GTK_EVENT_CONTROLLER(graph_view->extraction_drop_target));
|
||||
|
||||
gtk_drawing_area_set_content_width(
|
||||
GTK_DRAWING_AREA(
|
||||
graph_view->drawing_area
|
||||
|
|
@ -4414,6 +4462,16 @@ void investigation_graph_view_set_node_moved_callback(
|
|||
user_data;
|
||||
}
|
||||
|
||||
void investigation_graph_view_set_extraction_drop_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewExtractionDropCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (graph_view == NULL) return;
|
||||
graph_view->extraction_drop_callback = callback;
|
||||
graph_view->extraction_drop_user_data = user_data;
|
||||
}
|
||||
|
||||
const EntityRecord *investigation_graph_view_get_selected_entity(
|
||||
const InvestigationGraphView *graph_view
|
||||
)
|
||||
|
|
@ -4585,6 +4643,39 @@ void investigation_graph_view_reset_view(
|
|||
}
|
||||
}
|
||||
|
||||
gboolean investigation_graph_view_get_view_transform(
|
||||
const InvestigationGraphView *graph_view,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y)
|
||||
{
|
||||
if (graph_view == NULL || graph_view->graph_model == NULL ||
|
||||
zoom == NULL || offset_x == NULL || offset_y == NULL)
|
||||
return FALSE;
|
||||
*zoom = graph_view->zoom;
|
||||
*offset_x = graph_view->offset_x;
|
||||
*offset_y = graph_view->offset_y;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void investigation_graph_view_set_view_transform(
|
||||
InvestigationGraphView *graph_view,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y)
|
||||
{
|
||||
if (graph_view == NULL ||
|
||||
zoom < INVESTIGATION_GRAPH_VIEW_MIN_ZOOM ||
|
||||
zoom > INVESTIGATION_GRAPH_VIEW_MAX_ZOOM ||
|
||||
!isfinite(zoom) || !isfinite(offset_x) || !isfinite(offset_y))
|
||||
return;
|
||||
graph_view->zoom = zoom;
|
||||
graph_view->offset_x = offset_x;
|
||||
graph_view->offset_y = offset_y;
|
||||
if (graph_view->drawing_area != NULL)
|
||||
gtk_widget_queue_draw(graph_view->drawing_area);
|
||||
}
|
||||
|
||||
void investigation_graph_view_reset_layout(
|
||||
InvestigationGraphView *graph_view
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,38 @@
|
|||
#include <gtk/gtk.h>
|
||||
#include <string.h>
|
||||
|
||||
/** @brief Prépare le chemin du fichier pour un glisser-déposer. */
|
||||
static GdkContentProvider *investigation_tree_view_on_drag_prepare(
|
||||
GtkDragSource *source, double x, double y, gpointer user_data)
|
||||
{
|
||||
const char *path = g_object_get_data(G_OBJECT(source),
|
||||
"investigation-node-path");
|
||||
(void) x; (void) y; (void) user_data;
|
||||
if (path == NULL || path[0] == '\0') return NULL;
|
||||
return gdk_content_provider_new_typed(G_TYPE_STRING, path);
|
||||
}
|
||||
/** @brief Vérifie qu'un nœud est un fichier `.txt` sous `Extractions`. */
|
||||
static gboolean investigation_tree_view_node_is_extraction(
|
||||
const InvestigationNode *node)
|
||||
{
|
||||
const InvestigationNode *ancestor = NULL;
|
||||
const char *name = NULL;
|
||||
|
||||
if (node == NULL ||
|
||||
investigation_node_get_type(node) != INVESTIGATION_NODE_FILE)
|
||||
return FALSE;
|
||||
name = investigation_node_get_name(node);
|
||||
if (name == NULL || !g_str_has_suffix(name, ".txt")) return FALSE;
|
||||
ancestor = investigation_node_get_parent(node);
|
||||
while (ancestor != NULL)
|
||||
{
|
||||
if (g_strcmp0(investigation_node_get_name(ancestor),
|
||||
"Extractions") == 0) return TRUE;
|
||||
ancestor = investigation_node_get_parent(ancestor);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* InvestigationTreeItem
|
||||
* ---------------------
|
||||
|
|
@ -248,6 +280,7 @@ static void investigation_tree_view_factory_setup(
|
|||
GtkWidget *row_box = NULL;
|
||||
GtkWidget *image = NULL;
|
||||
GtkWidget *label = NULL;
|
||||
GtkDragSource *drag_source = NULL;
|
||||
|
||||
(void)factory;
|
||||
(void)user_data;
|
||||
|
|
@ -292,6 +325,14 @@ static void investigation_tree_view_factory_setup(
|
|||
row_box
|
||||
);
|
||||
|
||||
drag_source = gtk_drag_source_new();
|
||||
gtk_drag_source_set_actions(drag_source, GDK_ACTION_COPY);
|
||||
g_signal_connect(drag_source, "prepare",
|
||||
G_CALLBACK(investigation_tree_view_on_drag_prepare), NULL);
|
||||
g_object_set_data(G_OBJECT(row_box), "investigation-drag-source",
|
||||
drag_source);
|
||||
gtk_widget_add_controller(row_box, GTK_EVENT_CONTROLLER(drag_source));
|
||||
|
||||
gtk_list_item_set_child(
|
||||
list_item,
|
||||
expander
|
||||
|
|
@ -406,6 +447,7 @@ static void investigation_tree_view_factory_bind(
|
|||
GtkWidget *label = NULL;
|
||||
|
||||
const char *node_name = NULL;
|
||||
GtkDragSource *drag_source = NULL;
|
||||
|
||||
(void)factory;
|
||||
(void)user_data;
|
||||
|
|
@ -455,6 +497,13 @@ static void investigation_tree_view_factory_bind(
|
|||
return;
|
||||
}
|
||||
|
||||
drag_source = GTK_DRAG_SOURCE(g_object_get_data(G_OBJECT(row_box),
|
||||
"investigation-drag-source"));
|
||||
if (drag_source != NULL)
|
||||
g_object_set_data_full(G_OBJECT(drag_source), "investigation-node-path",
|
||||
investigation_tree_view_node_is_extraction(node)
|
||||
? g_strdup(investigation_node_get_path(node)) : NULL, g_free);
|
||||
|
||||
image = gtk_widget_get_first_child(
|
||||
row_box
|
||||
);
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ struct Workspace
|
|||
gpointer
|
||||
graph_node_moved_user_data;
|
||||
|
||||
WorkspaceExtractionDropCallback extraction_drop_callback;
|
||||
gpointer extraction_drop_user_data;
|
||||
|
||||
WorkspaceResetGraphLayoutCallback
|
||||
reset_graph_layout_callback;
|
||||
|
||||
|
|
@ -155,6 +158,8 @@ struct Workspace
|
|||
gpointer person_name_user_data;
|
||||
WorkspacePersonEvidenceCallback person_evidence_callback;
|
||||
gpointer person_evidence_user_data;
|
||||
WorkspaceEvidenceActivatedCallback evidence_activated_callback;
|
||||
gpointer evidence_activated_user_data;
|
||||
WorkspaceEntitySelectedCallback entity_selected_callback;
|
||||
gpointer entity_selected_user_data;
|
||||
|
||||
|
|
@ -177,6 +182,10 @@ struct Workspace
|
|||
OsintActionCatalog *osint_action_catalog;
|
||||
|
||||
WorkspaceGraphState graph_state;
|
||||
gboolean saved_graph_transform_valid;
|
||||
double saved_graph_zoom;
|
||||
double saved_graph_offset_x;
|
||||
double saved_graph_offset_y;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -960,6 +969,24 @@ static void workspace_on_person_evidence_requested(const char *identifier,
|
|||
workspace->person_evidence_user_data);
|
||||
}
|
||||
|
||||
static void workspace_on_evidence_activated(const char *identifier,
|
||||
gpointer user_data)
|
||||
{
|
||||
Workspace *workspace = user_data;
|
||||
if (workspace != NULL && workspace->evidence_activated_callback != NULL)
|
||||
workspace->evidence_activated_callback(identifier,
|
||||
workspace->evidence_activated_user_data);
|
||||
}
|
||||
|
||||
static void workspace_on_extraction_dropped(const char *file_path,
|
||||
const char *target_entity_identifier, gpointer user_data)
|
||||
{
|
||||
Workspace *workspace = user_data;
|
||||
if (workspace != NULL && workspace->extraction_drop_callback != NULL)
|
||||
workspace->extraction_drop_callback(file_path, target_entity_identifier,
|
||||
workspace->extraction_drop_user_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Désélectionne le nœud lorsque le volet est fermé manuellement.
|
||||
*/
|
||||
|
|
@ -1939,6 +1966,8 @@ Workspace *workspace_new(void)
|
|||
workspace_on_graph_node_moved,
|
||||
workspace
|
||||
);
|
||||
investigation_graph_view_set_extraction_drop_callback(
|
||||
workspace->graph_view, workspace_on_extraction_dropped, workspace);
|
||||
|
||||
entity_details_panel_set_close_callback(
|
||||
workspace->entity_details_panel,
|
||||
|
|
@ -1964,6 +1993,9 @@ Workspace *workspace_new(void)
|
|||
entity_details_panel_set_person_evidence_callback(
|
||||
workspace->entity_details_panel,
|
||||
workspace_on_person_evidence_requested, workspace);
|
||||
entity_details_panel_set_evidence_activated_callback(
|
||||
workspace->entity_details_panel, workspace_on_evidence_activated,
|
||||
workspace);
|
||||
entity_details_panel_set_osint_callback(workspace->entity_details_panel,
|
||||
workspace_on_entity_osint_requested, workspace);
|
||||
|
||||
|
|
@ -3037,6 +3069,13 @@ void workspace_set_graph_loading(
|
|||
workspace->entity_details_panel
|
||||
);
|
||||
|
||||
workspace->saved_graph_transform_valid =
|
||||
investigation_graph_view_get_view_transform(
|
||||
workspace->graph_view,
|
||||
&workspace->saved_graph_zoom,
|
||||
&workspace->saved_graph_offset_x,
|
||||
&workspace->saved_graph_offset_y);
|
||||
|
||||
investigation_graph_view_clear(
|
||||
workspace->graph_view
|
||||
);
|
||||
|
|
@ -3142,6 +3181,16 @@ void workspace_set_graph(
|
|||
graph_model
|
||||
);
|
||||
|
||||
if (workspace->saved_graph_transform_valid)
|
||||
{
|
||||
investigation_graph_view_set_view_transform(
|
||||
workspace->graph_view,
|
||||
workspace->saved_graph_zoom,
|
||||
workspace->saved_graph_offset_x,
|
||||
workspace->saved_graph_offset_y);
|
||||
workspace->saved_graph_transform_valid = FALSE;
|
||||
}
|
||||
|
||||
workspace->graph_state =
|
||||
WORKSPACE_GRAPH_STATE_READY;
|
||||
|
||||
|
|
@ -3180,6 +3229,24 @@ void workspace_set_graph(
|
|||
);
|
||||
}
|
||||
|
||||
gboolean workspace_get_graph_view_transform(
|
||||
Workspace *workspace, double *zoom, double *offset_x, double *offset_y)
|
||||
{
|
||||
if (workspace == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY)
|
||||
return FALSE;
|
||||
return investigation_graph_view_get_view_transform(
|
||||
workspace->graph_view, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void workspace_set_graph_view_transform(
|
||||
Workspace *workspace, double zoom, double offset_x, double offset_y)
|
||||
{
|
||||
if (workspace == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY)
|
||||
return;
|
||||
investigation_graph_view_set_view_transform(
|
||||
workspace->graph_view, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void workspace_set_graph_error(
|
||||
Workspace *workspace,
|
||||
const char *message
|
||||
|
|
@ -3406,6 +3473,16 @@ void workspace_set_graph_node_moved_callback(
|
|||
user_data;
|
||||
}
|
||||
|
||||
void workspace_set_extraction_drop_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceExtractionDropCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (workspace == NULL) return;
|
||||
workspace->extraction_drop_callback = callback;
|
||||
workspace->extraction_drop_user_data = user_data;
|
||||
}
|
||||
|
||||
void workspace_set_reset_graph_layout_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceResetGraphLayoutCallback callback,
|
||||
|
|
@ -3523,6 +3600,15 @@ void workspace_set_person_evidence_callback(Workspace *workspace,
|
|||
workspace->person_evidence_callback = callback;
|
||||
workspace->person_evidence_user_data = user_data;
|
||||
}
|
||||
void workspace_set_evidence_activated_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceEvidenceActivatedCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (workspace == NULL) return;
|
||||
workspace->evidence_activated_callback = callback;
|
||||
workspace->evidence_activated_user_data = user_data;
|
||||
}
|
||||
void workspace_set_entity_selected_callback(Workspace *workspace,
|
||||
WorkspaceEntitySelectedCallback callback, gpointer user_data)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
|
|||
"FROM investigation;"
|
||||
);
|
||||
|
||||
assert(strcmp(schema_version, "7") == 0);
|
||||
assert(strcmp(schema_version, "8") == 0);
|
||||
test_database_assert_table_exists(database, "graph_viewport");
|
||||
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 +990,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"6"
|
||||
"8"
|
||||
) == 0
|
||||
);
|
||||
|
||||
|
|
|
|||
183
tests/test_extraction_drop_service.c
Normal file
183
tests/test_extraction_drop_service.c
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#include "core/extraction_drop_service.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/evidence_dao.h"
|
||||
#include "dao/evidence_entity_dao.h"
|
||||
#include "database/database.h"
|
||||
#include "models/entity_record.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char *root;
|
||||
char *extractions;
|
||||
char *file_path;
|
||||
char *database_path;
|
||||
Database *database;
|
||||
} ExtractionDropFixture;
|
||||
|
||||
static void fixture_setup(ExtractionDropFixture *fixture, gconstpointer data)
|
||||
{
|
||||
GError *error = NULL;
|
||||
(void) data;
|
||||
fixture->root = g_dir_make_tmp("labfy-extraction-drop-XXXXXX", &error);
|
||||
g_assert_no_error(error);
|
||||
fixture->extractions = g_build_filename(fixture->root,
|
||||
"02_Preuves_Traitees", "Extractions", "OSINT", NULL);
|
||||
g_assert_cmpint(g_mkdir_with_parents(fixture->extractions, 0700), ==, 0);
|
||||
fixture->file_path = g_build_filename(fixture->extractions,
|
||||
"resultat.txt", NULL);
|
||||
g_assert_true(g_file_set_contents(fixture->file_path,
|
||||
"Cible : exemple.test\nAction : test\n\nRésultat public", -1, &error));
|
||||
g_assert_no_error(error);
|
||||
fixture->database_path = g_build_filename(fixture->root,
|
||||
"Enquete.sqlite", NULL);
|
||||
g_assert_true(database_initialize(fixture->database_path,
|
||||
"Extraction", fixture->root));
|
||||
fixture->database = database_open(fixture->database_path);
|
||||
g_assert_nonnull(fixture->database);
|
||||
g_assert_true(database_migrate_to_latest(fixture->database));
|
||||
}
|
||||
|
||||
static void fixture_teardown(ExtractionDropFixture *fixture,
|
||||
gconstpointer data)
|
||||
{
|
||||
(void) data;
|
||||
database_close(fixture->database);
|
||||
g_remove(fixture->file_path);
|
||||
g_remove(fixture->database_path);
|
||||
g_rmdir(fixture->extractions);
|
||||
{
|
||||
char *directory = g_path_get_dirname(fixture->extractions);
|
||||
g_rmdir(directory);
|
||||
g_free(directory);
|
||||
}
|
||||
{
|
||||
char *directory = g_build_filename(fixture->root,
|
||||
"02_Preuves_Traitees", NULL);
|
||||
g_rmdir(directory);
|
||||
g_free(directory);
|
||||
}
|
||||
g_rmdir(fixture->root);
|
||||
g_free(fixture->database_path);
|
||||
g_free(fixture->file_path);
|
||||
g_free(fixture->extractions);
|
||||
g_free(fixture->root);
|
||||
}
|
||||
|
||||
static char *insert_entity(Database *database)
|
||||
{
|
||||
EntityDao *dao = NULL;
|
||||
EntityRecord *record = NULL;
|
||||
char *identifier = g_uuid_string_random();
|
||||
GError *error = NULL;
|
||||
dao = entity_dao_new(database, &error);
|
||||
record = entity_record_new(identifier, "other", "Destination",
|
||||
"Destination", NULL, 50, "2026-07-23T12:00:00Z",
|
||||
"2026-07-23T12:00:00Z", ENTITY_STATUS_ACTIVE, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(entity_dao_insert(dao, record, &error));
|
||||
g_assert_no_error(error);
|
||||
entity_record_free(record);
|
||||
entity_dao_free(dao);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
static void test_prepare_rejects_outside(ExtractionDropFixture *fixture,
|
||||
gconstpointer data)
|
||||
{
|
||||
ExtractionDropInfo *info = NULL;
|
||||
char *outside = g_build_filename(fixture->root, "hors.txt", NULL);
|
||||
GError *error = NULL;
|
||||
(void) data;
|
||||
g_assert_true(g_file_set_contents(outside, "hors enquête", -1, &error));
|
||||
g_assert_no_error(error);
|
||||
info = extraction_drop_service_prepare(fixture->root, outside, &error);
|
||||
g_assert_null(info);
|
||||
g_assert_error(error, EXTRACTION_DROP_SERVICE_ERROR,
|
||||
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION);
|
||||
g_clear_error(&error);
|
||||
g_remove(outside);
|
||||
g_free(outside);
|
||||
}
|
||||
|
||||
static void test_attach_persists_and_is_idempotent(
|
||||
ExtractionDropFixture *fixture, gconstpointer data)
|
||||
{
|
||||
ExtractionDropInfo *info = NULL;
|
||||
EvidenceDao *evidence_dao = NULL;
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
GPtrArray *records = NULL;
|
||||
char *entity_identifier = NULL;
|
||||
gboolean exists = FALSE;
|
||||
GError *error = NULL;
|
||||
(void) data;
|
||||
|
||||
info = extraction_drop_service_prepare(fixture->root,
|
||||
fixture->file_path, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_nonnull(info);
|
||||
g_assert_cmpstr(extraction_drop_info_get_source(info), ==,
|
||||
"Cible : exemple.test");
|
||||
entity_identifier = insert_entity(fixture->database);
|
||||
g_assert_true(extraction_drop_service_attach(fixture->database, info,
|
||||
entity_identifier, &error));
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(extraction_drop_service_attach(fixture->database, info,
|
||||
entity_identifier, &error));
|
||||
g_assert_no_error(error);
|
||||
evidence_dao = evidence_dao_new(fixture->database, &error);
|
||||
records = evidence_dao_list_all(evidence_dao, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_cmpuint(records->len, ==, 1U);
|
||||
link_dao = evidence_entity_dao_new(fixture->database, &error);
|
||||
g_assert_true(evidence_entity_dao_exists(link_dao,
|
||||
evidence_record_get_identifier(g_ptr_array_index(records, 0U)),
|
||||
entity_identifier, &exists, &error));
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(exists);
|
||||
evidence_entity_dao_free(link_dao);
|
||||
g_ptr_array_unref(records);
|
||||
evidence_dao_free(evidence_dao);
|
||||
g_free(entity_identifier);
|
||||
extraction_drop_info_free(info);
|
||||
}
|
||||
|
||||
static void test_create_entity_persists(ExtractionDropFixture *fixture,
|
||||
gconstpointer data)
|
||||
{
|
||||
ExtractionDropInfo *info = NULL;
|
||||
EntityDao *entity_dao = NULL;
|
||||
EntityRecord *entity = NULL;
|
||||
char *identifier = NULL;
|
||||
GError *error = NULL;
|
||||
(void) data;
|
||||
info = extraction_drop_service_prepare(fixture->root,
|
||||
fixture->file_path, &error);
|
||||
g_assert_true(extraction_drop_service_create_entity(fixture->database,
|
||||
info, &identifier, &error));
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(g_uuid_string_is_valid(identifier));
|
||||
entity_dao = entity_dao_new(fixture->database, &error);
|
||||
entity = entity_dao_find_by_identifier(entity_dao, identifier, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_nonnull(entity);
|
||||
g_assert_cmpstr(entity_record_get_type_identifier(entity), ==, "other");
|
||||
entity_record_free(entity);
|
||||
entity_dao_free(entity_dao);
|
||||
g_free(identifier);
|
||||
extraction_drop_info_free(info);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add("/extraction-drop/outside", ExtractionDropFixture, NULL,
|
||||
fixture_setup, test_prepare_rejects_outside, fixture_teardown);
|
||||
g_test_add("/extraction-drop/attach", ExtractionDropFixture, NULL,
|
||||
fixture_setup, test_attach_persists_and_is_idempotent, fixture_teardown);
|
||||
g_test_add("/extraction-drop/create", ExtractionDropFixture, NULL,
|
||||
fixture_setup, test_create_entity_persists, fixture_teardown);
|
||||
return g_test_run();
|
||||
}
|
||||
Loading…
Reference in a new issue