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 ### 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 - Glisser-déposer confirmé des extractions texte vers le graphe, avec
persistance de leur association à une entité. persistance de leur association à une entité.
- Ouverture de laperçu des pièces jointes et preuves depuis les fiches - Ouverture de laperçu des pièces jointes et preuves depuis les fiches

View file

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

View file

@ -1,7 +1,7 @@
/****************************************************************************** /******************************************************************************
* Labfy Investigation * 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) 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. */ /* Migration idempotente des positions d'entités déjà enregistrées. */
INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at) INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at)
SELECT entity_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 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. 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 # 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_v7(Database *database);
bool schema_install_v8(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. * @brief Garantit la présence des extensions du schéma courant V2.

View file

@ -99,6 +99,21 @@ RelationRecord *relation_record_new(
GError **error 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. * @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 char *relation_record_get_relation_type(
const RelationRecord *relation_record 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. * @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, GtkWindow *parent,
const char *source_entity_identifier, const char *source_entity_identifier,
const GPtrArray *entities, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const GPtrArray *evidence_records,
const char *investigation_root_path, const char *investigation_root_path,
CreateRelationDialogCallback callback, CreateRelationDialogCallback callback,
@ -109,6 +110,7 @@ gboolean edit_relation_dialog_present(
GtkWindow *parent, GtkWindow *parent,
const RelationRecord *relation_record, const RelationRecord *relation_record,
const GPtrArray *entities, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers, const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path, 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 char *create_relation_dialog_result_get_relation_type(
const CreateRelationDialogResult *result 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. * @brief Retourne le libellé facultatif.

View file

@ -33,6 +33,9 @@
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include "dao/evidence_type_dao.h" #include "dao/evidence_type_dao.h"
#include "dao/graph_node_position_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_import_dialog.h"
#include "views/evidence_metadata_dialog.h" #include "views/evidence_metadata_dialog.h"
#include "views/create_relation_dialog.h" #include "views/create_relation_dialog.h"
@ -5593,6 +5596,7 @@ static void application_on_create_relation_completed(
gboolean relation_created = gboolean relation_created =
FALSE; FALSE;
gint64 relation_type_identifier = 0;
if (application == NULL || if (application == NULL ||
application->main_window == NULL) application->main_window == NULL)
@ -5707,11 +5711,33 @@ static void application_on_create_relation_completed(
goto cleanup; 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 =
relation_record_new( relation_record_new_canonical(
relation_identifier, relation_identifier,
source_identifier, source_identifier,
target_identifier, target_identifier,
relation_type_identifier,
create_relation_dialog_result_get_relation_type_code(result),
relation_type, relation_type,
create_relation_dialog_result_get_label( create_relation_dialog_result_get_label(
result result
@ -6240,6 +6266,7 @@ static void application_on_edit_relation_completed(
char *timestamp = NULL; char *timestamp = NULL;
GError *error = NULL; GError *error = NULL;
gboolean transaction_active = FALSE; gboolean transaction_active = FALSE;
gint64 relation_type_identifier = 0;
if (result == NULL || application == NULL || application->session == NULL) if (result == NULL || application == NULL || application->session == NULL)
goto cleanup; goto cleanup;
@ -6254,10 +6281,28 @@ static void application_on_edit_relation_completed(
now = g_date_time_new_now_utc(); now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now, timestamp = now != NULL ? g_date_time_format(now,
"%Y-%m-%dT%H:%M:%SZ") : NULL; "%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_identifier(existing),
relation_record_get_source_entity_identifier(existing), relation_record_get_source_entity_identifier(existing),
relation_record_get_target_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_relation_type(result),
create_relation_dialog_result_get_label(result), create_relation_dialog_result_get_label(result),
create_relation_dialog_result_get_justification(result), create_relation_dialog_result_get_justification(result),
@ -6348,6 +6393,8 @@ static void application_on_edit_relation_requested(
GPtrArray *entities = NULL; GPtrArray *entities = NULL;
GPtrArray *evidences = NULL; GPtrArray *evidences = NULL;
GPtrArray *selected_evidences = NULL; GPtrArray *selected_evidences = NULL;
GPtrArray *relation_types = NULL;
RelationTypeDao *relation_type_dao = NULL;
const InvestigationProject *project = NULL; const InvestigationProject *project = NULL;
ApplicationEditRelationContext *context = NULL; ApplicationEditRelationContext *context = NULL;
GError *error = NULL; GError *error = NULL;
@ -6359,6 +6406,7 @@ static void application_on_edit_relation_requested(
entity_dao = entity_dao_new(database, &error); entity_dao = entity_dao_new(database, &error);
evidence_dao = evidence_dao_new(database, &error); evidence_dao = evidence_dao_new(database, &error);
relation_evidence_dao = relation_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); project = investigation_session_get_project(application->session);
if (relation_dao != NULL) if (relation_dao != NULL)
relation = relation_dao_find_by_identifier(relation_dao, 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 = if (relation_evidence_dao != NULL) selected_evidences =
relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao, relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao,
relation_identifier, &error); 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 || 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 = g_new0(ApplicationEditRelationContext, 1);
context->application = application; context->application = application;
context->relation_identifier = g_strdup(relation_identifier); context->relation_identifier = g_strdup(relation_identifier);
if (context->relation_identifier == NULL || !edit_relation_dialog_present( if (context->relation_identifier == NULL || !edit_relation_dialog_present(
main_window_get_window(application->main_window), relation, entities, main_window_get_window(application->main_window), relation, entities,
relation_types,
evidences, selected_evidences, evidences, selected_evidences,
investigation_project_get_root_path(project), investigation_project_get_root_path(project),
application_on_edit_relation_completed, context, &error)) application_on_edit_relation_completed, context, &error))
@ -6396,6 +6448,8 @@ cleanup:
entity_dao_free(entity_dao); entity_dao_free(entity_dao);
evidence_dao_free(evidence_dao); evidence_dao_free(evidence_dao);
relation_evidence_dao_free(relation_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); relation_dao_free(relation_dao);
} }
@ -6462,11 +6516,13 @@ static void application_on_add_relation_requested(
NULL; NULL;
EvidenceDao *evidence_dao = NULL; EvidenceDao *evidence_dao = NULL;
RelationTypeDao *relation_type_dao = NULL;
const InvestigationProject *project = NULL; const InvestigationProject *project = NULL;
GPtrArray *entities = GPtrArray *entities =
NULL; NULL;
GPtrArray *evidences = NULL; GPtrArray *evidences = NULL;
GPtrArray *relation_types = NULL;
GError *error = GError *error =
NULL; NULL;
@ -6533,11 +6589,17 @@ static void application_on_add_relation_requested(
evidences = evidence_dao_list_all(evidence_dao, &error); evidences = evidence_dao_list_all(evidence_dao, &error);
evidence_dao_free(evidence_dao); 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_free(
entity_dao entity_dao
); );
if (entities == NULL || evidences == NULL || project == NULL) if (entities == NULL || evidences == NULL || relation_types == NULL ||
project == NULL)
{ {
application_present_error( application_present_error(
application, application,
@ -6553,6 +6615,7 @@ static void application_on_add_relation_requested(
g_clear_pointer(&entities, g_ptr_array_unref); g_clear_pointer(&entities, g_ptr_array_unref);
g_clear_pointer(&evidences, g_ptr_array_unref); g_clear_pointer(&evidences, g_ptr_array_unref);
g_clear_pointer(&relation_types, g_ptr_array_unref);
return; return;
} }
@ -6563,6 +6626,7 @@ static void application_on_add_relation_requested(
), ),
source_entity_identifier, source_entity_identifier,
entities, entities,
relation_types,
evidences, evidences,
investigation_project_get_root_path(project), investigation_project_get_root_path(project),
application_on_create_relation_completed, application_on_create_relation_completed,
@ -6587,6 +6651,7 @@ static void application_on_add_relation_requested(
entities entities
); );
g_ptr_array_unref(evidences); 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( osint_dns_integration_build_relation_key(
relation_record_get_source_entity_identifier(relation), relation_record_get_source_entity_identifier(relation),
relation_record_get_target_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)) 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/error.h"
#include "database/statement.h" #include "database/statement.h"
#include "dao/relation_type_dao.h"
#include "core/relation_type_normalizer.h"
#include <glib.h> #include <glib.h>
#include <stdint.h> #include <stdint.h>
@ -31,6 +33,7 @@ static const char *const relation_dao_insert_sql =
" entite_source_id," " entite_source_id,"
" entite_cible_id," " entite_cible_id,"
" type_relation," " type_relation,"
" relation_type_id,"
" label," " label,"
" justification," " justification,"
" confiance," " confiance,"
@ -49,14 +52,15 @@ static const char *const relation_dao_insert_sql =
" ?," " ?,"
" ?," " ?,"
" ?," " ?,"
" ?,"
" ?" " ?"
");"; ");";
/** @brief Requête de modification des attributs d'une relation. */ /** @brief Requête de modification des attributs d'une relation. */
static const char *const relation_dao_update_sql = static const char *const relation_dao_update_sql =
"UPDATE relations SET " "UPDATE relations SET "
"type_relation = ?, label = ?, justification = ?, confiance = ?, " "type_relation = ?, relation_type_id = ?, label = ?, justification = ?, "
"updated_at = ?, status = ? WHERE id = ?;"; "confiance = ?, updated_at = ?, status = ? WHERE id = ?;";
/** @brief Requête de suppression d'une relation. */ /** @brief Requête de suppression d'une relation. */
static const char *const relation_dao_delete_sql = 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 = static const char *const relation_dao_find_by_identifier_sql =
"SELECT " "SELECT "
" id," " r.id,"
" entite_source_id," " r.entite_source_id,"
" entite_cible_id," " r.entite_cible_id,"
" type_relation," " rt.id,"
" label," " rt.code,"
" justification," " rt.label,"
" confiance," " r.label,"
" created_at," " r.justification,"
" updated_at," " r.confiance,"
" status " " r.created_at,"
"FROM relations " " r.updated_at,"
"WHERE id = ?;"; " 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. * @brief Requête de chargement de toutes les relations.
*/ */
static const char *const relation_dao_list_all_sql = static const char *const relation_dao_list_all_sql =
"SELECT " "SELECT "
" id," " r.id,"
" entite_source_id," " r.entite_source_id,"
" entite_cible_id," " r.entite_cible_id,"
" type_relation," " rt.id,"
" label," " rt.code,"
" justification," " rt.label,"
" confiance," " r.label,"
" created_at," " r.justification,"
" updated_at," " r.confiance,"
" status " " r.created_at,"
"FROM relations " " r.updated_at,"
" r.status "
"FROM relations r JOIN relation_types rt ON rt.id=r.relation_type_id "
"ORDER BY " "ORDER BY "
" created_at ASC," " r.created_at ASC,"
" id ASC;"; " r.id ASC;";
/** /**
* @brief Requête de comptage. * @brief Requête de comptage.
@ -133,7 +141,7 @@ static const char *const relation_dao_duplicate_exists_sql =
"FROM relations " "FROM relations "
"WHERE entite_source_id = ? " "WHERE entite_source_id = ? "
"AND entite_cible_id = ? " "AND entite_cible_id = ? "
"AND type_relation = ? " "AND relation_type_id = ? "
"LIMIT 1;"; "LIMIT 1;";
/** /**
@ -492,7 +500,7 @@ static gboolean relation_dao_duplicate_exists(
RelationDao *relation_dao, RelationDao *relation_dao,
const char *source_entity_identifier, const char *source_entity_identifier,
const char *target_entity_identifier, const char *target_entity_identifier,
const char *relation_type, gint64 relation_type_identifier,
gboolean *out_exists, gboolean *out_exists,
GError **error GError **error
) )
@ -510,7 +518,7 @@ static gboolean relation_dao_duplicate_exists(
relation_dao->database == NULL || relation_dao->database == NULL ||
source_entity_identifier == NULL || source_entity_identifier == NULL ||
target_entity_identifier == NULL || target_entity_identifier == NULL ||
relation_type == NULL || relation_type_identifier <= 0 ||
out_exists == NULL) out_exists == NULL)
{ {
relation_dao_set_error_literal( relation_dao_set_error_literal(
@ -553,10 +561,10 @@ static gboolean relation_dao_duplicate_exists(
2, 2,
target_entity_identifier target_entity_identifier
) || ) ||
!database_statement_bind_text( !database_statement_bind_int64(
statement, statement,
3, 3,
relation_type relation_type_identifier
)) ))
{ {
relation_dao_set_database_error( relation_dao_set_database_error(
@ -626,6 +634,8 @@ static RelationRecord *relation_dao_read_current_record(
char *relation_type = char *relation_type =
NULL; NULL;
char *relation_type_code = NULL;
int64_t relation_type_identifier = 0;
char *label = char *label =
NULL; NULL;
@ -679,39 +689,49 @@ static RelationRecord *relation_dao_read_current_record(
2, 2,
&target_entity_identifier &target_entity_identifier
) || ) ||
!database_statement_column_text( !database_statement_column_int64(
statement, statement,
3, 3,
&relation_type &relation_type_identifier
) || ) ||
!database_statement_column_text( !database_statement_column_text(
statement, statement,
4, 4,
&label &relation_type_code
) || ) ||
!database_statement_column_text( !database_statement_column_text(
statement, statement,
5, 5,
&justification &relation_type
) || ) ||
!database_statement_column_int64( !database_statement_column_text(
statement, statement,
6, 6,
&confidence_value &label
) || ) ||
!database_statement_column_text( !database_statement_column_text(
statement, statement,
7, 7,
&created_at &justification
) || ) ||
!database_statement_column_text( !database_statement_column_int64(
statement, statement,
8, 8,
&updated_at &confidence_value
) || ) ||
!database_statement_column_text( !database_statement_column_text(
statement, statement,
9, 9,
&created_at
) ||
!database_statement_column_text(
statement,
10,
&updated_at
) ||
!database_statement_column_text(
statement,
11,
&status_text &status_text
)) ))
{ {
@ -751,10 +771,12 @@ static RelationRecord *relation_dao_read_current_record(
} }
relation_record = relation_record =
relation_record_new( relation_record_new_canonical(
identifier, identifier,
source_entity_identifier, source_entity_identifier,
target_entity_identifier, target_entity_identifier,
(gint64) relation_type_identifier,
relation_type_code,
relation_type, relation_type,
label, label,
justification, justification,
@ -816,6 +838,7 @@ cleanup:
g_free( g_free(
relation_type relation_type
); );
g_free(relation_type_code);
g_free( g_free(
target_entity_identifier 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( gboolean relation_dao_insert(
RelationDao *relation_dao, RelationDao *relation_dao,
const RelationRecord *relation_record, const RelationRecord *relation_record,
@ -962,6 +1015,9 @@ gboolean relation_dao_insert(
gboolean success = gboolean success =
FALSE; FALSE;
RelationType *canonical_type = NULL;
gint64 canonical_type_identifier = 0;
const char *canonical_type_label = NULL;
g_return_val_if_fail( g_return_val_if_fail(
error == NULL || *error == NULL, error == NULL || *error == NULL,
@ -1161,16 +1217,24 @@ gboolean relation_dao_insert(
return FALSE; 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( if (!relation_dao_duplicate_exists(
relation_dao, relation_dao,
source_entity_identifier, source_entity_identifier,
target_entity_identifier, target_entity_identifier,
relation_type, canonical_type_identifier,
&value_exists, &value_exists,
error error
)) ))
{ {
return FALSE; goto cleanup;
} }
if (value_exists) if (value_exists)
@ -1181,7 +1245,7 @@ gboolean relation_dao_insert(
"Cette relation orientée existe déjà pour ce type." "Cette relation orientée existe déjà pour ce type."
); );
return FALSE; goto cleanup;
} }
statement = statement =
@ -1220,36 +1284,41 @@ gboolean relation_dao_insert(
!database_statement_bind_text( !database_statement_bind_text(
statement, statement,
4, 4,
relation_type canonical_type_label
) || ) ||
!relation_dao_bind_optional_text( !database_statement_bind_int64(
statement, statement,
5, 5,
label canonical_type_identifier
) || ) ||
!relation_dao_bind_optional_text( !relation_dao_bind_optional_text(
statement, statement,
6, 6,
label
) ||
!relation_dao_bind_optional_text(
statement,
7,
justification justification
) || ) ||
!database_statement_bind_int64( !database_statement_bind_int64(
statement, statement,
7, 8,
(int64_t) confidence (int64_t) confidence
) || ) ||
!database_statement_bind_text( !database_statement_bind_text(
statement, statement,
8, 9,
created_at created_at
) || ) ||
!database_statement_bind_text( !database_statement_bind_text(
statement, statement,
9, 10,
updated_at updated_at
) || ) ||
!database_statement_bind_text( !database_statement_bind_text(
statement, statement,
10, 11,
status_text status_text
)) ))
{ {
@ -1304,6 +1373,7 @@ cleanup:
database_statement_finalize( database_statement_finalize(
statement statement
); );
relation_type_free(canonical_type);
return success; return success;
} }
@ -1314,6 +1384,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
DatabaseStatement *statement = NULL; DatabaseStatement *statement = NULL;
const char *status_text = NULL; const char *status_text = NULL;
gboolean success = FALSE; gboolean success = FALSE;
RelationType *canonical_type = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (relation_dao == NULL || relation_record == NULL) 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( status_text = relation_dao_status_to_text(
relation_record_get_status(relation_record)); 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, statement = database_statement_prepare(relation_dao->database,
relation_dao_update_sql); relation_dao_update_sql);
if (statement == NULL) if (statement == NULL)
@ -1332,21 +1406,25 @@ gboolean relation_dao_update(RelationDao *relation_dao,
relation_dao_set_database_error(relation_dao, error, relation_dao_set_database_error(relation_dao, error,
RELATION_DAO_ERROR_PREPARE, RELATION_DAO_ERROR_PREPARE,
"Impossible de préparer la modification de la relation"); "Impossible de préparer la modification de la relation");
return FALSE; goto cleanup;
} }
if (!database_statement_bind_text(statement, 1, if (!database_statement_bind_text(statement, 1,
relation_record_get_relation_type(relation_record)) || relation_type_get_code(canonical_type) != NULL
!relation_dao_bind_optional_text(statement, 2, ? relation_type_get_code(canonical_type)
relation_record_get_label(relation_record)) || : 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_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)) || relation_record_get_justification(relation_record)) ||
!database_statement_bind_int64(statement, 4, !database_statement_bind_int64(statement, 5,
relation_record_get_confidence(relation_record)) || 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)) || relation_record_get_updated_at(relation_record)) ||
status_text == NULL || status_text == NULL ||
!database_statement_bind_text(statement, 6, status_text) || !database_statement_bind_text(statement, 7, status_text) ||
!database_statement_bind_text(statement, 7, !database_statement_bind_text(statement, 8,
relation_record_get_identifier(relation_record))) relation_record_get_identifier(relation_record)))
{ {
relation_dao_set_database_error(relation_dao, error, relation_dao_set_database_error(relation_dao, error,
@ -1364,6 +1442,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
success = TRUE; success = TRUE;
cleanup: cleanup:
database_statement_finalize(statement); database_statement_finalize(statement);
relation_type_free(canonical_type);
return success; 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. * @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. * @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. * @brief Nom de l'application enregistré dans les métadonnées.
@ -782,6 +782,21 @@ rollback:
return false; 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. * @brief Garantit atomiquement la présence des extensions du schéma courant.
*/ */
@ -1003,6 +1018,10 @@ bool database_migrate_to_latest(
return false; return false;
schema_version = 8; schema_version = 8;
break; break;
case 8:
if (!database_migrate_v8_to_v9(database)) return false;
schema_version = 9;
break;
default: default:
database_set_error( database_set_error(
@ -1135,6 +1154,13 @@ bool database_initialize(
goto rollback; goto rollback;
} }
if (!schema_install_v7(database) ||
!schema_install_v8(database) ||
!schema_install_v9(database))
{
goto rollback;
}
if (!schema_ensure_current( if (!schema_ensure_current(
database database
)) ))

View file

@ -6,6 +6,7 @@
#include "database/schema.h" #include "database/schema.h"
#include "database_internal.h" #include "database_internal.h"
#include "core/relation_type_normalizer.h"
#include <glib.h> #include <glib.h>
#include <sqlite3.h> #include <sqlite3.h>
@ -239,6 +240,113 @@ bool schema_install_v8(Database *database)
"la migration SQLite V8"); "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( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -16,6 +16,8 @@ struct RelationRecord
char *source_entity_identifier; char *source_entity_identifier;
char *target_entity_identifier; char *target_entity_identifier;
char *relation_type; char *relation_type;
gint64 relation_type_identifier;
char *relation_type_code;
char *label; char *label;
char *justification; char *justification;
@ -511,6 +513,32 @@ cleanup:
return relation_record; 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( void relation_record_free(
RelationRecord *relation_record RelationRecord *relation_record
) )
@ -539,6 +567,7 @@ void relation_record_free(
g_free( g_free(
relation_record->relation_type relation_record->relation_type
); );
g_free(relation_record->relation_type_code);
g_free( g_free(
relation_record->target_entity_identifier relation_record->target_entity_identifier
@ -589,10 +618,30 @@ const char *relation_record_get_relation_type(
) )
{ {
return relation_record != NULL return relation_record != NULL
? relation_record->relation_type ? (relation_record->relation_type_code != NULL
? relation_record->relation_type_code
: relation_record->relation_type)
: NULL; : 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 char *relation_record_get_label(
const RelationRecord *relation_record 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/entity_record.h"
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include "models/relation_record.h" #include "models/relation_record.h"
#include "models/relation_type.h"
#include "core/relation_type_normalizer.h"
#include <glib.h> #include <glib.h>
@ -20,6 +22,8 @@ struct CreateRelationDialogResult
char *source_entity_identifier; char *source_entity_identifier;
char *target_entity_identifier; char *target_entity_identifier;
char *relation_type; char *relation_type;
char *relation_type_code;
gint64 relation_type_identifier;
char *label; char *label;
char *justification; char *justification;
GPtrArray *evidence_identifiers; GPtrArray *evidence_identifiers;
@ -56,7 +60,10 @@ typedef struct
GtkWindow *window; GtkWindow *window;
GtkDropDown *target_drop_down; 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; GtkEntry *label_entry;
GtkTextView *justification_text_view; GtkTextView *justification_text_view;
GtkSpinButton *confidence_spin_button; GtkSpinButton *confidence_spin_button;
@ -64,6 +71,9 @@ typedef struct
char *source_entity_identifier; char *source_entity_identifier;
GPtrArray *target_identifiers; GPtrArray *target_identifiers;
GArray *relation_type_identifiers;
GPtrArray *relation_type_codes;
GPtrArray *relation_type_labels;
GPtrArray *evidence_options; GPtrArray *evidence_options;
GPtrArray *evidence_check_buttons; GPtrArray *evidence_check_buttons;
GtkWidget *evidence_preview_stack; GtkWidget *evidence_preview_stack;
@ -78,6 +88,51 @@ typedef struct
gboolean completed; gboolean completed;
} CreateRelationDialogState; } 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. * @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, &state->target_identifiers,
g_ptr_array_unref 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); create_relation_dialog_clear_preview(state);
g_clear_pointer(&state->evidence_options, g_ptr_array_unref); g_clear_pointer(&state->evidence_options, g_ptr_array_unref);
g_clear_pointer(&state->evidence_check_buttons, 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 selected_index
); );
relation_type = guint relation_type_index =
gtk_editable_get_text( gtk_drop_down_get_selected(state->relation_type_drop_down);
GTK_EDITABLE( if (relation_type_index == GTK_INVALID_LIST_POSITION ||
state->relation_type_entry 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 = label =
gtk_editable_get_text( gtk_editable_get_text(
@ -617,6 +681,10 @@ static void create_relation_dialog_on_create_clicked(
create_relation_dialog_duplicate_required_text( create_relation_dialog_duplicate_required_text(
relation_type 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 = result->label =
create_relation_dialog_duplicate_optional_text( create_relation_dialog_duplicate_optional_text(
@ -810,6 +878,7 @@ static gboolean create_relation_dialog_present_internal(
const char *source_entity_identifier, const char *source_entity_identifier,
const RelationRecord *existing_relation, const RelationRecord *existing_relation,
const GPtrArray *entities, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers, const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path, const char *investigation_root_path,
@ -823,6 +892,7 @@ static gboolean create_relation_dialog_present_internal(
GtkStringList *target_labels = GtkStringList *target_labels =
NULL; NULL;
GtkStringList *relation_type_labels = NULL;
GtkWidget *root_box = GtkWidget *root_box =
NULL; NULL;
@ -1381,15 +1451,48 @@ static gboolean create_relation_dialog_present_internal(
TRUE TRUE
); );
state->relation_type_entry = relation_type_labels = gtk_string_list_new(NULL);
GTK_ENTRY( state->relation_type_string_list = g_object_ref(relation_type_labels);
gtk_entry_new() state->relation_type_identifiers =
); g_array_new(FALSE, FALSE, sizeof(gint64));
state->relation_type_codes = g_ptr_array_new_with_free_func(g_free);
gtk_entry_set_placeholder_text( state->relation_type_labels = g_ptr_array_new_with_free_func(g_free);
state->relation_type_entry, guint selected_relation_type_index = 0U;
"Ex. : utilise, contrôle, appartient à" 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 = state->label_entry =
GTK_ENTRY( GTK_ENTRY(
@ -1424,13 +1527,9 @@ static gboolean create_relation_dialog_present_internal(
if (existing_relation != NULL) 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 *label = relation_record_get_label(existing_relation);
const char *justification = relation_record_get_justification( const char *justification = relation_record_get_justification(
existing_relation); 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), gtk_editable_set_text(GTK_EDITABLE(state->label_entry),
label != NULL ? label : ""); label != NULL ? label : "");
gtk_text_buffer_set_text(gtk_text_view_get_buffer( gtk_text_buffer_set_text(gtk_text_view_get_buffer(
@ -1568,7 +1667,7 @@ static gboolean create_relation_dialog_present_internal(
grid grid
), ),
GTK_WIDGET( GTK_WIDGET(
state->relation_type_entry state->relation_type_control
), ),
1, 1,
1, 1,
@ -1809,7 +1908,7 @@ static gboolean create_relation_dialog_present_internal(
gtk_widget_grab_focus( gtk_widget_grab_focus(
GTK_WIDGET( 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, gboolean create_relation_dialog_present(GtkWindow *parent,
const char *source_entity_identifier, const GPtrArray *entities, const char *source_entity_identifier, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const char *investigation_root_path, const GPtrArray *evidence_records, const char *investigation_root_path,
CreateRelationDialogCallback callback, gpointer user_data, GError **error) CreateRelationDialogCallback callback, gpointer user_data, GError **error)
{ {
return create_relation_dialog_present_internal(parent, 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); investigation_root_path, callback, user_data, error);
} }
gboolean edit_relation_dialog_present(GtkWindow *parent, gboolean edit_relation_dialog_present(GtkWindow *parent,
const RelationRecord *relation_record, const GPtrArray *entities, const RelationRecord *relation_record, const GPtrArray *entities,
const GPtrArray *relation_types,
const GPtrArray *evidence_records, const GPtrArray *evidence_records,
const GPtrArray *selected_evidence_identifiers, const GPtrArray *selected_evidence_identifiers,
const char *investigation_root_path, const char *investigation_root_path,
@ -1846,7 +1948,7 @@ gboolean edit_relation_dialog_present(GtkWindow *parent,
} }
return create_relation_dialog_present_internal(parent, return create_relation_dialog_present_internal(parent,
relation_record_get_source_entity_identifier(relation_record), 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, selected_evidence_identifiers, investigation_root_path,
callback, user_data, error); callback, user_data, error);
} }
@ -1867,6 +1969,7 @@ void create_relation_dialog_result_free(
g_free( g_free(
result->label result->label
); );
g_free(result->relation_type_code);
g_free( g_free(
result->relation_type result->relation_type
@ -1920,6 +2023,18 @@ const char *create_relation_dialog_result_get_relation_type(
: NULL; : 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 char *create_relation_dialog_result_get_label(
const CreateRelationDialogResult *result const CreateRelationDialogResult *result
) )

View file

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

View file

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

View file

@ -14,6 +14,7 @@
#include "widgets/investigation_graph_view.h" #include "widgets/investigation_graph_view.h"
#include <glib.h> #include <glib.h>
#include <gio/gio.h>
#define WORKSPACE_PAGE_WELCOME "welcome" #define WORKSPACE_PAGE_WELCOME "welcome"
#define WORKSPACE_PAGE_NODE_INFORMATION "node-information" #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); content = gtk_video_new_for_filename(workspace->evidence_preview_path);
gtk_video_set_autoplay(GTK_VIDEO(content), FALSE); 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 else
{ {
content = gtk_picture_new_for_filename(workspace->evidence_preview_path); 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); 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. */ /** @brief Convertit le statut d'une relation en texte utilisateur. */
static const char *workspace_get_relation_status_text(RelationStatus status) static const char *workspace_get_relation_status_text(RelationStatus status)
{ {
@ -842,7 +877,7 @@ static void workspace_on_graph_node_selected(
"Source inconnue", "Source inconnue",
target_entity != NULL ? entity_record_get_value(target_entity) : target_entity != NULL ? entity_record_get_value(target_entity) :
"Cible inconnue", "Cible inconnue",
relation_record_get_relation_type(relation_record), relation_record_get_relation_type_label(relation_record),
relation_record_get_confidence(relation_record), relation_record_get_confidence(relation_record),
workspace_get_relation_status_text( workspace_get_relation_status_text(
relation_record_get_status(relation_record)), relation_record_get_status(relation_record)),
@ -1370,7 +1405,7 @@ Workspace *workspace_new(void)
evidence_content = evidence_content =
gtk_box_new( gtk_box_new(
GTK_ORIENTATION_VERTICAL, GTK_ORIENTATION_VERTICAL,
16 8
); );
if (evidence_content == NULL) if (evidence_content == NULL)
@ -1384,22 +1419,22 @@ Workspace *workspace_new(void)
gtk_widget_set_margin_start( gtk_widget_set_margin_start(
evidence_content, evidence_content,
24 12
); );
gtk_widget_set_margin_end( gtk_widget_set_margin_end(
evidence_content, evidence_content,
24 12
); );
gtk_widget_set_margin_top( gtk_widget_set_margin_top(
evidence_content, evidence_content,
24 12
); );
gtk_widget_set_margin_bottom( gtk_widget_set_margin_bottom(
evidence_content, evidence_content,
24 12
); );
workspace->evidence_name_label = workspace->evidence_name_label =
@ -1454,8 +1489,13 @@ Workspace *workspace_new(void)
workspace->evidence_preview_video = gtk_video_new(); workspace->evidence_preview_video = gtk_video_new();
workspace->evidence_preview_expand_button = workspace->evidence_preview_expand_button =
gtk_button_new_with_label("Agrandir laperçu"); 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_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( gtk_picture_set_can_shrink(GTK_PICTURE(
workspace->evidence_preview_picture), TRUE); workspace->evidence_preview_picture), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE( gtk_picture_set_content_fit(GTK_PICTURE(
@ -1473,6 +1513,23 @@ Workspace *workspace_new(void)
workspace->evidence_preview_picture, "image"); workspace->evidence_preview_picture, "image");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_video, "video"); 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( gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status"); workspace->evidence_preview_stack), "status");
gtk_widget_set_halign(workspace->evidence_preview_expand_button, gtk_widget_set_halign(workspace->evidence_preview_expand_button,
@ -2962,7 +3019,8 @@ void workspace_set_evidence_preview(Workspace *workspace,
g_free(contents); g_free(contents);
} }
message = g_strdup_printf( 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", display_name != NULL ? display_name : "ce fichier",
error != NULL ? " : " : "", error != NULL ? " : " : "",
error != NULL ? error->message : "format non pris en charge"); error != NULL ? error->message : "format non pris en charge");
@ -2970,6 +3028,12 @@ void workspace_set_evidence_preview(Workspace *workspace,
message); message);
gtk_stack_set_visible_child_name(GTK_STACK( gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status"); 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); g_free(message);
goto cleanup; goto cleanup;
} }

View file

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

View file

@ -491,6 +491,7 @@ static void test_graph_loader_insert_inconsistent_relation(
" entite_source_id," " entite_source_id,"
" entite_cible_id," " entite_cible_id,"
" type_relation," " type_relation,"
" relation_type_id,"
" confiance," " confiance,"
" created_at," " created_at,"
" updated_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_source_id,"
" entite_cible_id," " entite_cible_id,"
" type_relation," " type_relation,"
" relation_type_id,"
" confiance," " confiance,"
" created_at," " created_at,"
" updated_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();
}