feat(relations): centraliser les types et améliorer les aperçus

This commit is contained in:
grayTerminal-sh 2026-07-23 16:51:31 +02:00
parent 569a99aa5f
commit 8bc3b43d63
31 changed files with 1472 additions and 109 deletions

View file

@ -2,6 +2,10 @@
### Added
- Référentiel persistant et normalisé des types de relations, avec codes
système stables, types personnalisés, renommage et fusion transactionnelle.
- Sélecteur canonique dans les formulaires de création et de modification des
relations.
- Glisser-déposer confirmé des extractions texte vers le graphe, avec
persistance de leur association à une entité.
- Ouverture de laperçu des pièces jointes et preuves depuis les fiches

View file

@ -23,7 +23,8 @@ TEST_CFLAGS = -std=c17 \
-Iinclude \
$(shell $(PKG_CONFIG) --cflags glib-2.0 gio-2.0)
TEST_LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0)
TEST_LDFLAGS = src/core/relation_type_normalizer.c \
$(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0)
EVIDENCE_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
EVIDENCE_TYPE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
@ -129,9 +130,29 @@ 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
TEST_RELATION_TYPE_NORMALIZER := tests/test_relation_type_normalizer
TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service
all: $(TARGET)
$(TEST_RELATION_TYPE_NORMALIZER): \
tests/test_relation_type_normalizer.c \
src/core/relation_type_normalizer.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(shell $(PKG_CONFIG) --libs glib-2.0)
$(TEST_RELATION_TYPE_SERVICE): \
tests/test_relation_type_service.c \
src/core/relation_type_service.c \
src/dao/relation_type_dao.c \
src/models/relation_type.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
$(TARGET): $(OBJ)
$(CC) $(OBJ) -o $@ $(LDFLAGS)
@ -480,6 +501,8 @@ $(TEST_RELATION_RECORD): \
$(TEST_RELATION_DAO): \
tests/test_relation_dao.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/dao/entity_dao.c \
src/models/relation_record.c \
src/models/entity_record.c \
@ -494,6 +517,8 @@ $(TEST_RELATION_EVIDENCE_DAO): \
tests/test_relation_evidence_dao.c \
src/dao/relation_evidence_dao.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/dao/evidence_dao.c \
src/dao/entity_dao.c \
src/models/relation_record.c \
@ -510,6 +535,8 @@ $(TEST_RELATION_SERVICE): \
tests/test_relation_service.c \
src/core/relation_service.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/dao/relation_evidence_dao.c \
src/dao/evidence_dao.c \
src/dao/entity_dao.c \
@ -537,6 +564,8 @@ $(TEST_INVESTIGATION_GRAPH_LOADER): \
src/core/investigation_graph_loader.c \
src/dao/entity_dao.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/models/investigation_graph_model.c \
src/models/entity_record.c \
src/models/relation_record.c \
@ -596,6 +625,8 @@ $(TEST_OSINT_DNS_INTEGRATION): \
src/models/osint_execution_record.c \
src/dao/entity_dao.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/dao/osint_execution_dao.c \
src/database/database.c \
src/database/schema.c \
@ -691,6 +722,8 @@ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
src/models/graph_node_position.c \
src/dao/graph_node_position_dao.c \
src/dao/relation_dao.c \
src/dao/relation_type_dao.c \
src/models/relation_type.c \
src/models/investigation_graph_model.c \
src/models/entity_record.c \
src/models/relation_record.c \
@ -764,7 +797,9 @@ test: \
$(TEST_IBAN_ANALYZER) \
$(TEST_EXIFTOOL_METADATA) \
$(TEST_PDF_PASSWORD_RECOVERY) \
$(TEST_EXTRACTION_DROP_SERVICE)
$(TEST_EXTRACTION_DROP_SERVICE) \
$(TEST_RELATION_TYPE_NORMALIZER) \
$(TEST_RELATION_TYPE_SERVICE)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -828,6 +863,8 @@ test: \
@$(TEST_EXIFTOOL_METADATA)
@$(TEST_PDF_PASSWORD_RECOVERY)
@$(TEST_EXTRACTION_DROP_SERVICE)
@$(TEST_RELATION_TYPE_NORMALIZER)
@$(TEST_RELATION_TYPE_SERVICE)
@echo "Tous les tests sont valides."
%.o: %.c
@ -894,7 +931,9 @@ clean:
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER) \
$(TEST_EXTRACTION_DROP_SERVICE)
$(TEST_EXTRACTION_DROP_SERVICE) \
$(TEST_RELATION_TYPE_NORMALIZER) \
$(TEST_RELATION_TYPE_SERVICE)
-include $(DEP)

View file

@ -1,7 +1,7 @@
/******************************************************************************
* Labfy Investigation
*
* Extensions idempotentes du schéma SQLite courant V8
* Extensions idempotentes du schéma SQLite courant V9
******************************************************************************/
/*
@ -102,6 +102,19 @@ CREATE TABLE IF NOT EXISTS graph_viewport
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
);
CREATE TABLE IF NOT EXISTS relation_types
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
label TEXT NOT NULL,
normalized_key TEXT NOT NULL UNIQUE,
description TEXT,
is_system INTEGER NOT NULL DEFAULT 0 CHECK (is_system IN (0, 1)),
CHECK (code IS NULL OR length(trim(code)) > 0),
CHECK (length(trim(label)) > 0),
CHECK (length(normalized_key) > 0)
);
/* 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

24
database/schema_v9.sql Normal file
View file

@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS relation_types
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
label TEXT NOT NULL,
normalized_key TEXT NOT NULL UNIQUE,
description TEXT,
is_system INTEGER NOT NULL DEFAULT 0 CHECK (is_system IN (0, 1)),
CHECK (code IS NULL OR length(trim(code)) > 0),
CHECK (length(trim(label)) > 0),
CHECK (length(normalized_key) > 0)
);
INSERT OR IGNORE INTO relation_types(code,label,normalized_key,description,is_system) VALUES
('resolves_to','Résout vers','résout vers','Résolution DNS vers une adresse IP.',1),
('aliases_to','Alias de','alias de','Alias DNS canonique.',1),
('uses_name_server','Utilise le serveur de noms','utilise le serveur de noms','Serveur DNS faisant autorité.',1),
('links_to','Lié à','lié à','Lien générique entre deux entités.',1),
('sends','Envoie','envoie','Transmission dun élément.',1),
('uses','Utilise','utilise','Utilisation dune ressource ou identité.',1),
('controls','Contrôle','contrôle','Contrôle dune entité.',1),
('owns','Possède','possède','Possession dune entité.',1),
('knows','Connaît','connaît','Connaissance entre personnes.',1),
('redirects_to','Redirige vers','redirige vers','Redirection vers une cible.',1);

View file

@ -2275,6 +2275,32 @@ 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.
## 11.6 Types canoniques de relations
`relation_types` constitue le référentiel persistant des catégories de
relations. Chaque type possède un identifiant SQLite, un libellé canonique,
une clé Unicode normalisée unique, une description facultative et un
indicateur système. Les types utilisés automatiquement possèdent en plus un
`code` métier stable, indépendant de la langue et du libellé affiché.
Les quatre notions sont distinctes :
- le **code métier stable** pilote les producteurs automatiques tels que DNS ;
- le **libellé canonique** est affiché dans le graphe et linterface ;
- la **clé normalisée** sert uniquement à garantir lunicité Unicode ;
- le **libellé libre de la relation** décrit le fait précis observé.
Depuis le schéma V9, `relations.relation_type_id` référence ce référentiel.
Lunicité dune relation orientée repose sur la source, la cible et cet
identifiant canonique. La colonne historique `type_relation` nest conservée
que pour la compatibilité des bases antérieures et nest plus une identité.
La migration V8 vers V9 parcourt les relations par date puis UUID. Elle
normalise les espaces Unicode, applique NFC et le casefold Unicode, conserve
la première graphie comme libellé des types personnalisés et rattache chaque
relation au type obtenu. Les UUID, métadonnées et tables dassociation ne sont
pas reconstruits. Un contrôle `foreign_key_check` précède le COMMIT.
---
# 12. Conclusion

View file

@ -0,0 +1,26 @@
/******************************************************************************
* @file relation_type_normalizer.h
* @brief Normalisation métier des libellés de types de relations.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RELATION_TYPE_NORMALIZER_H
#define LABFY_INVESTIGATION_RELATION_TYPE_NORMALIZER_H
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Produit la clé d'identité Unicode d'un libellé de type.
*
* La fonction applique NFC, replie toute suite d'espaces Unicode en un espace,
* supprime les espaces périphériques puis effectue un casefold Unicode.
*
* @return Nouvelle chaîne à libérer avec g_free(), ou NULL si le libellé est
* invalide ou vide.
*/
char *relation_type_normalize_key(const char *label);
G_END_DECLS
#endif

View file

