feat(relations): centraliser les types et améliorer les aperçus
This commit is contained in:
parent
569a99aa5f
commit
8bc3b43d63
31 changed files with 1472 additions and 109 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Référentiel persistant et normalisé des types de relations, avec codes
|
||||
système stables, types personnalisés, renommage et fusion transactionnelle.
|
||||
- Sélecteur canonique dans les formulaires de création et de modification des
|
||||
relations.
|
||||
- Glisser-déposer confirmé des extractions texte vers le graphe, avec
|
||||
persistance de leur association à une entité.
|
||||
- Ouverture de l’aperçu des pièces jointes et preuves depuis les fiches
|
||||
|
|
|
|||
45
Makefile
45
Makefile
|
|
@ -23,7 +23,8 @@ TEST_CFLAGS = -std=c17 \
|
|||
-Iinclude \
|
||||
$(shell $(PKG_CONFIG) --cflags glib-2.0 gio-2.0)
|
||||
|
||||
TEST_LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0)
|
||||
TEST_LDFLAGS = src/core/relation_type_normalizer.c \
|
||||
$(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0)
|
||||
|
||||
EVIDENCE_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
EVIDENCE_TYPE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
|
|
@ -129,9 +130,29 @@ TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
|||
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
||||
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
||||
TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service
|
||||
TEST_RELATION_TYPE_NORMALIZER := tests/test_relation_type_normalizer
|
||||
TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TEST_RELATION_TYPE_NORMALIZER): \
|
||||
tests/test_relation_type_normalizer.c \
|
||||
src/core/relation_type_normalizer.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
|
||||
$(shell $(PKG_CONFIG) --libs glib-2.0)
|
||||
|
||||
$(TEST_RELATION_TYPE_SERVICE): \
|
||||
tests/test_relation_type_service.c \
|
||||
src/core/relation_type_service.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/database/database.c \
|
||||
src/database/schema.c \
|
||||
src/database/statement.c \
|
||||
src/database/transaction.c \
|
||||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TARGET): $(OBJ)
|
||||
$(CC) $(OBJ) -o $@ $(LDFLAGS)
|
||||
|
||||
|
|
@ -480,6 +501,8 @@ $(TEST_RELATION_RECORD): \
|
|||
$(TEST_RELATION_DAO): \
|
||||
tests/test_relation_dao.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/models/relation_record.c \
|
||||
src/models/entity_record.c \
|
||||
|
|
@ -494,6 +517,8 @@ $(TEST_RELATION_EVIDENCE_DAO): \
|
|||
tests/test_relation_evidence_dao.c \
|
||||
src/dao/relation_evidence_dao.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/dao/evidence_dao.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/models/relation_record.c \
|
||||
|
|
@ -510,6 +535,8 @@ $(TEST_RELATION_SERVICE): \
|
|||
tests/test_relation_service.c \
|
||||
src/core/relation_service.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/dao/relation_evidence_dao.c \
|
||||
src/dao/evidence_dao.c \
|
||||
src/dao/entity_dao.c \
|
||||
|
|
@ -537,6 +564,8 @@ $(TEST_INVESTIGATION_GRAPH_LOADER): \
|
|||
src/core/investigation_graph_loader.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/models/investigation_graph_model.c \
|
||||
src/models/entity_record.c \
|
||||
src/models/relation_record.c \
|
||||
|
|
@ -596,6 +625,8 @@ $(TEST_OSINT_DNS_INTEGRATION): \
|
|||
src/models/osint_execution_record.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/dao/osint_execution_dao.c \
|
||||
src/database/database.c \
|
||||
src/database/schema.c \
|
||||
|
|
@ -691,6 +722,8 @@ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
|
|||
src/models/graph_node_position.c \
|
||||
src/dao/graph_node_position_dao.c \
|
||||
src/dao/relation_dao.c \
|
||||
src/dao/relation_type_dao.c \
|
||||
src/models/relation_type.c \
|
||||
src/models/investigation_graph_model.c \
|
||||
src/models/entity_record.c \
|
||||
src/models/relation_record.c \
|
||||
|
|
@ -764,7 +797,9 @@ test: \
|
|||
$(TEST_IBAN_ANALYZER) \
|
||||
$(TEST_EXIFTOOL_METADATA) \
|
||||
$(TEST_PDF_PASSWORD_RECOVERY) \
|
||||
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
$(TEST_EXTRACTION_DROP_SERVICE) \
|
||||
$(TEST_RELATION_TYPE_NORMALIZER) \
|
||||
$(TEST_RELATION_TYPE_SERVICE)
|
||||
@echo "Exécution des tests..."
|
||||
@./$(TEST_NODE)
|
||||
@./$(TEST_TREE_MODEL)
|
||||
|
|
@ -828,6 +863,8 @@ test: \
|
|||
@$(TEST_EXIFTOOL_METADATA)
|
||||
@$(TEST_PDF_PASSWORD_RECOVERY)
|
||||
@$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
@$(TEST_RELATION_TYPE_NORMALIZER)
|
||||
@$(TEST_RELATION_TYPE_SERVICE)
|
||||
@echo "Tous les tests sont valides."
|
||||
|
||||
%.o: %.c
|
||||
|
|
@ -894,7 +931,9 @@ clean:
|
|||
$(TEST_SOCIAL_PLATFORM) \
|
||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||
$(TEST_EML_ANALYZER) \
|
||||
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||
$(TEST_EXTRACTION_DROP_SERVICE) \
|
||||
$(TEST_RELATION_TYPE_NORMALIZER) \
|
||||
$(TEST_RELATION_TYPE_SERVICE)
|
||||
|
||||
-include $(DEP)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/******************************************************************************
|
||||
* Labfy Investigation
|
||||
*
|
||||
* Extensions idempotentes du schéma SQLite courant V8
|
||||
* Extensions idempotentes du schéma SQLite courant V9
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
|
|
@ -102,6 +102,19 @@ CREATE TABLE IF NOT EXISTS graph_viewport
|
|||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS relation_types
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT UNIQUE,
|
||||
label TEXT NOT NULL,
|
||||
normalized_key TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_system INTEGER NOT NULL DEFAULT 0 CHECK (is_system IN (0, 1)),
|
||||
CHECK (code IS NULL OR length(trim(code)) > 0),
|
||||
CHECK (length(trim(label)) > 0),
|
||||
CHECK (length(normalized_key) > 0)
|
||||
);
|
||||
|
||||
/* Migration idempotente des positions d'entités déjà enregistrées. */
|
||||
INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at)
|
||||
SELECT entity_id, x, y, updated_at
|
||||
|
|
|
|||
24
database/schema_v9.sql
Normal file
24
database/schema_v9.sql
Normal 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 d’un élément.',1),
|
||||
('uses','Utilise','utilise','Utilisation d’une ressource ou identité.',1),
|
||||
('controls','Contrôle','contrôle','Contrôle d’une entité.',1),
|
||||
('owns','Possède','possède','Possession d’une entité.',1),
|
||||
('knows','Connaît','connaît','Connaissance entre personnes.',1),
|
||||
('redirects_to','Redirige vers','redirige vers','Redirection vers une cible.',1);
|
||||
|
|
@ -2275,6 +2275,32 @@ et vertical du canevas. Ces valeurs sont enregistrées à la fermeture de
|
|||
l'enquête et restaurées lors de son premier chargement, afin que la vue
|
||||
réapparaisse exactement à l'endroit où l'utilisateur l'avait laissée.
|
||||
|
||||
## 11.6 Types canoniques de relations
|
||||
|
||||
`relation_types` constitue le référentiel persistant des catégories de
|
||||
relations. Chaque type possède un identifiant SQLite, un libellé canonique,
|
||||
une clé Unicode normalisée unique, une description facultative et un
|
||||
indicateur système. Les types utilisés automatiquement possèdent en plus un
|
||||
`code` métier stable, indépendant de la langue et du libellé affiché.
|
||||
|
||||
Les quatre notions sont distinctes :
|
||||
|
||||
- le **code métier stable** pilote les producteurs automatiques tels que DNS ;
|
||||
- le **libellé canonique** est affiché dans le graphe et l’interface ;
|
||||
- la **clé normalisée** sert uniquement à garantir l’unicité 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.
|
||||
L’unicité d’une relation orientée repose sur la source, la cible et cet
|
||||
identifiant canonique. La colonne historique `type_relation` n’est conservée
|
||||
que pour la compatibilité des bases antérieures et n’est 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 d’association ne sont
|
||||
pas reconstruits. Un contrôle `foreign_key_check` précède le COMMIT.
|
||||
|
||||
---
|
||||
|
||||
# 12. Conclusion
|
||||
|
|
|
|||
26
include/core/relation_type_normalizer.h
Normal file
26
include/core/relation_type_normalizer.h
Normal 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
|
||||
39
include/core/relation_type_service.h
Normal file
39
include/core/relation_type_service.h
Normal 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
|
||||
46
include/dao/relation_type_dao.h
Normal file
46
include/dao/relation_type_dao.h
Normal 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
|
||||
|
|
@ -89,6 +89,7 @@ bool schema_install_v6(Database *database);
|
|||
|
||||
bool schema_install_v7(Database *database);
|
||||
bool schema_install_v8(Database *database);
|
||||
bool schema_install_v9(Database *database);
|
||||
|
||||
/**
|
||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,21 @@ RelationRecord *relation_record_new(
|
|||
GError **error
|
||||
);
|
||||
|
||||
RelationRecord *relation_record_new_canonical(
|
||||
const char *identifier,
|
||||
const char *source_entity_identifier,
|
||||
const char *target_entity_identifier,
|
||||
gint64 relation_type_identifier,
|
||||
const char *relation_type_code,
|
||||
const char *relation_type_label,
|
||||
const char *label,
|
||||
const char *justification,
|
||||
gint confidence,
|
||||
const char *created_at,
|
||||
const char *updated_at,
|
||||
RelationStatus status,
|
||||
GError **error);
|
||||
|
||||
/**
|
||||
* @brief Libère une relation.
|
||||
*
|
||||
|
|
@ -135,6 +150,12 @@ const char *relation_record_get_target_entity_identifier(
|
|||
const char *relation_record_get_relation_type(
|
||||
const RelationRecord *relation_record
|
||||
);
|
||||
gint64 relation_record_get_relation_type_identifier(
|
||||
const RelationRecord *relation_record);
|
||||
const char *relation_record_get_relation_type_code(
|
||||
const RelationRecord *relation_record);
|
||||
const char *relation_record_get_relation_type_label(
|
||||
const RelationRecord *relation_record);
|
||||
|
||||
/**
|
||||
* @brief Retourne le libellé facultatif.
|
||||
|
|
|
|||
38
include/models/relation_type.h
Normal file
38
include/models/relation_type.h
Normal 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
|
||||
|
|
@ -80,6 +80,7 @@ gboolean create_relation_dialog_present(
|
|||
GtkWindow *parent,
|
||||
const char *source_entity_identifier,
|
||||
const GPtrArray *entities,
|
||||
const GPtrArray *relation_types,
|
||||
const GPtrArray *evidence_records,
|
||||
const char *investigation_root_path,
|
||||
CreateRelationDialogCallback callback,
|
||||
|
|
@ -109,6 +110,7 @@ gboolean edit_relation_dialog_present(
|
|||
GtkWindow *parent,
|
||||
const RelationRecord *relation_record,
|
||||
const GPtrArray *entities,
|
||||
const GPtrArray *relation_types,
|
||||
const GPtrArray *evidence_records,
|
||||
const GPtrArray *selected_evidence_identifiers,
|
||||
const char *investigation_root_path,
|
||||
|
|
@ -168,6 +170,10 @@ const char *create_relation_dialog_result_get_target_identifier(
|
|||
const char *create_relation_dialog_result_get_relation_type(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
gint64 create_relation_dialog_result_get_relation_type_identifier(
|
||||
const CreateRelationDialogResult *result);
|
||||
const char *create_relation_dialog_result_get_relation_type_code(
|
||||
const CreateRelationDialogResult *result);
|
||||
|
||||
/**
|
||||
* @brief Retourne le libellé facultatif.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@
|
|||
#include "models/evidence_record.h"
|
||||
#include "dao/evidence_type_dao.h"
|
||||
#include "dao/graph_node_position_dao.h"
|
||||
#include "dao/relation_type_dao.h"
|
||||
#include "models/relation_type.h"
|
||||
#include "core/relation_type_service.h"
|
||||
#include "views/evidence_import_dialog.h"
|
||||
#include "views/evidence_metadata_dialog.h"
|
||||
#include "views/create_relation_dialog.h"
|
||||
|
|
@ -5593,6 +5596,7 @@ static void application_on_create_relation_completed(
|
|||
|
||||
gboolean relation_created =
|
||||
FALSE;
|
||||
gint64 relation_type_identifier = 0;
|
||||
|
||||
if (application == NULL ||
|
||||
application->main_window == NULL)
|
||||
|
|
@ -5707,11 +5711,33 @@ static void application_on_create_relation_completed(
|
|||
goto cleanup;
|
||||
}
|
||||
|
||||
relation_type_identifier =
|
||||
create_relation_dialog_result_get_relation_type_identifier(result);
|
||||
if (relation_type_identifier == 0)
|
||||
{
|
||||
RelationTypeService *type_service =
|
||||
relation_type_service_new(database, &error);
|
||||
if (type_service == NULL ||
|
||||
!relation_type_service_create_custom(type_service, relation_type,
|
||||
NULL, &relation_type_identifier, &error))
|
||||
{
|
||||
relation_type_service_free(type_service);
|
||||
application_present_error(application,
|
||||
"Création du type impossible",
|
||||
error != NULL ? error->message :
|
||||
"Le nouveau type n'a pas pu être créé.");
|
||||
goto cleanup;
|
||||
}
|
||||
relation_type_service_free(type_service);
|
||||
}
|
||||
|
||||
relation_record =
|
||||
relation_record_new(
|
||||
relation_record_new_canonical(
|
||||
relation_identifier,
|
||||
source_identifier,
|
||||
target_identifier,
|
||||
relation_type_identifier,
|
||||
create_relation_dialog_result_get_relation_type_code(result),
|
||||
relation_type,
|
||||
create_relation_dialog_result_get_label(
|
||||
result
|
||||
|
|
@ -6240,6 +6266,7 @@ static void application_on_edit_relation_completed(
|
|||
char *timestamp = NULL;
|
||||
GError *error = NULL;
|
||||
gboolean transaction_active = FALSE;
|
||||
gint64 relation_type_identifier = 0;
|
||||
|
||||
if (result == NULL || application == NULL || application->session == NULL)
|
||||
goto cleanup;
|
||||
|
|
@ -6254,10 +6281,28 @@ static void application_on_edit_relation_completed(
|
|||
now = g_date_time_new_now_utc();
|
||||
timestamp = now != NULL ? g_date_time_format(now,
|
||||
"%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||
updated = relation_record_new(
|
||||
relation_type_identifier =
|
||||
create_relation_dialog_result_get_relation_type_identifier(result);
|
||||
if (relation_type_identifier == 0)
|
||||
{
|
||||
RelationTypeService *type_service =
|
||||
relation_type_service_new(database, &error);
|
||||
if (type_service == NULL ||
|
||||
!relation_type_service_create_custom(type_service,
|
||||
create_relation_dialog_result_get_relation_type(result),
|
||||
NULL, &relation_type_identifier, &error))
|
||||
{
|
||||
relation_type_service_free(type_service);
|
||||
goto failure;
|
||||
}
|
||||
relation_type_service_free(type_service);
|
||||
}
|
||||
updated = relation_record_new_canonical(
|
||||
relation_record_get_identifier(existing),
|
||||
relation_record_get_source_entity_identifier(existing),
|
||||
relation_record_get_target_entity_identifier(existing),
|
||||
relation_type_identifier,
|
||||
create_relation_dialog_result_get_relation_type_code(result),
|
||||
create_relation_dialog_result_get_relation_type(result),
|
||||
create_relation_dialog_result_get_label(result),
|
||||
create_relation_dialog_result_get_justification(result),
|
||||
|
|
@ -6348,6 +6393,8 @@ static void application_on_edit_relation_requested(
|
|||
GPtrArray *entities = NULL;
|
||||
GPtrArray *evidences = NULL;
|
||||
GPtrArray *selected_evidences = NULL;
|
||||
GPtrArray *relation_types = NULL;
|
||||
RelationTypeDao *relation_type_dao = NULL;
|
||||
const InvestigationProject *project = NULL;
|
||||
ApplicationEditRelationContext *context = NULL;
|
||||
GError *error = NULL;
|
||||
|
|
@ -6359,6 +6406,7 @@ static void application_on_edit_relation_requested(
|
|||
entity_dao = entity_dao_new(database, &error);
|
||||
evidence_dao = evidence_dao_new(database, &error);
|
||||
relation_evidence_dao = relation_evidence_dao_new(database, &error);
|
||||
relation_type_dao = relation_type_dao_new(database, &error);
|
||||
project = investigation_session_get_project(application->session);
|
||||
if (relation_dao != NULL)
|
||||
relation = relation_dao_find_by_identifier(relation_dao,
|
||||
|
|
@ -6369,13 +6417,17 @@ static void application_on_edit_relation_requested(
|
|||
if (relation_evidence_dao != NULL) selected_evidences =
|
||||
relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao,
|
||||
relation_identifier, &error);
|
||||
if (relation_type_dao != NULL)
|
||||
relation_types = relation_type_dao_list_all(relation_type_dao, &error);
|
||||
if (relation == NULL || entities == NULL || evidences == NULL ||
|
||||
selected_evidences == NULL || project == NULL) goto failure;
|
||||
selected_evidences == NULL || relation_types == NULL ||
|
||||
project == NULL) goto failure;
|
||||
context = g_new0(ApplicationEditRelationContext, 1);
|
||||
context->application = application;
|
||||
context->relation_identifier = g_strdup(relation_identifier);
|
||||
if (context->relation_identifier == NULL || !edit_relation_dialog_present(
|
||||
main_window_get_window(application->main_window), relation, entities,
|
||||
relation_types,
|
||||
evidences, selected_evidences,
|
||||
investigation_project_get_root_path(project),
|
||||
application_on_edit_relation_completed, context, &error))
|
||||
|
|
@ -6396,6 +6448,8 @@ cleanup:
|
|||
entity_dao_free(entity_dao);
|
||||
evidence_dao_free(evidence_dao);
|
||||
relation_evidence_dao_free(relation_evidence_dao);
|
||||
relation_type_dao_free(relation_type_dao);
|
||||
g_clear_pointer(&relation_types, g_ptr_array_unref);
|
||||
relation_dao_free(relation_dao);
|
||||
}
|
||||
|
||||
|
|
@ -6462,11 +6516,13 @@ static void application_on_add_relation_requested(
|
|||
NULL;
|
||||
|
||||
EvidenceDao *evidence_dao = NULL;
|
||||
RelationTypeDao *relation_type_dao = NULL;
|
||||
const InvestigationProject *project = NULL;
|
||||
|
||||
GPtrArray *entities =
|
||||
NULL;
|
||||
GPtrArray *evidences = NULL;
|
||||
GPtrArray *relation_types = NULL;
|
||||
|
||||
GError *error =
|
||||
NULL;
|
||||
|
|
@ -6533,11 +6589,17 @@ static void application_on_add_relation_requested(
|
|||
evidences = evidence_dao_list_all(evidence_dao, &error);
|
||||
evidence_dao_free(evidence_dao);
|
||||
|
||||
relation_type_dao = relation_type_dao_new(database, &error);
|
||||
if (relation_type_dao != NULL)
|
||||
relation_types = relation_type_dao_list_all(relation_type_dao, &error);
|
||||
relation_type_dao_free(relation_type_dao);
|
||||
|
||||
entity_dao_free(
|
||||
entity_dao
|
||||
);
|
||||
|
||||
if (entities == NULL || evidences == NULL || project == NULL)
|
||||
if (entities == NULL || evidences == NULL || relation_types == NULL ||
|
||||
project == NULL)
|
||||
{
|
||||
application_present_error(
|
||||
application,
|
||||
|
|
@ -6553,6 +6615,7 @@ static void application_on_add_relation_requested(
|
|||
|
||||
g_clear_pointer(&entities, g_ptr_array_unref);
|
||||
g_clear_pointer(&evidences, g_ptr_array_unref);
|
||||
g_clear_pointer(&relation_types, g_ptr_array_unref);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -6563,6 +6626,7 @@ static void application_on_add_relation_requested(
|
|||
),
|
||||
source_entity_identifier,
|
||||
entities,
|
||||
relation_types,
|
||||
evidences,
|
||||
investigation_project_get_root_path(project),
|
||||
application_on_create_relation_completed,
|
||||
|
|
@ -6587,6 +6651,7 @@ static void application_on_add_relation_requested(
|
|||
entities
|
||||
);
|
||||
g_ptr_array_unref(evidences);
|
||||
g_ptr_array_unref(relation_types);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -183,7 +183,9 @@ gboolean osint_dns_integration_apply(
|
|||
osint_dns_integration_build_relation_key(
|
||||
relation_record_get_source_entity_identifier(relation),
|
||||
relation_record_get_target_entity_identifier(relation),
|
||||
relation_record_get_relation_type(relation)
|
||||
relation_record_get_relation_type_code(relation) != NULL
|
||||
? relation_record_get_relation_type_code(relation)
|
||||
: relation_record_get_relation_type(relation)
|
||||
),
|
||||
g_strdup(relation_record_get_identifier(relation))
|
||||
);
|
||||
|
|
|
|||
47
src/core/relation_type_normalizer.c
Normal file
47
src/core/relation_type_normalizer.c
Normal 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;
|
||||
}
|
||||
152
src/core/relation_type_service.c
Normal file
152
src/core/relation_type_service.c
Normal 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;
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
#include "database/error.h"
|
||||
#include "database/statement.h"
|
||||
#include "dao/relation_type_dao.h"
|
||||
#include "core/relation_type_normalizer.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdint.h>
|
||||
|
|
@ -31,6 +33,7 @@ static const char *const relation_dao_insert_sql =
|
|||
" entite_source_id,"
|
||||
" entite_cible_id,"
|
||||
" type_relation,"
|
||||
" relation_type_id,"
|
||||
" label,"
|
||||
" justification,"
|
||||
" confiance,"
|
||||
|
|
@ -49,14 +52,15 @@ static const char *const relation_dao_insert_sql =
|
|||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" ?"
|
||||
");";
|
||||
|
||||
/** @brief Requête de modification des attributs d'une relation. */
|
||||
static const char *const relation_dao_update_sql =
|
||||
"UPDATE relations SET "
|
||||
"type_relation = ?, label = ?, justification = ?, confiance = ?, "
|
||||
"updated_at = ?, status = ? WHERE id = ?;";
|
||||
"type_relation = ?, relation_type_id = ?, label = ?, justification = ?, "
|
||||
"confiance = ?, updated_at = ?, status = ? WHERE id = ?;";
|
||||
|
||||
/** @brief Requête de suppression d'une relation. */
|
||||
static const char *const relation_dao_delete_sql =
|
||||
|
|
@ -67,38 +71,42 @@ static const char *const relation_dao_delete_sql =
|
|||
*/
|
||||
static const char *const relation_dao_find_by_identifier_sql =
|
||||
"SELECT "
|
||||
" id,"
|
||||
" entite_source_id,"
|
||||
" entite_cible_id,"
|
||||
" type_relation,"
|
||||
" label,"
|
||||
" justification,"
|
||||
" confiance,"
|
||||
" created_at,"
|
||||
" updated_at,"
|
||||
" status "
|
||||
"FROM relations "
|
||||
"WHERE id = ?;";
|
||||
" r.id,"
|
||||
" r.entite_source_id,"
|
||||
" r.entite_cible_id,"
|
||||
" rt.id,"
|
||||
" rt.code,"
|
||||
" rt.label,"
|
||||
" r.label,"
|
||||
" r.justification,"
|
||||
" r.confiance,"
|
||||
" r.created_at,"
|
||||
" r.updated_at,"
|
||||
" r.status "
|
||||
"FROM relations r JOIN relation_types rt ON rt.id=r.relation_type_id "
|
||||
"WHERE r.id = ?;";
|
||||
|
||||
/**
|
||||
* @brief Requête de chargement de toutes les relations.
|
||||
*/
|
||||
static const char *const relation_dao_list_all_sql =
|
||||
"SELECT "
|
||||
" id,"
|
||||
" entite_source_id,"
|
||||
" entite_cible_id,"
|
||||
" type_relation,"
|
||||
" label,"
|
||||
" justification,"
|
||||
" confiance,"
|
||||
" created_at,"
|
||||
" updated_at,"
|
||||
" status "
|
||||
"FROM relations "
|
||||
" r.id,"
|
||||
" r.entite_source_id,"
|
||||
" r.entite_cible_id,"
|
||||
" rt.id,"
|
||||
" rt.code,"
|
||||
" rt.label,"
|
||||
" r.label,"
|
||||
" r.justification,"
|
||||
" r.confiance,"
|
||||
" r.created_at,"
|
||||
" r.updated_at,"
|
||||
" r.status "
|
||||
"FROM relations r JOIN relation_types rt ON rt.id=r.relation_type_id "
|
||||
"ORDER BY "
|
||||
" created_at ASC,"
|
||||
" id ASC;";
|
||||
" r.created_at ASC,"
|
||||
" r.id ASC;";
|
||||
|
||||
/**
|
||||
* @brief Requête de comptage.
|
||||
|
|
@ -133,7 +141,7 @@ static const char *const relation_dao_duplicate_exists_sql =
|
|||
"FROM relations "
|
||||
"WHERE entite_source_id = ? "
|
||||
"AND entite_cible_id = ? "
|
||||
"AND type_relation = ? "
|
||||
"AND relation_type_id = ? "
|
||||
"LIMIT 1;";
|
||||
|
||||
/**
|
||||
|
|
@ -492,7 +500,7 @@ static gboolean relation_dao_duplicate_exists(
|
|||
RelationDao *relation_dao,
|
||||
const char *source_entity_identifier,
|
||||
const char *target_entity_identifier,
|
||||
const char *relation_type,
|
||||
gint64 relation_type_identifier,
|
||||
gboolean *out_exists,
|
||||
GError **error
|
||||
)
|
||||
|
|
@ -510,7 +518,7 @@ static gboolean relation_dao_duplicate_exists(
|
|||
relation_dao->database == NULL ||
|
||||
source_entity_identifier == NULL ||
|
||||
target_entity_identifier == NULL ||
|
||||
relation_type == NULL ||
|
||||
relation_type_identifier <= 0 ||
|
||||
out_exists == NULL)
|
||||
{
|
||||
relation_dao_set_error_literal(
|
||||
|
|
@ -553,10 +561,10 @@ static gboolean relation_dao_duplicate_exists(
|
|||
2,
|
||||
target_entity_identifier
|
||||
) ||
|
||||
!database_statement_bind_text(
|
||||
!database_statement_bind_int64(
|
||||
statement,
|
||||
3,
|
||||
relation_type
|
||||
relation_type_identifier
|
||||
))
|
||||
{
|
||||
relation_dao_set_database_error(
|
||||
|
|
@ -626,6 +634,8 @@ static RelationRecord *relation_dao_read_current_record(
|
|||
|
||||
char *relation_type =
|
||||
NULL;
|
||||
char *relation_type_code = NULL;
|
||||
int64_t relation_type_identifier = 0;
|
||||
|
||||
char *label =
|
||||
NULL;
|
||||
|
|
@ -679,39 +689,49 @@ static RelationRecord *relation_dao_read_current_record(
|
|||
2,
|
||||
&target_entity_identifier
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
!database_statement_column_int64(
|
||||
statement,
|
||||
3,
|
||||
&relation_type
|
||||
&relation_type_identifier
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
4,
|
||||
&label
|
||||
&relation_type_code
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
5,
|
||||
&justification
|
||||
&relation_type
|
||||
) ||
|
||||
!database_statement_column_int64(
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
6,
|
||||
&confidence_value
|
||||
&label
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
7,
|
||||
&created_at
|
||||
&justification
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
!database_statement_column_int64(
|
||||
statement,
|
||||
8,
|
||||
&updated_at
|
||||
&confidence_value
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
9,
|
||||
&created_at
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
10,
|
||||
&updated_at
|
||||
) ||
|
||||
!database_statement_column_text(
|
||||
statement,
|
||||
11,
|
||||
&status_text
|
||||
))
|
||||
{
|
||||
|
|
@ -751,10 +771,12 @@ static RelationRecord *relation_dao_read_current_record(
|
|||
}
|
||||
|
||||
relation_record =
|
||||
relation_record_new(
|
||||
relation_record_new_canonical(
|
||||
identifier,
|
||||
source_entity_identifier,
|
||||
target_entity_identifier,
|
||||
(gint64) relation_type_identifier,
|
||||
relation_type_code,
|
||||
relation_type,
|
||||
label,
|
||||
justification,
|
||||
|
|
@ -816,6 +838,7 @@ cleanup:
|
|||
g_free(
|
||||
relation_type
|
||||
);
|
||||
g_free(relation_type_code);
|
||||
|
||||
g_free(
|
||||
target_entity_identifier
|
||||
|
|
@ -915,6 +938,36 @@ void relation_dao_free(
|
|||
);
|
||||
}
|
||||
|
||||
static RelationType *relation_dao_resolve_type(
|
||||
RelationDao *relation_dao, const RelationRecord *record, GError **error)
|
||||
{
|
||||
RelationTypeDao *type_dao = NULL;
|
||||
RelationType *type = NULL;
|
||||
char *key = NULL;
|
||||
gint64 identifier = relation_record_get_relation_type_identifier(record);
|
||||
type_dao = relation_type_dao_new(relation_dao->database, error);
|
||||
if (type_dao == NULL) return NULL;
|
||||
if (identifier > 0)
|
||||
type = relation_type_dao_find_by_identifier(type_dao, identifier, error);
|
||||
else if (relation_record_get_relation_type_code(record) != NULL)
|
||||
type = relation_type_dao_find_by_code(type_dao,
|
||||
relation_record_get_relation_type_code(record), error);
|
||||
else
|
||||
{
|
||||
key = relation_type_normalize_key(
|
||||
relation_record_get_relation_type(record));
|
||||
if (key != NULL)
|
||||
type = relation_type_dao_find_by_code(type_dao,
|
||||
relation_record_get_relation_type(record), error);
|
||||
if (type == NULL && (error == NULL || *error == NULL))
|
||||
type = relation_type_dao_find_by_normalized_key(type_dao, key, error);
|
||||
}
|
||||
if (type == NULL && error != NULL && *error == NULL)
|
||||
relation_dao_set_error_literal(error, RELATION_DAO_ERROR_CONSTRAINT,
|
||||
"Le type de relation n'existe pas dans le référentiel.");
|
||||
g_free(key); relation_type_dao_free(type_dao); return type;
|
||||
}
|
||||
|
||||
gboolean relation_dao_insert(
|
||||
RelationDao *relation_dao,
|
||||
const RelationRecord *relation_record,
|
||||
|
|
@ -962,6 +1015,9 @@ gboolean relation_dao_insert(
|
|||
|
||||
gboolean success =
|
||||
FALSE;
|
||||
RelationType *canonical_type = NULL;
|
||||
gint64 canonical_type_identifier = 0;
|
||||
const char *canonical_type_label = NULL;
|
||||
|
||||
g_return_val_if_fail(
|
||||
error == NULL || *error == NULL,
|
||||
|
|
@ -1161,16 +1217,24 @@ gboolean relation_dao_insert(
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
canonical_type = relation_dao_resolve_type(
|
||||
relation_dao, relation_record, error);
|
||||
if (canonical_type == NULL) return FALSE;
|
||||
canonical_type_identifier = relation_type_get_identifier(canonical_type);
|
||||
canonical_type_label = relation_type_get_code(canonical_type) != NULL
|
||||
? relation_type_get_code(canonical_type)
|
||||
: relation_type_get_label(canonical_type);
|
||||
|
||||
if (!relation_dao_duplicate_exists(
|
||||
relation_dao,
|
||||
source_entity_identifier,
|
||||
target_entity_identifier,
|
||||
relation_type,
|
||||
canonical_type_identifier,
|
||||
&value_exists,
|
||||
error
|
||||
))
|
||||
{
|
||||
return FALSE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (value_exists)
|
||||
|
|
@ -1181,7 +1245,7 @@ gboolean relation_dao_insert(
|
|||
"Cette relation orientée existe déjà pour ce type."
|
||||
);
|
||||
|
||||
return FALSE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
statement =
|
||||
|
|
@ -1220,36 +1284,41 @@ gboolean relation_dao_insert(
|
|||
!database_statement_bind_text(
|
||||
statement,
|
||||
4,
|
||||
relation_type
|
||||
canonical_type_label
|
||||
) ||
|
||||
!relation_dao_bind_optional_text(
|
||||
!database_statement_bind_int64(
|
||||
statement,
|
||||
5,
|
||||
label
|
||||
canonical_type_identifier
|
||||
) ||
|
||||
!relation_dao_bind_optional_text(
|
||||
statement,
|
||||
6,
|
||||
label
|
||||
) ||
|
||||
!relation_dao_bind_optional_text(
|
||||
statement,
|
||||
7,
|
||||
justification
|
||||
) ||
|
||||
!database_statement_bind_int64(
|
||||
statement,
|
||||
7,
|
||||
8,
|
||||
(int64_t) confidence
|
||||
) ||
|
||||
!database_statement_bind_text(
|
||||
statement,
|
||||
8,
|
||||
9,
|
||||
created_at
|
||||
) ||
|
||||
!database_statement_bind_text(
|
||||
statement,
|
||||
9,
|
||||
10,
|
||||
updated_at
|
||||
) ||
|
||||
!database_statement_bind_text(
|
||||
statement,
|
||||
10,
|
||||
11,
|
||||
status_text
|
||||
))
|
||||
{
|
||||
|
|
@ -1304,6 +1373,7 @@ cleanup:
|
|||
database_statement_finalize(
|
||||
statement
|
||||
);
|
||||
relation_type_free(canonical_type);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
|
@ -1314,6 +1384,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
|
|||
DatabaseStatement *statement = NULL;
|
||||
const char *status_text = NULL;
|
||||
gboolean success = FALSE;
|
||||
RelationType *canonical_type = NULL;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (relation_dao == NULL || relation_record == NULL)
|
||||
|
|
@ -1325,6 +1396,9 @@ gboolean relation_dao_update(RelationDao *relation_dao,
|
|||
}
|
||||
status_text = relation_dao_status_to_text(
|
||||
relation_record_get_status(relation_record));
|
||||
canonical_type = relation_dao_resolve_type(
|
||||
relation_dao, relation_record, error);
|
||||
if (canonical_type == NULL) return FALSE;
|
||||
statement = database_statement_prepare(relation_dao->database,
|
||||
relation_dao_update_sql);
|
||||
if (statement == NULL)
|
||||
|
|
@ -1332,21 +1406,25 @@ gboolean relation_dao_update(RelationDao *relation_dao,
|
|||
relation_dao_set_database_error(relation_dao, error,
|
||||
RELATION_DAO_ERROR_PREPARE,
|
||||
"Impossible de préparer la modification de la relation");
|
||||
return FALSE;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!database_statement_bind_text(statement, 1,
|
||||
relation_record_get_relation_type(relation_record)) ||
|
||||
!relation_dao_bind_optional_text(statement, 2,
|
||||
relation_record_get_label(relation_record)) ||
|
||||
relation_type_get_code(canonical_type) != NULL
|
||||
? relation_type_get_code(canonical_type)
|
||||
: relation_type_get_label(canonical_type)) ||
|
||||
!database_statement_bind_int64(statement, 2,
|
||||
relation_type_get_identifier(canonical_type)) ||
|
||||
!relation_dao_bind_optional_text(statement, 3,
|
||||
relation_record_get_label(relation_record)) ||
|
||||
!relation_dao_bind_optional_text(statement, 4,
|
||||
relation_record_get_justification(relation_record)) ||
|
||||
!database_statement_bind_int64(statement, 4,
|
||||
!database_statement_bind_int64(statement, 5,
|
||||
relation_record_get_confidence(relation_record)) ||
|
||||
!database_statement_bind_text(statement, 5,
|
||||
!database_statement_bind_text(statement, 6,
|
||||
relation_record_get_updated_at(relation_record)) ||
|
||||
status_text == NULL ||
|
||||
!database_statement_bind_text(statement, 6, status_text) ||
|
||||
!database_statement_bind_text(statement, 7,
|
||||
!database_statement_bind_text(statement, 7, status_text) ||
|
||||
!database_statement_bind_text(statement, 8,
|
||||
relation_record_get_identifier(relation_record)))
|
||||
{
|
||||
relation_dao_set_database_error(relation_dao, error,
|
||||
|
|
@ -1364,6 +1442,7 @@ gboolean relation_dao_update(RelationDao *relation_dao,
|
|||
success = TRUE;
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
relation_type_free(canonical_type);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
|
|
|||
189
src/dao/relation_type_dao.c
Normal file
189
src/dao/relation_type_dao.c
Normal 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;
|
||||
}
|
||||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @brief Version actuelle du schéma SQLite.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 8
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 9
|
||||
|
||||
/**
|
||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "8"
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "9"
|
||||
|
||||
/**
|
||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||
|
|
@ -782,6 +782,21 @@ rollback:
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v8_to_v9(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v9(database) ||
|
||||
!database_update_schema_version(database, "9") ||
|
||||
!database_transaction_commit(database)) goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V8 vers V9.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||
*/
|
||||
|
|
@ -1003,6 +1018,10 @@ bool database_migrate_to_latest(
|
|||
return false;
|
||||
schema_version = 8;
|
||||
break;
|
||||
case 8:
|
||||
if (!database_migrate_v8_to_v9(database)) return false;
|
||||
schema_version = 9;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
|
|
@ -1135,6 +1154,13 @@ bool database_initialize(
|
|||
goto rollback;
|
||||
}
|
||||
|
||||
if (!schema_install_v7(database) ||
|
||||
!schema_install_v8(database) ||
|
||||
!schema_install_v9(database))
|
||||
{
|
||||
goto rollback;
|
||||
}
|
||||
|
||||
if (!schema_ensure_current(
|
||||
database
|
||||
))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "database/schema.h"
|
||||
|
||||
#include "database_internal.h"
|
||||
#include "core/relation_type_normalizer.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <sqlite3.h>
|
||||
|
|
@ -239,6 +240,113 @@ bool schema_install_v8(Database *database)
|
|||
"la migration SQLite V8");
|
||||
}
|
||||
|
||||
static gboolean schema_v9_column_exists(sqlite3 *handle)
|
||||
{
|
||||
sqlite3_stmt *statement = NULL;
|
||||
gboolean found = FALSE;
|
||||
if (sqlite3_prepare_v2(handle, "PRAGMA table_info(relations);", -1,
|
||||
&statement, NULL) != SQLITE_OK) return FALSE;
|
||||
while (sqlite3_step(statement) == SQLITE_ROW)
|
||||
if (g_strcmp0((const char *) sqlite3_column_text(statement, 1),
|
||||
"relation_type_id") == 0) found = TRUE;
|
||||
sqlite3_finalize(statement);
|
||||
return found;
|
||||
}
|
||||
|
||||
static gboolean schema_v9_migrate_relation_types(Database *database)
|
||||
{
|
||||
sqlite3 *handle = database_get_handle(database);
|
||||
sqlite3_stmt *read_statement = NULL;
|
||||
sqlite3_stmt *find_statement = NULL;
|
||||
sqlite3_stmt *insert_statement = NULL;
|
||||
sqlite3_stmt *update_statement = NULL;
|
||||
gboolean success = FALSE;
|
||||
|
||||
if (!schema_v9_column_exists(handle) &&
|
||||
sqlite3_exec(handle, "ALTER TABLE relations ADD COLUMN "
|
||||
"relation_type_id INTEGER REFERENCES relation_types(id) "
|
||||
"ON UPDATE CASCADE ON DELETE RESTRICT;", NULL, NULL, NULL) != SQLITE_OK)
|
||||
return FALSE;
|
||||
if (sqlite3_prepare_v2(handle, "SELECT id,type_relation FROM relations "
|
||||
"WHERE relation_type_id IS NULL ORDER BY created_at,id;", -1,
|
||||
&read_statement, NULL) != SQLITE_OK ||
|
||||
sqlite3_prepare_v2(handle, "SELECT id FROM relation_types WHERE "
|
||||
"code=?1 OR normalized_key=?2 ORDER BY code IS NULL LIMIT 1;", -1,
|
||||
&find_statement, NULL) != SQLITE_OK ||
|
||||
sqlite3_prepare_v2(handle, "INSERT INTO relation_types(code,label,"
|
||||
"normalized_key,description,is_system) VALUES(NULL,?1,?2,NULL,0);",
|
||||
-1, &insert_statement, NULL) != SQLITE_OK ||
|
||||
sqlite3_prepare_v2(handle, "UPDATE relations SET relation_type_id=?1 "
|
||||
"WHERE id=?2;", -1, &update_statement, NULL) != SQLITE_OK)
|
||||
goto cleanup;
|
||||
|
||||
while (sqlite3_step(read_statement) == SQLITE_ROW)
|
||||
{
|
||||
const char *relation_id =
|
||||
(const char *) sqlite3_column_text(read_statement, 0);
|
||||
const char *label =
|
||||
(const char *) sqlite3_column_text(read_statement, 1);
|
||||
char *key = relation_type_normalize_key(label);
|
||||
sqlite3_int64 type_id = 0;
|
||||
if (key == NULL) goto cleanup;
|
||||
sqlite3_bind_text(find_statement, 1, label, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(find_statement, 2, key, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(find_statement) == SQLITE_ROW)
|
||||
type_id = sqlite3_column_int64(find_statement, 0);
|
||||
sqlite3_reset(find_statement); sqlite3_clear_bindings(find_statement);
|
||||
if (type_id == 0)
|
||||
{
|
||||
sqlite3_bind_text(insert_statement, 1, label, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(insert_statement, 2, key, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(insert_statement) != SQLITE_DONE)
|
||||
{ g_free(key); goto cleanup; }
|
||||
type_id = sqlite3_last_insert_rowid(handle);
|
||||
sqlite3_reset(insert_statement);
|
||||
sqlite3_clear_bindings(insert_statement);
|
||||
}
|
||||
sqlite3_bind_int64(update_statement, 1, type_id);
|
||||
sqlite3_bind_text(update_statement, 2, relation_id, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(update_statement) != SQLITE_DONE)
|
||||
{ g_free(key); goto cleanup; }
|
||||
sqlite3_reset(update_statement); sqlite3_clear_bindings(update_statement);
|
||||
g_free(key);
|
||||
}
|
||||
if (sqlite3_exec(handle,
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_relations_canonical_type "
|
||||
"ON relations(entite_source_id,entite_cible_id,relation_type_id);"
|
||||
"CREATE INDEX IF NOT EXISTS idx_relations_relation_type_id "
|
||||
"ON relations(relation_type_id);"
|
||||
"CREATE TRIGGER IF NOT EXISTS relations_require_canonical_type_insert "
|
||||
"BEFORE INSERT ON relations WHEN NEW.relation_type_id IS NULL "
|
||||
"BEGIN SELECT RAISE(ABORT,'relation_type_id is required'); END;"
|
||||
"CREATE TRIGGER IF NOT EXISTS relations_require_canonical_type_update "
|
||||
"BEFORE UPDATE OF relation_type_id ON relations "
|
||||
"WHEN NEW.relation_type_id IS NULL "
|
||||
"BEGIN SELECT RAISE(ABORT,'relation_type_id is required'); END;",
|
||||
NULL, NULL, NULL) != SQLITE_OK)
|
||||
goto cleanup;
|
||||
{
|
||||
sqlite3_stmt *check = NULL;
|
||||
if (sqlite3_prepare_v2(handle, "PRAGMA foreign_key_check;", -1,
|
||||
&check, NULL) != SQLITE_OK) goto cleanup;
|
||||
success = sqlite3_step(check) == SQLITE_DONE;
|
||||
sqlite3_finalize(check);
|
||||
}
|
||||
cleanup:
|
||||
sqlite3_finalize(update_statement); sqlite3_finalize(insert_statement);
|
||||
sqlite3_finalize(find_statement); sqlite3_finalize(read_statement);
|
||||
if (!success) database_set_error(database, DATABASE_ERROR_SQLITE,
|
||||
sqlite3_errmsg(handle));
|
||||
return success;
|
||||
}
|
||||
|
||||
bool schema_install_v9(Database *database)
|
||||
{
|
||||
return schema_execute_file(database, "database/schema_v9.sql",
|
||||
"la migration SQLite V9") &&
|
||||
schema_v9_migrate_relation_types(database);
|
||||
}
|
||||
|
||||
bool schema_ensure_current(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ struct RelationRecord
|
|||
char *source_entity_identifier;
|
||||
char *target_entity_identifier;
|
||||
char *relation_type;
|
||||
gint64 relation_type_identifier;
|
||||
char *relation_type_code;
|
||||
char *label;
|
||||
char *justification;
|
||||
|
||||
|
|
@ -511,6 +513,32 @@ cleanup:
|
|||
return relation_record;
|
||||
}
|
||||
|
||||
RelationRecord *relation_record_new_canonical(
|
||||
const char *identifier, const char *source_entity_identifier,
|
||||
const char *target_entity_identifier, gint64 relation_type_identifier,
|
||||
const char *relation_type_code, const char *relation_type_label,
|
||||
const char *label, const char *justification, gint confidence,
|
||||
const char *created_at, const char *updated_at, RelationStatus status,
|
||||
GError **error)
|
||||
{
|
||||
RelationRecord *record = NULL;
|
||||
if (relation_type_identifier <= 0)
|
||||
{
|
||||
g_set_error_literal(error, RELATION_RECORD_ERROR,
|
||||
RELATION_RECORD_ERROR_INVALID_TYPE,
|
||||
"L'identifiant du type canonique est invalide.");
|
||||
return NULL;
|
||||
}
|
||||
record = relation_record_new(identifier, source_entity_identifier,
|
||||
target_entity_identifier, relation_type_label, label, justification,
|
||||
confidence, created_at, updated_at, status, error);
|
||||
if (record == NULL) return NULL;
|
||||
record->relation_type_identifier = relation_type_identifier;
|
||||
record->relation_type_code = relation_record_duplicate_optional(
|
||||
relation_type_code);
|
||||
return record;
|
||||
}
|
||||
|
||||
void relation_record_free(
|
||||
RelationRecord *relation_record
|
||||
)
|
||||
|
|
@ -539,6 +567,7 @@ void relation_record_free(
|
|||
g_free(
|
||||
relation_record->relation_type
|
||||
);
|
||||
g_free(relation_record->relation_type_code);
|
||||
|
||||
g_free(
|
||||
relation_record->target_entity_identifier
|
||||
|
|
@ -589,10 +618,30 @@ const char *relation_record_get_relation_type(
|
|||
)
|
||||
{
|
||||
return relation_record != NULL
|
||||
? relation_record->relation_type
|
||||
? (relation_record->relation_type_code != NULL
|
||||
? relation_record->relation_type_code
|
||||
: relation_record->relation_type)
|
||||
: NULL;
|
||||
}
|
||||
|
||||
gint64 relation_record_get_relation_type_identifier(
|
||||
const RelationRecord *relation_record)
|
||||
{
|
||||
return relation_record != NULL ? relation_record->relation_type_identifier : 0;
|
||||
}
|
||||
|
||||
const char *relation_record_get_relation_type_code(
|
||||
const RelationRecord *relation_record)
|
||||
{
|
||||
return relation_record != NULL ? relation_record->relation_type_code : NULL;
|
||||
}
|
||||
|
||||
const char *relation_record_get_relation_type_label(
|
||||
const RelationRecord *relation_record)
|
||||
{
|
||||
return relation_record != NULL ? relation_record->relation_type : NULL;
|
||||
}
|
||||
|
||||
const char *relation_record_get_label(
|
||||
const RelationRecord *relation_record
|
||||
)
|
||||
|
|
|
|||
89
src/models/relation_type.c
Normal file
89
src/models/relation_type.c
Normal 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(©, 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; }
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
#include "models/entity_record.h"
|
||||
#include "models/evidence_record.h"
|
||||
#include "models/relation_record.h"
|
||||
#include "models/relation_type.h"
|
||||
#include "core/relation_type_normalizer.h"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
|
@ -20,6 +22,8 @@ struct CreateRelationDialogResult
|
|||
char *source_entity_identifier;
|
||||
char *target_entity_identifier;
|
||||
char *relation_type;
|
||||
char *relation_type_code;
|
||||
gint64 relation_type_identifier;
|
||||
char *label;
|
||||
char *justification;
|
||||
GPtrArray *evidence_identifiers;
|
||||
|
|
@ -56,7 +60,10 @@ typedef struct
|
|||
GtkWindow *window;
|
||||
|
||||
GtkDropDown *target_drop_down;
|
||||
GtkEntry *relation_type_entry;
|
||||
GtkDropDown *relation_type_drop_down;
|
||||
GtkStringList *relation_type_string_list;
|
||||
GtkEntry *new_relation_type_entry;
|
||||
GtkWidget *relation_type_control;
|
||||
GtkEntry *label_entry;
|
||||
GtkTextView *justification_text_view;
|
||||
GtkSpinButton *confidence_spin_button;
|
||||
|
|
@ -64,6 +71,9 @@ typedef struct
|
|||
|
||||
char *source_entity_identifier;
|
||||
GPtrArray *target_identifiers;
|
||||
GArray *relation_type_identifiers;
|
||||
GPtrArray *relation_type_codes;
|
||||
GPtrArray *relation_type_labels;
|
||||
GPtrArray *evidence_options;
|
||||
GPtrArray *evidence_check_buttons;
|
||||
GtkWidget *evidence_preview_stack;
|
||||
|
|
@ -78,6 +88,51 @@ typedef struct
|
|||
gboolean completed;
|
||||
} CreateRelationDialogState;
|
||||
|
||||
static void create_relation_dialog_show_error(
|
||||
CreateRelationDialogState *state, const char *message);
|
||||
|
||||
static void create_relation_dialog_on_new_type_clicked(
|
||||
GtkButton *button, gpointer user_data)
|
||||
{
|
||||
CreateRelationDialogState *state = user_data;
|
||||
const char *label = NULL;
|
||||
char *key = NULL;
|
||||
(void) button;
|
||||
if (state == NULL) return;
|
||||
label = gtk_editable_get_text(GTK_EDITABLE(state->new_relation_type_entry));
|
||||
key = relation_type_normalize_key(label);
|
||||
if (key == NULL)
|
||||
{
|
||||
create_relation_dialog_show_error(state,
|
||||
"Saisissez le libellé du nouveau type.");
|
||||
return;
|
||||
}
|
||||
for (guint index = 0U; index < state->relation_type_labels->len; index++)
|
||||
{
|
||||
char *existing_key = relation_type_normalize_key(
|
||||
g_ptr_array_index(state->relation_type_labels, index));
|
||||
gboolean duplicate = g_strcmp0(key, existing_key) == 0;
|
||||
g_free(existing_key);
|
||||
if (duplicate)
|
||||
{
|
||||
create_relation_dialog_show_error(state,
|
||||
"Un type de relation équivalent existe déjà.");
|
||||
gtk_drop_down_set_selected(state->relation_type_drop_down, index);
|
||||
g_free(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
gint64 identifier = 0;
|
||||
gtk_string_list_append(state->relation_type_string_list, label);
|
||||
g_array_append_val(state->relation_type_identifiers, identifier);
|
||||
g_ptr_array_add(state->relation_type_codes, NULL);
|
||||
g_ptr_array_add(state->relation_type_labels, g_strdup(label));
|
||||
gtk_drop_down_set_selected(state->relation_type_drop_down,
|
||||
state->relation_type_labels->len - 1U);
|
||||
gtk_editable_set_text(GTK_EDITABLE(state->new_relation_type_entry), "");
|
||||
g_free(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retourne une copie nettoyée ou NULL pour une valeur vide.
|
||||
*/
|
||||
|
|
@ -381,6 +436,10 @@ static void create_relation_dialog_state_free(
|
|||
&state->target_identifiers,
|
||||
g_ptr_array_unref
|
||||
);
|
||||
g_clear_pointer(&state->relation_type_identifiers, g_array_unref);
|
||||
g_clear_pointer(&state->relation_type_codes, g_ptr_array_unref);
|
||||
g_clear_pointer(&state->relation_type_labels, g_ptr_array_unref);
|
||||
g_clear_object(&state->relation_type_string_list);
|
||||
create_relation_dialog_clear_preview(state);
|
||||
g_clear_pointer(&state->evidence_options, g_ptr_array_unref);
|
||||
g_clear_pointer(&state->evidence_check_buttons, g_ptr_array_unref);
|
||||
|
|
@ -533,12 +592,17 @@ static void create_relation_dialog_on_create_clicked(
|
|||
selected_index
|
||||
);
|
||||
|
||||
relation_type =
|
||||
gtk_editable_get_text(
|
||||
GTK_EDITABLE(
|
||||
state->relation_type_entry
|
||||
)
|
||||
);
|
||||
guint relation_type_index =
|
||||
gtk_drop_down_get_selected(state->relation_type_drop_down);
|
||||
if (relation_type_index == GTK_INVALID_LIST_POSITION ||
|
||||
relation_type_index >= state->relation_type_labels->len)
|
||||
{
|
||||
create_relation_dialog_show_error(
|
||||
state, "Sélectionnez un type de relation canonique.");
|
||||
return;
|
||||
}
|
||||
relation_type = g_ptr_array_index(
|
||||
state->relation_type_labels, relation_type_index);
|
||||
|
||||
label =
|
||||
gtk_editable_get_text(
|
||||
|
|
@ -617,6 +681,10 @@ static void create_relation_dialog_on_create_clicked(
|
|||
create_relation_dialog_duplicate_required_text(
|
||||
relation_type
|
||||
);
|
||||
result->relation_type_identifier = g_array_index(
|
||||
state->relation_type_identifiers, gint64, relation_type_index);
|
||||
result->relation_type_code = g_strdup(g_ptr_array_index(
|
||||
state->relation_type_codes, relation_type_index));
|
||||
|
||||
result->label =
|
||||
create_relation_dialog_duplicate_optional_text(
|
||||
|
|
@ -810,6 +878,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
const char *source_entity_identifier,
|
||||
const RelationRecord *existing_relation,
|
||||
const GPtrArray *entities,
|
||||
const GPtrArray *relation_types,
|
||||
const GPtrArray *evidence_records,
|
||||
const GPtrArray *selected_evidence_identifiers,
|
||||
const char *investigation_root_path,
|
||||
|
|
@ -823,6 +892,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
|
||||
GtkStringList *target_labels =
|
||||
NULL;
|
||||
GtkStringList *relation_type_labels = NULL;
|
||||
|
||||
GtkWidget *root_box =
|
||||
NULL;
|
||||
|
|
@ -1381,15 +1451,48 @@ static gboolean create_relation_dialog_present_internal(
|
|||
TRUE
|
||||
);
|
||||
|
||||
state->relation_type_entry =
|
||||
GTK_ENTRY(
|
||||
gtk_entry_new()
|
||||
);
|
||||
|
||||
gtk_entry_set_placeholder_text(
|
||||
state->relation_type_entry,
|
||||
"Ex. : utilise, contrôle, appartient à"
|
||||
);
|
||||
relation_type_labels = gtk_string_list_new(NULL);
|
||||
state->relation_type_string_list = g_object_ref(relation_type_labels);
|
||||
state->relation_type_identifiers =
|
||||
g_array_new(FALSE, FALSE, sizeof(gint64));
|
||||
state->relation_type_codes = g_ptr_array_new_with_free_func(g_free);
|
||||
state->relation_type_labels = g_ptr_array_new_with_free_func(g_free);
|
||||
guint selected_relation_type_index = 0U;
|
||||
for (guint type_index = 0U;
|
||||
relation_types != NULL && type_index < relation_types->len;
|
||||
type_index++)
|
||||
{
|
||||
const RelationType *type = g_ptr_array_index(relation_types, type_index);
|
||||
gint64 type_identifier = relation_type_get_identifier(type);
|
||||
gtk_string_list_append(relation_type_labels,
|
||||
relation_type_get_label(type));
|
||||
g_array_append_val(state->relation_type_identifiers, type_identifier);
|
||||
g_ptr_array_add(state->relation_type_codes,
|
||||
g_strdup(relation_type_get_code(type)));
|
||||
g_ptr_array_add(state->relation_type_labels,
|
||||
g_strdup(relation_type_get_label(type)));
|
||||
if (existing_relation != NULL &&
|
||||
type_identifier ==
|
||||
relation_record_get_relation_type_identifier(existing_relation))
|
||||
selected_relation_type_index = type_index;
|
||||
}
|
||||
state->relation_type_drop_down = GTK_DROP_DOWN(
|
||||
gtk_drop_down_new(G_LIST_MODEL(relation_type_labels), NULL));
|
||||
gtk_drop_down_set_selected(state->relation_type_drop_down,
|
||||
selected_relation_type_index);
|
||||
state->new_relation_type_entry = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->new_relation_type_entry,
|
||||
"Nouveau type…");
|
||||
GtkWidget *new_type_button = gtk_button_new_with_label("Ajouter");
|
||||
state->relation_type_control = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(state->relation_type_drop_down), TRUE);
|
||||
gtk_box_append(GTK_BOX(state->relation_type_control),
|
||||
GTK_WIDGET(state->relation_type_drop_down));
|
||||
gtk_box_append(GTK_BOX(state->relation_type_control),
|
||||
GTK_WIDGET(state->new_relation_type_entry));
|
||||
gtk_box_append(GTK_BOX(state->relation_type_control), new_type_button);
|
||||
g_signal_connect(new_type_button, "clicked",
|
||||
G_CALLBACK(create_relation_dialog_on_new_type_clicked), state);
|
||||
|
||||
state->label_entry =
|
||||
GTK_ENTRY(
|
||||
|
|
@ -1424,13 +1527,9 @@ static gboolean create_relation_dialog_present_internal(
|
|||
|
||||
if (existing_relation != NULL)
|
||||
{
|
||||
const char *relation_type = relation_record_get_relation_type(
|
||||
existing_relation);
|
||||
const char *label = relation_record_get_label(existing_relation);
|
||||
const char *justification = relation_record_get_justification(
|
||||
existing_relation);
|
||||
gtk_editable_set_text(GTK_EDITABLE(state->relation_type_entry),
|
||||
relation_type != NULL ? relation_type : "");
|
||||
gtk_editable_set_text(GTK_EDITABLE(state->label_entry),
|
||||
label != NULL ? label : "");
|
||||
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
|
||||
|
|
@ -1568,7 +1667,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
grid
|
||||
),
|
||||
GTK_WIDGET(
|
||||
state->relation_type_entry
|
||||
state->relation_type_control
|
||||
),
|
||||
1,
|
||||
1,
|
||||
|
|
@ -1809,7 +1908,7 @@ static gboolean create_relation_dialog_present_internal(
|
|||
|
||||
gtk_widget_grab_focus(
|
||||
GTK_WIDGET(
|
||||
state->relation_type_entry
|
||||
state->relation_type_drop_down
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -1822,16 +1921,19 @@ static gboolean create_relation_dialog_present_internal(
|
|||
|
||||
gboolean create_relation_dialog_present(GtkWindow *parent,
|
||||
const char *source_entity_identifier, const GPtrArray *entities,
|
||||
const GPtrArray *relation_types,
|
||||
const GPtrArray *evidence_records, const char *investigation_root_path,
|
||||
CreateRelationDialogCallback callback, gpointer user_data, GError **error)
|
||||
{
|
||||
return create_relation_dialog_present_internal(parent,
|
||||
source_entity_identifier, NULL, entities, evidence_records, NULL,
|
||||
source_entity_identifier, NULL, entities, relation_types,
|
||||
evidence_records, NULL,
|
||||
investigation_root_path, callback, user_data, error);
|
||||
}
|
||||
|
||||
gboolean edit_relation_dialog_present(GtkWindow *parent,
|
||||
const RelationRecord *relation_record, const GPtrArray *entities,
|
||||
const GPtrArray *relation_types,
|
||||
const GPtrArray *evidence_records,
|
||||
const GPtrArray *selected_evidence_identifiers,
|
||||
const char *investigation_root_path,
|
||||
|
|
@ -1846,7 +1948,7 @@ gboolean edit_relation_dialog_present(GtkWindow *parent,
|
|||
}
|
||||
return create_relation_dialog_present_internal(parent,
|
||||
relation_record_get_source_entity_identifier(relation_record),
|
||||
relation_record, entities, evidence_records,
|
||||
relation_record, entities, relation_types, evidence_records,
|
||||
selected_evidence_identifiers, investigation_root_path,
|
||||
callback, user_data, error);
|
||||
}
|
||||
|
|
@ -1867,6 +1969,7 @@ void create_relation_dialog_result_free(
|
|||
g_free(
|
||||
result->label
|
||||
);
|
||||
g_free(result->relation_type_code);
|
||||
|
||||
g_free(
|
||||
result->relation_type
|
||||
|
|
@ -1920,6 +2023,18 @@ const char *create_relation_dialog_result_get_relation_type(
|
|||
: NULL;
|
||||
}
|
||||
|
||||
gint64 create_relation_dialog_result_get_relation_type_identifier(
|
||||
const CreateRelationDialogResult *result)
|
||||
{
|
||||
return result != NULL ? result->relation_type_identifier : 0;
|
||||
}
|
||||
|
||||
const char *create_relation_dialog_result_get_relation_type_code(
|
||||
const CreateRelationDialogResult *result)
|
||||
{
|
||||
return result != NULL ? result->relation_type_code : NULL;
|
||||
}
|
||||
|
||||
const char *create_relation_dialog_result_get_label(
|
||||
const CreateRelationDialogResult *result
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3551,7 +3551,7 @@ static const char *investigation_graph_view_get_relation_title(
|
|||
}
|
||||
|
||||
relation_type =
|
||||
relation_record_get_relation_type(
|
||||
relation_record_get_relation_type_label(
|
||||
relation_record
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ static gboolean sidebar_filter_relation_row(
|
|||
return sidebar_text_contains_search(
|
||||
relation_record_get_label(relation_record), search_text) ||
|
||||
sidebar_text_contains_search(
|
||||
relation_record_get_relation_type(relation_record), search_text) ||
|
||||
relation_record_get_relation_type_label(relation_record), search_text) ||
|
||||
sidebar_text_contains_search(
|
||||
entity_record_get_value(source_entity), search_text) ||
|
||||
sidebar_text_contains_search(
|
||||
|
|
@ -1450,7 +1450,7 @@ void sidebar_set_graph_model(
|
|||
GtkWidget *title_label = gtk_label_new(
|
||||
label != NULL && label[0] != '\0'
|
||||
? label
|
||||
: relation_record_get_relation_type(relation_record)
|
||||
: relation_record_get_relation_type_label(relation_record)
|
||||
);
|
||||
GtkWidget *details_label = gtk_label_new(details);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "widgets/investigation_graph_view.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <gio/gio.h>
|
||||
|
||||
#define WORKSPACE_PAGE_WELCOME "welcome"
|
||||
#define WORKSPACE_PAGE_NODE_INFORMATION "node-information"
|
||||
|
|
@ -698,6 +699,26 @@ static void workspace_on_expand_evidence_preview(GtkButton *button,
|
|||
content = gtk_video_new_for_filename(workspace->evidence_preview_path);
|
||||
gtk_video_set_autoplay(GTK_VIDEO(content), FALSE);
|
||||
}
|
||||
else if (gtk_stack_get_visible_child(GTK_STACK(
|
||||
workspace->evidence_preview_stack)) ==
|
||||
workspace->evidence_preview_status)
|
||||
{
|
||||
GFile *file = g_file_new_for_path(workspace->evidence_preview_path);
|
||||
char *uri = g_file_get_uri(file);
|
||||
GError *error = NULL;
|
||||
if (uri == NULL || !g_app_info_launch_default_for_uri(uri, NULL,
|
||||
&error))
|
||||
{
|
||||
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
|
||||
error != NULL ? error->message :
|
||||
"Impossible d’ouvrir ce document.");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
g_free(uri);
|
||||
g_object_unref(file);
|
||||
gtk_window_destroy(window);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
content = gtk_picture_new_for_filename(workspace->evidence_preview_path);
|
||||
|
|
@ -708,6 +729,20 @@ static void workspace_on_expand_evidence_preview(GtkButton *button,
|
|||
gtk_window_present(window);
|
||||
}
|
||||
|
||||
/** @brief Ouvre l’aperçu agrandi lorsqu’un utilisateur clique dessus. */
|
||||
static void workspace_on_evidence_preview_clicked(GtkGestureClick *gesture,
|
||||
gint n_press, gdouble x, gdouble y, gpointer user_data)
|
||||
{
|
||||
Workspace *workspace = user_data;
|
||||
(void) gesture;
|
||||
(void) x;
|
||||
(void) y;
|
||||
if (workspace == NULL || n_press != 1 ||
|
||||
workspace->evidence_preview_path == NULL)
|
||||
return;
|
||||
workspace_on_expand_evidence_preview(NULL, workspace);
|
||||
}
|
||||
|
||||
/** @brief Convertit le statut d'une relation en texte utilisateur. */
|
||||
static const char *workspace_get_relation_status_text(RelationStatus status)
|
||||
{
|
||||
|
|
@ -842,7 +877,7 @@ static void workspace_on_graph_node_selected(
|
|||
"Source inconnue",
|
||||
target_entity != NULL ? entity_record_get_value(target_entity) :
|
||||
"Cible inconnue",
|
||||
relation_record_get_relation_type(relation_record),
|
||||
relation_record_get_relation_type_label(relation_record),
|
||||
relation_record_get_confidence(relation_record),
|
||||
workspace_get_relation_status_text(
|
||||
relation_record_get_status(relation_record)),
|
||||
|
|
@ -1370,7 +1405,7 @@ Workspace *workspace_new(void)
|
|||
evidence_content =
|
||||
gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL,
|
||||
16
|
||||
8
|
||||
);
|
||||
|
||||
if (evidence_content == NULL)
|
||||
|
|
@ -1384,22 +1419,22 @@ Workspace *workspace_new(void)
|
|||
|
||||
gtk_widget_set_margin_start(
|
||||
evidence_content,
|
||||
24
|
||||
12
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_end(
|
||||
evidence_content,
|
||||
24
|
||||
12
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_top(
|
||||
evidence_content,
|
||||
24
|
||||
12
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_bottom(
|
||||
evidence_content,
|
||||
24
|
||||
12
|
||||
);
|
||||
|
||||
workspace->evidence_name_label =
|
||||
|
|
@ -1454,8 +1489,13 @@ Workspace *workspace_new(void)
|
|||
workspace->evidence_preview_video = gtk_video_new();
|
||||
workspace->evidence_preview_expand_button =
|
||||
gtk_button_new_with_label("Agrandir l’aperçu");
|
||||
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 360);
|
||||
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 180);
|
||||
gtk_widget_set_vexpand(workspace->evidence_preview_stack, FALSE);
|
||||
gtk_widget_set_hexpand(workspace->evidence_preview_stack, TRUE);
|
||||
gtk_widget_set_size_request(workspace->evidence_preview_picture, -1, 180);
|
||||
gtk_widget_set_size_request(workspace->evidence_preview_video, -1, 180);
|
||||
gtk_widget_set_vexpand(workspace->evidence_preview_picture, FALSE);
|
||||
gtk_widget_set_vexpand(workspace->evidence_preview_video, FALSE);
|
||||
gtk_picture_set_can_shrink(GTK_PICTURE(
|
||||
workspace->evidence_preview_picture), TRUE);
|
||||
gtk_picture_set_content_fit(GTK_PICTURE(
|
||||
|
|
@ -1473,6 +1513,23 @@ Workspace *workspace_new(void)
|
|||
workspace->evidence_preview_picture, "image");
|
||||
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
|
||||
workspace->evidence_preview_video, "video");
|
||||
{
|
||||
GtkGesture *gesture = gtk_gesture_click_new();
|
||||
gtk_widget_add_controller(workspace->evidence_preview_picture,
|
||||
GTK_EVENT_CONTROLLER(gesture));
|
||||
g_signal_connect(gesture, "released",
|
||||
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
|
||||
gesture = gtk_gesture_click_new();
|
||||
gtk_widget_add_controller(workspace->evidence_preview_video,
|
||||
GTK_EVENT_CONTROLLER(gesture));
|
||||
g_signal_connect(gesture, "released",
|
||||
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
|
||||
gesture = gtk_gesture_click_new();
|
||||
gtk_widget_add_controller(workspace->evidence_preview_text,
|
||||
GTK_EVENT_CONTROLLER(gesture));
|
||||
g_signal_connect(gesture, "released",
|
||||
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
|
||||
}
|
||||
gtk_stack_set_visible_child_name(GTK_STACK(
|
||||
workspace->evidence_preview_stack), "status");
|
||||
gtk_widget_set_halign(workspace->evidence_preview_expand_button,
|
||||
|
|
@ -2962,7 +3019,8 @@ void workspace_set_evidence_preview(Workspace *workspace,
|
|||
g_free(contents);
|
||||
}
|
||||
message = g_strdup_printf(
|
||||
"Aperçu indisponible pour %s%s%s.",
|
||||
"Aperçu intégré indisponible pour %s%s%s. Utilisez le bouton "
|
||||
"pour ouvrir le document avec l’application associée.",
|
||||
display_name != NULL ? display_name : "ce fichier",
|
||||
error != NULL ? " : " : "",
|
||||
error != NULL ? error->message : "format non pris en charge");
|
||||
|
|
@ -2970,6 +3028,12 @@ void workspace_set_evidence_preview(Workspace *workspace,
|
|||
message);
|
||||
gtk_stack_set_visible_child_name(GTK_STACK(
|
||||
workspace->evidence_preview_stack), "status");
|
||||
if (g_file_test(file_path, G_FILE_TEST_IS_REGULAR))
|
||||
{
|
||||
workspace->evidence_preview_path = g_strdup(file_path);
|
||||
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
|
||||
workspace->evidence_preview_path != NULL);
|
||||
}
|
||||
g_free(message);
|
||||
goto cleanup;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
|
|||
"FROM investigation;"
|
||||
);
|
||||
|
||||
assert(strcmp(schema_version, "8") == 0);
|
||||
assert(strcmp(schema_version, "9") == 0);
|
||||
test_database_assert_table_exists(database, "relation_types");
|
||||
test_database_assert_table_exists(database, "graph_viewport");
|
||||
test_database_assert_table_exists(database, "osint_executions");
|
||||
test_database_assert_table_exists(database, "osint_execution_entities");
|
||||
|
|
@ -990,7 +991,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"8"
|
||||
"9"
|
||||
) == 0
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ static void test_graph_loader_insert_inconsistent_relation(
|
|||
" entite_source_id,"
|
||||
" entite_cible_id,"
|
||||
" type_relation,"
|
||||
" relation_type_id,"
|
||||
" confiance,"
|
||||
" created_at,"
|
||||
" updated_at,"
|
||||
|
|
@ -502,6 +503,7 @@ static void test_graph_loader_insert_inconsistent_relation(
|
|||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" (SELECT id FROM relation_types WHERE code='uses'),"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
|
|
|
|||
|
|
@ -1990,6 +1990,7 @@ static void test_relation_dao_reflexive_schema_constraint(void)
|
|||
" entite_source_id,"
|
||||
" entite_cible_id,"
|
||||
" type_relation,"
|
||||
" relation_type_id,"
|
||||
" confiance,"
|
||||
" created_at,"
|
||||
" updated_at,"
|
||||
|
|
@ -2001,6 +2002,7 @@ static void test_relation_dao_reflexive_schema_constraint(void)
|
|||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" (SELECT id FROM relation_types WHERE code='uses'),"
|
||||
" ?,"
|
||||
" ?,"
|
||||
" ?,"
|
||||
|
|
|
|||
37
tests/test_relation_type_normalizer.c
Normal file
37
tests/test_relation_type_normalizer.c
Normal 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();
|
||||
}
|
||||
53
tests/test_relation_type_service.c
Normal file
53
tests/test_relation_type_service.c
Normal 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();
|
||||
}
|
||||
Loading…
Reference in a new issue