@ -0,0 +1,39 @@
/******************************************************************************
* @file relation_type_service.h
* @brief Gestion métier des types canoniques de relations.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RELATION_TYPE_SERVICE_H
#define LABFY_INVESTIGATION_RELATION_TYPE_SERVICE_H
#include "database/database.h"
#include "models/relation_type.h"
#include <glib.h>
G_BEGIN_DECLS
typedef struct RelationTypeService RelationTypeService;
typedef enum
{
RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
RELATION_TYPE_SERVICE_ERROR_DUPLICATE,
RELATION_TYPE_SERVICE_ERROR_IN_USE,
RELATION_TYPE_SERVICE_ERROR_CONFLICT,
RELATION_TYPE_SERVICE_ERROR_DATABASE
} RelationTypeServiceError;
#define RELATION_TYPE_SERVICE_ERROR relation_type_service_error_quark()
GQuark relation_type_service_error_quark(void);
RelationTypeService *relation_type_service_new(Database *database,
GError **error);
void relation_type_service_free(RelationTypeService *service);
GPtrArray *relation_type_service_list_all(RelationTypeService *service,
GError **error);
gboolean relation_type_service_create_custom(RelationTypeService *service,
const char *label, const char *description, gint64 *identifier,
GError **error);
gboolean relation_type_service_rename(RelationTypeService *service,
gint64 identifier, const char *label, GError **error);
gboolean relation_type_service_count_usage(RelationTypeService *service,
gint64 identifier, guint64 *count, GError **error);
gboolean relation_type_service_delete(RelationTypeService *service,
gint64 identifier, GError **error);
gboolean relation_type_service_merge(RelationTypeService *service,
gint64 source_identifier, gint64 target_identifier, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,46 @@
/******************************************************************************
* @file relation_type_dao.h
* @brief Persistance du référentiel des types de relations.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RELATION_TYPE_DAO_H
#define LABFY_INVESTIGATION_RELATION_TYPE_DAO_H
#include "database/database.h"
#include "models/relation_type.h"
#include <glib.h>
G_BEGIN_DECLS
typedef struct RelationTypeDao RelationTypeDao;
typedef enum
{
RELATION_TYPE_DAO_ERROR_INVALID_ARGUMENT,
RELATION_TYPE_DAO_ERROR_PREPARE,
RELATION_TYPE_DAO_ERROR_BIND,
RELATION_TYPE_DAO_ERROR_EXECUTE,
RELATION_TYPE_DAO_ERROR_CONSTRAINT,
RELATION_TYPE_DAO_ERROR_READ,
RELATION_TYPE_DAO_ERROR_MODEL
} RelationTypeDaoError;
#define RELATION_TYPE_DAO_ERROR relation_type_dao_error_quark()
GQuark relation_type_dao_error_quark(void);
RelationTypeDao *relation_type_dao_new(Database *database, GError **error);
void relation_type_dao_free(RelationTypeDao *dao);
GPtrArray *relation_type_dao_list_all(RelationTypeDao *dao, GError **error);
RelationType *relation_type_dao_find_by_identifier(RelationTypeDao *dao,
gint64 identifier, GError **error);
RelationType *relation_type_dao_find_by_code(RelationTypeDao *dao,
const char *code, GError **error);
RelationType *relation_type_dao_find_by_normalized_key(RelationTypeDao *dao,
const char *key, GError **error);
gboolean relation_type_dao_insert_custom(RelationTypeDao *dao,
const char *label, const char *normalized_key, const char *description,
gint64 *out_identifier, GError **error);
gboolean relation_type_dao_update_label(RelationTypeDao *dao,
gint64 identifier, const char *label, const char *normalized_key,
GError **error);
gboolean relation_type_dao_count_relations(RelationTypeDao *dao,
gint64 identifier, guint64 *count, GError **error);
gboolean relation_type_dao_delete_unused(RelationTypeDao *dao,
gint64 identifier, GError **error);
G_END_DECLS
#endif

View file

@ -89,6 +89,7 @@ bool schema_install_v6(Database *database);
bool schema_install_v7(Database *database);
bool schema_install_v8(Database *database);
bool schema_install_v9(Database *database);
/**
* @brief Garantit la présence des extensions du schéma courant V2.

View file

@ -99,6 +99,21 @@ RelationRecord *relation_record_new(
GError **error
);
RelationRecord *relation_record_new_canonical(
const char *identifier,
const char *source_entity_identifier,
const char *target_entity_identifier,
gint64 relation_type_identifier,
const char *relation_type_code,
const char *relation_type_label,
const char *label,
const char *justification,
gint confidence,
const char *created_at,
const char *updated_at,
RelationStatus status,
GError **error);
/**
* @brief Libère une relation.
*
@ -135,6 +150,12 @@ const char *relation_record_get_target_entity_identifier(
const char *relation_record_get_relation_type(
const RelationRecord *relation_record
);
gint64 relation_record_get_relation_type_identifier(
const RelationRecord *relation_record);
const char *relation_record_get_relation_type_code(
const RelationRecord *relation_record);
const char *relation_record_get_relation_type_label(
const RelationRecord *relation_record);
/**
* @brief Retourne le libellé facultatif.

View file

@ -0,0 +1,38 @@
/******************************************************************************
* @file relation_type.h
* @brief Type canonique d'une relation.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_RELATION_TYPE_H
#define LABFY_INVESTIGATION_RELATION_TYPE_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct RelationType RelationType;
typedef enum
{
RELATION_TYPE_ERROR_INVALID_ARGUMENT,
RELATION_TYPE_ERROR_INVALID_IDENTIFIER,
RELATION_TYPE_ERROR_INVALID_LABEL,
RELATION_TYPE_ERROR_INVALID_NORMALIZED_KEY
} RelationTypeError;
#define RELATION_TYPE_ERROR relation_type_error_quark()
GQuark relation_type_error_quark(void);
RelationType *relation_type_new(gint64 identifier, const char *code,
const char *label, const char *normalized_key, const char *description,
gboolean system_type, GError **error);
void relation_type_free(RelationType *relation_type);
gint64 relation_type_get_identifier(const RelationType *relation_type);
const char *relation_type_get_code(const RelationType *relation_type);
const char *relation_type_get_label(const RelationType *relation_type);
const char *relation_type_get_normalized_key(const RelationType *relation_type);
const char *relation_type_get_description(const RelationType *relation_type);
gboolean relation_type_is_system(const RelationType *relation_type);
G_END_DECLS
#endif

View file

@ -80,6 +80,7 @@ gboolean create_relation_dialog_present(
GtkWindow *parent,
const char *source_entity_identifier,
const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records,
const char *investigation_root_path,
CreateRelationDialogCallback callback,
@ -109,6 +110,7 @@ gboolean edit_relation_dialog_present(
GtkWindow *parent,
const RelationRecord *relation_record,
const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path,
@ -168,6 +170,10 @@ const char *create_relation_dialog_result_get_target_identifier(
const char *create_relation_dialog_result_get_relation_type(
const CreateRelationDialogResult *result
);
gint64 create_relation_dialog_result_get_relation_type_identifier(
const CreateRelationDialogResult *result);
const char *create_relation_dialog_result_get_relation_type_code(
const CreateRelationDialogResult *result);
/**
* @brief Retourne le libellé facultatif.

View file

@ -33,6 +33,9 @@
#include "models/evidence_record.h"
#include "dao/evidence_type_dao.h"
#include "dao/graph_node_position_dao.h"
#include "dao/relation_type_dao.h"
#include "models/relation_type.h"
#include "core/relation_type_service.h"
#include "views/evidence_import_dialog.h"
#include "views/evidence_metadata_dialog.h"
#include "views/create_relation_dialog.h"
@ -5593,6 +5596,7 @@ static void application_on_create_relation_completed(
gboolean relation_created =
FALSE;
gint64 relation_type_identifier = 0;
if (application == NULL ||
application->main_window == NULL)
@ -5707,11 +5711,33 @@ static void application_on_create_relation_completed(
goto cleanup;
}
relation_type_identifier =
create_relation_dialog_result_get_relation_type_identifier(result);
if (relation_type_identifier == 0)
{
RelationTypeService *type_service =
relation_type_service_new(database, &error);
if (type_service == NULL ||
!relation_type_service_create_custom(type_service, relation_type,
NULL, &relation_type_identifier, &error))
{
relation_type_service_free(type_service);
application_present_error(application,
"Création du type impossible",
error != NULL ? error->message :
"Le nouveau type n'a pas pu être créé.");
goto cleanup;
}
relation_type_service_free(type_service);
}
relation_record =
relation_record_new(
relation_record_new_canonical(
relation_identifier,
source_identifier,
target_identifier,
relation_type_identifier,
create_relation_dialog_result_get_relation_type_code(result),
relation_type,
create_relation_dialog_result_get_label(
result
@ -6240,6 +6266,7 @@ static void application_on_edit_relation_completed(
char *timestamp = NULL;
GError *error = NULL;
gboolean transaction_active = FALSE;
gint64 relation_type_identifier = 0;
if (result == NULL || application == NULL || application->session == NULL)
goto cleanup;
@ -6254,10 +6281,28 @@ static void application_on_edit_relation_completed(
now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now,
"%Y-%m-%dT%H:%M:%SZ") : NULL;
updated = relation_record_new(
relation_type_identifier =
create_relation_dialog_result_get_relation_type_identifier(result);
if (relation_type_identifier == 0)
{
RelationTypeService *type_service =
relation_type_service_new(database, &error);
if (type_service == NULL ||
!relation_type_service_create_custom(type_service,
create_relation_dialog_result_get_relation_type(result),
NULL, &relation_type_identifier, &error))
{
relation_type_service_free(type_service);
goto failure;
}
relation_type_service_free(type_service);
}
updated = relation_record_new_canonical(
relation_record_get_identifier(existing),
relation_record_get_source_entity_identifier(existing),
relation_record_get_target_entity_identifier(existing),
relation_type_identifier,
create_relation_dialog_result_get_relation_type_code(result),
create_relation_dialog_result_get_relation_type(result),
create_relation_dialog_result_get_label(result),
create_relation_dialog_result_get_justification(result),
@ -6348,6 +6393,8 @@ static void application_on_edit_relation_requested(
GPtrArray *entities = NULL;
GPtrArray *evidences = NULL;
GPtrArray *selected_evidences = NULL;
GPtrArray *relation_types = NULL;
RelationTypeDao *relation_type_dao = NULL;
const InvestigationProject *project = NULL;
ApplicationEditRelationContext *context = NULL;
GError *error = NULL;
@ -6359,6 +6406,7 @@ static void application_on_edit_relation_requested(
entity_dao = entity_dao_new(database, &error);
evidence_dao = evidence_dao_new(database, &error);
relation_evidence_dao = relation_evidence_dao_new(database, &error);
relation_type_dao = relation_type_dao_new(database, &error);
project = investigation_session_get_project(application->session);
if (relation_dao != NULL)
relation = relation_dao_find_by_identifier(relation_dao,
@ -6369,13 +6417,17 @@ static void application_on_edit_relation_requested(
if (relation_evidence_dao != NULL) selected_evidences =
relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao,
relation_identifier, &error);
if (relation_type_dao != NULL)
relation_types = relation_type_dao_list_all(relation_type_dao, &error);
if (relation == NULL || entities == NULL || evidences == NULL ||
selected_evidences == NULL || project == NULL) goto failure;
selected_evidences == NULL || relation_types == NULL ||
project == NULL) goto failure;
context = g_new0(ApplicationEditRelationContext, 1);
context->application = application;
context->relation_identifier = g_strdup(relation_identifier);
if (context->relation_identifier == NULL || !edit_relation_dialog_present(
main_window_get_window(application->main_window), relation, entities,
relation_types,
evidences, selected_evidences,
investigation_project_get_root_path(project),
application_on_edit_relation_completed, context, &error))
@ -6396,6 +6448,8 @@ cleanup:
entity_dao_free(entity_dao);
evidence_dao_free(evidence_dao);
relation_evidence_dao_free(relation_evidence_dao);
relation_type_dao_free(relation_type_dao);
g_clear_pointer(&relation_types, g_ptr_array_unref);
relation_dao_free(relation_dao);
}
@ -6462,11 +6516,13 @@ static void application_on_add_relation_requested(
NULL;
EvidenceDao *evidence_dao = NULL;
RelationTypeDao *relation_type_dao = NULL;
const InvestigationProject *project = NULL;
GPtrArray *entities =
NULL;
GPtrArray *evidences = NULL;
GPtrArray *relation_types = NULL;
GError *error =
NULL;
@ -6533,11 +6589,17 @@ static void application_on_add_relation_requested(
evidences = evidence_dao_list_all(evidence_dao, &error);
evidence_dao_free(evidence_dao);
relation_type_dao = relation_type_dao_new(database, &error);
if (relation_type_dao != NULL)
relation_types = relation_type_dao_list_all(relation_type_dao, &error);
relation_type_dao_free(relation_type_dao);
entity_dao_free(
entity_dao
);
if (entities == NULL || evidences == NULL || project == NULL)
if (entities == NULL || evidences == NULL || relation_types == NULL ||
project == NULL)
{
application_present_error(
application,
@ -6553,6 +6615,7 @@ static void application_on_add_relation_requested(
g_clear_pointer(&entities, g_ptr_array_unref);
g_clear_pointer(&evidences, g_ptr_array_unref);
g_clear_pointer(&relation_types, g_ptr_array_unref);
return;
}
@ -6563,6 +6626,7 @@ static void application_on_add_relation_requested(
),
source_entity_identifier,
entities,
relation_types,
evidences,
investigation_project_get_root_path(project),
application_on_create_relation_completed,
@ -6587,6 +6651,7 @@ static void application_on_add_relation_requested(
entities
);
g_ptr_array_unref(evidences);
g_ptr_array_unref(relation_types);
}
/**

View file

@ -183,7 +183,9 @@ gboolean osint_dns_integration_apply(
osint_dns_integration_build_relation_key(
relation_record_get_source_entity_identifier(relation),
relation_record_get_target_entity_identifier(relation),
relation_record_get_relation_type(relation)
relation_record_get_relation_type_code(relation) != NULL
? relation_record_get_relation_type_code(relation)
: relation_record_get_relation_type(relation)
),
g_strdup(relation_record_get_identifier(relation))
);

View file

@ -0,0 +1,47 @@
/******************************************************************************
* @file relation_type_normalizer.c
* @brief Normalisation Unicode des types de relations.
******************************************************************************/
#include "core/relation_type_normalizer.h"
char *relation_type_normalize_key(const char *label)
{
char *normalized = NULL;
char *folded = NULL;
GString *collapsed = NULL;
const char *cursor = NULL;
gboolean pending_space = FALSE;
if (label == NULL || !g_utf8_validate(label, -1, NULL)) return NULL;
normalized = g_utf8_normalize(label, -1, G_NORMALIZE_DEFAULT_COMPOSE);
if (normalized == NULL) return NULL;
collapsed = g_string_new(NULL);
for (cursor = normalized; *cursor != '\0'; cursor = g_utf8_next_char(cursor))
{
gunichar character = g_utf8_get_char(cursor);
if (g_unichar_isspace(character))
{
if (collapsed->len > 0U) pending_space = TRUE;
continue;
}
if (pending_space)
{
g_string_append_c(collapsed, ' ');
pending_space = FALSE;
}
g_string_append_unichar(collapsed, character);
}
g_free(normalized);
if (collapsed->len == 0U)
{
g_string_free(collapsed, TRUE);
return NULL;
}
folded = g_utf8_casefold(collapsed->str, -1);
g_string_free(collapsed, TRUE);
if (folded == NULL) return NULL;
normalized = g_utf8_normalize(folded, -1, G_NORMALIZE_DEFAULT_COMPOSE);
g_free(folded);
return normalized;
}

View file

@ -0,0 +1,152 @@
#include "core/relation_type_service.h"
#include "core/relation_type_normalizer.h"
#include "dao/relation_type_dao.h"
#include "database/error.h"
#include "database/statement.h"
#include "database/transaction.h"
struct RelationTypeService
{
Database *database;
RelationTypeDao *dao;
};
static void set_error(GError **error, RelationTypeServiceError code,
const char *message)
{
if (error != NULL && *error == NULL)
g_set_error_literal(error, RELATION_TYPE_SERVICE_ERROR, code, message);
}
GQuark relation_type_service_error_quark(void)
{ return g_quark_from_static_string("relation-type-service-error-quark"); }
RelationTypeService *relation_type_service_new(Database *database,
GError **error)
{
RelationTypeService *service = NULL;
if (database == NULL) { set_error(error,
RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
"La base est obligatoire."); return NULL; }
service = g_new0(RelationTypeService, 1);
service->database = database;
service->dao = relation_type_dao_new(database, error);
if (service->dao == NULL) { g_free(service); return NULL; }
return service;
}
void relation_type_service_free(RelationTypeService *service)
{
if (service == NULL) return;
relation_type_dao_free(service->dao); g_free(service);
}
GPtrArray *relation_type_service_list_all(RelationTypeService *service,
GError **error)
{ return service != NULL ? relation_type_dao_list_all(service->dao, error) : NULL; }
gboolean relation_type_service_create_custom(RelationTypeService *service,
const char *label, const char *description, gint64 *identifier,
GError **error)
{
char *key = relation_type_normalize_key(label);
RelationType *existing = NULL; gboolean result = FALSE;
if (service == NULL || key == NULL) { set_error(error,
RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
"Le libellé du type est invalide."); goto cleanup; }
existing = relation_type_dao_find_by_normalized_key(service->dao,key,error);
if (existing != NULL) { set_error(error, RELATION_TYPE_SERVICE_ERROR_DUPLICATE,
"Un type de relation équivalent existe déjà."); goto cleanup; }
if (error != NULL && *error != NULL) goto cleanup;
result = relation_type_dao_insert_custom(service->dao,label,key,description,
identifier,error);
cleanup:
relation_type_free(existing); g_free(key); return result;
}
gboolean relation_type_service_rename(RelationTypeService *service,
gint64 identifier, const char *label, GError **error)
{
char *key = relation_type_normalize_key(label);
RelationType *existing = NULL; gboolean result = FALSE;
if (service == NULL || identifier <= 0 || key == NULL) { set_error(error,
RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
"Le renommage demandé est invalide."); goto cleanup; }
existing = relation_type_dao_find_by_normalized_key(service->dao,key,error);
if (existing != NULL &&
relation_type_get_identifier(existing) != identifier)
{ set_error(error, RELATION_TYPE_SERVICE_ERROR_DUPLICATE,
"Un type de relation équivalent existe déjà."); goto cleanup; }
if (error != NULL && *error != NULL) goto cleanup;
result = relation_type_dao_update_label(service->dao,identifier,label,key,error);
cleanup:
relation_type_free(existing); g_free(key); return result;
}
gboolean relation_type_service_count_usage(RelationTypeService *service,
gint64 identifier, guint64 *count, GError **error)
{
if (service == NULL || identifier <= 0 || count == NULL) { set_error(error,
RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
"Le comptage demandé est invalide."); return FALSE; }
return relation_type_dao_count_relations(service->dao,identifier,count,error);
}
gboolean relation_type_service_delete(RelationTypeService *service,
gint64 identifier, GError **error)
{
guint64 count = 0;
if (!relation_type_service_count_usage(service,identifier,&count,error))
return FALSE;
if (count > 0U) { set_error(error, RELATION_TYPE_SERVICE_ERROR_IN_USE,
"Ce type est utilisé par des relations et ne peut pas être supprimé.");
return FALSE; }
return relation_type_dao_delete_unused(service->dao,identifier,error);
}
gboolean relation_type_service_merge(RelationTypeService *service,
gint64 source_identifier, gint64 target_identifier, GError **error)
{
DatabaseStatement *statement = NULL; gboolean active = FALSE;
gboolean success = FALSE;
if (service == NULL || source_identifier <= 0 || target_identifier <= 0 ||
source_identifier == target_identifier)
{ set_error(error, RELATION_TYPE_SERVICE_ERROR_INVALID_ARGUMENT,
"La fusion demandée est invalide."); return FALSE; }
statement = database_statement_prepare(service->database,
"SELECT 1 FROM relations source JOIN relations target ON "
"target.entite_source_id=source.entite_source_id AND "
"target.entite_cible_id=source.entite_cible_id AND "
"target.relation_type_id=? WHERE source.relation_type_id=? LIMIT 1;");
if (statement == NULL ||
!database_statement_bind_int64(statement,1,target_identifier) ||
!database_statement_bind_int64(statement,2,source_identifier))
goto cleanup;
if (database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW)
{ set_error(error, RELATION_TYPE_SERVICE_ERROR_CONFLICT,
"Fusion refusée : des relations source et cible entreraient en conflit.");
goto cleanup; }
database_statement_finalize(statement); statement = NULL;
if (!database_transaction_begin(service->database)) goto cleanup;
active = TRUE;
statement = database_statement_prepare(service->database,
"UPDATE relations SET relation_type_id=?,type_relation=("
"SELECT COALESCE(code,label) FROM relation_types WHERE id=?) "
"WHERE relation_type_id=?;");
if (statement == NULL ||
!database_statement_bind_int64(statement,1,target_identifier) ||
!database_statement_bind_int64(statement,2,target_identifier) ||
!database_statement_bind_int64(statement,3,source_identifier) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
goto cleanup;
database_statement_finalize(statement); statement = NULL;
if (!relation_type_dao_delete_unused(service->dao,source_identifier,error))
goto cleanup;
if (!database_transaction_commit(service->database)) goto cleanup;
active = FALSE; success = TRUE;
cleanup:
database_statement_finalize(statement);
if (!success && active) (void) database_transaction_rollback(service->database);
if (!success && error != NULL && *error == NULL)
g_set_error(error, RELATION_TYPE_SERVICE_ERROR,
RELATION_TYPE_SERVICE_ERROR_DATABASE, "Fusion impossible : %s",
database_error_get_message(service->database) != NULL
? database_error_get_message(service->database) : "erreur SQLite");
return success;
}

View file

@ -7,6 +7,8 @@
#include "database/error.h"
#include "database/statement.h"
#include "dao/relation_type_dao.h"
#include "core/relation_type_normalizer.h"
#include <glib.h>
#include <stdint.h>
@ -31,6 +33,7 @@ static const char *const relation_dao_insert_sql =
" entite_source_id,"
" entite_cible_id,"
" type_relation,"
" relation_type_id,"
" label,"
" justification,"
" confiance,"
@ -49,14 +52,15 @@ static const char *const relation_dao_insert_sql =
" ?,"
" ?,"
" ?,"
" ?,"
" ?"
");";
/** @brief Requête de modification des attributs d'une relation. */
static const char *const relation_dao_update_sql =
"UPDATE relations SET "
"type_relation = ?, label = ?, justification = ?, confiance = ?, "
"updated_at = ?, status = ? WHERE id = ?;";
"type_relation = ?, relation_type_id = ?, label = ?, justification = ?, "
"confiance = ?, updated_at = ?, status = ? WHERE id = ?;";
/** @brief Requête de suppression d'une relation. */
static const char *const relation_dao_delete_sql =
@ -67,38 +71,42 @@ static const char *const relation_dao_delete_sql =
*/
static const char *const relation_dao_find_by_identifier_sql =
"SELECT "
" id,"
" entite_source_id,"
" entite_cible_id,"
" type_relation,"
" label,"
" justification,"
" confiance,"
" created_at,"
" updated_at,"
" status "
"FROM relations "
"WHERE id = ?;";
" r.id,"
" r.entite_source_id,"
" r.entite_cible_id,"
" rt.id,"
" rt.code,"
" rt.label,"
" r.label,"
" r.justification,"
" r.confiance,"
" r.created_at,"
" r.updated_at,"
" r.status "
"FROM relations r JOIN relation_types rt ON rt.id=r.relation_type_id "
"WHERE r.id = ?;";
/**
* @brief Requête de chargement de toutes les relations.
*/
static const char *const relation_dao_list_all_sql =
"SELECT "
" id,"
" entite_source_id,"
" entite_cible_id,"
" type_relation,"
" label,"
" justification,"
" confiance,"
" created_at,"
" updated_at,"
" status "
"FROM relations "
" r.id,"
" r.entite_source_id,"
" r.entite_cible_id,"
" rt.id,"
" rt.code,"
" rt.label,"
" r.label,"
" r.justification,"
" r.confiance,"
" r.created_at,"
" r.updated_at,"
" r.status "
"FROM relations r JOIN relation_types rt ON rt.id=r.relation_type_id "
"ORDER BY "
" created_at ASC,"
" id ASC;";
" r.created_at ASC,"
" r.id ASC;";
/**
* @brief Requête de comptage.
@ -133,7 +141,7 @@ static const char *const relation_dao_duplicate_exists_sql =
"FROM relations "
"WHERE entite_source_id = ? "
"AND entite_cible_id = ? "
"AND type_relation = ? "
"AND relation_type_id = ? "
"LIMIT 1;";
/**
@ -492,7 +500,7 @@ static gboolean relation_dao_duplicate_exists(
RelationDao *relation_dao,
const char *source_entity_identifier,
const char *target_entity_identifier,
const char *relation_type,
gint64 relation_type_identifier,
gboolean *out_exists,
GError **error
)
@ -510,7 +518,7 @@ static gboolean relation_dao_duplicate_exists(
relation_dao->database == NULL ||
source_entity_identifier == NULL ||
target_entity_identifier == NULL ||
relation_type == NULL ||
relation_type_identifier <= 0 ||
out_exists == NULL)
{
relation_dao_set_error_literal(
@ -553,10 +561,10 @@ static gboolean relation_dao_duplicate_exists(
2,
target_entity_identifier
) ||
!database_statement_bind_text(
!database_statement_bind_int64(
statement,
3,
relation_type
relation_type_identifier
))
{
relation_dao_set_database_error(
@ -626,6 +634,8 @@ static RelationRecord *relation_dao_read_current_record(
char *relation_type =
NULL;
char *relation_type_code = NULL;
int64_t relation_type_identifier = 0;
char *label =
NULL;
@ -679,39 +689,49 @@ static RelationRecord *relation_dao_read_current_record(
2,
&target_entity_identifier
) ||
!database_statement_column_text(
!database_statement_column_int64(
statement,
3,
&relation_type
&relation_type_identifier
) ||
!database_statement_column_text(
statement,
4,
&label
&relation_type_code
) ||
!database_statement_column_text(
statement,
5,
&justification
&relation_type
) ||
!database_statement_column_int64(
!database_statement_column_text(
statement,
6,
&confidence_value
&label
) ||
!database_statement_column_text(
statement,
7,
&created_at
&justification
) ||
!database_statement_column_text(
!database_statement_column_int64(
statement,
8,
&updated_at
&confidence_value
) ||
!database_statement_column_text(
statement,
9,
&created_at
) ||
!database_statement_column_text(
statement,
10,
&updated_at
) ||
!database_statement_column_text(
statement,
11,
&status_text
))
{
@ -751,10 +771,12 @@ static RelationRecord *relation_dao_read_current_record(
}
relation_record =
relation_record_new(
relation_record_new_canonical(
identifier,
source_entity_identifier,
target_entity_identifier,
(gint64) relation_type_identifier,
relation_type_code,
relation_type,
label,
justification,
@ -816,6 +838,7 @@ cleanup:
g_free(
relation_type
);
g_free(relation_type_code);
g_free(
target_entity_identifier
@ -915,6 +938,36 @@ void relation_dao_free(
);
}
static RelationType *relation_dao_resolve_type(
RelationDao *relation_dao, const RelationRecord *record, GError **error)
{
RelationTypeDao *type_dao = NULL;
RelationType *type = NULL;
char *key = NULL;
gint64 identifier = relation_record_get_relation_type_identifier(record);
type_dao = relation_type_dao_new(relation_dao->database, error);
if (type_dao == NULL) return NULL;
if (identifier > 0)
type = relation_type_dao_find_by_identifier(type_dao, identifier, error);
else if (relation_record_get_relation_type_code(record) != NULL)
type = relation_type_dao_find_by_code(type_dao,
relation_record_get_relation_type_code(record), error);
else
{
key = relation_type_normalize_key(
relation_record_get_relation_type(record));
if (key != NULL)
type = relation_type_dao_find_by_code(type_dao,
relation_record_get_relation_type(record), error);
if (type == NULL && (error == NULL || *error == NULL))
type = relation_type_dao_find_by_normalized_key(type_dao, key, error);
}
if (type == NULL && error != NULL && *error == NULL)
relation_dao_set_error_literal(error, RELATION_DAO_ERROR_CONSTRAINT,
"Le type de relation n'existe pas dans le référentiel.");
g_free(key); relation_type_dao_free(type_dao); return type;
}
gboolean relation_dao_insert(
RelationDao *relation_dao,
const RelationRecord *relation_record,
@ -962,6 +1015,9 @@ gboolean relation_dao_insert(
gboolean success =
FALSE;
RelationType *canonical_type = NULL;
gint64 canonical_type_identifier = 0;
const char *canonical_type_label = NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
@ -1161,16 +1217,24 @@ gboolean relation_dao_insert(
return FALSE;
}
canonical_type = relation_dao_resolve_type(
relation_dao, relation_record, error);
if (canonical_type == NULL) return FALSE;
canonical_type_identifier = relation_type_get_identifier(canonical_type);
canonical_type_label = relation_type_get_code(canonical_type) != NULL
? relation_type_get_code(canonical_type)
: relation_type_get_label(canonical_type);
if (!relation_dao_duplicate_exists(
relation_dao,
source_entity_identifier,
target_entity_identifier,
relation_type,
canonical_type_identifier,
&value_exists,
error
))
{
return FALSE;
goto cleanup;
}
if (value_exists)
@ -1181,7 +1245,7 @@ gboolean relation_dao_insert(
"Cette relation orientée existe déjà pour ce type."
);
return FALSE;
goto cleanup;
}
statement =
@ -1220,36 +1284,41 @@ gboolean relation_dao_insert(
!database_statement_bind_text(
statement,
4,
relation_type
canonical_type_label
) ||
!relation_dao_bind_optional_text(
!database_statement_bind_int64(
statement,
5,
label
canonical_type_identifier
) ||
!relation_dao_bind_optional_text(
statement,
6,
label
) ||
!relation_dao_bind_optional_text(
statement,
7,
justification
) ||
!database_statement_bind_int64(
statement,
7,
8,
(int64_t) confidence
) ||
!database_statement_bind_text(
statement,
8,
9,
created_at
) ||
!database_statement_bind_text(
statement,
9,
10,
updated_at
) ||
!database_statement_bind_text(
statement,
10,
11,
status_text
))
{
@ -1304,6 +1373,7 @@ cleanup:
database_statement_finalize(
statement
);
relation_type_free(canonical_type);
return success;
}
@ -1314,6 +1384,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
DatabaseStatement *statement = NULL;
const char *status_text = NULL;
gboolean success = FALSE;
RelationType *canonical_type = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (relation_dao == NULL || relation_record == NULL)
@ -1325,6 +1396,9 @@ gboolean relation_dao_update(RelationDao *relation_dao,
}
status_text = relation_dao_status_to_text(
relation_record_get_status(relation_record));
canonical_type = relation_dao_resolve_type(
relation_dao, relation_record, error);
if (canonical_type == NULL) return FALSE;
statement = database_statement_prepare(relation_dao->database,
relation_dao_update_sql);
if (statement == NULL)
@ -1332,21 +1406,25 @@ gboolean relation_dao_update(RelationDao *relation_dao,
relation_dao_set_database_error(relation_dao, error,
RELATION_DAO_ERROR_PREPARE,
"Impossible de préparer la modification de la relation");
return FALSE;
goto cleanup;
}
if (!database_statement_bind_text(statement, 1,
relation_record_get_relation_type(relation_record)) ||
!relation_dao_bind_optional_text(statement, 2,
relation_record_get_label(relation_record)) ||
relation_type_get_code(canonical_type) != NULL
? relation_type_get_code(canonical_type)
: relation_type_get_label(canonical_type)) ||
!database_statement_bind_int64(statement, 2,
relation_type_get_identifier(canonical_type)) ||
!relation_dao_bind_optional_text(statement, 3,
relation_record_get_label(relation_record)) ||
!relation_dao_bind_optional_text(statement, 4,
relation_record_get_justification(relation_record)) ||
!database_statement_bind_int64(statement, 4,
!database_statement_bind_int64(statement, 5,
relation_record_get_confidence(relation_record)) ||
!database_statement_bind_text(statement, 5,
!database_statement_bind_text(statement, 6,
relation_record_get_updated_at(relation_record)) ||
status_text == NULL ||
!database_statement_bind_text(statement, 6, status_text) ||
!database_statement_bind_text(statement, 7,
!database_statement_bind_text(statement, 7, status_text) ||
!database_statement_bind_text(statement, 8,
relation_record_get_identifier(relation_record)))
{
relation_dao_set_database_error(relation_dao, error,
@ -1364,6 +1442,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
success = TRUE;
cleanup:
database_statement_finalize(statement);
relation_type_free(canonical_type);
return success;
}

189
src/dao/relation_type_dao.c Normal file
View file

@ -0,0 +1,189 @@
#include "dao/relation_type_dao.h"
#include "database/error.h"
#include "database/statement.h"
struct RelationTypeDao { Database *database; };
static void set_db_error(RelationTypeDao *dao, GError **error,
RelationTypeDaoError code, const char *context)
{
if (error != NULL && *error == NULL)
g_set_error(error, RELATION_TYPE_DAO_ERROR, code, "%s : %s", context,
dao != NULL && database_error_get_message(dao->database) != NULL
? database_error_get_message(dao->database) : "erreur inconnue");
}
static RelationType *read_type(DatabaseStatement *statement, GError **error)
{
int64_t id = 0; int64_t system = 0;
char *code = NULL; char *label = NULL; char *key = NULL; char *description = NULL;
RelationType *type = NULL; GError *model_error = NULL;
if (!database_statement_column_int64(statement, 0, &id) ||
!database_statement_column_text(statement, 1, &code) ||
!database_statement_column_text(statement, 2, &label) ||
!database_statement_column_text(statement, 3, &key) ||
!database_statement_column_text(statement, 4, &description) ||
!database_statement_column_int64(statement, 5, &system))
{
g_set_error_literal(error, RELATION_TYPE_DAO_ERROR,
RELATION_TYPE_DAO_ERROR_READ, "Le type de relation est illisible.");
goto cleanup;
}
type = relation_type_new((gint64) id, code, label, key, description,
system != 0, &model_error);
if (type == NULL)
g_set_error(error, RELATION_TYPE_DAO_ERROR,
RELATION_TYPE_DAO_ERROR_MODEL, "Type de relation invalide : %s",
model_error != NULL ? model_error->message : "erreur inconnue");
cleanup:
g_clear_error(&model_error); g_free(description); g_free(key);
g_free(label); g_free(code); return type;
}
static RelationType *find_one(RelationTypeDao *dao, const char *sql,
gboolean bind_integer, gint64 integer, const char *text, GError **error)
{
DatabaseStatement *statement = NULL; RelationType *type = NULL;
if (dao == NULL || dao->database == NULL) return NULL;
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL) { set_db_error(dao, error,
RELATION_TYPE_DAO_ERROR_PREPARE, "Lecture du type impossible"); return NULL; }
if ((bind_integer && !database_statement_bind_int64(statement, 1, integer)) ||
(!bind_integer && !database_statement_bind_text(statement, 1, text)))
{ set_db_error(dao, error, RELATION_TYPE_DAO_ERROR_BIND,
"Paramètre du type invalide"); goto cleanup; }
DatabaseStatementStepResult step = database_statement_step(statement);
if (step == DATABASE_STATEMENT_STEP_ROW) type = read_type(statement, error);
else if (step == DATABASE_STATEMENT_STEP_ERROR) set_db_error(dao, error,
RELATION_TYPE_DAO_ERROR_EXECUTE, "Recherche du type impossible");
cleanup:
database_statement_finalize(statement); return type;
}
GQuark relation_type_dao_error_quark(void)
{ return g_quark_from_static_string("relation-type-dao-error-quark"); }
RelationTypeDao *relation_type_dao_new(Database *database, GError **error)
{
if (database == NULL) { g_set_error_literal(error, RELATION_TYPE_DAO_ERROR,
RELATION_TYPE_DAO_ERROR_INVALID_ARGUMENT, "La base est obligatoire.");
return NULL; }
RelationTypeDao *dao = g_new0(RelationTypeDao, 1); dao->database = database;
return dao;
}
void relation_type_dao_free(RelationTypeDao *dao) { g_free(dao); }
GPtrArray *relation_type_dao_list_all(RelationTypeDao *dao, GError **error)
{
static const char *sql = "SELECT id,code,label,normalized_key,description,"
"is_system FROM relation_types ORDER BY label COLLATE NOCASE,id;";
DatabaseStatement *statement = NULL;
GPtrArray *items = g_ptr_array_new_with_free_func(
(GDestroyNotify) relation_type_free);
if (dao == NULL || dao->database == NULL) goto fail;
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL) { set_db_error(dao, error,
RELATION_TYPE_DAO_ERROR_PREPARE, "Liste des types impossible"); goto fail; }
while (TRUE)
{
DatabaseStatementStepResult step = database_statement_step(statement);
if (step == DATABASE_STATEMENT_STEP_DONE) break;
if (step == DATABASE_STATEMENT_STEP_ERROR) { set_db_error(dao, error,
RELATION_TYPE_DAO_ERROR_EXECUTE, "Liste des types impossible"); goto fail; }
RelationType *type = read_type(statement, error);
if (type == NULL) goto fail;
g_ptr_array_add(items, type);
}
database_statement_finalize(statement); return items;
fail:
database_statement_finalize(statement); g_ptr_array_unref(items); return NULL;
}
#define TYPE_COLUMNS "SELECT id,code,label,normalized_key,description,is_system FROM relation_types WHERE "
RelationType *relation_type_dao_find_by_identifier(RelationTypeDao *dao,
gint64 id, GError **error)
{ return find_one(dao, TYPE_COLUMNS "id=?;", TRUE, id, NULL, error); }
RelationType *relation_type_dao_find_by_code(RelationTypeDao *dao,
const char *code, GError **error)
{ return find_one(dao, TYPE_COLUMNS "code=?;", FALSE, 0, code, error); }
RelationType *relation_type_dao_find_by_normalized_key(RelationTypeDao *dao,
const char *key, GError **error)
{ return find_one(dao, TYPE_COLUMNS "normalized_key=?;", FALSE, 0, key, error); }
gboolean relation_type_dao_insert_custom(RelationTypeDao *dao,
const char *label, const char *key, const char *description,
gint64 *out_identifier, GError **error)
{
const char *sql = "INSERT INTO relation_types(code,label,normalized_key,"
"description,is_system) VALUES(NULL,?,?,?,0);";
DatabaseStatement *s = database_statement_prepare(dao->database, sql);
if (s == NULL) { set_db_error(dao,error,RELATION_TYPE_DAO_ERROR_PREPARE,
"Création du type impossible"); return FALSE; }
if (!database_statement_bind_text(s,1,label) ||
!database_statement_bind_text(s,2,key) ||
(description != NULL ? !database_statement_bind_text(s,3,description)
: !database_statement_bind_null(s,3)) ||
database_statement_step(s) != DATABASE_STATEMENT_STEP_DONE)
{ set_db_error(dao,error,RELATION_TYPE_DAO_ERROR_CONSTRAINT,
"Ce libellé de type existe déjà"); database_statement_finalize(s); return FALSE; }
database_statement_finalize(s);
if (out_identifier != NULL)
{
int64_t identifier = 0;
s = database_statement_prepare(dao->database,
"SELECT last_insert_rowid();");
if (s == NULL ||
database_statement_step(s) != DATABASE_STATEMENT_STEP_ROW ||
!database_statement_column_int64(s, 0, &identifier))
{
set_db_error(dao, error, RELATION_TYPE_DAO_ERROR_READ,
"Identifiant du type indisponible");
database_statement_finalize(s);
return FALSE;
}
*out_identifier = (gint64) identifier;
database_statement_finalize(s);
}
return TRUE;
}
gboolean relation_type_dao_update_label(RelationTypeDao *dao, gint64 id,
const char *label, const char *key, GError **error)
{
DatabaseStatement *s = database_statement_prepare(dao->database,
"UPDATE relation_types SET label=?,normalized_key=? WHERE id=?;");
if (s == NULL || !database_statement_bind_text(s,1,label) ||
!database_statement_bind_text(s,2,key) ||
!database_statement_bind_int64(s,3,id) ||
database_statement_step(s) != DATABASE_STATEMENT_STEP_DONE)
{ set_db_error(dao,error,RELATION_TYPE_DAO_ERROR_CONSTRAINT,
"Renommage du type impossible"); database_statement_finalize(s); return FALSE; }
database_statement_finalize(s); return TRUE;
}
gboolean relation_type_dao_count_relations(RelationTypeDao *dao, gint64 id,
guint64 *count, GError **error)
{
DatabaseStatement *s = database_statement_prepare(dao->database,
"SELECT COUNT(*) FROM relations WHERE relation_type_id=?;");
int64_t value = 0;
if (s == NULL || !database_statement_bind_int64(s,1,id) ||
database_statement_step(s) != DATABASE_STATEMENT_STEP_ROW ||
!database_statement_column_int64(s,0,&value))
{ set_db_error(dao,error,RELATION_TYPE_DAO_ERROR_EXECUTE,
"Comptage des relations impossible"); database_statement_finalize(s); return FALSE; }
*count = (guint64) value; database_statement_finalize(s); return TRUE;
}
gboolean relation_type_dao_delete_unused(RelationTypeDao *dao, gint64 id,
GError **error)
{
DatabaseStatement *s = database_statement_prepare(dao->database,
"DELETE FROM relation_types WHERE id=? AND is_system=0 AND NOT EXISTS"
"(SELECT 1 FROM relations WHERE relation_type_id=?);");
if (s == NULL || !database_statement_bind_int64(s,1,id) ||
!database_statement_bind_int64(s,2,id) ||
database_statement_step(s) != DATABASE_STATEMENT_STEP_DONE)
{ set_db_error(dao,error,RELATION_TYPE_DAO_ERROR_CONSTRAINT,
"Suppression du type refusée"); database_statement_finalize(s); return FALSE; }
database_statement_finalize(s); return TRUE;
}

View file

@ -16,12 +16,12 @@
/**
* @brief Version actuelle du schéma SQLite.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT 8
#define DATABASE_SCHEMA_VERSION_CURRENT 9
/**
* @brief Version actuelle sous forme textuelle pour metadata.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "8"
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "9"
/**
* @brief Nom de l'application enregistré dans les métadonnées.
@ -782,6 +782,21 @@ rollback:
return false;
}
static bool database_migrate_v8_to_v9(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v9(database) ||
!database_update_schema_version(database, "9") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V8 vers V9.");
return false;
}
/**
* @brief Garantit atomiquement la présence des extensions du schéma courant.
*/
@ -1003,6 +1018,10 @@ bool database_migrate_to_latest(
return false;
schema_version = 8;
break;
case 8:
if (!database_migrate_v8_to_v9(database)) return false;
schema_version = 9;
break;
default:
database_set_error(
@ -1135,6 +1154,13 @@ bool database_initialize(
goto rollback;
}
if (!schema_install_v7(database) ||
!schema_install_v8(database) ||
!schema_install_v9(database))
{
goto rollback;
}
if (!schema_ensure_current(
database
))

View file

@ -6,6 +6,7 @@
#include "database/schema.h"
#include "database_internal.h"
#include "core/relation_type_normalizer.h"
#include <glib.h>
#include <sqlite3.h>
@ -239,6 +240,113 @@ bool schema_install_v8(Database *database)
"la migration SQLite V8");
}
static gboolean schema_v9_column_exists(sqlite3 *handle)
{
sqlite3_stmt *statement = NULL;
gboolean found = FALSE;
if (sqlite3_prepare_v2(handle, "PRAGMA table_info(relations);", -1,
&statement, NULL) != SQLITE_OK) return FALSE;
while (sqlite3_step(statement) == SQLITE_ROW)
if (g_strcmp0((const char *) sqlite3_column_text(statement, 1),
"relation_type_id") == 0) found = TRUE;
sqlite3_finalize(statement);
return found;
}
static gboolean schema_v9_migrate_relation_types(Database *database)
{
sqlite3 *handle = database_get_handle(database);
sqlite3_stmt *read_statement = NULL;
sqlite3_stmt *find_statement = NULL;
sqlite3_stmt *insert_statement = NULL;
sqlite3_stmt *update_statement = NULL;
gboolean success = FALSE;
if (!schema_v9_column_exists(handle) &&
sqlite3_exec(handle, "ALTER TABLE relations ADD COLUMN "
"relation_type_id INTEGER REFERENCES relation_types(id) "
"ON UPDATE CASCADE ON DELETE RESTRICT;", NULL, NULL, NULL) != SQLITE_OK)
return FALSE;
if (sqlite3_prepare_v2(handle, "SELECT id,type_relation FROM relations "
"WHERE relation_type_id IS NULL ORDER BY created_at,id;", -1,
&read_statement, NULL) != SQLITE_OK ||
sqlite3_prepare_v2(handle, "SELECT id FROM relation_types WHERE "
"code=?1 OR normalized_key=?2 ORDER BY code IS NULL LIMIT 1;", -1,
&find_statement, NULL) != SQLITE_OK ||
sqlite3_prepare_v2(handle, "INSERT INTO relation_types(code,label,"
"normalized_key,description,is_system) VALUES(NULL,?1,?2,NULL,0);",
-1, &insert_statement, NULL) != SQLITE_OK ||
sqlite3_prepare_v2(handle, "UPDATE relations SET relation_type_id=?1 "
"WHERE id=?2;", -1, &update_statement, NULL) != SQLITE_OK)
goto cleanup;
while (sqlite3_step(read_statement) == SQLITE_ROW)
{
const char *relation_id =
(const char *) sqlite3_column_text(read_statement, 0);
const char *label =
(const char *) sqlite3_column_text(read_statement, 1);
char *key = relation_type_normalize_key(label);
sqlite3_int64 type_id = 0;
if (key == NULL) goto cleanup;
sqlite3_bind_text(find_statement, 1, label, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(find_statement, 2, key, -1, SQLITE_TRANSIENT);
if (sqlite3_step(find_statement) == SQLITE_ROW)
type_id = sqlite3_column_int64(find_statement, 0);
sqlite3_reset(find_statement); sqlite3_clear_bindings(find_statement);
if (type_id == 0)
{
sqlite3_bind_text(insert_statement, 1, label, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insert_statement, 2, key, -1, SQLITE_TRANSIENT);
if (sqlite3_step(insert_statement) != SQLITE_DONE)
{ g_free(key); goto cleanup; }
type_id = sqlite3_last_insert_rowid(handle);
sqlite3_reset(insert_statement);
sqlite3_clear_bindings(insert_statement);
}
sqlite3_bind_int64(update_statement, 1, type_id);
sqlite3_bind_text(update_statement, 2, relation_id, -1, SQLITE_TRANSIENT);
if (sqlite3_step(update_statement) != SQLITE_DONE)
{ g_free(key); goto cleanup; }
sqlite3_reset(update_statement); sqlite3_clear_bindings(update_statement);
g_free(key);
}
if (sqlite3_exec(handle,
"CREATE UNIQUE INDEX IF NOT EXISTS idx_relations_canonical_type "
"ON relations(entite_source_id,entite_cible_id,relation_type_id);"
"CREATE INDEX IF NOT EXISTS idx_relations_relation_type_id "
"ON relations(relation_type_id);"
"CREATE TRIGGER IF NOT EXISTS relations_require_canonical_type_insert "
"BEFORE INSERT ON relations WHEN NEW.relation_type_id IS NULL "
"BEGIN SELECT RAISE(ABORT,'relation_type_id is required'); END;"
"CREATE TRIGGER IF NOT EXISTS relations_require_canonical_type_update "
"BEFORE UPDATE OF relation_type_id ON relations "
"WHEN NEW.relation_type_id IS NULL "
"BEGIN SELECT RAISE(ABORT,'relation_type_id is required'); END;",
NULL, NULL, NULL) != SQLITE_OK)
goto cleanup;
{
sqlite3_stmt *check = NULL;
if (sqlite3_prepare_v2(handle, "PRAGMA foreign_key_check;", -1,
&check, NULL) != SQLITE_OK) goto cleanup;
success = sqlite3_step(check) == SQLITE_DONE;
sqlite3_finalize(check);
}
cleanup:
sqlite3_finalize(update_statement); sqlite3_finalize(insert_statement);
sqlite3_finalize(find_statement); sqlite3_finalize(read_statement);
if (!success) database_set_error(database, DATABASE_ERROR_SQLITE,
sqlite3_errmsg(handle));
return success;
}
bool schema_install_v9(Database *database)
{
return schema_execute_file(database, "database/schema_v9.sql",
"la migration SQLite V9") &&
schema_v9_migrate_relation_types(database);
}
bool schema_ensure_current(
Database *database
)

View file

@ -16,6 +16,8 @@ struct RelationRecord
char *source_entity_identifier;
char *target_entity_identifier;
char *relation_type;
gint64 relation_type_identifier;
char *relation_type_code;
char *label;
char *justification;
@ -511,6 +513,32 @@ cleanup:
return relation_record;
}
RelationRecord *relation_record_new_canonical(
const char *identifier, const char *source_entity_identifier,
const char *target_entity_identifier, gint64 relation_type_identifier,
const char *relation_type_code, const char *relation_type_label,
const char *label, const char *justification, gint confidence,
const char *created_at, const char *updated_at, RelationStatus status,
GError **error)
{
RelationRecord *record = NULL;
if (relation_type_identifier <= 0)
{
g_set_error_literal(error, RELATION_RECORD_ERROR,
RELATION_RECORD_ERROR_INVALID_TYPE,
"L'identifiant du type canonique est invalide.");
return NULL;
}
record = relation_record_new(identifier, source_entity_identifier,
target_entity_identifier, relation_type_label, label, justification,
confidence, created_at, updated_at, status, error);
if (record == NULL) return NULL;
record->relation_type_identifier = relation_type_identifier;
record->relation_type_code = relation_record_duplicate_optional(
relation_type_code);
return record;
}
void relation_record_free(
RelationRecord *relation_record
)
@ -539,6 +567,7 @@ void relation_record_free(
g_free(
relation_record->relation_type
);
g_free(relation_record->relation_type_code);
g_free(
relation_record->target_entity_identifier
@ -589,10 +618,30 @@ const char *relation_record_get_relation_type(
)
{
return relation_record != NULL
? relation_record->relation_type
? (relation_record->relation_type_code != NULL
? relation_record->relation_type_code
: relation_record->relation_type)
: NULL;
}
gint64 relation_record_get_relation_type_identifier(
const RelationRecord *relation_record)
{
return relation_record != NULL ? relation_record->relation_type_identifier : 0;
}
const char *relation_record_get_relation_type_code(
const RelationRecord *relation_record)
{
return relation_record != NULL ? relation_record->relation_type_code : NULL;
}
const char *relation_record_get_relation_type_label(
const RelationRecord *relation_record)
{
return relation_record != NULL ? relation_record->relation_type : NULL;
}
const char *relation_record_get_label(
const RelationRecord *relation_record
)

View file

@ -0,0 +1,89 @@
#include "models/relation_type.h"
#include "core/relation_type_normalizer.h"
struct RelationType
{
gint64 identifier;
char *code;
char *label;
char *normalized_key;
char *description;
gboolean system_type;
};
static char *relation_type_optional(const char *value)
{
char *copy = value != NULL ? g_strdup(value) : NULL;
if (copy != NULL) g_strstrip(copy);
if (copy != NULL && copy[0] == '\0') g_clear_pointer(&copy, g_free);
return copy;
}
GQuark relation_type_error_quark(void)
{
return g_quark_from_static_string("relation-type-error-quark");
}
RelationType *relation_type_new(gint64 identifier, const char *code,
const char *label, const char *normalized_key, const char *description,
gboolean system_type, GError **error)
{
RelationType *type = NULL;
char *label_copy = NULL;
char *expected_key = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (identifier <= 0)
{
g_set_error_literal(error, RELATION_TYPE_ERROR,
RELATION_TYPE_ERROR_INVALID_IDENTIFIER,
"L'identifiant du type de relation est invalide.");
return NULL;
}
label_copy = relation_type_optional(label);
expected_key = relation_type_normalize_key(label_copy);
if (label_copy == NULL || expected_key == NULL)
{
g_set_error_literal(error, RELATION_TYPE_ERROR,
RELATION_TYPE_ERROR_INVALID_LABEL,
"Le libellé du type de relation est obligatoire.");
goto cleanup;
}
if (normalized_key == NULL || g_strcmp0(expected_key, normalized_key) != 0)
{
g_set_error_literal(error, RELATION_TYPE_ERROR,
RELATION_TYPE_ERROR_INVALID_NORMALIZED_KEY,
"La clé normalisée du type de relation est incohérente.");
goto cleanup;
}
type = g_new0(RelationType, 1);
type->identifier = identifier;
type->code = relation_type_optional(code);
type->label = label_copy;
type->normalized_key = g_strdup(normalized_key);
type->description = relation_type_optional(description);
type->system_type = system_type;
label_copy = NULL;
cleanup:
g_free(expected_key);
g_free(label_copy);
return type;
}
void relation_type_free(RelationType *type)
{
if (type == NULL) return;
g_free(type->description); g_free(type->normalized_key);
g_free(type->label); g_free(type->code); g_free(type);
}
gint64 relation_type_get_identifier(const RelationType *type)
{ return type != NULL ? type->identifier : 0; }
const char *relation_type_get_code(const RelationType *type)
{ return type != NULL ? type->code : NULL; }
const char *relation_type_get_label(const RelationType *type)
{ return type != NULL ? type->label : NULL; }
const char *relation_type_get_normalized_key(const RelationType *type)
{ return type != NULL ? type->normalized_key : NULL; }
const char *relation_type_get_description(const RelationType *type)
{ return type != NULL ? type->description : NULL; }
gboolean relation_type_is_system(const RelationType *type)
{ return type != NULL && type->system_type; }

View file

@ -8,6 +8,8 @@
#include "models/entity_record.h"
#include "models/evidence_record.h"
#include "models/relation_record.h"
#include "models/relation_type.h"
#include "core/relation_type_normalizer.h"
#include <glib.h>
@ -20,6 +22,8 @@ struct CreateRelationDialogResult
char *source_entity_identifier;
char *target_entity_identifier;
char *relation_type;
char *relation_type_code;
gint64 relation_type_identifier;
char *label;
char *justification;
GPtrArray *evidence_identifiers;
@ -56,7 +60,10 @@ typedef struct
GtkWindow *window;
GtkDropDown *target_drop_down;
GtkEntry *relation_type_entry;
GtkDropDown *relation_type_drop_down;
GtkStringList *relation_type_string_list;
GtkEntry *new_relation_type_entry;
GtkWidget *relation_type_control;
GtkEntry *label_entry;
GtkTextView *justification_text_view;
GtkSpinButton *confidence_spin_button;
@ -64,6 +71,9 @@ typedef struct
char *source_entity_identifier;
GPtrArray *target_identifiers;
GArray *relation_type_identifiers;
GPtrArray *relation_type_codes;
GPtrArray *relation_type_labels;
GPtrArray *evidence_options;
GPtrArray *evidence_check_buttons;
GtkWidget *evidence_preview_stack;
@ -78,6 +88,51 @@ typedef struct
gboolean completed;
} CreateRelationDialogState;
static void create_relation_dialog_show_error(
CreateRelationDialogState *state, const char *message);
static void create_relation_dialog_on_new_type_clicked(
GtkButton *button, gpointer user_data)
{
CreateRelationDialogState *state = user_data;
const char *label = NULL;
char *key = NULL;
(void) button;
if (state == NULL) return;
label = gtk_editable_get_text(GTK_EDITABLE(state->new_relation_type_entry));
key = relation_type_normalize_key(label);
if (key == NULL)
{
create_relation_dialog_show_error(state,
"Saisissez le libellé du nouveau type.");
return;
}
for (guint index = 0U; index < state->relation_type_labels->len; index++)
{
char *existing_key = relation_type_normalize_key(
g_ptr_array_index(state->relation_type_labels, index));
gboolean duplicate = g_strcmp0(key, existing_key) == 0;
g_free(existing_key);
if (duplicate)
{
create_relation_dialog_show_error(state,
"Un type de relation équivalent existe déjà.");
gtk_drop_down_set_selected(state->relation_type_drop_down, index);
g_free(key);
return;
}
}
gint64 identifier = 0;
gtk_string_list_append(state->relation_type_string_list, label);
g_array_append_val(state->relation_type_identifiers, identifier);
g_ptr_array_add(state->relation_type_codes, NULL);
g_ptr_array_add(state->relation_type_labels, g_strdup(label));
gtk_drop_down_set_selected(state->relation_type_drop_down,
state->relation_type_labels->len - 1U);
gtk_editable_set_text(GTK_EDITABLE(state->new_relation_type_entry), "");
g_free(key);
}
/**
* @brief Retourne une copie nettoyée ou NULL pour une valeur vide.
*/
@ -381,6 +436,10 @@ static void create_relation_dialog_state_free(
&state->target_identifiers,
g_ptr_array_unref
);
g_clear_pointer(&state->relation_type_identifiers, g_array_unref);
g_clear_pointer(&state->relation_type_codes, g_ptr_array_unref);
g_clear_pointer(&state->relation_type_labels, g_ptr_array_unref);
g_clear_object(&state->relation_type_string_list);
create_relation_dialog_clear_preview(state);
g_clear_pointer(&state->evidence_options, g_ptr_array_unref);
g_clear_pointer(&state->evidence_check_buttons, g_ptr_array_unref);
@ -533,12 +592,17 @@ static void create_relation_dialog_on_create_clicked(
selected_index
);
relation_type =
gtk_editable_get_text(
GTK_EDITABLE(
state->relation_type_entry
)
);
guint relation_type_index =
gtk_drop_down_get_selected(state->relation_type_drop_down);
if (relation_type_index == GTK_INVALID_LIST_POSITION ||
relation_type_index >= state->relation_type_labels->len)
{
create_relation_dialog_show_error(
state, "Sélectionnez un type de relation canonique.");
return;
}
relation_type = g_ptr_array_index(
state->relation_type_labels, relation_type_index);
label =
gtk_editable_get_text(
@ -617,6 +681,10 @@ static void create_relation_dialog_on_create_clicked(
create_relation_dialog_duplicate_required_text(
relation_type
);
result->relation_type_identifier = g_array_index(
state->relation_type_identifiers, gint64, relation_type_index);
result->relation_type_code = g_strdup(g_ptr_array_index(
state->relation_type_codes, relation_type_index));
result->label =
create_relation_dialog_duplicate_optional_text(
@ -810,6 +878,7 @@ static gboolean create_relation_dialog_present_internal(
const char *source_entity_identifier,
const RelationRecord *existing_relation,
const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path,
@ -823,6 +892,7 @@ static gboolean create_relation_dialog_present_internal(
GtkStringList *target_labels =
NULL;
GtkStringList *relation_type_labels = NULL;
GtkWidget *root_box =
NULL;
@ -1381,15 +1451,48 @@ static gboolean create_relation_dialog_present_internal(
TRUE
);
state->relation_type_entry =
GTK_ENTRY(
gtk_entry_new()
);
gtk_entry_set_placeholder_text(
state->relation_type_entry,
"Ex. : utilise, contrôle, appartient à"
);
relation_type_labels = gtk_string_list_new(NULL);
state->relation_type_string_list = g_object_ref(relation_type_labels);
state->relation_type_identifiers =
g_array_new(FALSE, FALSE, sizeof(gint64));
state->relation_type_codes = g_ptr_array_new_with_free_func(g_free);
state->relation_type_labels = g_ptr_array_new_with_free_func(g_free);
guint selected_relation_type_index = 0U;
for (guint type_index = 0U;
relation_types != NULL && type_index < relation_types->len;
type_index++)
{
const RelationType *type = g_ptr_array_index(relation_types, type_index);
gint64 type_identifier = relation_type_get_identifier(type);
gtk_string_list_append(relation_type_labels,
relation_type_get_label(type));
g_array_append_val(state->relation_type_identifiers, type_identifier);
g_ptr_array_add(state->relation_type_codes,
g_strdup(relation_type_get_code(type)));
g_ptr_array_add(state->relation_type_labels,
g_strdup(relation_type_get_label(type)));
if (existing_relation != NULL &&
type_identifier ==
relation_record_get_relation_type_identifier(existing_relation))
selected_relation_type_index = type_index;
}
state->relation_type_drop_down = GTK_DROP_DOWN(
gtk_drop_down_new(G_LIST_MODEL(relation_type_labels), NULL));
gtk_drop_down_set_selected(state->relation_type_drop_down,
selected_relation_type_index);
state->new_relation_type_entry = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->new_relation_type_entry,
"Nouveau type…");
GtkWidget *new_type_button = gtk_button_new_with_label("Ajouter");
state->relation_type_control = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_hexpand(GTK_WIDGET(state->relation_type_drop_down), TRUE);
gtk_box_append(GTK_BOX(state->relation_type_control),
GTK_WIDGET(state->relation_type_drop_down));
gtk_box_append(GTK_BOX(state->relation_type_control),
GTK_WIDGET(state->new_relation_type_entry));
gtk_box_append(GTK_BOX(state->relation_type_control), new_type_button);
g_signal_connect(new_type_button, "clicked",
G_CALLBACK(create_relation_dialog_on_new_type_clicked), state);
state->label_entry =
GTK_ENTRY(
@ -1424,13 +1527,9 @@ static gboolean create_relation_dialog_present_internal(
if (existing_relation != NULL)
{
const char *relation_type = relation_record_get_relation_type(
existing_relation);
const char *label = relation_record_get_label(existing_relation);
const char *justification = relation_record_get_justification(
existing_relation);
gtk_editable_set_text(GTK_EDITABLE(state->relation_type_entry),
relation_type != NULL ? relation_type : "");
gtk_editable_set_text(GTK_EDITABLE(state->label_entry),
label != NULL ? label : "");
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
@ -1568,7 +1667,7 @@ static gboolean create_relation_dialog_present_internal(
grid
),
GTK_WIDGET(
state->relation_type_entry
state->relation_type_control
),
1,
1,
@ -1809,7 +1908,7 @@ static gboolean create_relation_dialog_present_internal(
gtk_widget_grab_focus(
GTK_WIDGET(
state->relation_type_entry
state->relation_type_drop_down
)
);
@ -1822,16 +1921,19 @@ static gboolean create_relation_dialog_present_internal(
gboolean create_relation_dialog_present(GtkWindow *parent,
const char *source_entity_identifier, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const char *investigation_root_path,
CreateRelationDialogCallback callback, gpointer user_data, GError **error)
{
return create_relation_dialog_present_internal(parent,
source_entity_identifier, NULL, entities, evidence_records, NULL,
source_entity_identifier, NULL, entities, relation_types,
evidence_records, NULL,
investigation_root_path, callback, user_data, error);
}
gboolean edit_relation_dialog_present(GtkWindow *parent,
const RelationRecord *relation_record, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path,
@ -1846,7 +1948,7 @@ gboolean edit_relation_dialog_present(GtkWindow *parent,
}
return create_relation_dialog_present_internal(parent,
relation_record_get_source_entity_identifier(relation_record),
relation_record, entities, evidence_records,
relation_record, entities, relation_types, evidence_records,
selected_evidence_identifiers, investigation_root_path,
callback, user_data, error);
}
@ -1867,6 +1969,7 @@ void create_relation_dialog_result_free(
g_free(
result->label
);
g_free(result->relation_type_code);
g_free(
result->relation_type
@ -1920,6 +2023,18 @@ const char *create_relation_dialog_result_get_relation_type(
: NULL;
}
gint64 create_relation_dialog_result_get_relation_type_identifier(
const CreateRelationDialogResult *result)
{
return result != NULL ? result->relation_type_identifier : 0;
}
const char *create_relation_dialog_result_get_relation_type_code(
const CreateRelationDialogResult *result)
{
return result != NULL ? result->relation_type_code : NULL;
}
const char *create_relation_dialog_result_get_label(
const CreateRelationDialogResult *result
)

View file

@ -3551,7 +3551,7 @@ static const char *investigation_graph_view_get_relation_title(
}
relation_type =
relation_record_get_relation_type(
relation_record_get_relation_type_label(
relation_record
);

View file

@ -184,7 +184,7 @@ static gboolean sidebar_filter_relation_row(
return sidebar_text_contains_search(
relation_record_get_label(relation_record), search_text) ||
sidebar_text_contains_search(
relation_record_get_relation_type(relation_record), search_text) ||
relation_record_get_relation_type_label(relation_record), search_text) ||
sidebar_text_contains_search(
entity_record_get_value(source_entity), search_text) ||
sidebar_text_contains_search(
@ -1450,7 +1450,7 @@ void sidebar_set_graph_model(
GtkWidget *title_label = gtk_label_new(
label != NULL && label[0] != '\0'
? label
: relation_record_get_relation_type(relation_record)
: relation_record_get_relation_type_label(relation_record)
);
GtkWidget *details_label = gtk_label_new(details);

View file

@ -14,6 +14,7 @@
#include "widgets/investigation_graph_view.h"
#include <glib.h>
#include <gio/gio.h>
#define WORKSPACE_PAGE_WELCOME "welcome"
#define WORKSPACE_PAGE_NODE_INFORMATION "node-information"
@ -698,6 +699,26 @@ static void workspace_on_expand_evidence_preview(GtkButton *button,
content = gtk_video_new_for_filename(workspace->evidence_preview_path);
gtk_video_set_autoplay(GTK_VIDEO(content), FALSE);
}
else if (gtk_stack_get_visible_child(GTK_STACK(
workspace->evidence_preview_stack)) ==
workspace->evidence_preview_status)
{
GFile *file = g_file_new_for_path(workspace->evidence_preview_path);
char *uri = g_file_get_uri(file);
GError *error = NULL;
if (uri == NULL || !g_app_info_launch_default_for_uri(uri, NULL,
&error))
{
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
error != NULL ? error->message :
"Impossible douvrir ce document.");
g_clear_error(&error);
}
g_free(uri);
g_object_unref(file);
gtk_window_destroy(window);
return;
}
else
{
content = gtk_picture_new_for_filename(workspace->evidence_preview_path);
@ -708,6 +729,20 @@ static void workspace_on_expand_evidence_preview(GtkButton *button,
gtk_window_present(window);
}
/** @brief Ouvre laperçu agrandi lorsquun utilisateur clique dessus. */
static void workspace_on_evidence_preview_clicked(GtkGestureClick *gesture,
gint n_press, gdouble x, gdouble y, gpointer user_data)
{
Workspace *workspace = user_data;
(void) gesture;
(void) x;
(void) y;
if (workspace == NULL || n_press != 1 ||
workspace->evidence_preview_path == NULL)
return;
workspace_on_expand_evidence_preview(NULL, workspace);
}
/** @brief Convertit le statut d'une relation en texte utilisateur. */
static const char *workspace_get_relation_status_text(RelationStatus status)
{
@ -842,7 +877,7 @@ static void workspace_on_graph_node_selected(
"Source inconnue",
target_entity != NULL ? entity_record_get_value(target_entity) :
"Cible inconnue",
relation_record_get_relation_type(relation_record),
relation_record_get_relation_type_label(relation_record),
relation_record_get_confidence(relation_record),
workspace_get_relation_status_text(
relation_record_get_status(relation_record)),
@ -1370,7 +1405,7 @@ Workspace *workspace_new(void)
evidence_content =
gtk_box_new(
GTK_ORIENTATION_VERTICAL,
16
8
);
if (evidence_content == NULL)
@ -1384,22 +1419,22 @@ Workspace *workspace_new(void)
gtk_widget_set_margin_start(
evidence_content,
24
12
);
gtk_widget_set_margin_end(
evidence_content,
24
12
);
gtk_widget_set_margin_top(
evidence_content,
24
12
);
gtk_widget_set_margin_bottom(
evidence_content,
24
12
);
workspace->evidence_name_label =
@ -1454,8 +1489,13 @@ Workspace *workspace_new(void)
workspace->evidence_preview_video = gtk_video_new();
workspace->evidence_preview_expand_button =
gtk_button_new_with_label("Agrandir laperçu");
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 360);
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 180);
gtk_widget_set_vexpand(workspace->evidence_preview_stack, FALSE);
gtk_widget_set_hexpand(workspace->evidence_preview_stack, TRUE);
gtk_widget_set_size_request(workspace->evidence_preview_picture, -1, 180);
gtk_widget_set_size_request(workspace->evidence_preview_video, -1, 180);
gtk_widget_set_vexpand(workspace->evidence_preview_picture, FALSE);
gtk_widget_set_vexpand(workspace->evidence_preview_video, FALSE);
gtk_picture_set_can_shrink(GTK_PICTURE(
workspace->evidence_preview_picture), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(
@ -1473,6 +1513,23 @@ Workspace *workspace_new(void)
workspace->evidence_preview_picture, "image");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_video, "video");
{
GtkGesture *gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_picture,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_video,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_text,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
}
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
gtk_widget_set_halign(workspace->evidence_preview_expand_button,
@ -2962,7 +3019,8 @@ void workspace_set_evidence_preview(Workspace *workspace,
g_free(contents);
}
message = g_strdup_printf(
"Aperçu indisponible pour %s%s%s.",
"Aperçu intégré indisponible pour %s%s%s. Utilisez le bouton "
"pour ouvrir le document avec lapplication associée.",
display_name != NULL ? display_name : "ce fichier",
error != NULL ? " : " : "",
error != NULL ? error->message : "format non pris en charge");
@ -2970,6 +3028,12 @@ void workspace_set_evidence_preview(Workspace *workspace,
message);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
if (g_file_test(file_path, G_FILE_TEST_IS_REGULAR))
{
workspace->evidence_preview_path = g_strdup(file_path);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
workspace->evidence_preview_path != NULL);
}
g_free(message);
goto cleanup;
}

View file

@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;"
);
assert(strcmp(schema_version, "8") == 0);
assert(strcmp(schema_version, "9") == 0);
test_database_assert_table_exists(database, "relation_types");
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");
@ -990,7 +991,7 @@ static void test_database_migrate_v1_to_v2(void)
assert(
strcmp(
schema_version,
"8"
"9"
) == 0
);

View file

@ -491,6 +491,7 @@ static void test_graph_loader_insert_inconsistent_relation(
" entite_source_id,"
" entite_cible_id,"
" type_relation,"
" relation_type_id,"
" confiance,"
" created_at,"
" updated_at,"
@ -502,6 +503,7 @@ static void test_graph_loader_insert_inconsistent_relation(
" ?,"
" ?,"
" ?,"
" (SELECT id FROM relation_types WHERE code='uses'),"
" ?,"
" ?,"
" ?,"

View file

@ -1990,6 +1990,7 @@ static void test_relation_dao_reflexive_schema_constraint(void)
" entite_source_id,"
" entite_cible_id,"
" type_relation,"
" relation_type_id,"
" confiance,"
" created_at,"
" updated_at,"
@ -2001,6 +2002,7 @@ static void test_relation_dao_reflexive_schema_constraint(void)
" ?,"
" ?,"
" ?,"
" (SELECT id FROM relation_types WHERE code='uses'),"
" ?,"
" ?,"
" ?,"

View file

@ -0,0 +1,37 @@
#include "core/relation_type_normalizer.h"
#include <glib.h>
static void assert_same(const char *left, const char *right)
{
char *left_key = relation_type_normalize_key(left);
char *right_key = relation_type_normalize_key(right);
g_assert_cmpstr(left_key, ==, right_key);
g_free(right_key); g_free(left_key);
}
static void test_spaces(void)
{
assert_same("email", " Email ");
assert_same("Réseaux sociaux", "Réseaux Sociaux");
assert_same("Réseaux sociaux", "Réseaux\t\nSociaux");
}
static void test_unicode(void)
{
assert_same("RéSEAUX", "re\xCC\x81seaux");
assert_same("STRASSE", "Straße");
}
static void test_distinct_meanings(void)
{
char *first = relation_type_normalize_key("Envoie fausses places");
char *second = relation_type_normalize_key("Envoie des fausses places");
g_assert_cmpstr(first, !=, second);
g_free(second); g_free(first);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/relation-type-normalizer/spaces", test_spaces);
g_test_add_func("/relation-type-normalizer/unicode", test_unicode);
g_test_add_func("/relation-type-normalizer/distinct", test_distinct_meanings);
return g_test_run();
}

View file

@ -0,0 +1,53 @@
#include "core/relation_type_service.h"
#include "database/database.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_service_crud_and_merge(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-relation-types-XXXXXX", &error);
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
Database *database = NULL;
RelationTypeService *service = NULL;
gint64 email_id = 0; gint64 source_id = 0; gint64 target_id = 0;
guint64 count = 99;
g_assert_no_error(error);
g_assert_true(database_initialize(path, "Types", directory));
database = database_open(path);
g_assert_nonnull(database);
g_assert_true(database_migrate_to_latest(database));
service = relation_type_service_new(database, &error);
g_assert_no_error(error); g_assert_nonnull(service);
g_assert_true(relation_type_service_create_custom(
service, " Email ", NULL, &email_id, &error));
g_assert_cmpint(email_id, >, 0);
g_assert_false(relation_type_service_create_custom(
service, "EMAIL", NULL, NULL, &error));
g_assert_error(error, RELATION_TYPE_SERVICE_ERROR,
RELATION_TYPE_SERVICE_ERROR_DUPLICATE);
g_clear_error(&error);
g_assert_true(relation_type_service_rename(
service, email_id, "Adresse e-mail", &error));
g_assert_true(relation_type_service_count_usage(
service, email_id, &count, &error));
g_assert_cmpuint(count, ==, 0U);
g_assert_true(relation_type_service_delete(service, email_id, &error));
g_assert_true(relation_type_service_create_custom(
service, "Source temporaire", NULL, &source_id, &error));
g_assert_true(relation_type_service_create_custom(
service, "Cible temporaire", NULL, &target_id, &error));
g_assert_true(relation_type_service_merge(
service, source_id, target_id, &error));
relation_type_service_free(service); database_close(database);
g_remove(path); g_rmdir(directory); g_free(path); g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/relation-type-service/crud-merge",
test_service_crud_and_merge);
return g_test_run();
}