feat(web): checkpoint web-only assisted investigation
This commit is contained in:
parent
3488f67b2a
commit
9495e1258a
135 changed files with 5108 additions and 48089 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,17 @@
|
|||
## [0.1.0-dev] — en préparation, non publiée
|
||||
|
||||
### Interface Web finale locale
|
||||
|
||||
- Web devient l’interface utilisateur `CURRENT` unique : coque Catppuccin
|
||||
Mocha/Lavender Agent | Graphe | Activité, drawer transversal et cartes
|
||||
SPECIMEN pour conversation, plan, hypothèse, autorisation et résultat.
|
||||
- Retrait du client, des vues, widgets, tests, headers et dépendance de build
|
||||
GTK4 ; les services C, SQLite V20, fondation financière V21, capabilities et
|
||||
JobStore V4 sont conservés.
|
||||
- Ajout de `tools/labfy` : instance loopback vérifiée, navigateur ouvert et
|
||||
session HttpOnly/SameSite automatique sans code ni jeton dans l’URL ; les
|
||||
mutations restent protégées par Host, Origin exact et CSRF.
|
||||
|
||||
### Connexion Web
|
||||
|
||||
- La session de `3600` secondes (une heure) commence à l'authentification, et non au démarrage
|
||||
|
|
@ -146,6 +158,21 @@
|
|||
- Finalisation JobStore V3 : budgets opposables au claim, temps monotone,
|
||||
réserve conservée après crash, états de plan exportés et scénario Firefox J8.
|
||||
|
||||
# Changements locaux non publiés — recherche OSINT assistée V1
|
||||
|
||||
- JobStore V4 additif à V3 : persistance atomique et idempotente des plans,
|
||||
actions, grants, campagnes, résultats et reçus de recherche, sans évolution
|
||||
de V20 ni de la tranche financière V21.
|
||||
- Policy explicite de sélection, exclusions, révocation, expiration, identité
|
||||
d'enquête/plan/action et budgets ; aucune propagation implicite par seed,
|
||||
CNAME, redirection ou nouvelle URL.
|
||||
- Transport commun indisponible sans backend lié et libcurl couvert seulement
|
||||
par fixtures/loopback ; aucun fournisseur public n'est configuré ou qualifié.
|
||||
- Adapters normalisateurs DNS, RDAP, CDX, Brave, SearXNG et page, avec résultats
|
||||
bornés, provenance C et absence de suivi implicite de liens, scripts ou pièces jointes.
|
||||
- Parcours Web synthétique de préparation, approbation/refus, deux vagues et
|
||||
révocation durable, avec isolation vérifiée entre espaces de travail A/B.
|
||||
|
||||
# Changements locaux non publiés — J9
|
||||
|
||||
## Espace local et import navigateur (non publié)
|
||||
|
|
|
|||
132
Makefile
132
Makefile
|
|
@ -1,7 +1,7 @@
|
|||
CC = gcc
|
||||
|
||||
PKG_CONFIG = pkg-config
|
||||
REQUIRED_PACKAGES = gtk4 sqlite3 libheif poppler-glib json-glib-1.0
|
||||
REQUIRED_PACKAGES = glib-2.0 gio-2.0 sqlite3 libheif poppler-glib json-glib-1.0
|
||||
ifeq ($(shell $(PKG_CONFIG) --exists $(REQUIRED_PACKAGES) && echo yes),)
|
||||
$(error Dépendances de compilation manquantes : $(REQUIRED_PACKAGES). Voir docs/DEPENDENCE.md)
|
||||
endif
|
||||
|
|
@ -11,13 +11,9 @@ endif
|
|||
SOURCE_SIZE_LIMIT := 2000
|
||||
# Exceptions historiques : plafonds constatés avant la tranche V18.
|
||||
SOURCE_SIZE_EXCEPTIONS := \
|
||||
src/core/application.c:9325 \
|
||||
src/widgets/investigation_graph_view.c:4901 \
|
||||
src/widgets/workspace.c:4071 \
|
||||
src/views/main_window.c:2239 \
|
||||
src/views/create_relation_dialog.c:2043
|
||||
src/core/local_job_store.c:2000
|
||||
|
||||
.PHONY: check-source-size web web-start web-status web-stop web-code web-workspace web-workspace-j7 web-workspace-j8 web-workspace-j9 web-workspace-local web-workspace-review-check web-workspace-web-check
|
||||
.PHONY: check-source-size check-research-libcurl web web-start web-status web-stop web-workspace web-workspace-j7 web-workspace-j8 web-workspace-j9 web-workspace-local web-workspace-review-check web-workspace-web-check
|
||||
.NOTPARALLEL: web-workspace-review-check
|
||||
|
||||
# CONTRACT: le poste Web possède sa bibliothèque explicite, séparée des
|
||||
|
|
@ -27,10 +23,10 @@ PORT ?= 8081
|
|||
LIBRARY ?= $(shell python3 -c 'import os; from pathlib import Path; print(Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "labfy-investigation" / "web-8081-lab")')
|
||||
|
||||
web: tools/local-jobs
|
||||
tools/labfy-web serve --port "$(PORT)" --library "$(LIBRARY)"
|
||||
LABFY_LIBRARY="$(LIBRARY)" tools/labfy
|
||||
|
||||
web-start: tools/local-jobs
|
||||
tools/labfy-web start --port "$(PORT)" --library "$(LIBRARY)"
|
||||
tools/labfy-web start --automatic-session --open-browser --port "$(PORT)" --library "$(LIBRARY)"
|
||||
|
||||
web-status:
|
||||
tools/labfy-web status
|
||||
|
|
@ -38,14 +34,12 @@ web-status:
|
|||
web-stop:
|
||||
tools/labfy-web stop
|
||||
|
||||
web-code:
|
||||
tools/labfy-web code
|
||||
|
||||
check-source-size:
|
||||
@limit=$(SOURCE_SIZE_LIMIT); failed=0; \
|
||||
files="$$(git ls-files --cached --others --exclude-standard -- \
|
||||
'src/*.c' 'src/**/*.c' | sort -u)"; \
|
||||
for file in $$files; do \
|
||||
[ -f "$$file" ] || continue; \
|
||||
lines=$$(wc -l < "$$file"); allowed=$$limit; historical=0; \
|
||||
for exception in $(SOURCE_SIZE_EXCEPTIONS); do \
|
||||
case "$$exception" in "$$file":*) \
|
||||
|
|
@ -61,6 +55,14 @@ check-source-size:
|
|||
done; \
|
||||
exit $$failed
|
||||
|
||||
# CONTRACT: le diagnostic libcurl est limité aux cibles qui compilent le
|
||||
# transport HTTP concret ; les tests de policy/adapters restent indépendants.
|
||||
check-research-libcurl:
|
||||
@$(PKG_CONFIG) --exists libcurl || { \
|
||||
echo "Dépendance de compilation manquante : libcurl (requis par le transport HTTP de recherche)." >&2; \
|
||||
exit 2; \
|
||||
}
|
||||
|
||||
web-workspace: tools/local-jobs
|
||||
@test -n "$(WORKSPACE)" || (echo "Usage: make web-workspace WORKSPACE=/tmp/labfy-local-workspace"; exit 2)
|
||||
python3 prototypes/web-graph/run_web_workspace.py --workspace "$(WORKSPACE)"
|
||||
|
|
@ -111,9 +113,9 @@ CFLAGS = -std=c17 \
|
|||
-Iinclude \
|
||||
-MMD \
|
||||
-MP \
|
||||
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3 libheif poppler-glib json-glib-1.0)
|
||||
$(shell $(PKG_CONFIG) --cflags glib-2.0 gio-2.0 sqlite3 libheif poppler-glib json-glib-1.0)
|
||||
|
||||
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3 libheif poppler-glib json-glib-1.0) -ljpeg
|
||||
LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0 sqlite3 libheif poppler-glib json-glib-1.0) -ljpeg
|
||||
|
||||
TEST_CFLAGS = -std=c17 \
|
||||
-Wall \
|
||||
|
|
@ -130,16 +132,6 @@ EVIDENCE_TYPE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
|||
ENTITY_TYPE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
EVIDENCE_TYPE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
GRAPH_NODE_POSITION_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS := \
|
||||
-std=c17 \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Werror \
|
||||
-Iinclude \
|
||||
$(shell $(PKG_CONFIG) --cflags gtk4)
|
||||
|
||||
EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := $(shell $(PKG_CONFIG) --libs gtk4)
|
||||
|
||||
EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
ENTITY_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
ENTITY_TYPE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
|
||||
|
|
@ -285,6 +277,31 @@ EML_GRAPH_DEMO := tools/eml_graph_demo
|
|||
LOCAL_TOOLKIT_DEMO := tools/local_toolkit_demo
|
||||
LOCAL_JOBS := tools/local-jobs
|
||||
LOCAL_JOBS_TEST := tools/local-jobs-test
|
||||
TEST_RESEARCH_STORE := tests/test_research_store
|
||||
TEST_RESEARCH_POLICY := tests/test_research_policy
|
||||
TEST_RESEARCH_TRANSPORT := tests/test_research_transport
|
||||
TEST_RESEARCH_ADAPTERS := tests/test_research_adapters
|
||||
|
||||
# CONTRACT: seul le transport HTTP concret lie libcurl ; les contrats et
|
||||
# adapters restent utilisables dans les tests et outils sans backend réseau.
|
||||
RESEARCH_SOURCES := \
|
||||
src/core/research_contracts.c \
|
||||
src/core/research_policy.c \
|
||||
src/core/research_transport.c \
|
||||
src/core/research_http_transport_curl.c \
|
||||
src/core/research_adapter_dns.c \
|
||||
src/core/research_adapter_rdap.c \
|
||||
src/core/research_adapter_cdx.c \
|
||||
src/core/research_adapter_search.c \
|
||||
src/core/research_adapter_page.c
|
||||
RESEARCH_CURL_CFLAGS := $(shell $(PKG_CONFIG) --cflags libcurl)
|
||||
RESEARCH_CURL_LDFLAGS := $(shell $(PKG_CONFIG) --libs libcurl)
|
||||
RESEARCH_CURL_TARGETS := $(TARGET) $(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) $(TEST_WORKSPACE_IDENTITY_OCR_GTK)
|
||||
|
||||
$(RESEARCH_CURL_TARGETS): check-research-libcurl
|
||||
$(RESEARCH_CURL_TARGETS): CFLAGS += $(RESEARCH_CURL_CFLAGS)
|
||||
$(RESEARCH_CURL_TARGETS): LDFLAGS += $(RESEARCH_CURL_LDFLAGS)
|
||||
|
||||
LOCAL_REVIEW_PREVIEW_SOURCES := \
|
||||
src/core/observation_review_service.c \
|
||||
|
|
@ -362,6 +379,28 @@ $(TEST_LOCAL_CAPABILITY_REGISTRY): tests/test_local_capability_registry.c \
|
|||
$(TEST_LOCAL_JOB_STORE): tests/test_local_job_store.c src/core/local_job_store.c
|
||||
$(CC) $(CORE_GRAPH_CFLAGS) -DLOCAL_JOB_STORE_ENABLE_TEST_HOOKS $^ -o $@ $(CORE_GRAPH_LDFLAGS)
|
||||
|
||||
$(TEST_RESEARCH_STORE): tests/test_research_store.c \
|
||||
src/core/local_job_store.c src/core/research_contracts.c
|
||||
$(CC) $(CORE_GRAPH_CFLAGS) $^ -o $@ $(CORE_GRAPH_LDFLAGS)
|
||||
|
||||
$(TEST_RESEARCH_POLICY): tests/test_research_policy.c \
|
||||
src/core/research_contracts.c src/core/research_policy.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_RESEARCH_TRANSPORT): check-research-libcurl \
|
||||
tests/test_research_transport.c src/core/research_contracts.c \
|
||||
src/core/research_policy.c src/core/research_transport.c \
|
||||
src/core/research_http_transport_curl.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic -DRESEARCH_TRANSPORT_ENABLE_TEST_HOOKS \
|
||||
$(RESEARCH_CURL_CFLAGS) $(filter %.c,$^) -o $@ $(TEST_LDFLAGS) \
|
||||
$(RESEARCH_CURL_LDFLAGS)
|
||||
|
||||
$(TEST_RESEARCH_ADAPTERS): tests/test_research_adapters.c \
|
||||
src/core/research_contracts.c src/core/research_adapter_dns.c \
|
||||
src/core/research_adapter_rdap.c src/core/research_adapter_cdx.c \
|
||||
src/core/research_adapter_search.c src/core/research_adapter_page.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_LOCAL_CORRELATION): tests/test_local_correlation.c \
|
||||
src/core/local_correlation_service.c \
|
||||
src/dao/evidence_dao.c src/dao/evidence_entity_dao.c \
|
||||
|
|
@ -387,13 +426,17 @@ $(TEST_OBSERVATION_REVIEW_SERVICE): tests/test_observation_review_service.c \
|
|||
|
||||
$(LOCAL_JOBS): tools/local_jobs.c src/core/local_job_store.c \
|
||||
src/core/local_planner_service.c \
|
||||
$(LOCAL_REVIEW_PREVIEW_SOURCES) $(LOCAL_TOOLKIT_SOURCES)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
$(LOCAL_REVIEW_PREVIEW_SOURCES) $(LOCAL_TOOLKIT_SOURCES) $(RESEARCH_SOURCES) \
|
||||
check-research-libcurl
|
||||
$(CC) $(CFLAGS) $(RESEARCH_CURL_CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS) \
|
||||
$(RESEARCH_CURL_LDFLAGS)
|
||||
|
||||
$(LOCAL_JOBS_TEST): tools/local_jobs.c src/core/local_job_store.c \
|
||||
src/core/local_planner_service.c \
|
||||
$(LOCAL_REVIEW_PREVIEW_SOURCES) $(LOCAL_TOOLKIT_SOURCES)
|
||||
$(CC) $(CFLAGS) -DLOCAL_JOBS_ENABLE_CRASH_HOOK $^ -o $@ $(LDFLAGS)
|
||||
$(LOCAL_REVIEW_PREVIEW_SOURCES) $(LOCAL_TOOLKIT_SOURCES) $(RESEARCH_SOURCES) \
|
||||
check-research-libcurl
|
||||
$(CC) $(CFLAGS) -DLOCAL_JOBS_ENABLE_CRASH_HOOK $(RESEARCH_CURL_CFLAGS) \
|
||||
$(filter %.c,$^) -o $@ $(LDFLAGS) $(RESEARCH_CURL_LDFLAGS)
|
||||
|
||||
$(TEST_DIALOG_GEOMETRY_GTK): tests/test_dialog_geometry_gtk.c \
|
||||
src/views/dialog_geometry.c
|
||||
|
|
@ -454,7 +497,10 @@ $(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK): tests/test_person_ocr_projection_edito
|
|||
src/models/identity_traceability.c
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
all: $(TARGET)
|
||||
# CONTRACT: le produit utilisateur est le poste Web local. La cible principale
|
||||
# construit le bridge C qui conserve les services, SQLite et le JobStore V4,
|
||||
# sans lier un renderer de bureau historique.
|
||||
all: $(LOCAL_JOBS)
|
||||
|
||||
.PHONY: core-graph-demo eml-graph-demo local-toolkit-demo
|
||||
core-graph-demo: $(CORE_GRAPH_DEMO)
|
||||
|
|
@ -1399,7 +1445,7 @@ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
|
|||
$(CC) $(INVESTIGATION_GRAPH_LOAD_TASK_TEST_CFLAGS) $^ -o $@ \
|
||||
$(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
test: \
|
||||
test-gtk-legacy: \
|
||||
$(TEST_NODE) \
|
||||
$(TEST_TREE_MODEL) \
|
||||
$(TEST_TREE_BUILDER) \
|
||||
|
|
@ -1494,6 +1540,10 @@ test: \
|
|||
$(TEST_LOCAL_TOOL_RUNNER) \
|
||||
$(TEST_LOCAL_CAPABILITY_REGISTRY) \
|
||||
$(TEST_LOCAL_JOB_STORE) \
|
||||
$(TEST_RESEARCH_STORE) \
|
||||
$(TEST_RESEARCH_POLICY) \
|
||||
$(TEST_RESEARCH_TRANSPORT) \
|
||||
$(TEST_RESEARCH_ADAPTERS) \
|
||||
$(TEST_LOCAL_CORRELATION) \
|
||||
$(TEST_LOCAL_PLANNER) \
|
||||
$(TEST_LOCAL_REPORT) \
|
||||
|
|
@ -1609,6 +1659,10 @@ test: \
|
|||
@$(TEST_LOCAL_TOOL_RUNNER)
|
||||
@$(TEST_LOCAL_CAPABILITY_REGISTRY)
|
||||
@$(TEST_LOCAL_JOB_STORE)
|
||||
@$(TEST_RESEARCH_STORE)
|
||||
@$(TEST_RESEARCH_POLICY)
|
||||
@$(TEST_RESEARCH_TRANSPORT)
|
||||
@$(TEST_RESEARCH_ADAPTERS)
|
||||
@$(TEST_LOCAL_CORRELATION)
|
||||
@$(TEST_LOCAL_REPORT)
|
||||
@$(TEST_OBSERVATION_REVIEW_SERVICE)
|
||||
|
|
@ -1632,6 +1686,24 @@ test: \
|
|||
%.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
# CONTRACT: la suite canonique valide les services restant dans le produit
|
||||
# Web-only. Les anciennes assertions de widgets GTK sont retirées avec leur
|
||||
# renderer, sans retirer les tests C de persistance, graphes, jobs et policy.
|
||||
test: $(TEST_LOCAL_JOB_STORE) $(TEST_RESEARCH_STORE) $(TEST_RESEARCH_POLICY) \
|
||||
$(TEST_RESEARCH_TRANSPORT) $(TEST_RESEARCH_ADAPTERS) \
|
||||
$(TEST_SCHEMA_V21) $(TEST_FINANCIAL_DAO) $(TEST_FINANCIAL_FOUNDATION) \
|
||||
$(TEST_CORE_GRAPH) $(LOCAL_JOBS_TEST)
|
||||
@./$(TEST_LOCAL_JOB_STORE)
|
||||
@./$(TEST_RESEARCH_STORE)
|
||||
@./$(TEST_RESEARCH_POLICY)
|
||||
@./$(TEST_RESEARCH_TRANSPORT)
|
||||
@./$(TEST_RESEARCH_ADAPTERS)
|
||||
@./$(TEST_SCHEMA_V21)
|
||||
@./$(TEST_FINANCIAL_DAO)
|
||||
@./$(TEST_FINANCIAL_FOUNDATION)
|
||||
@./$(TEST_CORE_GRAPH)
|
||||
@echo "Tous les tests Web-only C sont valides."
|
||||
|
||||
run: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -3,6 +3,11 @@
|
|||
Le parcours `CURRENT` de jobs locaux persistants et de reprise J5 est documenté
|
||||
dans [docs/architecture/LOCAL_JOBS_RECOVERY.md](docs/architecture/LOCAL_JOBS_RECOVERY.md).
|
||||
|
||||
La [recherche OSINT assistée V1](docs/architecture/OSINT_ASSISTED_RESEARCH.md)
|
||||
ajoute au JobStore V4 des plans, grants et reçus de policy distincts, validés
|
||||
uniquement avec des fixtures et un fournisseur loopback `SPECIMEN`. Aucun
|
||||
fournisseur public n'est configuré ou qualifié ; V20 et V21 ne sont pas modifiés.
|
||||
|
||||
Le premier poste Web local pilotable J6 est documenté dans
|
||||
[docs/architecture/WEB_WORKSPACE_CONTROL.md](docs/architecture/WEB_WORKSPACE_CONTROL.md).
|
||||
|
||||
|
|
@ -67,14 +72,14 @@ données. Labfy vise à relier leurs résultats dans une enquête cohérente :
|
|||
La cible de développement est **v0.1.0**. Le projet reste en développement actif
|
||||
et n'est pas prêt pour un usage opérationnel en production.
|
||||
|
||||
**Interface GTK conservée ; poste Web local contrôlé disponible.** La fondation
|
||||
v0.1.0 reste en développement : le poste Web réutilise le moteur de jobs
|
||||
persistant existant et une bibliothèque locale explicitement choisie, sans
|
||||
constituer une interface de production ni une autonomie générale.
|
||||
**Web est l’interface utilisateur CURRENT ; GTK est supprimé.** La fondation
|
||||
v0.1.0 reste en développement : le poste local réutilise le moteur de jobs
|
||||
persistant, la bibliothèque explicitement choisie et les capabilities C, sans
|
||||
constituer une autonomie générale ni un service distant.
|
||||
|
||||
### Disponible aujourd'hui
|
||||
|
||||
L'application actuelle est une application de bureau GTK4 en C17. Elle fournit
|
||||
Le produit actuel est un poste Web local-first devant le cœur C17. Il fournit
|
||||
notamment :
|
||||
|
||||
- création et ouverture d'enquêtes autonomes avec SQLite versionné ;
|
||||
|
|
@ -207,10 +212,11 @@ effectuées par des adapters, sous contrôle des politiques applicables.
|
|||
|
||||
## Interface et expérience cible
|
||||
|
||||
GTK demeure temporairement l'interface fonctionnelle existante. La future
|
||||
interface principale sera Web, avec le graphe comme espace de travail central.
|
||||
Le protocole local, le mécanisme d'événements et le framework frontend ne sont
|
||||
pas encore sélectionnés.
|
||||
Le Web est l’interface fonctionnelle unique, avec le graphe comme espace de
|
||||
travail central. Sa coque organise **Agent | Graphe | Activité**, complétée par
|
||||
un drawer transversal de détails, provenance, timeline, recherches,
|
||||
observations, finance, rapports et tâches. Qwen local, tool calling et le
|
||||
governor AMD/RAM/swap restent `TARGET`.
|
||||
|
||||
La cible comprend :
|
||||
|
||||
|
|
@ -278,7 +284,7 @@ Les dépendances et procédures Arch Linux et Ubuntu sont décrites dans
|
|||
```bash
|
||||
make -j8
|
||||
make test
|
||||
make run
|
||||
tools/labfy
|
||||
```
|
||||
|
||||
La démonstration cœur/Web synthétique se lance séparément :
|
||||
|
|
@ -293,21 +299,23 @@ python3 run_core_demo.py --port 8765
|
|||
# ou : python3 run_local_toolkit_demo.py --port 8765
|
||||
```
|
||||
|
||||
Le projet exige C17, GTK4, GLib/GIO, SQLite, libheif et Poppler GLib. Plusieurs
|
||||
outils d'analyse sont optionnels et ne sont jamais installés automatiquement.
|
||||
Le projet exige C17, GLib/GIO, SQLite, libheif et Poppler GLib. GTK4 n’est plus
|
||||
une dépendance de compilation. Plusieurs outils d'analyse sont optionnels et
|
||||
ne sont jamais installés automatiquement.
|
||||
outils d'analyse sont optionnels et ne sont jamais installés automatiquement.
|
||||
|
||||
Le poste Web local se lance depuis une bibliothèque explicitement désignée ;
|
||||
il écoute par défaut sur `127.0.0.1:8081` :
|
||||
|
||||
```bash
|
||||
make web PORT=8081 LIBRARY=/chemin/vers/la-bibliotheque-locale
|
||||
LABFY_LIBRARY=/chemin/vers/la-bibliotheque-locale tools/labfy
|
||||
```
|
||||
|
||||
Les commandes `start`, `status`, `stop` et `code` de ce lanceur, ainsi que les
|
||||
contrats de stockage local et de session, sont décrits dans
|
||||
[le contrôle du poste Web](docs/architecture/WEB_WORKSPACE_CONTROL.md). Le
|
||||
code éphémère affiché au démarrage reste privé : il ne doit ni être ajouté à une
|
||||
commande, ni enregistré dans la documentation.
|
||||
Le lanceur rejoint ou démarre une instance locale possédée, ouvre le navigateur
|
||||
et établit une session HttpOnly sans code manuel ni jeton dans l’URL. Les
|
||||
contrats de stockage local et de session sont décrits dans
|
||||
[le contrôle du poste Web](docs/architecture/WEB_WORKSPACE_CONTROL.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
@ -318,7 +326,9 @@ commande, ni enregistré dans la documentation.
|
|||
- [Décisions architecturales](docs/architecture/decisions/0001-web-local-first.md)
|
||||
- [Toolkit et statuts d'intégration](docs/osint/TOOLKIT.md)
|
||||
- [Toolkit local et runner borné J4](docs/architecture/LOCAL_TOOLKIT_RUNNER.md)
|
||||
- [Recherche OSINT assistée V1 en laboratoire](docs/architecture/OSINT_ASSISTED_RESEARCH.md)
|
||||
- [Design system cible](docs/ui/DESIGN_SYSTEM.md)
|
||||
- [Matrice de parité Web / GTK](docs/ui/WEB_GTK_PARITY.md)
|
||||
- [Sécurité, scope et secrets](docs/security/SECURITY_MODEL.md)
|
||||
- [Roadmap canonique](docs/ROADMAP.md)
|
||||
- [Backlog préparé](docs/BACKLOG.md)
|
||||
|
|
|
|||
|
|
@ -8,10 +8,19 @@ Le contrôle navigateur local sur espace exclusivement synthétique est décrit
|
|||
dans [WEB_WORKSPACE_CONTROL.md](architecture/WEB_WORKSPACE_CONTROL.md). Cette
|
||||
surface mutationnelle J6 est distincte des démonstrations read-only J2–J5.
|
||||
|
||||
La [recherche OSINT assistée V1](architecture/OSINT_ASSISTED_RESEARCH.md) est
|
||||
`CURRENT` dans le worktree uniquement pour son parcours de laboratoire : le
|
||||
JobStore V4 est additif à V3, les grants sont explicites et aucun fournisseur
|
||||
public n'est configuré ou qualifié. Elle ne modifie ni V20 ni V21.
|
||||
|
||||
> **Statut :** direction canonique v0.1.0
|
||||
> **Dernière mise à jour :** 2026-09-25
|
||||
> **Dernière mise à jour :** 2026-09-27
|
||||
> **Portée :** architecture actuelle et cible Labfy V2
|
||||
|
||||
> **Interface CURRENT :** le Web local est l'unique interface utilisateur ;
|
||||
> GTK et ses tests/widgets de renderer sont retirés. Le cœur C17, SQLite, les
|
||||
> services, capabilities et JobStore V4 n'en dépendent pas.
|
||||
|
||||
## 1. Objet et niveaux de maturité
|
||||
|
||||
Labfy Investigation est un environnement local-first d'investigation numérique
|
||||
|
|
@ -123,11 +132,11 @@ une hypothèse. Il ne devient pas silencieusement une identité confirmée.
|
|||
|
||||
## 3. Architecture actuelle — CURRENT
|
||||
|
||||
L'application existante est principalement écrite en C17 avec GTK4, GLib/GIO
|
||||
Le produit actuel est principalement écrit en C17 avec GLib/GIO
|
||||
et SQLite.
|
||||
|
||||
```text
|
||||
GTK views/widgets
|
||||
Web frontend local
|
||||
│ intentions et affichage
|
||||
Application / services / tasks
|
||||
│ orchestration
|
||||
|
|
@ -154,7 +163,7 @@ Le socle comprend déjà :
|
|||
|
||||
- enquêtes autonomes et base versionnée ;
|
||||
- preuves, extractions, entités et relations ;
|
||||
- graphe interactif GTK et positions persistantes ;
|
||||
- graphe Web interactif et positions persistantes ;
|
||||
- tâches asynchrones en mémoire ;
|
||||
- registre initial d'outils ;
|
||||
- premiers parcours DNS et documentaires ;
|
||||
|
|
@ -208,7 +217,7 @@ pas une réécriture générale du cœur fonctionnel.
|
|||
SQLite
|
||||
```
|
||||
|
||||
La future interface principale est Web. GTK reste l'interface fonctionnelle
|
||||
L'interface principale actuelle est Web. GTK est supprimé ;
|
||||
temporaire et ne sera pas supprimé avant qu'un remplacement validé couvre les
|
||||
parcours nécessaires.
|
||||
|
||||
|
|
@ -599,7 +608,7 @@ Le parcours local est limité à une instance loopback et à des validations sur
|
|||
fixtures `SPECIMEN`. Son lanceur peut ouvrir une bibliothèque locale
|
||||
explicitement choisie, en créer les enquêtes par le bridge C et n'en active
|
||||
qu'une seule dans une instance. Il ne rouvre pas implicitement l'enquête d'une
|
||||
session précédente. Cette surface ne change pas son statut : GTK reste le
|
||||
session précédente. Cette surface ne change pas son statut : le Web reste le
|
||||
parcours fonctionnel actuel et le serveur, son packaging ainsi que le choix du
|
||||
renderer de production restent `UNDECIDED`.
|
||||
|
||||
|
|
@ -647,3 +656,25 @@ signaux exploratoires ; ils ne prouvent ni identité ni relation métier.
|
|||
Le durcissement P01–P06 couvre le cycle de vie des imports, les réservations,
|
||||
le rejeu, les états terminaux et le framing HTTP du corps. Il ne transforme pas
|
||||
le protocole local en service distant ni en garantie de production.
|
||||
|
||||
## 18. Recherche OSINT assistée V1 — CURRENT de laboratoire
|
||||
|
||||
Le JobStore V4 conserve le worker mono-propriétaire et ajoute des objets
|
||||
distincts de `local_plan.v1` et de session : plan de recherche, action,
|
||||
`ScopeGrant`, campagne, résultat et reçu de policy. Les admissions sont
|
||||
idempotentes et atomiques ; la policy expose les refus d'exclusion, révocation,
|
||||
expiration, identité d'enquête/plan/action ou budget. Une seed dérivée, une
|
||||
redirection, un CNAME ou une URL nouvelle ne propage jamais le scope.
|
||||
|
||||
Le transport commun demeure indisponible sans backend lié. La variante libcurl
|
||||
est validée seulement contre le fournisseur loopback `SPECIMEN` du harness et
|
||||
rejette les adresses non publiques ; elle ne qualifie aucun fournisseur public
|
||||
ni configuration de production. Les adapters DNS, RDAP, CDX, Brave, SearXNG et
|
||||
page normalisent des fixtures bornées ; une `next_piste` demande toujours une
|
||||
nouvelle préparation et décision humaine.
|
||||
|
||||
Le panneau Web local prépare un plan, permet l'approbation ou le refus explicite
|
||||
des actions, puis admet une campagne en deux vagues séparées. Les sessions et
|
||||
CSRF Web restent hors des contrats durables ; les tests synthétiques vérifient
|
||||
l'isolation des espaces A/B. Le détail canonique, les procédures de harness et
|
||||
les limites sont dans [OSINT_ASSISTED_RESEARCH.md](architecture/OSINT_ASSISTED_RESEARCH.md).
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ n’est persisté.
|
|||
| Vidéo GTK | `gst-plugins-base`, `gst-plugins-good` | `gstreamer1.0-plugins-base`, `gstreamer1.0-plugins-good` |
|
||||
|
||||
Les codecs vidéo supplémentaires restent optionnels.
|
||||
Le Makefile vérifie explicitement GTK4, SQLite, libheif, Poppler GLib et
|
||||
Le Makefile vérifie explicitement GLib/GIO, SQLite, libheif, Poppler GLib et
|
||||
JSON-GLib avant
|
||||
compilation et renvoie vers ce document si une dépendance manque.
|
||||
|
||||
|
|
@ -54,6 +54,7 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
|
|||
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
|
||||
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
|
||||
| Requêtes HTTPS | `curl` | `sudo pacman -S curl` | `sudo apt install curl` |
|
||||
| Transport HTTP C de recherche (harness V1) | `libcurl` | `sudo pacman -S curl` | `sudo apt install libcurl4-openssl-dev` |
|
||||
| Certificats/TLS | `openssl` | `sudo pacman -S openssl` | `sudo apt install openssl` |
|
||||
| Récupération PDF | `qpdf` | `sudo pacman -S qpdf` | `sudo apt install qpdf` |
|
||||
| Audit de mot de passe PDF | `john`, `pdf2john` | `sudo pacman -S john` | `sudo apt install john` |
|
||||
|
|
@ -138,3 +139,14 @@ données distincte, ni installation automatique. Ses fichiers d'instance et de
|
|||
configuration suivent XDG (`XDG_RUNTIME_DIR`, `XDG_STATE_HOME`) ; la
|
||||
bibliothèque d'enquêtes est une destination locale explicitement fournie par
|
||||
`LIBRARY`, et non une dépendance à installer.
|
||||
|
||||
Le harness de recherche OSINT assistée V1 utilise libcurl pour ses tests de
|
||||
transport avec un fournisseur loopback `SPECIMEN`. Cette dépendance ne
|
||||
configure, ne qualifie et ne contacte aucun fournisseur public ; les
|
||||
fournisseurs externes restent indisponibles tant que leur configuration et
|
||||
leur autorisation ne sont pas établies par une tranche dédiée.
|
||||
|
||||
GTK4 n’est plus une dépendance de Labfy : le build Web-only exige GLib/GIO,
|
||||
SQLite, libheif, Poppler GLib, JSON-GLib, Python 3 et Firefox/`puppeteer-core`
|
||||
pour les validations visuelles. Aucun environnement Wayland, GSK_RENDERER ou
|
||||
display GTK n’est requis par la suite canonique.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Documentation de Labfy Investigation
|
||||
|
||||
> **Version cible :** v0.1.0 Foundation, en préparation et non publiée
|
||||
> **Interface actuelle :** GTK4
|
||||
> **Interface actuelle :** Web local-first ; GTK est retiré du produit.
|
||||
> **Interface cible :** Web local-first ; prototype expérimental J2/J3 présent
|
||||
|
||||
Cette page sépare l'usage du logiciel actuel, la direction acceptée et les
|
||||
|
|
@ -18,9 +18,11 @@ déjà disponible.
|
|||
- [test manuel du pivot EML](testing/EML_PIVOT_MANUAL_TEST.md).
|
||||
- [poste Web local contrôlé](architecture/WEB_WORKSPACE_CONTROL.md) et
|
||||
[guide du poste Web](ui/WEB_WORKBENCH.md).
|
||||
- [recherche OSINT assistée V1 en laboratoire](architecture/OSINT_ASSISTED_RESEARCH.md) :
|
||||
JobStore V4 additif, grants explicites et parcours Web synthétique.
|
||||
|
||||
Les guides GTK restent applicables tant que l'interface actuelle est maintenue.
|
||||
Le poste Web local `CURRENT` ne retire donc aucun parcours GTK ni ne décide le
|
||||
Les guides GTK archivés décrivent des parcours retirés ; ils ne sont plus des
|
||||
instructions `CURRENT`. Le poste Web local `CURRENT` remplace le renderer GTK et décide le
|
||||
serveur, le renderer ou le packaging de production.
|
||||
|
||||
## Direction v0.1.0 — TARGET
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ construction sans démarrer la refonte fonctionnelle.
|
|||
**Dépendances.** État réel du dépôt et préservation des travaux locaux, dont la
|
||||
tranche financière V21 indépendante.
|
||||
|
||||
> **Interface CURRENT local :** la tranche Web finale a remplacé GTK par une
|
||||
> surface locale Agent | Graphe | Activité + drawer. Le raccord Qwen local,
|
||||
> tool calling et resource governor AMD/RAM/swap restent `TARGET`.
|
||||
|
||||
**Livrables.** README, bannière et source éditable, index documentaire,
|
||||
architecture canonique, contrat du graphe, ADR Web/graph-first/polyglotte,
|
||||
Toolkit, sécurité, design system, roadmap et backlog préparé.
|
||||
|
|
|
|||
163
docs/architecture/OSINT_ASSISTED_RESEARCH.md
Normal file
163
docs/architecture/OSINT_ASSISTED_RESEARCH.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Recherche OSINT assistée V1 — contrat de laboratoire
|
||||
|
||||
> **Statut :** `CURRENT` dans le worktree, validation exclusivement synthétique
|
||||
> **Portée :** persistance V4, décisions de scope et parcours Web local de
|
||||
> recherche assistée ; pas une qualification de fournisseur public
|
||||
|
||||
## 1. Limites de ce qui est livré
|
||||
|
||||
La recherche assistée V1 ajoute une persistance opérationnelle au `JobStore`
|
||||
local. La migration est additive de V3 vers V4 : elle conserve les objets et
|
||||
contrats de jobs locaux V3. Elle ne remplace pas la base métier V20 et ne
|
||||
modifie pas la tranche financière V21.
|
||||
|
||||
Le `JobStore` reste servi par son worker mono-propriétaire existant. Les
|
||||
opérations de recherche n'introduisent ni second ordonnanceur ni exécution
|
||||
concurrente implicite. L'admission des objets de recherche ne démarre pas, à
|
||||
elle seule, une campagne.
|
||||
|
||||
Cette tranche est qualifiée uniquement par des fixtures `SPECIMEN`, des
|
||||
réponses injectées et un fournisseur de laboratoire loopback explicitement
|
||||
configuré par le harness. Aucun fournisseur public n'est configuré ou qualifié
|
||||
par ce contrat. Une absence de configuration ou d'autorisation de stockage
|
||||
rend l'adapter concerné indisponible ; elle ne déclenche ni installation, ni
|
||||
appel externe, ni repli vers un endpoint implicite.
|
||||
|
||||
## 2. Contrats persistants distincts
|
||||
|
||||
Les contrats versionnés de recherche assistée sont distincts des contrats
|
||||
`labfy.local_plan.v1` et de session Web. Un cookie, un CSRF ou l'état de
|
||||
l'interface ne font pas partie de leur contenu durable.
|
||||
|
||||
| Objet | Contrat | Rôle durable |
|
||||
| --- | --- | --- |
|
||||
| `ResearchPlan` | `labfy.research_plan.v1` | intention bornée, seeds, actions exactes et empreintes d'entrée |
|
||||
| `ResearchAction` | composant du plan | capability, fournisseur, endpoint, sujet, divulgation, classe de contact et plafonds |
|
||||
| `ScopeGrant` | `labfy.scope_grant.v1` | décision liée à une enquête et à un plan, actions sélectionnées, exclusions, expiration et budgets |
|
||||
| `ResearchCampaign` | `labfy.research_campaign.v1` | identité idempotente d'une campagne admise ; elle ne vaut pas démarrage automatique |
|
||||
| `ResearchResult` | `labfy.research_result.v1` | résultat brut référencé par chemin relatif et empreinte de contenu |
|
||||
| `ResearchReceipt` | `labfy.research_receipt.v1` | décision de policy associée au résultat, à l'action, au grant et à la campagne |
|
||||
|
||||
L'admission est atomique : une même clé d'idempotence et le même contenu
|
||||
retournent l'objet durable existant ; une même clé et un contenu différent sont
|
||||
en conflit. Un plan ou un grant invalide ne laisse pas d'écriture partielle.
|
||||
Un résultat ne peut pas être publié sans reçu visant une action explicitement
|
||||
sélectionnée dans le grant de sa campagne.
|
||||
|
||||
## 3. Scope, policy et destinations
|
||||
|
||||
Avant tout contact, une décision est évaluée sur l'enquête, l'empreinte du
|
||||
plan, l'action exacte, le grant et les budgets demandés. Les motifs observables
|
||||
incluent notamment l'exclusion, la révocation, l'expiration, une enquête ou un
|
||||
plan différent, une action non sélectionnée ou altérée, et un budget dépassé.
|
||||
La révocation est durable et ne supprime pas l'historique du grant.
|
||||
|
||||
La validation HTTP compare exactement le sujet, l'endpoint, le port et la
|
||||
sélection. Le profil réseau est une allowlist d'endpoints et de ports, jamais
|
||||
une règle par suffixe. Les adresses non publiquement routables sont refusées
|
||||
par le contrat du transport de production. Une redirection, un CNAME, une URL
|
||||
nouvelle ou une seed dérivée ne propage jamais le scope : chacun constitue une
|
||||
nouvelle destination à décider explicitement.
|
||||
|
||||
Le transport commun reste sans backend réseau lié et retourne donc
|
||||
`UNAVAILABLE` sans contact. Son contrat interdit proxy, cookies, `netrc`,
|
||||
authentification ambiante, redirection automatique et résolution fournie par
|
||||
l'appelant. La variante libcurl est testée uniquement avec le routage de
|
||||
fixture vers loopback ; son callback réseau refuse les adresses non publiques.
|
||||
Les préfixes privés, locaux, documentaires et de benchmark ne sont pas des
|
||||
adresses publiques. Les plafonds du corps compressé reçu sur le fil, du corps
|
||||
décompressé et des en-têtes sont appliqués séparément, y compris sans
|
||||
`Content-Length` et avec un transfert chunked.
|
||||
Cela ne qualifie aucune configuration de production ni aucun fournisseur
|
||||
externe.
|
||||
|
||||
## 4. Adapters normalisateurs — CURRENT en laboratoire
|
||||
|
||||
Les adapters suivants normalisent des données de fixture et préservent leurs
|
||||
limites de périmètre :
|
||||
|
||||
- DNS : types bornés A, AAAA, CNAME, MX, NS, TXT, SOA et PTR ; ni `ANY`, ni
|
||||
AXFR, ni trace n'appartiennent au type public. Un cycle CNAME est signalé et
|
||||
ne crée pas de piste suivante implicite.
|
||||
- RDAP : sélection du service depuis un bootstrap versionné et borné ; le
|
||||
bootstrap routé ne vaut jamais scope pour le service retourné.
|
||||
- CDX : correspondance d'URL ou d'hôte exact, captures normalisées et curseur
|
||||
explicite ; il n'y a pas d'élargissement de la cible.
|
||||
- Brave et SearXNG : disponibilité explicite (`UNAVAILABLE_CONFIG` ou
|
||||
`UNAVAILABLE_STORAGE_AUTHORIZATION`) tant que l'endpoint, la référence de
|
||||
secret et l'autorisation nécessaires ne sont pas réunis. Aucun secret n'est
|
||||
persisté dans ces contrats.
|
||||
- Page : production de texte inerte borné ; scripts, liens, sous-ressources et
|
||||
pièces jointes ne sont ni exécutés ni suivis.
|
||||
|
||||
Ces résultats peuvent indiquer une `next_piste`, qui reste une information à
|
||||
préparer et approuver ; elle ne confère aucune autorisation dérivée.
|
||||
|
||||
## 5. Publication et provenance C
|
||||
|
||||
Le cœur C sépare la décision, l'artefact brut, le résultat et le reçu. Le
|
||||
résultat conserve un chemin relatif d'artefact et son SHA-256 ; la publication
|
||||
atomique exige le reçu de policy correspondant. Cette provenance permet de
|
||||
revenir au grant, à l'action et à la campagne, sans assimiler un contenu reçu
|
||||
à une identité confirmée ou à une autorisation de poursuivre.
|
||||
|
||||
Les résultats de laboratoire du parcours Web sont stockés comme artefacts
|
||||
privés de l'espace de travail de test. Le bridge expose un snapshot de
|
||||
recherche, pas un accès arbitraire au répertoire d'artefacts ni une base de
|
||||
provenance indépendante du cœur C.
|
||||
|
||||
La publication fichier/SQLite utilise un journal privé créé avant le renommage
|
||||
final. Il lie l'identité déterministe du résultat à l'empreinte attendue. Si le
|
||||
processus s'interrompt après le renommage, le rejeu de la même campagne vérifie
|
||||
ce journal et l'artefact, puis enregistre atomiquement résultat et reçu sans
|
||||
recontacter le fournisseur. Un état possédé incohérent est compensé avant de
|
||||
retourner une erreur. Le journal est supprimé après publication durable ; cette
|
||||
reprise n'ajoute ni table ni numéro de schéma.
|
||||
|
||||
## 6. Poste Web local — préparation et décision humaine
|
||||
|
||||
Le panneau de recherche du poste Web local prépare un plan depuis une
|
||||
sélection et une révision de snapshot précises. L'interface présente les
|
||||
actions et leurs divulgations, puis exige une décision explicite :
|
||||
|
||||
1. préparer un plan ;
|
||||
2. sélectionner ou refuser les actions pour former le `ScopeGrant` ;
|
||||
3. admettre une campagne pour les seules actions accordées ;
|
||||
4. préparer et approuver séparément une seconde vague éventuelle.
|
||||
|
||||
L'action refusée reste refusée ; elle ne devient pas admissible au lancement
|
||||
d'une autre action. La révocation du grant est une mutation explicite et
|
||||
durable. Une révision de sélection périmée, un identifiant étranger ou une
|
||||
forme de requête invalide sont refusés.
|
||||
|
||||
Les sessions, cookies et CSRF restent des contrôles de la surface Web et sont
|
||||
distincts des contrats ci-dessus. Les validations couvrent l'isolation de deux
|
||||
espaces de travail synthétiques A/B : un plan, grant, campagne ou résultat de
|
||||
l'espace A n'est pas visible ni réutilisable dans l'espace B.
|
||||
|
||||
## 7. Procédure de validation autorisée
|
||||
|
||||
Les seuls parcours documentés ici utilisent les harnesses synthétiques du
|
||||
dépôt et leurs répertoires temporaires. Les contrôles Web dédiés sont :
|
||||
|
||||
```sh
|
||||
python3 prototypes/web-graph/tests/test_research_web.py
|
||||
node prototypes/web-graph/tests/test_research_browser.mjs
|
||||
```
|
||||
|
||||
Ils démarrent leur fournisseur `SPECIMEN` de loopback à durée de vie bornée et
|
||||
ne nécessitent ni endpoint public, ni jeton, ni enquête réelle. Les tests C
|
||||
`test_research_store`, `test_research_policy`, `test_research_transport` et
|
||||
`test_research_adapters` couvrent respectivement la migration/persistance, les
|
||||
décisions de policy, les bornes du transport/libcurl et les normalisations de
|
||||
fixtures. Leur exécution relève du harness de validation de la tranche ; elle
|
||||
ne constitue pas une autorisation de contacter des services externes.
|
||||
|
||||
## 8. Ce qui reste TARGET ou non qualifié
|
||||
|
||||
Restent hors de ce livrable : qualification et configuration d'un fournisseur
|
||||
public, gestion opérationnelle de secrets, exécution de recherche hors
|
||||
laboratoire, propagation automatique de scope, ordonnancement autonome de
|
||||
recherche, ainsi que le serveur, le protocole d'événements, le renderer et le
|
||||
packaging Web de production. Le Web local ne change pas le statut de GTK comme
|
||||
parcours fonctionnel existant.
|
||||
21
docs/ui/WEB_GTK_PARITY.md
Normal file
21
docs/ui/WEB_GTK_PARITY.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Matrice de parité Web avant retrait GTK
|
||||
|
||||
> État : `CURRENT` local — `LABFY_WEB_FINAL_INTERFACE_AND_REMOVE_GTK_V1`.
|
||||
> Les contrôles utilisent exclusivement les workspaces `SPECIMEN`.
|
||||
|
||||
| Fonction GTK historique | Équivalent Web CURRENT | Test Web | État |
|
||||
| --- | --- | --- | --- |
|
||||
| Graphe, sélection, pan, zoom, focus et retour | SVG interactif central, recherche et actions contextuelles | `test_workbench_browser.mjs` | remplacé |
|
||||
| Positions et zoom persistés | Projection C et positions épinglées conservées au rafraîchissement | `test_workbench_populated_browser.mjs` | remplacé |
|
||||
| Arbre / détails d'entité, preuve et relation | Drawer Détails et inspecteur à onglets | `test_local_workspace_import_browser.mjs` | remplacé |
|
||||
| Provenance et revue | Onglets Provenance et Observations, promotion explicite | `test_j9_browser.mjs` | remplacé |
|
||||
| Activité, tâches et annulation | Drawer Tâches, JobStore V4 et contrôles de file | `test_j8_runtime_browser.mjs` | remplacé |
|
||||
| Actions OSINT contextuelles | Capability → service → job, plan/grant/campagne explicites | `test_research_browser.mjs` | remplacé |
|
||||
| Import, aperçu et analyse de preuve | Réception privée, preview dérivé, analyses C | `test_local_workspace_import_browser.mjs` | remplacé |
|
||||
| Rapports | Prévisualisation et dossier hors ligne vérifiable | `test_j9_browser.mjs` | remplacé |
|
||||
| Dialogues de création GTK et formulaires spécialisés | Parcours non exposés par le bridge Web actuel | aucun | retiré, pas de logique métier supprimée |
|
||||
| Widgets OCR de rendu GTK | Données, correction et provenance conservées dans services/DAO ; renderer retiré | tests C de services conservés | retiré, renderer seulement |
|
||||
|
||||
Les parcours retirés ne constituaient pas une API métier : ils dépendaient du
|
||||
renderer GTK. Les DAO, services, migrations V20, fondation financière V21 et
|
||||
JobStore V4 restent hors de cette suppression.
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
# Poste Web local — guide d’utilisation
|
||||
|
||||
> **Statut CURRENT local :** poste utilisable au-dessus des services C et du
|
||||
> JobStore V3 existants, depuis une bibliothèque d'enquêtes explicitement
|
||||
> **Statut CURRENT local :** interface utilisateur Web unique, utilisable au-dessus des services C et du
|
||||
> JobStore V4 existants, depuis une bibliothèque d'enquêtes explicitement
|
||||
> choisie. Les validations emploient des fixtures `SPECIMEN`. Il ne constitue
|
||||
> ni une autorisation d’ouvrir une enquête réelle, ni un service distant.
|
||||
|
||||
## Organisation
|
||||
|
||||
À l'ouverture, la bibliothèque affiche les enquêtes locales enregistrées et
|
||||
À l'ouverture, le lanceur `tools/labfy` démarre ou rejoint l’instance loopback
|
||||
possédée, ouvre le navigateur et établit une session HttpOnly locale sans code
|
||||
manuel. La bibliothèque affiche les enquêtes locales enregistrées et
|
||||
permet d'en créer une avec un titre explicite. Une enquête ouverte reste la
|
||||
seule enquête active de cette instance ; le poste n'en restaure pas une autre
|
||||
automatiquement. Le titre et la génération de la bibliothèque identifient le
|
||||
contexte affiché, afin qu'une réponse tardive d'une enquête précédente ne le
|
||||
repeuple pas.
|
||||
|
||||
Le graphe reste la surface principale. La barre haute porte l’identité de
|
||||
Le graphe reste la surface principale. La coque est **Agent | Graphe |
|
||||
Activité**, plus un tiroir transversal. La barre haute porte l’identité de
|
||||
l’enquête, le statut local et la recherche. La rangée suivante change de
|
||||
projection (`Réseau`, `Preuves`, `Chronologie`, `Infrastructure locale`) sans
|
||||
créer une seconde vérité métier.
|
||||
|
|
@ -93,10 +96,10 @@ Une date sans fuseau ou invalide reste non positionnable.
|
|||
Lancer le poste local sur une bibliothèque explicitement choisie :
|
||||
|
||||
```bash
|
||||
make web PORT=8081 LIBRARY=/chemin/vers/la-bibliotheque-locale
|
||||
LABFY_LIBRARY=/chemin/vers/la-bibliotheque-locale tools/labfy
|
||||
```
|
||||
|
||||
L’écoute reste loopback, la session est authentifiée et les mutations exigent
|
||||
L’écoute reste loopback, la session est HttpOnly/SameSite et les mutations exigent
|
||||
le jeton CSRF. Le poste n’expose pas le répertoire des preuves et n’installe
|
||||
aucun outil. Les projections vides, erreurs, annulations et limites sont des
|
||||
états explicites ; elles ne prouvent jamais une absence d’information.
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file application.h
|
||||
* @brief Interface publique du cycle de vie de l'application.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_APPLICATION_H
|
||||
#define LABFY_INVESTIGATION_APPLICATION_H
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque d'une session d'enquête.
|
||||
*/
|
||||
typedef struct InvestigationSession InvestigationSession;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de l'application.
|
||||
*
|
||||
* Les détails GTK sont volontairement masqués au reste du programme.
|
||||
*/
|
||||
typedef struct Application Application;
|
||||
|
||||
/**
|
||||
* @brief Crée et initialise une application Labfy Investigation.
|
||||
*
|
||||
* @return Une nouvelle application, ou NULL en cas d'échec.
|
||||
*/
|
||||
Application *application_new(void);
|
||||
|
||||
/**
|
||||
* @brief Lance la boucle principale de l'application.
|
||||
*
|
||||
* @param application Application à exécuter.
|
||||
* @param argc Nombre d'arguments de la ligne de commande.
|
||||
* @param argv Tableau des arguments de la ligne de commande.
|
||||
*
|
||||
* @return Code de sortie retourné par GTK.
|
||||
*/
|
||||
int application_run(
|
||||
Application *application,
|
||||
int argc,
|
||||
char **argv
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la session d'enquête actuellement ouverte.
|
||||
*
|
||||
* Le pointeur retourné appartient à l'application.
|
||||
* Le code appelant ne doit pas fermer cette session.
|
||||
*
|
||||
* @param application Application concernée.
|
||||
*
|
||||
* @return La session active, ou NULL si aucune enquête n'est ouverte
|
||||
* ou si application vaut NULL.
|
||||
*/
|
||||
const InvestigationSession *application_get_session(
|
||||
const Application *application
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère les ressources possédées par l'application.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* @param application Application à libérer.
|
||||
*/
|
||||
void application_free(
|
||||
Application *application
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/******************************************************************************
|
||||
* @file local_job_store.h
|
||||
* @brief Stockage opérationnel V3 des jobs et plans locaux J5/J8.
|
||||
* @brief Stockage opérationnel V4 des jobs, plans locaux et grants assistés.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_LOCAL_JOB_STORE_H
|
||||
#define LABFY_INVESTIGATION_LOCAL_JOB_STORE_H
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define LOCAL_JOB_STORE_SCHEMA_VERSION 3
|
||||
#define LOCAL_JOB_STORE_SCHEMA_VERSION 4
|
||||
#define LOCAL_JOB_STORE_MAX_JOBS 128U
|
||||
#define LOCAL_JOB_STORE_MAX_ATTEMPTS 3U
|
||||
#define LOCAL_JOB_DIAGNOSTIC_MAX 2048U
|
||||
|
|
|
|||
14
include/core/research_adapter_cdx.h
Normal file
14
include/core/research_adapter_cdx.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_ADAPTER_CDX_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_ADAPTER_CDX_H
|
||||
#include "core/research_contracts.h"
|
||||
G_BEGIN_DECLS
|
||||
typedef enum { RESEARCH_CDX_EXACT_URL, RESEARCH_CDX_EXACT_HOST } ResearchCdxMatch;
|
||||
typedef struct { const char *original_url; const char *timestamp; const char *digest;
|
||||
guint status_code; } ResearchCdxRow;
|
||||
typedef struct { const ResearchCdxRow *rows; gsize row_count; const char *next_cursor; } ResearchCdxFixture;
|
||||
typedef struct { GPtrArray *captures; char *next_cursor; gboolean next_piste; } ResearchCdxResult;
|
||||
gboolean research_adapter_cdx_normalize(const ResearchAction *action,
|
||||
ResearchCdxMatch match,const ResearchCdxFixture *fixture,ResearchCdxResult *out,GError **error);
|
||||
void research_cdx_result_clear(ResearchCdxResult *result);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
23
include/core/research_adapter_dns.h
Normal file
23
include/core/research_adapter_dns.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_ADAPTER_DNS_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_ADAPTER_DNS_H
|
||||
#include "core/research_contracts.h"
|
||||
G_BEGIN_DECLS
|
||||
typedef enum { RESEARCH_DNS_A, RESEARCH_DNS_AAAA, RESEARCH_DNS_CNAME,
|
||||
RESEARCH_DNS_MX, RESEARCH_DNS_NS, RESEARCH_DNS_TXT, RESEARCH_DNS_SOA,
|
||||
RESEARCH_DNS_PTR } ResearchDnsType;
|
||||
typedef enum { RESEARCH_DNS_STATUS_OK, RESEARCH_DNS_STATUS_NXDOMAIN,
|
||||
RESEARCH_DNS_STATUS_NODATA, RESEARCH_DNS_STATUS_SERVFAIL,
|
||||
RESEARCH_DNS_STATUS_TIMEOUT } ResearchDnsStatus;
|
||||
typedef struct { ResearchDnsType type; const char *owner; const char *value;
|
||||
guint ttl; guint preference; } ResearchDnsRecord;
|
||||
typedef struct { ResearchDnsStatus status; const ResearchDnsRecord *records;
|
||||
gsize record_count; } ResearchDnsFixture;
|
||||
typedef struct { ResearchDnsStatus status; GPtrArray *records;
|
||||
gboolean cname_cycle; gboolean next_piste; } ResearchDnsResult;
|
||||
/** CONTRACT: ANY, AXFR et trace n'existent pas dans le type public. */
|
||||
gboolean research_adapter_dns_normalize(const ResearchAction *action,
|
||||
ResearchDnsType requested_type, const ResearchDnsFixture *fixture,
|
||||
ResearchDnsResult *out, GError **error);
|
||||
void research_dns_result_clear(ResearchDnsResult *result);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
13
include/core/research_adapter_page.h
Normal file
13
include/core/research_adapter_page.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_ADAPTER_PAGE_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_ADAPTER_PAGE_H
|
||||
#include "core/research_contracts.h"
|
||||
G_BEGIN_DECLS
|
||||
typedef struct { const char *content_type; const guint8 *body; gsize body_size; } ResearchPageFixture;
|
||||
typedef struct { char *inert_text; gboolean links_ignored; gboolean attachments_ignored;
|
||||
gboolean next_piste; } ResearchPageResult;
|
||||
/** CONTRACT: produit du texte inerte; aucun script, lien, sous-ressource ou attachment n'est suivi. */
|
||||
gboolean research_adapter_page_normalize(const ResearchAction *action,
|
||||
const ResearchPageFixture *fixture,gsize max_text_bytes,ResearchPageResult *out,GError **error);
|
||||
void research_page_result_clear(ResearchPageResult *result);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
19
include/core/research_adapter_rdap.h
Normal file
19
include/core/research_adapter_rdap.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_ADAPTER_RDAP_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_ADAPTER_RDAP_H
|
||||
#include "core/research_contracts.h"
|
||||
G_BEGIN_DECLS
|
||||
typedef enum { RESEARCH_RDAP_DOMAIN_SERVICE, RESEARCH_RDAP_IPV4_SERVICE,
|
||||
RESEARCH_RDAP_IPV6_SERVICE } ResearchRdapServiceKind;
|
||||
typedef struct { ResearchRdapServiceKind kind; const char *prefix;
|
||||
const char *base_url; } ResearchRdapBootstrapEntry;
|
||||
typedef struct { const ResearchRdapBootstrapEntry *entries; gsize entry_count;
|
||||
const char *fetched_at; const char *expires_at; const char *etag; } ResearchRdapBootstrap;
|
||||
typedef struct { char *service_url; guint prefix_length; char *cache_fetched_at;
|
||||
char *cache_expires_at; char *cache_etag; gboolean next_piste; } ResearchRdapResult;
|
||||
/** WHY: le bootstrap routé ne vaut jamais autorisation sur l'objet retourné. */
|
||||
gboolean research_adapter_rdap_select(const ResearchAction *action,
|
||||
ResearchRdapServiceKind kind, const ResearchRdapBootstrap *bootstrap,
|
||||
ResearchRdapResult *out, GError **error);
|
||||
void research_rdap_result_clear(ResearchRdapResult *result);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
19
include/core/research_adapter_search.h
Normal file
19
include/core/research_adapter_search.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_ADAPTER_SEARCH_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_ADAPTER_SEARCH_H
|
||||
#include "core/research_contracts.h"
|
||||
G_BEGIN_DECLS
|
||||
typedef enum { RESEARCH_SEARCH_BRAVE, RESEARCH_SEARCH_SEARXNG } ResearchSearchProvider;
|
||||
typedef enum { RESEARCH_SEARCH_AVAILABLE, RESEARCH_SEARCH_UNAVAILABLE_CONFIG,
|
||||
RESEARCH_SEARCH_UNAVAILABLE_STORAGE_AUTHORIZATION } ResearchSearchAvailability;
|
||||
typedef struct { ResearchSearchProvider provider; const char *endpoint;
|
||||
const char *secret_reference; gboolean storage_authorized; } ResearchSearchConfig;
|
||||
typedef struct { const char *title; const char *url; const char *snippet; } ResearchSearchHit;
|
||||
typedef struct { ResearchSearchAvailability availability; GPtrArray *hits;
|
||||
gboolean next_piste; } ResearchSearchResult;
|
||||
ResearchSearchAvailability research_adapter_search_availability(const ResearchSearchConfig *config);
|
||||
gboolean research_adapter_search_normalize(const ResearchAction *action,
|
||||
const ResearchSearchConfig *config,const ResearchSearchHit *hits,gsize hit_count,
|
||||
ResearchSearchResult *out,GError **error);
|
||||
void research_search_result_clear(ResearchSearchResult *result);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
172
include/core/research_contracts.h
Normal file
172
include/core/research_contracts.h
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/******************************************************************************
|
||||
* @file research_contracts.h
|
||||
* @brief Contrats versionnés de recherche assistée, sans exécution réseau.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_RESEARCH_CONTRACTS_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_CONTRACTS_H
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define RESEARCH_PLAN_CONTRACT "labfy.research_plan.v1"
|
||||
#define RESEARCH_GRANT_CONTRACT "labfy.scope_grant.v1"
|
||||
#define RESEARCH_CAMPAIGN_CONTRACT "labfy.research_campaign.v1"
|
||||
#define RESEARCH_RESULT_CONTRACT "labfy.research_result.v1"
|
||||
#define RESEARCH_RECEIPT_CONTRACT "labfy.research_receipt.v1"
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_SEED_DOMAIN,
|
||||
RESEARCH_SEED_IP,
|
||||
RESEARCH_SEED_HTTP_URL,
|
||||
RESEARCH_SEED_EMAIL,
|
||||
RESEARCH_SEED_SEARCH_TERM
|
||||
} ResearchSeedKind;
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_CONTACT_NONE,
|
||||
RESEARCH_CONTACT_THIRD_PARTY,
|
||||
RESEARCH_CONTACT_TARGET
|
||||
} ResearchContactClass;
|
||||
|
||||
typedef struct {
|
||||
ResearchSeedKind kind;
|
||||
const char *subject;
|
||||
} ResearchSeed;
|
||||
|
||||
typedef struct {
|
||||
const char *action_id;
|
||||
const char *capability_id;
|
||||
const char *provider_id;
|
||||
const char *endpoint;
|
||||
const char *subject;
|
||||
ResearchContactClass contact;
|
||||
const char *disclosure;
|
||||
guint max_requests;
|
||||
guint64 max_response_bytes;
|
||||
guint max_active_ms;
|
||||
} ResearchAction;
|
||||
|
||||
typedef struct {
|
||||
const char *contract;
|
||||
const char *plan_id;
|
||||
const char *idempotency_key;
|
||||
const char *content_fingerprint;
|
||||
const char *input_fingerprint;
|
||||
guint64 input_revision;
|
||||
const char *created_at;
|
||||
const ResearchSeed *seeds;
|
||||
gsize seed_count;
|
||||
const ResearchAction *actions;
|
||||
gsize action_count;
|
||||
} ResearchPlan;
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_ACTION_AUTHORIZE,
|
||||
RESEARCH_ACTION_DEFER,
|
||||
RESEARCH_ACTION_REFUSE
|
||||
} ResearchActionDecisionCode;
|
||||
|
||||
typedef struct {
|
||||
const char *action_id;
|
||||
ResearchActionDecisionCode code;
|
||||
} ResearchActionDecision;
|
||||
|
||||
typedef struct {
|
||||
const char *contract;
|
||||
const char *grant_id;
|
||||
const char *investigation_id;
|
||||
const char *idempotency_key;
|
||||
const char *content_fingerprint;
|
||||
const char *plan_fingerprint;
|
||||
const char *created_at;
|
||||
const char *expires_at;
|
||||
const char *const *selected_action_ids;
|
||||
gsize selected_action_count;
|
||||
const char *const *excluded_subjects;
|
||||
gsize excluded_subject_count;
|
||||
guint max_requests;
|
||||
guint64 max_response_bytes;
|
||||
guint max_active_ms;
|
||||
} ScopeGrant;
|
||||
|
||||
typedef struct {
|
||||
const char *contract;
|
||||
const char *campaign_id;
|
||||
const char *investigation_id;
|
||||
const char *grant_id;
|
||||
const char *idempotency_key;
|
||||
const char *content_fingerprint;
|
||||
const char *created_at;
|
||||
} ResearchCampaign;
|
||||
|
||||
typedef struct {
|
||||
const char *contract;
|
||||
const char *result_id;
|
||||
const char *campaign_id;
|
||||
const char *action_id;
|
||||
const char *raw_artifact_relative_path;
|
||||
const char *content_sha256;
|
||||
const char *status;
|
||||
} ResearchResult;
|
||||
|
||||
typedef struct {
|
||||
const char *contract;
|
||||
const char *receipt_id;
|
||||
const char *result_id;
|
||||
const char *campaign_id;
|
||||
const char *grant_id;
|
||||
const char *action_id;
|
||||
const char *decided_at;
|
||||
const char *decision_code;
|
||||
} ResearchReceipt;
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_POLICY_ALLOW,
|
||||
RESEARCH_POLICY_DENY_EXCLUDED,
|
||||
RESEARCH_POLICY_DENY_REVOKED,
|
||||
RESEARCH_POLICY_DENY_EXPIRED,
|
||||
RESEARCH_POLICY_DENY_INVESTIGATION,
|
||||
RESEARCH_POLICY_DENY_PLAN,
|
||||
RESEARCH_POLICY_DENY_ACTION,
|
||||
RESEARCH_POLICY_DENY_ACTION_CONTENT,
|
||||
RESEARCH_POLICY_DENY_BUDGET
|
||||
} ResearchPolicyDecision;
|
||||
|
||||
typedef struct {
|
||||
const char *investigation_id;
|
||||
const char *plan_fingerprint;
|
||||
const ResearchAction *action;
|
||||
const char *now;
|
||||
guint requested_requests;
|
||||
guint64 requested_response_bytes;
|
||||
guint requested_active_ms;
|
||||
} ResearchPolicyRequest;
|
||||
|
||||
/**
|
||||
* Vérifie la forme complète d'un plan avant toute écriture.
|
||||
* CONTRACT: les identités et empreintes sont stables et toutes les actions
|
||||
* sont bornées. INVARIANT: une seed dérivée n'accorde jamais de scope.
|
||||
*/
|
||||
gboolean research_plan_validate(const ResearchPlan *plan, GError **error);
|
||||
|
||||
/**
|
||||
* Vérifie le contenu durable d'un grant, à l'exclusion des cookies/CSRF qui
|
||||
* n'appartiennent pas à ce contrat persistant.
|
||||
*/
|
||||
gboolean research_scope_grant_validate(const ScopeGrant *grant,
|
||||
GError **error);
|
||||
gboolean research_campaign_validate(const ResearchCampaign *campaign,
|
||||
GError **error);
|
||||
gboolean research_result_validate(const ResearchResult *result,
|
||||
GError **error);
|
||||
gboolean research_receipt_validate(const ResearchReceipt *receipt,
|
||||
GError **error);
|
||||
|
||||
const char *research_seed_kind_code(ResearchSeedKind kind);
|
||||
const char *research_contact_class_code(ResearchContactClass contact);
|
||||
const char *research_policy_decision_code(ResearchPolicyDecision decision);
|
||||
const char *research_action_decision_code(ResearchActionDecisionCode decision);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
28
include/core/research_http_transport_curl.h
Normal file
28
include/core/research_http_transport_curl.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef LABFY_INVESTIGATION_RESEARCH_HTTP_TRANSPORT_CURL_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_HTTP_TRANSPORT_CURL_H
|
||||
|
||||
#include "core/research_transport.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* CONTRACT: exécute une requête préalablement validée, sans état ambiant.
|
||||
* INVARIANT: le callback socket refuse toute adresse qui n'est pas publique.
|
||||
*/
|
||||
gboolean research_http_transport_curl_execute(
|
||||
const ResearchTransportRequest *request, ResearchTransportReply *out_reply,
|
||||
GError **error);
|
||||
|
||||
#ifdef RESEARCH_TRANSPORT_ENABLE_TEST_HOOKS
|
||||
/**
|
||||
* TEST-ONLY: route une autorité exacte vers une adresse loopback finie.
|
||||
* Cette fonction n'existe pas dans l'API de production.
|
||||
*/
|
||||
gboolean research_http_transport_curl_execute_fixture(
|
||||
const ResearchTransportRequest *request, const char *authority,
|
||||
const char *loopback_address, ResearchTransportReply *out_reply,
|
||||
GError **error);
|
||||
#endif
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
55
include/core/research_policy.h
Normal file
55
include/core/research_policy.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/******************************************************************************
|
||||
* @file research_policy.h
|
||||
* @brief Policy pure pour les destinations de recherche assistée V1.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_RESEARCH_POLICY_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_POLICY_H
|
||||
|
||||
#include "core/research_contracts.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct {
|
||||
const guint16 *allowed_ports;
|
||||
gsize allowed_port_count;
|
||||
const char *const *allowed_endpoints;
|
||||
gsize allowed_endpoint_count;
|
||||
} ResearchNetworkProfile;
|
||||
|
||||
typedef struct {
|
||||
char *scheme;
|
||||
char *host;
|
||||
guint16 port;
|
||||
char *path_and_query;
|
||||
} ResearchHttpTarget;
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_REDIRECT_REQUIRES_DECISION,
|
||||
RESEARCH_REDIRECT_INVALID
|
||||
} ResearchRedirectDecision;
|
||||
|
||||
/**
|
||||
* CONTRACT: valide l'action et le grant exacts avant tout contact réseau.
|
||||
* WHY: le profil décrit une allowlist, jamais une règle par suffixe.
|
||||
* INVARIANT: sujet, endpoint, port et sélection sont comparés exactement.
|
||||
*/
|
||||
gboolean research_policy_validate_http_action(
|
||||
const ResearchAction *action, const ScopeGrant *grant,
|
||||
const ResearchNetworkProfile *profile, ResearchHttpTarget *out_target,
|
||||
GError **error);
|
||||
|
||||
/** INVARIANT: seules les adresses globalement routables sont acceptées. */
|
||||
gboolean research_policy_address_is_public(const char *address);
|
||||
|
||||
/**
|
||||
* CONTRACT: une redirection valide reste une nouvelle destination à décider.
|
||||
* Cette fonction ne propage jamais l'autorisation de l'action initiale.
|
||||
*/
|
||||
ResearchRedirectDecision research_policy_check_redirect(
|
||||
const char *location, const ResearchNetworkProfile *profile,
|
||||
ResearchHttpTarget *out_target, GError **error);
|
||||
|
||||
void research_http_target_clear(ResearchHttpTarget *target);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
72
include/core/research_store.h
Normal file
72
include/core/research_store.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/******************************************************************************
|
||||
* @file research_store.h
|
||||
* @brief Persistance V4 des autorisations de recherche assistée.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_RESEARCH_STORE_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_STORE_H
|
||||
|
||||
#include "core/local_job_store.h"
|
||||
#include "core/research_contracts.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* Admet atomiquement un plan et ses seeds/actions exactes.
|
||||
* CONTRACT: même clé et même contenu retourne le même objet durable; une même
|
||||
* clé avec un contenu différent est un conflit.
|
||||
* INVARIANT: aucune action invalide ne laisse un plan partiellement écrit.
|
||||
*/
|
||||
gboolean research_store_admit_plan(LocalJobStore *store,
|
||||
const ResearchPlan *plan,
|
||||
gboolean *out_reused, GError **error);
|
||||
|
||||
/**
|
||||
* Admet un grant lié au plan et matérialise les sujets/provider/endpoints
|
||||
* autorisés au moment de la décision. Toutes les décisions humaines sont
|
||||
* persistées dans la même transaction. REFUSE est terminal et rend l'action
|
||||
* durablement inadmissible; la vague 2 exige une vague 1 autorisée, exécutée
|
||||
* et documentée par son résultat et son reçu.
|
||||
*/
|
||||
gboolean research_store_admit_grant(LocalJobStore *store,
|
||||
const ScopeGrant *grant,
|
||||
const ResearchActionDecision *decisions,
|
||||
gsize decision_count,
|
||||
const char *decided_at,
|
||||
gboolean *out_reused, GError **error);
|
||||
|
||||
/** Révoque durablement un grant sans supprimer son historique. */
|
||||
gboolean research_store_revoke_grant(LocalJobStore *store,
|
||||
const char *grant_id,
|
||||
const char *revoked_at,
|
||||
GError **error);
|
||||
|
||||
/** Admet l'identité durable d'une campagne; ne démarre aucun travail. */
|
||||
gboolean research_store_admit_campaign(LocalJobStore *store,
|
||||
const ResearchCampaign *campaign,
|
||||
gboolean *out_reused, GError **error);
|
||||
|
||||
/**
|
||||
* Enregistre atomiquement le résultat brut et le reçu de policy correspondant.
|
||||
* INVARIANT: le reçu doit viser une action explicitement sélectionnée du grant
|
||||
* de la campagne; aucun résultat orphelin n'est publié.
|
||||
*/
|
||||
gboolean research_store_record_result(LocalJobStore *store,
|
||||
const ResearchResult *result,
|
||||
const ResearchReceipt *receipt,
|
||||
GError **error);
|
||||
|
||||
/**
|
||||
* Évalue un usage exact du grant sans créer de job ni réserver de budget.
|
||||
* WHY: l'unique worker existant reste le seul ordonnanceur; cette fonction
|
||||
* rend la décision de policy observable avant toute admission ultérieure.
|
||||
* INVARIANT: exclusion, révocation et expiration sont des refus explicites;
|
||||
* redirection, CNAME ou nouvelle URL ne correspondent jamais par construction.
|
||||
*/
|
||||
gboolean research_store_policy_decide(LocalJobStore *store,
|
||||
const char *grant_id,
|
||||
const ResearchPolicyRequest *request,
|
||||
ResearchPolicyDecision *out_decision,
|
||||
GError **error);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
94
include/core/research_transport.h
Normal file
94
include/core/research_transport.h
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/******************************************************************************
|
||||
* @file research_transport.h
|
||||
* @brief Transport borné et sans autorité implicite pour la recherche V1.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_RESEARCH_TRANSPORT_H
|
||||
#define LABFY_INVESTIGATION_RESEARCH_TRANSPORT_H
|
||||
|
||||
#include "core/research_policy.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef enum {
|
||||
RESEARCH_TRANSPORT_OK,
|
||||
RESEARCH_TRANSPORT_UNAVAILABLE,
|
||||
RESEARCH_TRANSPORT_CANCELLED,
|
||||
RESEARCH_TRANSPORT_TIMEOUT,
|
||||
RESEARCH_TRANSPORT_RATE_LIMITED,
|
||||
RESEARCH_TRANSPORT_FORBIDDEN,
|
||||
RESEARCH_TRANSPORT_REDIRECT,
|
||||
RESEARCH_TRANSPORT_TOO_LARGE,
|
||||
RESEARCH_TRANSPORT_INVALID_RESPONSE,
|
||||
RESEARCH_TRANSPORT_FAILED
|
||||
} ResearchTransportState;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *value;
|
||||
} ResearchHeader;
|
||||
|
||||
typedef struct {
|
||||
const ResearchAction *action;
|
||||
const ScopeGrant *grant;
|
||||
const ResearchNetworkProfile *profile;
|
||||
const char *method;
|
||||
const ResearchHeader *headers;
|
||||
gsize header_count;
|
||||
guint64 max_compressed_bytes;
|
||||
guint64 max_decompressed_bytes;
|
||||
guint64 max_header_bytes;
|
||||
GCancellable *cancellable;
|
||||
} ResearchTransportRequest;
|
||||
|
||||
typedef struct {
|
||||
ResearchTransportState state;
|
||||
guint status_code;
|
||||
guint retry_after_seconds;
|
||||
gboolean retry_after_present;
|
||||
gboolean body_is_compressed;
|
||||
GBytes *body;
|
||||
GHashTable *headers;
|
||||
char *redirect_location;
|
||||
char *diagnostic;
|
||||
} ResearchTransportReply;
|
||||
|
||||
/**
|
||||
* CONTRACT: la production n'utilise ni proxy, cookies, netrc, auth ambiante,
|
||||
* redirection automatique, ni résolution fournie par l'appelant.
|
||||
* CURRENT: aucun backend réseau n'est lié; retourne UNAVAILABLE sans contact.
|
||||
*/
|
||||
gboolean research_transport_execute(const ResearchTransportRequest *request,
|
||||
ResearchTransportReply *out_reply,
|
||||
GError **error);
|
||||
|
||||
gboolean research_transport_request_validate(
|
||||
const ResearchTransportRequest *request, ResearchHttpTarget *out_target,
|
||||
GError **error);
|
||||
void research_transport_reply_clear(ResearchTransportReply *reply);
|
||||
const char *research_transport_state_code(ResearchTransportState state);
|
||||
|
||||
#ifdef RESEARCH_TRANSPORT_ENABLE_TEST_HOOKS
|
||||
typedef struct {
|
||||
guint status_code;
|
||||
const ResearchHeader *headers;
|
||||
gsize header_count;
|
||||
const guint8 *body;
|
||||
gsize body_size;
|
||||
gboolean compressed;
|
||||
guint64 decompressed_size;
|
||||
const char *peer_address;
|
||||
const char *redirect_location;
|
||||
gboolean timed_out;
|
||||
gboolean unavailable;
|
||||
} ResearchTransportFixture;
|
||||
|
||||
/** TEST-ONLY: réponse finie injectée, sans socket ni résolution réseau. */
|
||||
gboolean research_transport_execute_fixture(
|
||||
const ResearchTransportRequest *request,
|
||||
const ResearchTransportFixture *fixture,
|
||||
const char *const *loopback_authorities, gsize authority_count,
|
||||
ResearchTransportReply *out_reply, GError **error);
|
||||
#endif
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file application_message_dialog.h
|
||||
* @brief Fenêtre modale affichant un message à l'utilisateur.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_APPLICATION_MESSAGE_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_APPLICATION_MESSAGE_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Nature du message affiché.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
APPLICATION_MESSAGE_DIALOG_ERROR,
|
||||
APPLICATION_MESSAGE_DIALOG_WARNING,
|
||||
APPLICATION_MESSAGE_DIALOG_INFORMATION
|
||||
} ApplicationMessageDialogType;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé à la fermeture d'un dialogue de confirmation.
|
||||
*
|
||||
* Le callback est appelé une seule fois après un clic sur l'un des boutons
|
||||
* ou après une fermeture manuelle de la fenêtre.
|
||||
*
|
||||
* @param confirmed TRUE si l'utilisateur confirme, sinon FALSE.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*ApplicationMessageDialogConfirmationCallback)(
|
||||
gboolean confirmed,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé par l'action facultative d'un dialogue détaillé.
|
||||
*
|
||||
* @param user_data Données associées à l'action.
|
||||
*/
|
||||
typedef void (*ApplicationMessageDialogActionCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche une fenêtre modale contenant un message.
|
||||
*
|
||||
* Les chaînes sont copiées par les widgets GTK.
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param message_type Type de message.
|
||||
* @param title Titre de la fenêtre, ou NULL.
|
||||
* @param message Message principal, ou NULL.
|
||||
*/
|
||||
void application_message_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
ApplicationMessageDialogType message_type,
|
||||
const char *title,
|
||||
const char *message
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche un message accompagné d'un contenu textuel défilable.
|
||||
*
|
||||
* Le contenu détaillé est affiché dans une zone monospace sélectionnable.
|
||||
* Les chaînes sont copiées par les widgets GTK.
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param message_type Type de message.
|
||||
* @param title Titre de la fenêtre, ou NULL.
|
||||
* @param message Message principal, ou NULL.
|
||||
* @param details Contenu détaillé, ou NULL.
|
||||
*/
|
||||
void application_message_dialog_present_details(
|
||||
GtkWindow *parent_window,
|
||||
ApplicationMessageDialogType message_type,
|
||||
const char *title,
|
||||
const char *message,
|
||||
const char *details
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche un message détaillé avec une action facultative.
|
||||
*
|
||||
* Le dialogue devient propriétaire de action_data lorsque l'affichage est
|
||||
* créé. Les données sont libérées avec action_data_destroy à sa fermeture.
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param message_type Type de message.
|
||||
* @param title Titre de la fenêtre, ou NULL.
|
||||
* @param message Message principal, ou NULL.
|
||||
* @param details Contenu détaillé, ou NULL.
|
||||
* @param action_label Libellé de l'action, ou NULL.
|
||||
* @param action_callback Callback de l'action, ou NULL.
|
||||
* @param action_data Données transmises au callback.
|
||||
* @param action_data_destroy Destructeur des données, ou NULL.
|
||||
*/
|
||||
void application_message_dialog_present_details_action(
|
||||
GtkWindow *parent_window,
|
||||
ApplicationMessageDialogType message_type,
|
||||
const char *title,
|
||||
const char *message,
|
||||
const char *details,
|
||||
const char *action_label,
|
||||
ApplicationMessageDialogActionCallback action_callback,
|
||||
gpointer action_data,
|
||||
GDestroyNotify action_data_destroy
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche un dialogue modal demandant une confirmation.
|
||||
*
|
||||
* Le bouton d'annulation transmet FALSE. Le bouton de confirmation transmet
|
||||
* TRUE. Fermer la fenêtre manuellement équivaut à une annulation.
|
||||
*
|
||||
* callback et user_data sont empruntés jusqu'à la fermeture du dialogue.
|
||||
* confirm_label peut être NULL ou vide pour utiliser « Confirmer ».
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param message_type Type de message.
|
||||
* @param title Titre de la fenêtre, ou NULL.
|
||||
* @param message Message principal, ou NULL.
|
||||
* @param confirm_label Libellé du bouton de confirmation, ou NULL.
|
||||
* @param callback Callback facultatif recevant la réponse.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void application_message_dialog_present_confirmation(
|
||||
GtkWindow *parent_window,
|
||||
ApplicationMessageDialogType message_type,
|
||||
const char *title,
|
||||
const char *message,
|
||||
const char *confirm_label,
|
||||
ApplicationMessageDialogConfirmationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_investigation_dialog.h
|
||||
* @brief Interface publique du dialogue de création d'une enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_CREATE_INVESTIGATION_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_CREATE_INVESTIGATION_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque le dialogue est validé ou annulé.
|
||||
*
|
||||
* Lors d'une validation réussie :
|
||||
*
|
||||
* - parent_directory contient le dossier parent sélectionné ;
|
||||
* - investigation_name contient le nom nettoyé de l'enquête.
|
||||
*
|
||||
* Lors d'une annulation :
|
||||
*
|
||||
* - parent_directory vaut NULL ;
|
||||
* - investigation_name vaut NULL.
|
||||
*
|
||||
* Les chaînes transmises restent valides uniquement pendant l'appel du
|
||||
* callback. Le code appelant doit les copier s'il souhaite les conserver.
|
||||
*
|
||||
* @param parent_directory Dossier parent sélectionné, ou NULL.
|
||||
* @param investigation_name Nom de l'enquête, ou NULL.
|
||||
* @param user_data Données utilisateur fournies lors de l'ouverture.
|
||||
*/
|
||||
typedef void (*CreateInvestigationDialogCallback)(
|
||||
const char *parent_directory,
|
||||
const char *investigation_name,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Présente le dialogue de création d'une nouvelle enquête.
|
||||
*
|
||||
* Le dialogue permet :
|
||||
*
|
||||
* - de sélectionner un dossier parent existant ;
|
||||
* - de saisir le nom de l'enquête ;
|
||||
* - de valider ou d'annuler la création.
|
||||
*
|
||||
* Cette fonction ne crée aucun fichier et n'accède pas à SQLite.
|
||||
*
|
||||
* @param parent_window Fenêtre GTK parente, ou NULL.
|
||||
* @param callback Fonction appelée lors de la validation ou de l'annulation.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void create_investigation_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
CreateInvestigationDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_person_dialog.h
|
||||
* @brief Formulaire GTK de création d'une personne observée.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
|
||||
#include "core/person_entity_service.h"
|
||||
#include "core/evidence_staging.h"
|
||||
#include "models/evidence_record.h"
|
||||
#include "models/person_evidence_selection.h"
|
||||
#include "core/task_manager.h"
|
||||
#include "core/tool_registry.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
/** @brief Résultat opaque possédé par le callback. */
|
||||
typedef struct CreatePersonDialogResult CreatePersonDialogResult;
|
||||
/** @brief Callback appelé après validation ou annulation. */
|
||||
typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result,
|
||||
gpointer user_data);
|
||||
typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
|
||||
/**
|
||||
* @brief Présente le formulaire de création d'une personne.
|
||||
* @param parent Fenêtre parente.
|
||||
* @param evidence_records Preuves disponibles empruntées.
|
||||
* @param callback Callback de fin.
|
||||
* @param user_data Données privées du callback.
|
||||
* @return TRUE si le dialogue a été présenté.
|
||||
*/
|
||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||
Database *database, const GPtrArray *evidence_records,
|
||||
const char *investigation_root_path,
|
||||
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
||||
CreatePersonDialogSessionCheck session_check,
|
||||
CreatePersonDialogCallback callback, gpointer user_data,
|
||||
GDestroyNotify user_data_destroy);
|
||||
/** @brief Libère un résultat. */
|
||||
void create_person_dialog_result_free(CreatePersonDialogResult *result);
|
||||
/** @brief Retourne les données empruntées du résultat. */
|
||||
const PersonEntityInput *create_person_dialog_result_get_input(
|
||||
const CreatePersonDialogResult *result);
|
||||
const PersonEvidenceSelection *create_person_dialog_result_get_evidence_selection(
|
||||
const CreatePersonDialogResult *result);
|
||||
EvidenceStaging *create_person_dialog_result_steal_staging(
|
||||
CreatePersonDialogResult *result);
|
||||
const GPtrArray *create_person_dialog_result_get_ocr_runs(
|
||||
const CreatePersonDialogResult *result);
|
||||
const GPtrArray *create_person_dialog_result_get_factual_relations(
|
||||
const CreatePersonDialogResult *result);
|
||||
const GPtrArray *create_person_dialog_result_get_ocr_projections(
|
||||
const CreatePersonDialogResult *result);
|
||||
gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window);
|
||||
guint64 create_person_dialog_test_ocr_generation(GtkWindow *window);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_relation_dialog.h
|
||||
* @brief Dialogue GTK de préparation d'une relation entre deux entités.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Résultat opaque produit par le dialogue.
|
||||
*/
|
||||
typedef struct CreateRelationDialogResult CreateRelationDialogResult;
|
||||
typedef struct RelationRecord RelationRecord;
|
||||
|
||||
/**
|
||||
* @brief Codes d'erreur produits par CreateRelationDialog.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT,
|
||||
CREATE_RELATION_DIALOG_ERROR_EMPTY_TARGETS,
|
||||
CREATE_RELATION_DIALOG_ERROR_MEMORY,
|
||||
CREATE_RELATION_DIALOG_ERROR_INVALID_DATA
|
||||
} CreateRelationDialogError;
|
||||
|
||||
/**
|
||||
* @brief Domaine d'erreur du dialogue.
|
||||
*/
|
||||
#define CREATE_RELATION_DIALOG_ERROR \
|
||||
create_relation_dialog_error_quark()
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après validation ou annulation.
|
||||
*
|
||||
* En cas de validation, result appartient au callback et doit être libéré
|
||||
* avec create_relation_dialog_result_free().
|
||||
*
|
||||
* En cas d'annulation, result vaut NULL.
|
||||
*
|
||||
* @param result Résultat possédé par le callback, ou NULL.
|
||||
* @param user_data Données privées fournies lors de l'ouverture.
|
||||
*/
|
||||
typedef void (*CreateRelationDialogCallback)(
|
||||
CreateRelationDialogResult *result,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le domaine d'erreur du dialogue.
|
||||
*/
|
||||
GQuark create_relation_dialog_error_quark(void);
|
||||
|
||||
/**
|
||||
* @brief Présente le dialogue de création d'une relation.
|
||||
*
|
||||
* entities doit contenir des pointeurs EntityRecord valides. Le dialogue
|
||||
* copie tous les identifiants et libellés nécessaires et ne conserve aucun
|
||||
* pointeur vers les modèles fournis.
|
||||
*
|
||||
* L'entité source est retirée de la liste des cibles.
|
||||
*
|
||||
* Cette fonction ne crée aucune relation et n'accède pas à SQLite.
|
||||
*
|
||||
* @param parent Fenêtre GTK parente.
|
||||
* @param source_entity_identifier UUID de l'entité source.
|
||||
* @param entities Tableau de EntityRecord.
|
||||
* @param evidence_records Tableau de EvidenceRecord disponibles.
|
||||
* @param investigation_root_path Racine canonique de l'enquête.
|
||||
* @param callback Fonction appelée après validation ou annulation.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
* @param error Emplacement facultatif recevant une erreur immédiate.
|
||||
*
|
||||
* @return TRUE si le dialogue a été créé et présenté.
|
||||
*/
|
||||
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
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Présente le dialogue prérempli pour modifier une relation.
|
||||
*
|
||||
* La source et la cible sont conservées ; les champs métier sont modifiables.
|
||||
* Le dialogue ne réalise aucune écriture SQLite.
|
||||
*
|
||||
* @param parent Fenêtre GTK parente.
|
||||
* @param relation_record Relation existante à modifier.
|
||||
* @param entities Tableau de EntityRecord.
|
||||
* @param evidence_records Tableau de EvidenceRecord disponibles.
|
||||
* @param selected_evidence_identifiers UUID des preuves déjà liées.
|
||||
* @param investigation_root_path Racine canonique de l'enquête.
|
||||
* @param callback Fonction appelée après validation ou annulation.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
* @param error Emplacement facultatif recevant une erreur immédiate.
|
||||
*
|
||||
* @return TRUE si le dialogue a été créé et présenté.
|
||||
*/
|
||||
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,
|
||||
CreateRelationDialogCallback callback,
|
||||
gpointer user_data,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Valide les valeurs obligatoires d'une relation.
|
||||
*
|
||||
* Cette fonction est indépendante de l'affichage et pourra être testée
|
||||
* sans ouvrir de fenêtre GTK.
|
||||
*
|
||||
* @param source_entity_identifier UUID de l'entité source.
|
||||
* @param target_entity_identifier UUID de l'entité cible.
|
||||
* @param relation_type Type de relation non vide.
|
||||
* @param confidence Confiance comprise entre 0 et 100.
|
||||
* @param error Emplacement facultatif recevant une erreur.
|
||||
*
|
||||
* @return TRUE lorsque les valeurs sont valides.
|
||||
*/
|
||||
gboolean create_relation_dialog_validate_values(
|
||||
const char *source_entity_identifier,
|
||||
const char *target_entity_identifier,
|
||||
const char *relation_type,
|
||||
gint confidence,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère un résultat.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*/
|
||||
void create_relation_dialog_result_free(
|
||||
CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne l'UUID de l'entité source.
|
||||
*/
|
||||
const char *create_relation_dialog_result_get_source_identifier(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne l'UUID de l'entité cible.
|
||||
*/
|
||||
const char *create_relation_dialog_result_get_target_identifier(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le type de relation.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
const char *create_relation_dialog_result_get_label(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la justification facultative.
|
||||
*/
|
||||
const char *create_relation_dialog_result_get_justification(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le niveau de confiance.
|
||||
*/
|
||||
gint create_relation_dialog_result_get_confidence(
|
||||
const CreateRelationDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne les UUID de preuves sélectionnés.
|
||||
*
|
||||
* @param result Résultat validé du dialogue.
|
||||
*
|
||||
* @return Tableau emprunté d'UUID, ou NULL.
|
||||
*/
|
||||
const GPtrArray *create_relation_dialog_result_get_evidence_identifiers(
|
||||
const CreateRelationDialogResult *result);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_social_account_dialog.h
|
||||
* @brief Dialogue GTK de création d'un compte social observé.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||
|
||||
#include "models/evidence_record.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/** @brief Résultat opaque du dialogue de compte social. */
|
||||
typedef struct CreateSocialAccountDialogResult CreateSocialAccountDialogResult;
|
||||
|
||||
/** @brief Callback appelé après validation ou annulation. */
|
||||
typedef void (*CreateSocialAccountDialogCallback)(
|
||||
CreateSocialAccountDialogResult *result,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Présente le formulaire de création d'un compte social.
|
||||
*
|
||||
* Le tableau de preuves est uniquement emprunté pendant cet appel ; les
|
||||
* informations nécessaires au formulaire sont copiées.
|
||||
*
|
||||
* @param parent Fenêtre parente.
|
||||
* @param evidence_records Tableau facultatif de EvidenceRecord.
|
||||
* @param callback Callback de fin.
|
||||
* @param user_data Données privées du callback.
|
||||
* @return TRUE si le dialogue a été présenté.
|
||||
*/
|
||||
gboolean create_social_account_dialog_present(
|
||||
GtkWindow *parent,
|
||||
const GPtrArray *evidence_records,
|
||||
CreateSocialAccountDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Libère un résultat, y compris toutes ses chaînes. */
|
||||
void create_social_account_dialog_result_free(
|
||||
CreateSocialAccountDialogResult *result
|
||||
);
|
||||
|
||||
/** @brief Retourne le code de la plateforme sélectionnée. */
|
||||
const char *create_social_account_dialog_result_get_platform(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'URL complète du profil. */
|
||||
const char *create_social_account_dialog_result_get_profile_url(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne le pseudonyme affiché. */
|
||||
const char *create_social_account_dialog_result_get_username(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'identifiant stable facultatif de la plateforme. */
|
||||
const char *create_social_account_dialog_result_get_platform_identifier(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne la date UTC de première observation. */
|
||||
const char *create_social_account_dialog_result_get_first_observed_at(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'état observé du compte. */
|
||||
const char *create_social_account_dialog_result_get_account_state(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne les notes factuelles facultatives. */
|
||||
const char *create_social_account_dialog_result_get_notes(const CreateSocialAccountDialogResult *result);
|
||||
/** @brief Retourne l'UUID de la preuve associée, ou NULL. */
|
||||
const char *create_social_account_dialog_result_get_evidence_identifier(const CreateSocialAccountDialogResult *result);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#ifndef LABFY_DIALOG_GEOMETRY_H
|
||||
#define LABFY_DIALOG_GEOMETRY_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
typedef struct {
|
||||
int initial_width;
|
||||
int initial_height;
|
||||
int minimum_width;
|
||||
int minimum_height;
|
||||
} LabfyDialogGeometry;
|
||||
|
||||
LabfyDialogGeometry labfy_dialog_geometry_calculate(
|
||||
int workarea_width, int workarea_height);
|
||||
|
||||
void labfy_dialog_apply_standard_geometry(
|
||||
GtkWindow *window, GtkWindow *parent);
|
||||
|
||||
void labfy_dialog_prepare(
|
||||
GtkWindow *window,
|
||||
GtkWindow *parent,
|
||||
gboolean modal,
|
||||
gboolean destroy_with_parent);
|
||||
|
||||
void labfy_dialog_present(GtkWindow *window);
|
||||
|
||||
void labfy_paned_apply_initial_ratio(
|
||||
GtkPaned *paned, double ratio, int minimum_start, int minimum_end);
|
||||
gboolean labfy_paned_initial_ratio_applied(GtkPaned *paned);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#ifndef LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H
|
||||
#define LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H
|
||||
#include "database/database.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct DocumentAuthenticityEditor DocumentAuthenticityEditor;
|
||||
DocumentAuthenticityEditor *document_authenticity_editor_new(Database *database,const char *evidence_identifier);
|
||||
GtkWidget *document_authenticity_editor_get_widget(DocumentAuthenticityEditor *editor);
|
||||
gboolean document_authenticity_editor_refresh(DocumentAuthenticityEditor *editor,GError **error);
|
||||
void document_authenticity_editor_free(DocumentAuthenticityEditor *editor);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#ifndef LABFY_DOCUMENT_IDENTITY_MISUSE_EDITOR_H
|
||||
#define LABFY_DOCUMENT_IDENTITY_MISUSE_EDITOR_H
|
||||
#include "database/database.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct DocumentIdentityMisuseEditor DocumentIdentityMisuseEditor;
|
||||
DocumentIdentityMisuseEditor *document_identity_misuse_editor_new(Database *database,const char *evidence_identifier);
|
||||
GtkWidget *document_identity_misuse_editor_get_widget(DocumentIdentityMisuseEditor *editor);
|
||||
gboolean document_identity_misuse_editor_refresh(DocumentIdentityMisuseEditor *editor,GError **error);
|
||||
void document_identity_misuse_editor_free(DocumentIdentityMisuseEditor *editor);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file eml_analysis_dialog.h
|
||||
* @brief Présentation en lecture seule d'une analyse EML.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
|
||||
#include "core/eml_processing.h"
|
||||
#include "core/eml_integration.h"
|
||||
#include "core/eml_pipeline_task.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
/** @brief Callback recevant les propositions sélectionnées, ou NULL. */
|
||||
typedef void (*EmlAnalysisDialogCallback)(GPtrArray *proposals,
|
||||
gpointer user_data);
|
||||
/**
|
||||
* @brief Affiche les métadonnées et indicateurs extraits d'une copie EML.
|
||||
* @param parent Fenêtre parente.
|
||||
* @param result Résultat emprunté pendant la construction de la fenêtre.
|
||||
*/
|
||||
void eml_analysis_dialog_present(GtkWindow *parent,
|
||||
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
|
||||
gpointer user_data);
|
||||
void eml_analysis_dialog_present_pipeline(
|
||||
GtkWindow *parent,
|
||||
const EmlPipelineResult *result,
|
||||
const char *evidence_name,
|
||||
const char *relative_path,
|
||||
const char *source_sha256,
|
||||
EmlAnalysisDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
#ifndef LABFY_EVIDENCE_IDENTITY_OCR_DIALOG_H
|
||||
#define LABFY_EVIDENCE_IDENTITY_OCR_DIALOG_H
|
||||
|
||||
#include "core/tool_registry.h"
|
||||
#include "core/background_task.h"
|
||||
#include "models/entity_record.h"
|
||||
#include "models/identity_ocr.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct EvidenceIdentityOcrDialogResult
|
||||
EvidenceIdentityOcrDialogResult;
|
||||
typedef gboolean (*EvidenceIdentityOcrDialogSessionCheck)(gpointer user_data);
|
||||
typedef void (*EvidenceIdentityOcrDialogCallback)(
|
||||
EvidenceIdentityOcrDialogResult *result, gpointer user_data);
|
||||
|
||||
gboolean evidence_identity_ocr_dialog_file_is_compatible(
|
||||
const char *file_path);
|
||||
gboolean evidence_identity_ocr_dialog_present(
|
||||
GtkWindow *parent, const char *file_path, const GPtrArray *persons,
|
||||
const char *preselected_person_identifier,
|
||||
const ToolInfo *tesseract_tool,
|
||||
EvidenceIdentityOcrDialogSessionCheck session_check,
|
||||
EvidenceIdentityOcrDialogCallback callback,
|
||||
gpointer user_data, GDestroyNotify user_data_destroy,
|
||||
GError **error);
|
||||
gboolean evidence_identity_ocr_dialog_present_review(
|
||||
GtkWindow *parent,const char *file_path,const GPtrArray *persons,
|
||||
const char *preselected_person_identifier,
|
||||
const IdentityOcrRun *persisted_run,
|
||||
EvidenceIdentityOcrDialogSessionCheck session_check,
|
||||
EvidenceIdentityOcrDialogCallback callback,
|
||||
gpointer user_data,GDestroyNotify user_data_destroy,GError **error);
|
||||
void evidence_identity_ocr_dialog_result_free(
|
||||
EvidenceIdentityOcrDialogResult *result);
|
||||
gboolean evidence_identity_ocr_dialog_result_has_ocr(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
const char *evidence_identity_ocr_dialog_result_get_person_identifier(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
const char *evidence_identity_ocr_dialog_result_get_temporary_identifier(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
const char *evidence_identity_ocr_dialog_result_get_sha256(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
guint64 evidence_identity_ocr_dialog_result_get_size(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
const char *evidence_identity_ocr_dialog_result_get_mime_type(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
IdentityOcrRun *evidence_identity_ocr_dialog_result_steal_run(
|
||||
EvidenceIdentityOcrDialogResult *result);
|
||||
GtkWindow *evidence_identity_ocr_dialog_result_get_dialog(
|
||||
const EvidenceIdentityOcrDialogResult *result);
|
||||
void evidence_identity_ocr_dialog_finish_import(
|
||||
GtkWindow *dialog, const GError *error);
|
||||
void evidence_identity_ocr_dialog_set_submission_task(
|
||||
GtkWindow *dialog, BackgroundTask *task);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file evidence_import_dialog.h
|
||||
* @brief Dialogue GTK de saisie des métadonnées d'une preuve.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_EVIDENCE_IMPORT_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_IMPORT_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Résultat opaque produit par le dialogue d'import.
|
||||
*/
|
||||
typedef struct EvidenceImportDialogResult EvidenceImportDialogResult;
|
||||
|
||||
/**
|
||||
* @brief Codes d'erreur produits par EvidenceImportDialog.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
/**
|
||||
* Un argument public est invalide.
|
||||
*/
|
||||
EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_ARGUMENT,
|
||||
|
||||
/**
|
||||
* Aucun type de preuve n'est disponible.
|
||||
*/
|
||||
EVIDENCE_IMPORT_DIALOG_ERROR_EMPTY_TYPES,
|
||||
|
||||
/**
|
||||
* Une allocation ou la création du dialogue a échoué.
|
||||
*/
|
||||
EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY,
|
||||
|
||||
/**
|
||||
* Les métadonnées saisies sont invalides.
|
||||
*/
|
||||
EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA
|
||||
} EvidenceImportDialogError;
|
||||
|
||||
/**
|
||||
* @brief Domaine d'erreur du dialogue d'import.
|
||||
*/
|
||||
#define EVIDENCE_IMPORT_DIALOG_ERROR \
|
||||
evidence_import_dialog_error_quark()
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après la fermeture du dialogue.
|
||||
*
|
||||
* En cas de validation, result est non NULL et appartient au callback.
|
||||
* Il doit être libéré avec evidence_import_dialog_result_free().
|
||||
*
|
||||
* En cas d'annulation, result vaut NULL.
|
||||
*
|
||||
* @param result Résultat possédé par le callback, ou NULL.
|
||||
* @param user_data Données privées fournies lors de l'ouverture.
|
||||
*/
|
||||
typedef void (*EvidenceImportDialogCallback)(
|
||||
EvidenceImportDialogResult *result,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le domaine d'erreur du dialogue.
|
||||
*
|
||||
* @return Quark GLib associé à EvidenceImportDialog.
|
||||
*/
|
||||
GQuark evidence_import_dialog_error_quark(void);
|
||||
|
||||
/**
|
||||
* @brief Présente le dialogue de saisie des métadonnées.
|
||||
*
|
||||
* Le chemin du fichier est affiché en lecture seule.
|
||||
*
|
||||
* evidence_types doit contenir des pointeurs EvidenceType valides.
|
||||
* Le dialogue copie les codes et les libellés dont il a besoin :
|
||||
* le tableau peut donc être libéré après le retour de cette fonction.
|
||||
*
|
||||
* Le type dont le code vaut "document" est sélectionné par défaut
|
||||
* lorsqu'il est disponible. Sinon, le premier type est sélectionné.
|
||||
*
|
||||
* @param parent Fenêtre GTK parente.
|
||||
* @param file_path Chemin du fichier sélectionné.
|
||||
* @param evidence_types Tableau de EvidenceType.
|
||||
* @param callback Fonction appelée après validation ou annulation.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
* @param error Emplacement facultatif recevant une erreur immédiate.
|
||||
*
|
||||
* @return TRUE si le dialogue a été créé et présenté.
|
||||
*/
|
||||
gboolean evidence_import_dialog_present(
|
||||
GtkWindow *parent,
|
||||
const char *file_path,
|
||||
const GPtrArray *evidence_types,
|
||||
EvidenceImportDialogCallback callback,
|
||||
gpointer user_data,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Valide les valeurs obligatoires d'un import.
|
||||
*
|
||||
* Cette fonction ne dépend pas de l'affichage du dialogue et peut être
|
||||
* testée sans ouvrir de fenêtre GTK.
|
||||
*
|
||||
* Le chemin doit désigner un fichier existant.
|
||||
* Le code du type doit être non vide.
|
||||
* La date doit être une date ISO 8601 valide comprenant un fuseau horaire.
|
||||
*
|
||||
* @param file_path Chemin du fichier à importer.
|
||||
* @param type_identifier Code métier du type de preuve.
|
||||
* @param collected_at Date de collecte au format ISO 8601.
|
||||
* @param error Emplacement facultatif recevant une erreur.
|
||||
*
|
||||
* @return TRUE lorsque les valeurs sont valides.
|
||||
*/
|
||||
gboolean evidence_import_dialog_validate_values(
|
||||
const char *file_path,
|
||||
const char *type_identifier,
|
||||
const char *collected_at,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère le résultat produit par le dialogue.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* @param result Résultat à libérer.
|
||||
*/
|
||||
void evidence_import_dialog_result_free(
|
||||
EvidenceImportDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le code du type sélectionné.
|
||||
*
|
||||
* @return Chaîne empruntée, ou NULL.
|
||||
*/
|
||||
const char *evidence_import_dialog_result_get_type_identifier(
|
||||
const EvidenceImportDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la date ISO 8601 de collecte.
|
||||
*
|
||||
* @return Chaîne empruntée, ou NULL.
|
||||
*/
|
||||
const char *evidence_import_dialog_result_get_collected_at(
|
||||
const EvidenceImportDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la source renseignée.
|
||||
*
|
||||
* @return Chaîne empruntée, ou NULL lorsque la source est vide.
|
||||
*/
|
||||
const char *evidence_import_dialog_result_get_source(
|
||||
const EvidenceImportDialogResult *result
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la description renseignée.
|
||||
*
|
||||
* @return Chaîne empruntée, ou NULL lorsque la description est vide.
|
||||
*/
|
||||
const char *evidence_import_dialog_result_get_description(
|
||||
const EvidenceImportDialogResult *result
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file evidence_metadata_dialog.h
|
||||
* @brief Dialogue de modification des métadonnées éditables d'une preuve.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
|
||||
|
||||
#include "models/evidence_record.h"
|
||||
#include "database/database.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/** @brief Résultat opaque possédé du dialogue. */
|
||||
typedef struct EvidenceMetadataDialogResult EvidenceMetadataDialogResult;
|
||||
|
||||
/** @brief Callback recevant le résultat possédé, ou NULL après annulation. */
|
||||
typedef void (*EvidenceMetadataDialogCallback)(
|
||||
EvidenceMetadataDialogResult *result, gpointer user_data
|
||||
);
|
||||
typedef void (*EvidenceMetadataDialogAnalyzeCallback)(
|
||||
GtkWindow *dialog, const char *evidence_identifier,
|
||||
gboolean revise_existing, const char *ocr_run_identifier,
|
||||
gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Présente le dialogue prérempli de modification des métadonnées.
|
||||
* @param parent Fenêtre parente.
|
||||
* @param record Preuve empruntée.
|
||||
* @param evidence_types Tableau emprunté de EvidenceType.
|
||||
* @param callback Callback appelé une fois à la fermeture.
|
||||
* @param user_data Données empruntées du callback.
|
||||
* @return TRUE si la fenêtre a été créée.
|
||||
*/
|
||||
gboolean evidence_metadata_dialog_present(
|
||||
GtkWindow *parent,
|
||||
const EvidenceRecord *record,
|
||||
const GPtrArray *evidence_types,
|
||||
EvidenceMetadataDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
gboolean evidence_metadata_dialog_present_with_ocr(
|
||||
GtkWindow *parent, const EvidenceRecord *record,
|
||||
const GPtrArray *evidence_types, Database *database,
|
||||
const char *investigation_root,
|
||||
EvidenceMetadataDialogAnalyzeCallback analyze_callback,
|
||||
EvidenceMetadataDialogCallback callback, gpointer user_data);
|
||||
|
||||
/** @brief Libère un résultat, ou accepte NULL. */
|
||||
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result);
|
||||
/** @brief Retourne le code de type emprunté. */
|
||||
const char *evidence_metadata_dialog_result_get_type_identifier(
|
||||
const EvidenceMetadataDialogResult *result);
|
||||
/** @brief Retourne la source empruntée, ou NULL. */
|
||||
const char *evidence_metadata_dialog_result_get_source(
|
||||
const EvidenceMetadataDialogResult *result);
|
||||
/** @brief Retourne la description empruntée, ou NULL. */
|
||||
const char *evidence_metadata_dialog_result_get_description(
|
||||
const EvidenceMetadataDialogResult *result);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file file_dialog.h
|
||||
* @brief Interface du dialogue de sélection d’un fichier de preuve.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_FILE_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_FILE_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Fonction appelée après la fermeture du sélecteur.
|
||||
*
|
||||
* Cas possibles :
|
||||
*
|
||||
* - succès : file_path non NULL et error NULL ;
|
||||
* - annulation : file_path NULL et error NULL ;
|
||||
* - erreur : file_path NULL et error non NULL.
|
||||
*
|
||||
* Le chemin et l’erreur restent valides uniquement pendant l’appel.
|
||||
*
|
||||
* @param file_path Chemin local sélectionné, ou NULL.
|
||||
* @param error Erreur de sélection, ou NULL.
|
||||
* @param user_data Données transmises lors de l’ouverture.
|
||||
*/
|
||||
typedef void (*FileDialogCallback)(
|
||||
const char *file_path,
|
||||
const GError *error,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Fonction appelée après une sélection multiple.
|
||||
*
|
||||
* paths est un tableau emprunté de chemins locaux possédés par le dialogue.
|
||||
* Il n'est valide que pendant l'appel. Une annulation transmet NULL sans
|
||||
* erreur.
|
||||
*
|
||||
* @param paths Chemins sélectionnés, ou NULL.
|
||||
* @param error Erreur de sélection, ou NULL.
|
||||
* @param user_data Données fournies lors de l'ouverture.
|
||||
*/
|
||||
typedef void (*FileDialogMultipleCallback)(
|
||||
const GPtrArray *paths,
|
||||
const GError *error,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Ouvre un dialogue permettant de sélectionner un fichier.
|
||||
*
|
||||
* @param parent Fenêtre parente, éventuellement NULL.
|
||||
* @param callback Fonction appelée lorsque le dialogue est terminé.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void file_dialog_select_file(
|
||||
GtkWindow *parent,
|
||||
FileDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Ouvre un dialogue permettant de sélectionner plusieurs fichiers.
|
||||
* @param parent Fenêtre parente, éventuellement NULL.
|
||||
* @param callback Fonction appelée à la fermeture du dialogue.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void file_dialog_select_files(
|
||||
GtkWindow *parent,
|
||||
FileDialogMultipleCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file folder_dialog.h
|
||||
* @brief Interface du dialogue de sélection d'un dossier d'enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_FOLDER_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_FOLDER_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Fonction appelée après la fermeture du dialogue de sélection.
|
||||
*
|
||||
* @param folder_path Chemin du dossier sélectionné, ou NULL si l'utilisateur
|
||||
* a annulé la sélection.
|
||||
* @param user_data Données transmises lors de l'ouverture du dialogue.
|
||||
*
|
||||
* Le chemin reçu reste valide uniquement pendant l'appel de cette fonction.
|
||||
* Il doit être copié si le code appelant souhaite le conserver.
|
||||
*
|
||||
* @param callback
|
||||
* Fonction appelée lorsque le dialogue est fermé.
|
||||
*
|
||||
* Si l'utilisateur annule la sélection,
|
||||
* folder_path vaut NULL.
|
||||
*/
|
||||
typedef void (*FolderDialogCallback)(
|
||||
const char *folder_path,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Ouvre un dialogue permettant de sélectionner un dossier.
|
||||
*
|
||||
* @param parent Fenêtre parente du dialogue.
|
||||
* Peut être NULL.
|
||||
* @param callback Fonction appelée lorsque le dialogue est terminé.
|
||||
* @param user_data Données transmises à la fonction de rappel.
|
||||
*/
|
||||
void folder_dialog_select_folder(
|
||||
GtkWindow *parent,
|
||||
FolderDialogCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#ifndef IDENTITY_OCR_LANGUAGE_LOADER_H
|
||||
#define IDENTITY_OCR_LANGUAGE_LOADER_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
void identity_ocr_language_loader_start(GtkWindow *window, const char *tesseract_path, GtkDropDown *language_dropdown);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,758 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file main_window.h
|
||||
* @brief Interface publique de la fenêtre principale.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_MAIN_WINDOW_H
|
||||
#define LABFY_INVESTIGATION_MAIN_WINDOW_H
|
||||
|
||||
#include "core/investigation_tree_model.h"
|
||||
#include "widgets/investigation_tree_view.h"
|
||||
#include "core/investigation_node.h"
|
||||
#include "core/task_manager.h"
|
||||
#include "widgets/evidence_category_model.h"
|
||||
#include "models/evidence_record.h"
|
||||
#include "models/osint_action_catalog.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du graphe d'enquête.
|
||||
*/
|
||||
typedef struct InvestigationGraphModel InvestigationGraphModel;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la disposition du graphe.
|
||||
*/
|
||||
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la fenêtre principale.
|
||||
*/
|
||||
typedef struct MainWindow MainWindow;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande une nouvelle enquête.
|
||||
*
|
||||
* @param user_data Données utilisateur associées au callback.
|
||||
*/
|
||||
typedef void (*MainWindowNewInvestigationCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande
|
||||
* l'import d'une preuve.
|
||||
*
|
||||
* @param user_data Données utilisateur associées au callback.
|
||||
*/
|
||||
typedef void (*MainWindowImportEvidenceCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour ajouter un compte de réseau social. */
|
||||
typedef void (*MainWindowAddSocialAccountCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour ajouter une personne. */
|
||||
typedef void (*MainWindowAddPersonCallback)(gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur revient au graphe.
|
||||
*
|
||||
* @param user_data Données utilisateur associées au callback.
|
||||
*/
|
||||
typedef void (*MainWindowShowGraphCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'une preuve est sélectionnée.
|
||||
*
|
||||
* L'identifiant est emprunté et uniquement valide pendant l'appel.
|
||||
* Une sélection vide ou une catégorie transmet NULL.
|
||||
*
|
||||
* @param evidence_identifier UUID de la preuve, ou NULL.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*MainWindowEvidenceSelectionCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande une vérification.
|
||||
*
|
||||
* L'identifiant est emprunté et uniquement valide pendant l'appel.
|
||||
*
|
||||
* @param evidence_identifier UUID de la preuve.
|
||||
* @param user_data Données privées du callback.
|
||||
*/
|
||||
typedef void (*MainWindowVerifyEvidenceCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour modifier les métadonnées d'une preuve. */
|
||||
typedef void (*MainWindowEditEvidenceCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour analyser une preuve EML. */
|
||||
typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
/** @brief Callback appelé pour analyser un RIB par OCR. */
|
||||
typedef void (*MainWindowAnalyzeRibCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
typedef void (*MainWindowIdentityOcrCallback)(
|
||||
const char *evidence_identifier, gboolean revise_existing,
|
||||
const char *ocr_run_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé pour extraire les métadonnées d'une preuve. */
|
||||
typedef void (*MainWindowExtractMetadataCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
||||
typedef void (*MainWindowRecoverPdfPasswordCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après le déplacement effectif d'un nœud.
|
||||
*
|
||||
* @param entity_identifier UUID de l'entité déplacée.
|
||||
* @param x Coordonnée horizontale logique.
|
||||
* @param y Coordonnée verticale logique.
|
||||
* @param user_data Données privées du callback.
|
||||
*/
|
||||
typedef void (*MainWindowGraphNodeMovedCallback)(
|
||||
const char *entity_identifier,
|
||||
double x,
|
||||
double y,
|
||||
gpointer user_data
|
||||
);
|
||||
typedef void (*MainWindowGraphTransformChangedCallback)(gpointer user_data);
|
||||
|
||||
typedef void (*MainWindowExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||
*
|
||||
* @param user_data Données privées du callback.
|
||||
*/
|
||||
typedef void (*MainWindowResetGraphLayoutCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande d'ajout d'une relation.
|
||||
*
|
||||
* @param source_entity_identifier UUID de l'entité source.
|
||||
* @param user_data Données privées du callback.
|
||||
*/
|
||||
typedef void (*MainWindowAddRelationCallback)(
|
||||
const char *source_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour modifier une relation. */
|
||||
typedef void (*MainWindowEditRelationCallback)(
|
||||
const char *relation_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour supprimer une relation. */
|
||||
typedef void (*MainWindowDeleteRelationCallback)(
|
||||
const char *relation_identifier, gpointer user_data);
|
||||
|
||||
/** @brief Callback appelé quand une relation est sélectionnée. */
|
||||
typedef void (*MainWindowRelationSelectedCallback)(
|
||||
const char *relation_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour catégoriser une personne. */
|
||||
typedef void (*MainWindowPersonRoleCallback)(const char *entity_identifier,
|
||||
PersonRole role, gpointer user_data);
|
||||
/** @brief Callback appelé pour modifier la confiance d'une personne. */
|
||||
typedef void (*MainWindowPersonConfidenceCallback)(
|
||||
const char *entity_identifier, gint confidence, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
||||
*
|
||||
* @param action_identifier Identifiant stable de l'action.
|
||||
* @param target_identifier UUID de la sélection ciblée.
|
||||
* @param target_value Valeur métier ciblée.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*MainWindowOsintActionCallback)(
|
||||
const char *action_identifier,
|
||||
const char *target_identifier,
|
||||
const char *target_value,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour modifier le nom affiché d'une personne. */
|
||||
typedef void (*MainWindowPersonNameCallback)(const char *entity_identifier,
|
||||
const char *display_name, gpointer user_data);
|
||||
/** @brief Callback appelé pour gérer les preuves d'une personne. */
|
||||
typedef void (*MainWindowPersonEvidenceCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé quand une entité est sélectionnée. */
|
||||
typedef void (*MainWindowEntitySelectedCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de vérification d'une preuve.
|
||||
*
|
||||
* MainWindow relaie la demande provenant du Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_verify_evidence_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowVerifyEvidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de modification de la preuve sélectionnée.
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_edit_evidence_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowEditEvidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback d'analyse locale d'une preuve EML. */
|
||||
void main_window_set_analyze_eml_callback(MainWindow *main_window,
|
||||
MainWindowAnalyzeEmlCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback d'analyse OCR d'un RIB. */
|
||||
void main_window_set_analyze_rib_callback(MainWindow *main_window,
|
||||
MainWindowAnalyzeRibCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback d'extraction locale des métadonnées. */
|
||||
void main_window_set_extract_metadata_callback(MainWindow *main_window,
|
||||
MainWindowExtractMetadataCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de récupération d'un mot de passe PDF. */
|
||||
void main_window_set_recover_pdf_password_callback(MainWindow *main_window,
|
||||
MainWindowRecoverPdfPasswordCallback callback, gpointer user_data);
|
||||
void main_window_set_identity_ocr_callback(
|
||||
MainWindow *main_window, MainWindowIdentityOcrCallback callback,
|
||||
gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de fin de déplacement d'un nœud.
|
||||
*
|
||||
* MainWindow relaie uniquement l'événement provenant du Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_graph_node_moved_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowGraphNodeMovedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
void main_window_set_graph_transform_changed_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowGraphTransformChangedCallback callback,
|
||||
gpointer user_data);
|
||||
|
||||
void main_window_set_extraction_drop_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||
*
|
||||
* MainWindow relaie uniquement la demande provenant du Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_reset_graph_layout_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowResetGraphLayoutCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande d'ajout d'une relation.
|
||||
*
|
||||
* MainWindow relaie uniquement la demande provenant du Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_add_relation_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowAddRelationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Définit le callback de modification d'une relation. */
|
||||
void main_window_set_edit_relation_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowEditRelationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback de suppression d'une relation. */
|
||||
void main_window_set_delete_relation_callback(MainWindow *main_window,
|
||||
MainWindowDeleteRelationCallback callback, gpointer user_data);
|
||||
|
||||
/** @brief Définit le callback de sélection d'une relation. */
|
||||
void main_window_set_relation_selected_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowRelationSelectedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Affiche les preuves associées à la relation sélectionnée. */
|
||||
void main_window_set_relation_evidences(
|
||||
MainWindow *main_window,
|
||||
const GPtrArray *evidence_records
|
||||
);
|
||||
|
||||
/** @brief Transmet les preuves associées à l'entité sélectionnée. */
|
||||
void main_window_set_person_evidences(MainWindow *main_window,
|
||||
const GPtrArray *records);
|
||||
void main_window_set_person_factual_relations(MainWindow *main_window,
|
||||
const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields);
|
||||
|
||||
|
||||
/** @brief Définit le callback de catégorisation d'une personne. */
|
||||
void main_window_set_person_role_callback(MainWindow *main_window,
|
||||
MainWindowPersonRoleCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de confiance d'une personne. */
|
||||
void main_window_set_person_confidence_callback(MainWindow *main_window,
|
||||
MainWindowPersonConfidenceCallback callback, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de déclenchement des actions OSINT.
|
||||
*
|
||||
* MainWindow relaie uniquement la demande provenant du Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_osint_action_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowOsintActionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback de modification du nom affiché. */
|
||||
void main_window_set_person_name_callback(MainWindow *main_window,
|
||||
MainWindowPersonNameCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de gestion des preuves d'une personne. */
|
||||
void main_window_set_person_evidence_callback(MainWindow *main_window,
|
||||
MainWindowPersonEvidenceCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de sélection d'une entité. */
|
||||
void main_window_set_entity_selected_callback(MainWindow *main_window,
|
||||
MainWindowEntitySelectedCallback callback, gpointer user_data);
|
||||
/** @brief Affiche les preuves de la personne sélectionnée. */
|
||||
void main_window_set_person_evidences(MainWindow *main_window,
|
||||
const GPtrArray *evidence_records);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton « Nouvelle enquête ».
|
||||
*
|
||||
* La fenêtre ne crée pas directement l'enquête. Elle transmet uniquement
|
||||
* la demande au contrôleur.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Fonction appelée lors du clic.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void main_window_set_new_investigation_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowNewInvestigationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle fenêtre principale.
|
||||
*
|
||||
* MainWindow ne devient pas propriétaire de task_manager.
|
||||
*
|
||||
* @param application Application GTK.
|
||||
* @param task_manager Gestionnaire de tâches de l'application.
|
||||
*
|
||||
* @return Nouvelle fenêtre, ou NULL en cas d'échec.
|
||||
*/
|
||||
MainWindow *main_window_new(
|
||||
GtkApplication *application,
|
||||
TaskManager *task_manager
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche la fenêtre principale.
|
||||
*
|
||||
* @param main_window Fenêtre à afficher.
|
||||
*/
|
||||
void main_window_present(MainWindow *main_window);
|
||||
|
||||
/**
|
||||
* @brief Retourne la fenêtre GTK associée.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
*
|
||||
* @return Fenêtre GTK.
|
||||
*/
|
||||
GtkWindow *main_window_get_window(
|
||||
const MainWindow *main_window
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Transmet un modèle d'arborescence à la fenêtre principale.
|
||||
*
|
||||
* La fenêtre principale ne devient pas propriétaire du modèle.
|
||||
* Elle le transmet uniquement à la Sidebar.
|
||||
*
|
||||
* @param main_window Fenêtre principale à mettre à jour.
|
||||
* @param tree_model Modèle d'arborescence en lecture seule.
|
||||
*/
|
||||
void main_window_set_tree_model(
|
||||
MainWindow *main_window,
|
||||
const InvestigationTreeModel *tree_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Transmet le modèle des catégories de preuves à la Sidebar.
|
||||
*
|
||||
* MainWindow ne devient pas propriétaire du modèle.
|
||||
* Le modèle doit rester valide pendant son affichage.
|
||||
*
|
||||
* Passer NULL vide l'onglet Preuves.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param evidence_category_model Modèle de catégories, ou NULL.
|
||||
*/
|
||||
void main_window_set_evidence_model(
|
||||
MainWindow *main_window,
|
||||
EvidenceCategoryModel *evidence_category_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Met à jour l'identité de l'enquête affichée.
|
||||
*
|
||||
* La fonction met à jour :
|
||||
*
|
||||
* - le titre de la fenêtre ;
|
||||
* - le texte de la barre d'état.
|
||||
*
|
||||
* Les chaînes sont copiées par GTK et restent la propriété de l'appelant.
|
||||
*
|
||||
* Cette fonction accepte des chaînes NULL ou vides.
|
||||
*
|
||||
* @param main_window Fenêtre principale à mettre à jour.
|
||||
* @param investigation_name Nom persistant de l'enquête.
|
||||
* @param investigation_root_path Chemin racine de l'enquête.
|
||||
*/
|
||||
void main_window_set_investigation(
|
||||
MainWindow *main_window,
|
||||
const char *investigation_name,
|
||||
const char *investigation_root_path
|
||||
);
|
||||
|
||||
void main_window_set_tree_selection_callback(
|
||||
MainWindow *main_window,
|
||||
InvestigationTreeViewSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection d'une preuve.
|
||||
*
|
||||
* MainWindow transmet uniquement le callback à la Sidebar.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_evidence_selection_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowEvidenceSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche dans le Workspace les informations du nœud sélectionné.
|
||||
*
|
||||
* La fenêtre principale ne devient pas propriétaire du nœud.
|
||||
*
|
||||
* @param main_window Fenêtre principale à mettre à jour.
|
||||
* @param node Nœud sélectionné en lecture seule, ou NULL.
|
||||
*/
|
||||
void main_window_set_selected_node(
|
||||
MainWindow *main_window,
|
||||
const InvestigationNode *node
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche la fiche de la preuve sélectionnée.
|
||||
*
|
||||
* MainWindow ne conserve pas le EvidenceRecord.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param evidence_record Preuve sélectionnée, ou NULL.
|
||||
*/
|
||||
void main_window_set_selected_evidence(
|
||||
MainWindow *main_window,
|
||||
const EvidenceRecord *evidence_record
|
||||
);
|
||||
void main_window_set_identity_ocr_runs(
|
||||
MainWindow *main_window, GPtrArray *owned_records);
|
||||
void main_window_set_evidence_observations(MainWindow *main_window,
|
||||
const GPtrArray *observations);
|
||||
void main_window_set_observation_remove_callback(MainWindow *main_window,
|
||||
void (*callback)(const char *, gpointer), gpointer user_data);
|
||||
void main_window_set_eml_analysis_available(
|
||||
MainWindow *main_window,
|
||||
gboolean available
|
||||
);
|
||||
|
||||
/** @brief Transmet au workspace un chemin absolu validé pour l'aperçu. */
|
||||
void main_window_set_evidence_preview(MainWindow *main_window,
|
||||
const char *file_path, const char *display_name);
|
||||
void main_window_show_evidence_preview(MainWindow *main_window,
|
||||
const char *investigation_root, const EvidenceRecord *record);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Affiche l'état de chargement du graphe.
|
||||
*
|
||||
* MainWindow transmet uniquement la demande au Workspace.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
*/
|
||||
void main_window_set_graph_loading(
|
||||
MainWindow *main_window
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche le graphe chargé et sa disposition dans le Workspace.
|
||||
*
|
||||
* MainWindow et Workspace empruntent graph_model et graph_layout.
|
||||
*
|
||||
* Passer NULL pour graph_layout conserve le placement automatique.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param graph_model Graphe emprunté, ou NULL.
|
||||
* @param graph_layout Disposition empruntée, ou NULL.
|
||||
*/
|
||||
void main_window_set_graph(
|
||||
MainWindow *main_window,
|
||||
const InvestigationGraphModel *graph_model,
|
||||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean main_window_get_graph_view_transform(
|
||||
MainWindow *main_window, double *zoom, double *offset_x, double *offset_y);
|
||||
void main_window_set_graph_view_transform(
|
||||
MainWindow *main_window, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/** @brief Sélectionne une relation dans le graphe affiché. */
|
||||
gboolean main_window_select_graph_relation(
|
||||
MainWindow *main_window,
|
||||
const char *relation_identifier
|
||||
);
|
||||
/** @brief Sélectionne une entité dans le graphe affiché. */
|
||||
gboolean main_window_select_graph_entity(MainWindow *main_window,
|
||||
const char *entity_identifier);
|
||||
|
||||
/**
|
||||
* @brief Affiche une erreur de chargement du graphe.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param message Message d'erreur, ou NULL.
|
||||
*/
|
||||
void main_window_set_graph_error(
|
||||
MainWindow *main_window,
|
||||
const char *message
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Détache le graphe du Workspace.
|
||||
*
|
||||
* Aucun graphe n'est libéré par MainWindow.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
*/
|
||||
void main_window_clear_graph(
|
||||
MainWindow *main_window
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Reconstruit visuellement la disposition automatique du graphe.
|
||||
*
|
||||
* Cette fonction ne modifie pas SQLite.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
*/
|
||||
void main_window_reset_graph_layout(
|
||||
MainWindow *main_window
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Met à jour le texte de la barre d'état.
|
||||
*
|
||||
* Si status_text vaut NULL, le texte par défaut est affiché.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param status_text Nouveau texte de statut, ou NULL.
|
||||
*/
|
||||
void main_window_set_status(
|
||||
MainWindow *main_window,
|
||||
const char *status_text
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Transmet au workspace l'état d'un outil OSINT externe.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param tool_identifier Identifiant stable de l'outil.
|
||||
* @param state État traduit par l'application.
|
||||
* @param version Version détectée facultative.
|
||||
*/
|
||||
void main_window_set_osint_tool_state(
|
||||
MainWindow *main_window,
|
||||
const char *tool_identifier,
|
||||
OsintActionToolState state,
|
||||
const char *version
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande l'ouverture
|
||||
* d'une enquête existante.
|
||||
*
|
||||
* @param user_data Données utilisateur associées au callback.
|
||||
*/
|
||||
typedef void (*MainWindowOpenInvestigationCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande à quitter.
|
||||
*
|
||||
* @param user_data Données utilisateur associées au callback.
|
||||
*/
|
||||
typedef void (*MainWindowQuitCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton « Ouvrir une enquête ».
|
||||
*
|
||||
* La fenêtre transmet uniquement la demande au contrôleur.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Fonction appelée lors du clic.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void main_window_set_open_investigation_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowOpenInvestigationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton « Quitter ».
|
||||
*
|
||||
* MainWindow transmet uniquement la demande au contrôleur.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Fonction appelée lors du clic.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void main_window_set_quit_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowQuitCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de l'action « Importer une preuve ».
|
||||
*
|
||||
* MainWindow transmet uniquement la demande au contrôleur.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Fonction appelée lors du clic.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void main_window_set_import_evidence_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowImportEvidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de l'action d'ajout d'un compte social.
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void main_window_set_add_social_account_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowAddSocialAccountCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback d'ajout d'une personne. */
|
||||
void main_window_set_add_person_callback(MainWindow *main_window,
|
||||
MainWindowAddPersonCallback callback, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton « Revenir au graphe ».
|
||||
*
|
||||
* MainWindow transmet uniquement la demande au contrôleur.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param callback Fonction appelée lors du clic, ou NULL.
|
||||
* @param user_data Données transmises au callback.
|
||||
*/
|
||||
void main_window_set_show_graph_callback(
|
||||
MainWindow *main_window,
|
||||
MainWindowShowGraphCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Active ou désactive l'action d'import d'une preuve.
|
||||
*
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param enabled TRUE pour autoriser l'import.
|
||||
*/
|
||||
void main_window_set_import_evidence_enabled(
|
||||
MainWindow *main_window,
|
||||
gboolean enabled
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Active ou désactive l'ajout d'un compte social.
|
||||
* @param main_window Fenêtre principale.
|
||||
* @param enabled TRUE si une enquête est ouverte.
|
||||
*/
|
||||
void main_window_set_add_social_account_enabled(
|
||||
MainWindow *main_window,
|
||||
gboolean enabled
|
||||
);
|
||||
/** @brief Active ou désactive l'ajout d'une personne. */
|
||||
void main_window_set_add_person_enabled(MainWindow *main_window, gboolean enabled);
|
||||
|
||||
/**
|
||||
* @brief Libère les ressources de la fenêtre.
|
||||
*
|
||||
* @param main_window Fenêtre à détruire.
|
||||
*/
|
||||
void main_window_free(MainWindow *main_window);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file manage_entity_evidence_dialog.h
|
||||
* @brief Dialogue de sélection des preuves associées à une entité.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_MANAGE_ENTITY_EVIDENCE_DIALOG_H
|
||||
#include "models/evidence_record.h"
|
||||
#include "core/task_manager.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef void (*ManageEntityEvidenceDialogCallback)(
|
||||
GPtrArray *selected_identifiers, gpointer user_data);
|
||||
typedef void (*ManageEntityEvidenceImportCallback)(gpointer user_data);
|
||||
/**
|
||||
* @brief Présente la sélection multiple de preuves avec aperçu.
|
||||
* @param parent Fenêtre parente.
|
||||
* @param evidence_records Preuves disponibles empruntées.
|
||||
* @param selected_identifiers UUID déjà associés empruntés.
|
||||
* @param investigation_root_path Racine de l'enquête.
|
||||
* @param callback Callback recevant un tableau possédé, ou NULL à l'annulation.
|
||||
* @param user_data Données empruntées du callback.
|
||||
* @param error Destination facultative d'une erreur immédiate.
|
||||
* @return TRUE si le dialogue a été présenté.
|
||||
*/
|
||||
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||
const GPtrArray *evidence_records, const GPtrArray *selected_identifiers,
|
||||
const char *investigation_root_path,
|
||||
TaskManager *task_manager,
|
||||
ManageEntityEvidenceDialogCallback callback, gpointer user_data,
|
||||
ManageEntityEvidenceImportCallback import_callback,
|
||||
GError **error);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file metadata_analysis_dialog.h
|
||||
* @brief Fenêtre de consultation des métadonnées extraites.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_METADATA_ANALYSIS_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_METADATA_ANALYSIS_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Affiche une synthèse ExifTool dans une fenêtre défilable.
|
||||
* @param parent Fenêtre parente facultative.
|
||||
* @param summary Métadonnées lisibles empruntées.
|
||||
* @param json_path Chemin emprunté de la sortie JSON conservée.
|
||||
* @param version Version empruntée d'ExifTool.
|
||||
*/
|
||||
void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
||||
const char *json_path, const char *version);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file osint_dns_review_dialog.h
|
||||
* @brief Dialogue de sélection des propositions issues d'une réponse DNS.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_OSINT_DNS_REVIEW_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_OSINT_DNS_REVIEW_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après confirmation de la sélection DNS.
|
||||
*
|
||||
* Le tableau et les propositions sont empruntés et uniquement valides pendant
|
||||
* l'appel. Le tableau contient seulement les propositions compatibles cochées.
|
||||
*
|
||||
* @param selected_proposals Propositions sélectionnées.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*OsintDnsReviewDialogConfirmCallback)(
|
||||
GPtrArray *selected_proposals,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche le dialogue de sélection avant intégration.
|
||||
*
|
||||
* Le dialogue conserve une référence sur proposals jusqu'à sa fermeture.
|
||||
* Fermer ou annuler ne déclenche pas le callback.
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param proposals Tableau de OsintDnsProposal à réviser.
|
||||
* @param callback Callback de confirmation facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void osint_dns_review_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
GPtrArray *proposals,
|
||||
OsintDnsReviewDialogConfirmCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file osint_execution_history_dialog.h
|
||||
* @brief Consultation en lecture seule de l'historique OSINT.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_OSINT_EXECUTION_HISTORY_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_OSINT_EXECUTION_HISTORY_DIALOG_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Affiche les exécutions et leurs objets liés dans une fenêtre modale.
|
||||
*
|
||||
* La fonction conserve une référence sur les deux conteneurs jusqu'à la
|
||||
* fermeture. records contient des OsintExecutionRecord. linked_objects associe
|
||||
* chaque UUID d'exécution à un GPtrArray de chaînes.
|
||||
*
|
||||
* @param parent_window Fenêtre parente, ou NULL.
|
||||
* @param records Historique non vide, de la plus récente à l'ancienne.
|
||||
* @param linked_objects Table des objets liés par UUID d'exécution.
|
||||
*/
|
||||
void osint_execution_history_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
GPtrArray *records,
|
||||
GHashTable *linked_objects
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file pdf_password_dialog.h
|
||||
* @brief Paramétrage d'une récupération de mot de passe PDF.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_PDF_PASSWORD_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_PDF_PASSWORD_DIALOG_H
|
||||
|
||||
#include "core/pdf_password_recovery.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/** @brief Callback recevant la méthode et son paramètre validé. */
|
||||
typedef void (*PdfPasswordDialogCallback)(PdfPasswordRecoveryMethod method,
|
||||
const char *parameter, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Présente le formulaire de récupération ciblée.
|
||||
* @param parent Fenêtre parente facultative.
|
||||
* @param callback Callback appelé après validation.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void pdf_password_dialog_present(GtkWindow *parent,
|
||||
PdfPasswordDialogCallback callback, gpointer user_data);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#ifndef LABFY_PERSON_FACTUAL_RELATION_EDITOR_H
|
||||
#define LABFY_PERSON_FACTUAL_RELATION_EDITOR_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include "core/person_creation_coordinator.h"
|
||||
#include "models/identity_ocr.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct PersonFactualRelationEditor PersonFactualRelationEditor;
|
||||
|
||||
PersonFactualRelationEditor *person_factual_relation_editor_new(void);
|
||||
|
||||
GtkWidget *person_factual_relation_editor_get_widget(
|
||||
PersonFactualRelationEditor *editor);
|
||||
|
||||
void person_factual_relation_editor_set_available_evidence(
|
||||
PersonFactualRelationEditor *editor,
|
||||
GtkStringList *evidence_labels,
|
||||
GPtrArray *evidence_identifiers);
|
||||
|
||||
void person_factual_relation_editor_set_available_ocr_runs(
|
||||
PersonFactualRelationEditor *editor,
|
||||
GPtrArray *ocr_runs);
|
||||
|
||||
gboolean person_factual_relation_editor_collect_relations(
|
||||
PersonFactualRelationEditor *editor, GPtrArray **relations,
|
||||
GError **error);
|
||||
guint person_factual_relation_editor_get_count(
|
||||
const PersonFactualRelationEditor *editor);
|
||||
gboolean person_factual_relation_editor_validate(
|
||||
PersonFactualRelationEditor *editor, GError **error);
|
||||
void person_factual_relation_editor_append_summary(
|
||||
PersonFactualRelationEditor *editor, GString *summary);
|
||||
|
||||
void person_factual_relation_editor_free(PersonFactualRelationEditor *editor);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#ifndef LABFY_PERSON_OCR_PROJECTION_EDITOR_H
|
||||
#define LABFY_PERSON_OCR_PROJECTION_EDITOR_H
|
||||
#include "models/identity_ocr.h"
|
||||
#include "models/person_ocr_projection.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct PersonOcrProjectionEditor PersonOcrProjectionEditor;
|
||||
PersonOcrProjectionEditor *person_ocr_projection_editor_new(void);
|
||||
GtkWidget *person_ocr_projection_editor_get_widget(PersonOcrProjectionEditor *editor);
|
||||
void person_ocr_projection_editor_set_runs(PersonOcrProjectionEditor *editor,const GPtrArray *runs);
|
||||
gboolean person_ocr_projection_editor_collect(PersonOcrProjectionEditor *editor,GPtrArray **projections,GError **error);
|
||||
void person_ocr_projection_editor_append_summary(
|
||||
PersonOcrProjectionEditor *editor, GString *summary);
|
||||
void person_ocr_projection_editor_free(PersonOcrProjectionEditor *editor);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#ifndef LABFY_PERSON_VOCABULARY_ADAPTER_H
|
||||
#define LABFY_PERSON_VOCABULARY_ADAPTER_H
|
||||
#include "database/database.h"
|
||||
#include "models/identity_traceability.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct PersonVocabularyAdapter PersonVocabularyAdapter;
|
||||
PersonVocabularyAdapter *person_vocabulary_adapter_new(
|
||||
Database *database, GError **error);
|
||||
void person_vocabulary_adapter_free(PersonVocabularyAdapter *adapter);
|
||||
const GPtrArray *person_vocabulary_adapter_get_roles(
|
||||
const PersonVocabularyAdapter *adapter);
|
||||
const GPtrArray *person_vocabulary_adapter_get_statuses(
|
||||
const PersonVocabularyAdapter *adapter);
|
||||
GtkStringList *person_vocabulary_adapter_create_role_labels(
|
||||
const PersonVocabularyAdapter *adapter);
|
||||
GtkStringList *person_vocabulary_adapter_create_status_labels(
|
||||
const PersonVocabularyAdapter *adapter);
|
||||
GtkStringList *person_vocabulary_adapter_create_relation_labels(void);
|
||||
const char *person_vocabulary_adapter_relation_code(guint index);
|
||||
const char *person_vocabulary_adapter_relation_label(const char *code);
|
||||
const char *person_vocabulary_adapter_relation_description(const char *code);
|
||||
const char *person_vocabulary_adapter_status_code(
|
||||
const PersonVocabularyAdapter *adapter, guint index);
|
||||
const char *person_vocabulary_adapter_status_label(
|
||||
const PersonVocabularyAdapter *adapter, guint index);
|
||||
GPtrArray *person_vocabulary_adapter_selected_role_labels(
|
||||
const GPtrArray *buttons);
|
||||
GPtrArray *person_vocabulary_adapter_create_role_buttons(
|
||||
const PersonVocabularyAdapter *adapter, GtkBox *container);
|
||||
GPtrArray *person_vocabulary_adapter_build_role_assignments(
|
||||
const PersonVocabularyAdapter *adapter, const GPtrArray *buttons,
|
||||
const char *evidence_identifier);
|
||||
gboolean person_vocabulary_adapter_justification_valid(
|
||||
const PersonRoleVocabularyEntry *entry, const char *justification);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file rib_ocr_review_dialog.h
|
||||
* @brief Révision humaine des résultats OCR d'un RIB.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_RIB_OCR_REVIEW_DIALOG_H
|
||||
#define LABFY_INVESTIGATION_RIB_OCR_REVIEW_DIALOG_H
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef void (*RibOcrReviewCallback)(const char *iban, gpointer user_data);
|
||||
/** @brief Présente le texte OCR et un IBAN révisable. */
|
||||
void rib_ocr_review_dialog_present(GtkWindow *parent, const char *ocr_text,
|
||||
const char *suggested_iban, RibOcrReviewCallback callback,
|
||||
gpointer user_data);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file controlled_vocab_dropdown.h
|
||||
* @brief Composant GTK4 (GtkDropDown) basé sur le vocabulaire contrôlé.
|
||||
******************************************************************************/
|
||||
#ifndef LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H
|
||||
#define LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H
|
||||
|
||||
#include "core/controlled_vocab.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Crée un widget GtkDropDown initialisé avec une catégorie de vocabulaire contrôlé.
|
||||
* @param category Catégorie de vocabulaire.
|
||||
* @param default_code Code sélectionné par défaut (facultatif).
|
||||
* @return Nouveau GtkWidget (GtkDropDown).
|
||||
*/
|
||||
GtkWidget *controlled_vocab_dropdown_new(ControlledVocabCategory category,
|
||||
const char *default_code);
|
||||
|
||||
/**
|
||||
* @brief Obtient le code technique sélectionné dans le dropdown.
|
||||
* @param dropdown Widget GtkDropDown créé avec controlled_vocab_dropdown_new.
|
||||
* @return Code technique emprunté, ou NULL si rien n'est sélectionné.
|
||||
*/
|
||||
const char *controlled_vocab_dropdown_get_selected_code(GtkWidget *dropdown);
|
||||
|
||||
/**
|
||||
* @brief Sélectionne un code technique dans le dropdown.
|
||||
* @param dropdown Widget GtkDropDown.
|
||||
* @param code Code à sélectionner.
|
||||
* @return TRUE si le code a été trouvé et sélectionné, FALSE sinon.
|
||||
*/
|
||||
gboolean controlled_vocab_dropdown_set_selected_code(GtkWidget *dropdown,
|
||||
const char *code);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H */
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file entity_details_panel.h
|
||||
* @brief Volet latéral affichant les détails d'une entité.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_ENTITY_DETAILS_PANEL_H
|
||||
#define LABFY_INVESTIGATION_ENTITY_DETAILS_PANEL_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include "models/entity_record.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Modèle opaque représentant une entité.
|
||||
*/
|
||||
typedef struct EntityRecord EntityRecord;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du volet de détails.
|
||||
*/
|
||||
typedef struct EntityDetailsPanel EntityDetailsPanel;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur ferme le volet.
|
||||
*
|
||||
* Le callback n'est pas déclenché lors d'une fermeture programmatique avec
|
||||
* entity_details_panel_clear().
|
||||
*
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*EntityDetailsPanelCloseCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque l'utilisateur demande une relation.
|
||||
*
|
||||
* L'identifiant est emprunté et uniquement valide pendant l'appel.
|
||||
*
|
||||
* @param source_entity_identifier UUID de l'entité source.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*EntityDetailsPanelAddRelationCallback)(
|
||||
const char *source_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé lors du changement de catégorie d'une personne. */
|
||||
typedef void (*EntityDetailsPanelPersonRoleCallback)(
|
||||
const char *entity_identifier,
|
||||
PersonRole role,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour enregistrer la confiance d'une personne. */
|
||||
typedef void (*EntityDetailsPanelPersonConfidenceCallback)(
|
||||
const char *entity_identifier,
|
||||
gint confidence,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour enregistrer le nom affiché d'une personne. */
|
||||
typedef void (*EntityDetailsPanelPersonNameCallback)(
|
||||
const char *entity_identifier,
|
||||
const char *display_name,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour gérer les preuves d'une personne. */
|
||||
typedef void (*EntityDetailsPanelPersonEvidenceCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé lorsqu'une preuve associée est activée. */
|
||||
typedef void (*EntityDetailsPanelEvidenceActivatedCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé pour ouvrir les recherches OSINT compatibles. */
|
||||
typedef void (*EntityDetailsPanelOsintCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Crée un volet de détails fermé.
|
||||
*
|
||||
* @return Nouveau volet, ou NULL en cas d'échec.
|
||||
*/
|
||||
EntityDetailsPanel *entity_details_panel_new(void);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine du volet.
|
||||
*
|
||||
* Le widget appartient au module EntityDetailsPanel.
|
||||
*
|
||||
* @param details_panel Volet à consulter.
|
||||
*
|
||||
* @return Widget GTK racine, ou NULL.
|
||||
*/
|
||||
GtkWidget *entity_details_panel_get_widget(
|
||||
const EntityDetailsPanel *details_panel
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche une entité et ouvre le volet.
|
||||
*
|
||||
* Les textes sont immédiatement copiés dans les widgets GTK.
|
||||
* Le pointeur EntityRecord n'est jamais conservé.
|
||||
*
|
||||
* Passer NULL équivaut à entity_details_panel_clear().
|
||||
*
|
||||
* @param details_panel Volet à mettre à jour.
|
||||
* @param entity_record Entité empruntée, ou NULL.
|
||||
*/
|
||||
void entity_details_panel_set_entity(
|
||||
EntityDetailsPanel *details_panel,
|
||||
const EntityRecord *entity_record
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Vide et referme le volet sans appeler le callback de fermeture.
|
||||
*
|
||||
* @param details_panel Volet à vider.
|
||||
*/
|
||||
void entity_details_panel_clear(
|
||||
EntityDetailsPanel *details_panel
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton de fermeture.
|
||||
*
|
||||
* Le callback et user_data sont empruntés.
|
||||
*
|
||||
* @param details_panel Volet à configurer.
|
||||
* @param callback Callback à appeler, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void entity_details_panel_set_close_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelCloseCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback du bouton d'ajout d'une relation.
|
||||
*
|
||||
* Le volet transmet uniquement l'UUID de l'entité actuellement affichée.
|
||||
*
|
||||
* @param details_panel Volet à configurer.
|
||||
* @param callback Callback à appeler, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void entity_details_panel_set_add_relation_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelAddRelationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Définit le callback de changement de catégorie d'une personne. */
|
||||
void entity_details_panel_set_person_role_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelPersonRoleCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de modification de la confiance d'une personne.
|
||||
*
|
||||
* @param details_panel Volet à configurer.
|
||||
* @param callback Callback à appeler, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void entity_details_panel_set_person_confidence_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelPersonConfidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Définit le callback de modification du nom affiché. */
|
||||
void entity_details_panel_set_person_name_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelPersonNameCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback de gestion des preuves d'une personne. */
|
||||
void entity_details_panel_set_person_evidence_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelPersonEvidenceCallback callback, gpointer user_data);
|
||||
void entity_details_panel_set_evidence_activated_callback(
|
||||
EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelEvidenceActivatedCallback callback,
|
||||
gpointer user_data);
|
||||
/** @brief Définit le callback du bouton de recherche OSINT. */
|
||||
void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
|
||||
EntityDetailsPanelOsintCallback callback, gpointer user_data);
|
||||
/**
|
||||
* @brief Affiche les preuves associées à la personne courante.
|
||||
* @param details_panel Volet à mettre à jour.
|
||||
* @param evidence_records Tableau emprunté de EvidenceRecord, ou NULL.
|
||||
*/
|
||||
void entity_details_panel_set_person_evidences(
|
||||
EntityDetailsPanel *details_panel, const GPtrArray *evidence_records);
|
||||
void entity_details_panel_set_person_factual_relations(
|
||||
EntityDetailsPanel *details_panel, const GPtrArray *relations, const GPtrArray *evidence_records);
|
||||
void entity_details_panel_set_person_profile_fields(EntityDetailsPanel *details_panel,const GHashTable *fields);
|
||||
|
||||
/**
|
||||
* @brief Indique si le volet est actuellement ouvert.
|
||||
*
|
||||
* @param details_panel Volet à consulter.
|
||||
*
|
||||
* @return TRUE si le volet est ouvert, sinon FALSE.
|
||||
*/
|
||||
gboolean entity_details_panel_is_open(
|
||||
const EntityDetailsPanel *details_panel
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure d'encapsulation du volet.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* @param details_panel Volet à libérer.
|
||||
*/
|
||||
void entity_details_panel_free(
|
||||
EntityDetailsPanel *details_panel
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_WIDGET_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_WIDGET_H
|
||||
|
||||
#include "core/evidence_preview.h"
|
||||
#include "core/task_manager.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct EvidencePreviewWidget EvidencePreviewWidget;
|
||||
typedef gboolean (*EvidencePreviewWidgetSessionCheck)(gpointer user_data);
|
||||
|
||||
EvidencePreviewWidget *evidence_preview_widget_new(
|
||||
TaskManager *task_manager,
|
||||
EvidencePreviewWidgetSessionCheck session_check,
|
||||
gpointer session_data);
|
||||
GtkWidget *evidence_preview_widget_get_widget(
|
||||
const EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_show(EvidencePreviewWidget *widget,
|
||||
const EvidencePreviewRequest *request, const char *metadata);
|
||||
void evidence_preview_widget_clear(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_cancel(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_free(EvidencePreviewWidget *widget);
|
||||
const char *evidence_preview_widget_get_state(
|
||||
const EvidencePreviewWidget *widget);
|
||||
char *evidence_preview_widget_dup_text(
|
||||
const EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_zoom_in(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_zoom_out(EvidencePreviewWidget *widget);
|
||||
void evidence_preview_widget_fit(EvidencePreviewWidget *widget);
|
||||
double evidence_preview_widget_get_zoom(
|
||||
const EvidencePreviewWidget *widget);
|
||||
gboolean evidence_preview_widget_is_fit(
|
||||
const EvidencePreviewWidget *widget);
|
||||
gboolean evidence_preview_widget_set_pdf_page(
|
||||
EvidencePreviewWidget *widget, guint page);
|
||||
guint evidence_preview_widget_get_pdf_page(
|
||||
const EvidencePreviewWidget *widget);
|
||||
guint evidence_preview_widget_get_pdf_page_count(
|
||||
const EvidencePreviewWidget *widget);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file evidence_tree_view.h
|
||||
* @brief Affichage GTK hiérarchique des catégories et preuves importées.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_EVIDENCE_TREE_VIEW_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_TREE_VIEW_H
|
||||
|
||||
#include "widgets/evidence_category_model.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Vue opaque affichant les preuves regroupées par catégorie.
|
||||
*/
|
||||
typedef struct EvidenceTreeView EvidenceTreeView;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'une preuve est sélectionnée.
|
||||
*
|
||||
* L'identifiant est emprunté et uniquement valide pendant l'appel.
|
||||
*
|
||||
* Lorsque la sélection est vide ou correspond à une catégorie,
|
||||
* evidence_identifier vaut NULL.
|
||||
*
|
||||
* @param evidence_identifier UUID de la preuve sélectionnée, ou NULL.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*EvidenceTreeViewSelectionCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une vue hiérarchique vide.
|
||||
*
|
||||
* @return Nouvelle vue, ou NULL.
|
||||
*/
|
||||
EvidenceTreeView *evidence_tree_view_new(void);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine.
|
||||
*
|
||||
* Le widget retourné est emprunté.
|
||||
*
|
||||
* @param evidence_tree_view Vue à consulter.
|
||||
*
|
||||
* @return GtkScrolledWindow racine, ou NULL.
|
||||
*/
|
||||
GtkWidget *evidence_tree_view_get_widget(
|
||||
const EvidenceTreeView *evidence_tree_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Installe un modèle de catégories.
|
||||
*
|
||||
* La vue ne devient pas propriétaire de EvidenceCategoryModel.
|
||||
* Le modèle doit rester valide pendant son utilisation.
|
||||
*
|
||||
* Passer NULL vide la vue.
|
||||
*
|
||||
* @param evidence_tree_view Vue à actualiser.
|
||||
* @param evidence_category_model Modèle de catégories, ou NULL.
|
||||
*/
|
||||
void evidence_tree_view_set_model(
|
||||
EvidenceTreeView *evidence_tree_view,
|
||||
EvidenceCategoryModel *evidence_category_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection.
|
||||
*
|
||||
* @param evidence_tree_view Vue à configurer.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void evidence_tree_view_set_selection_callback(
|
||||
EvidenceTreeView *evidence_tree_view,
|
||||
EvidenceTreeViewSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure d'encapsulation.
|
||||
*
|
||||
* Les widgets GTK intégrés dans la fenêtre restent gérés par l'arbre GTK.
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* @param evidence_tree_view Vue à libérer.
|
||||
*/
|
||||
void evidence_tree_view_free(
|
||||
EvidenceTreeView *evidence_tree_view
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file investigation_graph_view.h
|
||||
* @brief Vue GTK en lecture seule du graphe d'enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_VIEW_H
|
||||
#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_VIEW_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque d'une entité métier.
|
||||
*/
|
||||
typedef struct EntityRecord EntityRecord;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque d'une relation métier.
|
||||
*/
|
||||
typedef struct RelationRecord RelationRecord;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du graphe métier d'une enquête.
|
||||
*/
|
||||
typedef struct InvestigationGraphModel InvestigationGraphModel;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la disposition persistée du graphe.
|
||||
*/
|
||||
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la vue graphique.
|
||||
*/
|
||||
typedef struct InvestigationGraphView InvestigationGraphView;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'un changement de sélection.
|
||||
*
|
||||
* Les enregistrements sont empruntés au graphe et restent valides uniquement
|
||||
* pendant l'appel. Un seul des deux pointeurs est non NULL. Deux pointeurs
|
||||
* NULL représentent une désélection.
|
||||
*
|
||||
* @param entity_record Entité sélectionnée empruntée, ou NULL.
|
||||
* @param relation_record Relation sélectionnée empruntée, ou NULL.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*InvestigationGraphViewSelectionCallback)(
|
||||
const EntityRecord *entity_record,
|
||||
const RelationRecord *relation_record,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après le déplacement effectif d'un nœud.
|
||||
*
|
||||
* Les coordonnées sont exprimées dans l'espace logique du graphe.
|
||||
* node_identifier est emprunté au modèle et reste valable uniquement
|
||||
* pendant l'appel.
|
||||
*
|
||||
* @param node_identifier UUID du nœud déplacé (entité ou relation).
|
||||
* @param x Coordonnée horizontale logique du coin supérieur gauche.
|
||||
* @param y Coordonnée verticale logique du coin supérieur gauche.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*InvestigationGraphViewNodeMovedCallback)(
|
||||
const char *node_identifier,
|
||||
double x,
|
||||
double y,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
typedef void (*InvestigationGraphViewTransformChangedCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé lorsqu'une extraction est déposée sur le graphe. */
|
||||
typedef void (*InvestigationGraphViewExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle vue graphique vide.
|
||||
*
|
||||
* La vue ne possède jamais le graphe qui lui est transmis.
|
||||
*
|
||||
* @return Nouvelle vue graphique, ou NULL en cas d'échec.
|
||||
*/
|
||||
InvestigationGraphView *investigation_graph_view_new(void);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine de la vue.
|
||||
*
|
||||
* Le widget appartient à InvestigationGraphView.
|
||||
*
|
||||
* @param graph_view Vue graphique à consulter.
|
||||
*
|
||||
* @return Widget GTK racine, ou NULL.
|
||||
*/
|
||||
GtkWidget *investigation_graph_view_get_widget(
|
||||
const InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Associe un graphe métier à la vue.
|
||||
*
|
||||
* Le graphe est emprunté et doit rester valide pendant son affichage.
|
||||
* Passer NULL équivaut à investigation_graph_view_clear().
|
||||
*
|
||||
* @param graph_view Vue graphique à mettre à jour.
|
||||
* @param graph_model Graphe emprunté, ou NULL.
|
||||
*/
|
||||
void investigation_graph_view_set_graph(
|
||||
InvestigationGraphView *graph_view,
|
||||
const InvestigationGraphModel *graph_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Associe une disposition persistée à la vue.
|
||||
*
|
||||
* La disposition est empruntée et doit rester valide pendant son utilisation.
|
||||
*
|
||||
* Lorsqu'un graphe est déjà affiché, la disposition privée de ses nœuds est
|
||||
* reconstruite. Une position persistée remplace la position automatique
|
||||
* uniquement lorsqu'elle existe pour l'entité concernée.
|
||||
*
|
||||
* Passer NULL rétablit le placement automatique.
|
||||
*
|
||||
* @param graph_view Vue graphique à mettre à jour.
|
||||
* @param graph_layout Disposition persistée empruntée, ou NULL.
|
||||
*/
|
||||
void investigation_graph_view_set_layout(
|
||||
InvestigationGraphView *graph_view,
|
||||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Détache le graphe actuellement affiché.
|
||||
*
|
||||
* Le graphe précédemment emprunté n'est pas libéré.
|
||||
*
|
||||
* @param graph_view Vue graphique à vider.
|
||||
*/
|
||||
void investigation_graph_view_clear(
|
||||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de changement de sélection.
|
||||
*
|
||||
* Le callback et user_data sont empruntés par la vue.
|
||||
*
|
||||
* @param graph_view Vue graphique.
|
||||
* @param callback Callback à appeler, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void investigation_graph_view_set_selection_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de fin de déplacement d'un nœud.
|
||||
*
|
||||
* Le callback et user_data sont empruntés par la vue.
|
||||
*
|
||||
* Le callback n'est appelé ni pendant le déplacement du canvas, ni pour
|
||||
* un simple clic, ni pendant une reconstruction de la disposition.
|
||||
*
|
||||
* @param graph_view Vue graphique.
|
||||
* @param callback Callback à appeler, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void investigation_graph_view_set_node_moved_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewNodeMovedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
void investigation_graph_view_set_transform_changed_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewTransformChangedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
void investigation_graph_view_set_extraction_drop_callback(
|
||||
InvestigationGraphView *graph_view,
|
||||
InvestigationGraphViewExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne l'entité actuellement sélectionnée.
|
||||
*
|
||||
* Le pointeur retourné est emprunté au graphe.
|
||||
*
|
||||
* @param graph_view Vue graphique à consulter.
|
||||
*
|
||||
* @return Entité sélectionnée empruntée, ou NULL.
|
||||
*/
|
||||
const EntityRecord *investigation_graph_view_get_selected_entity(
|
||||
const InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Sélectionne une entité du graphe par son UUID.
|
||||
*
|
||||
* Le nœud trouvé est placé au centre de la zone visible. La vue reste
|
||||
* inchangée si l'identifiant est absent du graphe.
|
||||
*
|
||||
* @param graph_view Vue graphique à modifier.
|
||||
* @param entity_identifier UUID de l'entité.
|
||||
*
|
||||
* @return TRUE lorsque l'entité est trouvée et sélectionnée.
|
||||
*/
|
||||
gboolean investigation_graph_view_select_entity(
|
||||
InvestigationGraphView *graph_view,
|
||||
const char *entity_identifier
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Sélectionne une relation du graphe par son UUID.
|
||||
*
|
||||
* @param graph_view Vue graphique à modifier.
|
||||
* @param relation_identifier UUID de la relation.
|
||||
*
|
||||
* @return TRUE lorsque la relation est trouvée et sélectionnée.
|
||||
*/
|
||||
gboolean investigation_graph_view_select_relation(
|
||||
InvestigationGraphView *graph_view,
|
||||
const char *relation_identifier
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Supprime la sélection courante.
|
||||
*
|
||||
* Le callback reçoit NULL seulement si une sélection existait.
|
||||
*
|
||||
* @param graph_view Vue graphique.
|
||||
*/
|
||||
void investigation_graph_view_clear_selection(
|
||||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Réinitialise le zoom et la position du canvas.
|
||||
*
|
||||
* Le graphe emprunté reste associé à la vue.
|
||||
*
|
||||
* @param graph_view Vue graphique à recentrer.
|
||||
*/
|
||||
void investigation_graph_view_reset_view(
|
||||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Lit le zoom et le déplacement courants du canvas.
|
||||
*
|
||||
* @return TRUE lorsqu'un graphe est actuellement présenté.
|
||||
*/
|
||||
gboolean investigation_graph_view_get_view_transform(
|
||||
const InvestigationGraphView *graph_view,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y
|
||||
);
|
||||
|
||||
/** @brief Restaure un zoom et un déplacement précédemment lus. */
|
||||
void investigation_graph_view_set_view_transform(
|
||||
InvestigationGraphView *graph_view,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Restaure la disposition déterministe initiale des nœuds.
|
||||
*
|
||||
* Le zoom et le déplacement global du canvas sont conservés.
|
||||
*
|
||||
* @param graph_view Vue graphique à réorganiser.
|
||||
*/
|
||||
void investigation_graph_view_reset_layout(
|
||||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure d'encapsulation de la vue.
|
||||
*
|
||||
* Cette fonction accepte NULL et ne libère jamais le graphe emprunté.
|
||||
*
|
||||
* @param graph_view Vue graphique à libérer.
|
||||
*/
|
||||
void investigation_graph_view_free(
|
||||
InvestigationGraphView *graph_view
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file investigation_tree_view.h
|
||||
* @brief Interface publique de la vue arborescente d'une enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_INVESTIGATION_TREE_VIEW_H
|
||||
#define LABFY_INVESTIGATION_INVESTIGATION_TREE_VIEW_H
|
||||
|
||||
#include "core/investigation_tree_model.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la vue arborescente d'une enquête.
|
||||
*
|
||||
* La structure réelle est définie dans investigation_tree_view.c.
|
||||
* Les autres modules manipulent uniquement un pointeur vers
|
||||
* InvestigationTreeView.
|
||||
*/
|
||||
typedef struct InvestigationTreeView InvestigationTreeView;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'un nœud est sélectionné.
|
||||
*
|
||||
* @param node Nœud sélectionné, ou NULL si aucune sélection n'est active.
|
||||
* @param user_data Données privées fournies lors de l'enregistrement
|
||||
* du callback.
|
||||
*/
|
||||
typedef void (*InvestigationTreeViewSelectionCallback)(
|
||||
const InvestigationNode *node,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection.
|
||||
*
|
||||
* Le callback est appelé à chaque changement de sélection.
|
||||
*
|
||||
* Le composant ne devient pas propriétaire du callback ni de user_data.
|
||||
*
|
||||
* @param tree_view Vue arborescente.
|
||||
* @param callback Fonction appelée lors d'une sélection.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void investigation_tree_view_set_selection_callback(
|
||||
InvestigationTreeView *tree_view,
|
||||
InvestigationTreeViewSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle vue arborescente vide.
|
||||
*
|
||||
* Aucun modèle métier n'est associé au composant lors de sa création.
|
||||
*
|
||||
* @return Une nouvelle vue, ou NULL si sa création échoue.
|
||||
*/
|
||||
InvestigationTreeView *investigation_tree_view_new(void);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine de la vue arborescente.
|
||||
*
|
||||
* Le widget retourné appartient au module InvestigationTreeView.
|
||||
* Le code appelant ne doit ni le détruire ni en libérer la référence.
|
||||
*
|
||||
* @param tree_view Vue arborescente à consulter.
|
||||
*
|
||||
* @return Le widget racine, ou NULL si tree_view vaut NULL.
|
||||
*/
|
||||
GtkWidget *investigation_tree_view_get_widget(
|
||||
const InvestigationTreeView *tree_view
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Associe un modèle métier à la vue arborescente.
|
||||
*
|
||||
* La vue ne devient pas propriétaire du modèle. Le modèle doit rester valide
|
||||
* pendant toute la durée de son affichage.
|
||||
*
|
||||
* Passer NULL retire le modèle actuel et vide la vue.
|
||||
*
|
||||
* @param tree_view Vue arborescente à mettre à jour.
|
||||
* @param tree_model Modèle métier en lecture seule, ou NULL.
|
||||
*/
|
||||
void investigation_tree_view_set_model(
|
||||
InvestigationTreeView *tree_view,
|
||||
const InvestigationTreeModel *tree_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure d'encapsulation de la vue.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* Les objets GTK intégrés à l'arbre de widgets restent gérés par GTK.
|
||||
*
|
||||
* @param tree_view Vue arborescente à libérer.
|
||||
*/
|
||||
void investigation_tree_view_free(
|
||||
InvestigationTreeView *tree_view
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#ifndef LABFY_OCR_PROVENANCE_OVERLAY_H
|
||||
#define LABFY_OCR_PROVENANCE_OVERLAY_H
|
||||
|
||||
#include "models/identity_ocr.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
typedef struct OcrProvenanceOverlay OcrProvenanceOverlay;
|
||||
|
||||
OcrProvenanceOverlay *ocr_provenance_overlay_new(void);
|
||||
void ocr_provenance_overlay_free(OcrProvenanceOverlay *overlay);
|
||||
GtkWidget *ocr_provenance_overlay_get_widget(OcrProvenanceOverlay *overlay);
|
||||
void ocr_provenance_overlay_set_image(OcrProvenanceOverlay *overlay,
|
||||
GBytes *png_bytes, guint64 generation);
|
||||
void ocr_provenance_overlay_set_field(OcrProvenanceOverlay *overlay,
|
||||
const IdentityFieldObservation *field, guint64 generation);
|
||||
void ocr_provenance_overlay_clear(OcrProvenanceOverlay *overlay,
|
||||
guint64 generation);
|
||||
void ocr_provenance_overlay_set_page(
|
||||
OcrProvenanceOverlay *overlay, guint page, guint64 generation);
|
||||
void ocr_provenance_overlay_zoom_in(OcrProvenanceOverlay *overlay);
|
||||
void ocr_provenance_overlay_zoom_out(OcrProvenanceOverlay *overlay);
|
||||
void ocr_provenance_overlay_fit(OcrProvenanceOverlay *overlay);
|
||||
double ocr_provenance_overlay_get_zoom(
|
||||
const OcrProvenanceOverlay *overlay);
|
||||
gboolean ocr_provenance_overlay_has_region(
|
||||
const OcrProvenanceOverlay *overlay);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file sidebar.h
|
||||
* @brief Interface publique du panneau de navigation latéral.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_SIDEBAR_H
|
||||
#define LABFY_INVESTIGATION_SIDEBAR_H
|
||||
|
||||
#include "core/investigation_tree_model.h"
|
||||
#include "widgets/investigation_tree_view.h"
|
||||
#include "widgets/evidence_category_model.h"
|
||||
#include "models/investigation_graph_model.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du panneau latéral.
|
||||
*
|
||||
* La structure réelle est définie dans sidebar.c.
|
||||
* Les autres modules manipulent uniquement un pointeur vers Sidebar.
|
||||
*/
|
||||
typedef struct Sidebar Sidebar;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'une preuve est sélectionnée.
|
||||
*
|
||||
* L'identifiant est emprunté et uniquement valide pendant l'appel.
|
||||
* Une sélection vide ou une catégorie transmet NULL.
|
||||
*
|
||||
* @param evidence_identifier UUID de la preuve, ou NULL.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*SidebarEvidenceSelectionCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'une entité est sélectionnée.
|
||||
*
|
||||
* L'identifiant est emprunté et valide uniquement pendant l'appel.
|
||||
* Une sélection vide transmet NULL.
|
||||
*
|
||||
* @param entity_identifier UUID de l'entité, ou NULL.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*SidebarEntitySelectionCallback)(
|
||||
const char *entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsqu'une relation est sélectionnée.
|
||||
*
|
||||
* @param relation_identifier UUID emprunté de la relation, ou NULL.
|
||||
* @param user_data Données privées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*SidebarRelationSelectionCallback)(
|
||||
const char *relation_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Crée un nouveau panneau de navigation latéral.
|
||||
*
|
||||
* @return Un nouveau panneau latéral, ou NULL en cas d'échec.
|
||||
*/
|
||||
Sidebar *sidebar_new(void);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine du panneau latéral.
|
||||
*
|
||||
* Le widget retourné appartient au module Sidebar et ne doit pas être libéré
|
||||
* directement par le code appelant.
|
||||
*
|
||||
* @param sidebar Panneau latéral à consulter.
|
||||
*
|
||||
* @return Le widget GTK racine, ou NULL si sidebar est NULL.
|
||||
*/
|
||||
GtkWidget *sidebar_get_widget(
|
||||
const Sidebar *sidebar
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Associe un modèle d'arborescence à la barre latérale.
|
||||
*
|
||||
* La barre latérale ne devient pas propriétaire du modèle.
|
||||
* Le modèle doit rester valide pendant toute la durée de son utilisation.
|
||||
*
|
||||
* Pour ce ticket, seul le nom du nœud racine est affiché dans le titre.
|
||||
*
|
||||
* @param sidebar Barre latérale à mettre à jour.
|
||||
* @param tree_model Modèle d'arborescence en lecture seule.
|
||||
*/
|
||||
void sidebar_set_tree_model(
|
||||
Sidebar *sidebar,
|
||||
const InvestigationTreeModel *tree_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback appelé lors de la sélection d'un nœud.
|
||||
*
|
||||
* La Sidebar transmet simplement ce callback à InvestigationTreeView.
|
||||
*
|
||||
* @param sidebar Barre latérale à configurer.
|
||||
* @param callback Fonction appelée lors d'un changement de sélection.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void sidebar_set_selection_callback(
|
||||
Sidebar *sidebar,
|
||||
InvestigationTreeViewSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Installe le modèle des catégories de preuves.
|
||||
*
|
||||
* La Sidebar ne devient pas propriétaire du modèle.
|
||||
* Passer NULL rétablit l'état vide.
|
||||
*
|
||||
* @param sidebar Barre latérale à actualiser.
|
||||
* @param evidence_category_model Modèle de catégories, ou NULL.
|
||||
*/
|
||||
void sidebar_set_evidence_model(
|
||||
Sidebar *sidebar,
|
||||
EvidenceCategoryModel *evidence_category_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection d'une preuve.
|
||||
*
|
||||
* @param sidebar Barre latérale à configurer.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void sidebar_set_evidence_selection_callback(
|
||||
Sidebar *sidebar,
|
||||
SidebarEvidenceSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Installe les entités et relations du graphe dans la barre latérale.
|
||||
*
|
||||
* La Sidebar emprunte graph_model. Passer NULL vide la liste.
|
||||
*
|
||||
* @param sidebar Barre latérale à actualiser.
|
||||
* @param graph_model Graphe emprunté, ou NULL.
|
||||
*/
|
||||
void sidebar_set_graph_model(
|
||||
Sidebar *sidebar,
|
||||
const InvestigationGraphModel *graph_model
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection d'une entité.
|
||||
*
|
||||
* @param sidebar Barre latérale à configurer.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void sidebar_set_entity_selection_callback(
|
||||
Sidebar *sidebar,
|
||||
SidebarEntitySelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection d'une relation.
|
||||
*
|
||||
* @param sidebar Barre latérale à configurer.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données privées transmises au callback.
|
||||
*/
|
||||
void sidebar_set_relation_selection_callback(
|
||||
Sidebar *sidebar,
|
||||
SidebarRelationSelectionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure d'encapsulation du panneau latéral.
|
||||
*
|
||||
* Cette fonction accepte NULL.
|
||||
*
|
||||
* @param sidebar Panneau latéral à libérer.
|
||||
*/
|
||||
void sidebar_free(Sidebar *sidebar);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file task_panel.h
|
||||
* @brief Panneau GTK affichant les tâches suivies par TaskManager.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_TASK_PANEL_H
|
||||
#define LABFY_INVESTIGATION_TASK_PANEL_H
|
||||
|
||||
#include "core/task_manager.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du panneau d’activité.
|
||||
*/
|
||||
typedef struct TaskPanel TaskPanel;
|
||||
|
||||
/**
|
||||
* @brief Crée un panneau affichant les tâches d’un gestionnaire.
|
||||
*
|
||||
* Le panneau ne devient pas propriétaire de task_manager.
|
||||
* Le gestionnaire doit rester valide pendant toute la durée de vie
|
||||
* du panneau.
|
||||
*
|
||||
* @param task_manager Gestionnaire de tâches observé.
|
||||
*
|
||||
* @return Nouveau panneau, ou NULL si le gestionnaire est invalide.
|
||||
*/
|
||||
TaskPanel *task_panel_new(
|
||||
TaskManager *task_manager
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget racine du panneau.
|
||||
*
|
||||
* Le pointeur retourné est emprunté.
|
||||
*
|
||||
* @param task_panel Panneau concerné.
|
||||
*
|
||||
* @return Widget racine, ou NULL.
|
||||
*/
|
||||
GtkWidget *task_panel_get_widget(
|
||||
const TaskPanel *task_panel
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Reconstruit l’affichage à partir du gestionnaire.
|
||||
*
|
||||
* @param task_panel Panneau à actualiser.
|
||||
*/
|
||||
void task_panel_refresh(
|
||||
TaskPanel *task_panel
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère le panneau et arrête son rafraîchissement périodique.
|
||||
*
|
||||
* Le gestionnaire associé n’est pas libéré.
|
||||
*
|
||||
* @param task_panel Panneau à libérer.
|
||||
*/
|
||||
void task_panel_free(
|
||||
TaskPanel *task_panel
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,552 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file workspace.h
|
||||
* @brief Interface publique de la zone principale de travail.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_WORKSPACE_H
|
||||
#define LABFY_INVESTIGATION_WORKSPACE_H
|
||||
|
||||
#include "core/investigation_node.h"
|
||||
#include "models/evidence_record.h"
|
||||
#include "models/evidence_observation.h"
|
||||
#include "models/entity_record.h"
|
||||
#include "models/osint_action_catalog.h"
|
||||
#include "models/identity_ocr.h"
|
||||
#include "core/task_manager.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque du graphe d'enquête.
|
||||
*/
|
||||
typedef struct InvestigationGraphModel InvestigationGraphModel;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la disposition du graphe.
|
||||
*/
|
||||
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque de la zone de travail.
|
||||
*/
|
||||
typedef struct Workspace Workspace;
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande de vérification d'une preuve.
|
||||
*
|
||||
* @param evidence_identifier UUID de la preuve affichée.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*WorkspaceVerifyEvidenceCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé après le déplacement effectif d'un nœud.
|
||||
*
|
||||
* Les coordonnées sont exprimées dans l'espace logique du graphe.
|
||||
*
|
||||
* @param node_identifier UUID du nœud déplacé.
|
||||
* @param x Coordonnée horizontale logique.
|
||||
* @param y Coordonnée verticale logique.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*WorkspaceGraphNodeMovedCallback)(
|
||||
const char *node_identifier,
|
||||
double x,
|
||||
double y,
|
||||
gpointer user_data
|
||||
);
|
||||
typedef void (*WorkspaceGraphTransformChangedCallback)(gpointer user_data);
|
||||
|
||||
typedef void (*WorkspaceExtractionDropCallback)(
|
||||
const char *file_path,
|
||||
const char *target_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||
*
|
||||
* Le Workspace ne modifie ni SQLite ni le modèle de disposition. Il relaie
|
||||
* uniquement la demande vers la couche applicative.
|
||||
*
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*WorkspaceResetGraphLayoutCallback)(
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors d'une demande d'ajout d'une relation.
|
||||
*
|
||||
* @param source_entity_identifier UUID de l'entité source.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*WorkspaceAddRelationCallback)(
|
||||
const char *source_entity_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour modifier la relation sélectionnée. */
|
||||
typedef void (*WorkspaceEditRelationCallback)(
|
||||
const char *relation_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour supprimer la relation sélectionnée. */
|
||||
typedef void (*WorkspaceDeleteRelationCallback)(
|
||||
const char *relation_identifier, gpointer user_data);
|
||||
|
||||
/** @brief Callback appelé quand une relation devient sélectionnée. */
|
||||
typedef void (*WorkspaceRelationSelectedCallback)(
|
||||
const char *relation_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Callback appelé pour catégoriser une personne. */
|
||||
typedef void (*WorkspacePersonRoleCallback)(const char *entity_identifier,
|
||||
PersonRole role, gpointer user_data);
|
||||
/** @brief Callback appelé pour modifier la confiance d'une personne. */
|
||||
typedef void (*WorkspacePersonConfidenceCallback)(
|
||||
const char *entity_identifier, gint confidence, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
||||
*
|
||||
* Les chaînes sont empruntées et uniquement valides pendant l'appel.
|
||||
*
|
||||
* @param action_identifier Identifiant stable de l'action.
|
||||
* @param target_identifier UUID de la sélection ciblée.
|
||||
* @param target_value Valeur métier ciblée par l'action.
|
||||
* @param user_data Données empruntées fournies par l'appelant.
|
||||
*/
|
||||
typedef void (*WorkspaceOsintActionCallback)(
|
||||
const char *action_identifier,
|
||||
const char *target_identifier,
|
||||
const char *target_value,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour modifier le nom affiché d'une personne. */
|
||||
typedef void (*WorkspacePersonNameCallback)(const char *entity_identifier,
|
||||
const char *display_name, gpointer user_data);
|
||||
/** @brief Callback appelé pour gérer les preuves d'une personne. */
|
||||
typedef void (*WorkspacePersonEvidenceCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
typedef void (*WorkspaceEvidenceActivatedCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
/** @brief Callback appelé quand une entité devient sélectionnée. */
|
||||
typedef void (*WorkspaceEntitySelectedCallback)(
|
||||
const char *entity_identifier, gpointer user_data);
|
||||
|
||||
/** @brief Callback appelé pour modifier les métadonnées d'une preuve. */
|
||||
typedef void (*WorkspaceEditEvidenceCallback)(
|
||||
const char *evidence_identifier,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Callback appelé pour analyser une preuve EML. */
|
||||
typedef void (*WorkspaceAnalyzeEmlCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
/** @brief Callback appelé pour analyser un RIB par OCR. */
|
||||
typedef void (*WorkspaceAnalyzeRibCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
/** @brief Callback appelé pour extraire les métadonnées d'une preuve. */
|
||||
typedef void (*WorkspaceExtractMetadataCallback)(const char *evidence_identifier,
|
||||
gpointer user_data);
|
||||
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
||||
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
typedef void (*WorkspaceObservationRemoveCallback)(
|
||||
const char *observation_identifier, gpointer user_data);
|
||||
typedef void (*WorkspaceIdentityOcrCallback)(
|
||||
const char *evidence_identifier, gboolean revise_existing,
|
||||
const char *ocr_run_identifier, gpointer user_data);
|
||||
|
||||
typedef struct {
|
||||
IdentityOcrRun *run;
|
||||
char *person_identifier;
|
||||
char *executed_at;
|
||||
char *text_relative_path;
|
||||
char *text_sha256;
|
||||
char *tsv_relative_path;
|
||||
char *tsv_sha256;
|
||||
} WorkspaceIdentityOcrRecord;
|
||||
|
||||
WorkspaceIdentityOcrRecord *workspace_identity_ocr_record_new(
|
||||
IdentityOcrRun *owned_run, const char *person_identifier,
|
||||
const char *executed_at, const char *text_relative_path,
|
||||
const char *text_sha256, const char *tsv_relative_path,
|
||||
const char *tsv_sha256);
|
||||
void workspace_identity_ocr_record_free(WorkspaceIdentityOcrRecord *record);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle zone de travail.
|
||||
*
|
||||
* @return Une nouvelle zone de travail, ou NULL en cas d'échec.
|
||||
*/
|
||||
Workspace *workspace_new(TaskManager *task_manager);
|
||||
|
||||
/**
|
||||
* @brief Retourne le widget GTK racine de la zone de travail.
|
||||
*
|
||||
* Le widget retourné appartient au module Workspace.
|
||||
*
|
||||
* @param workspace Zone de travail à consulter.
|
||||
*
|
||||
* @return Widget racine, ou NULL.
|
||||
*/
|
||||
GtkWidget *workspace_get_widget(
|
||||
const Workspace *workspace
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche les informations d'un nœud sélectionné.
|
||||
*
|
||||
* Le Workspace emprunte le nœud.
|
||||
* Passer NULL restaure l'état principal courant.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param node Nœud emprunté, ou NULL.
|
||||
*/
|
||||
void workspace_set_selected_node(
|
||||
Workspace *workspace,
|
||||
const InvestigationNode *node
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche la fiche détaillée d'une preuve.
|
||||
*
|
||||
* Les textes sont copiés dans les widgets. Le Workspace ne conserve pas le
|
||||
* pointeur EvidenceRecord.
|
||||
*
|
||||
* Passer NULL restaure l'état principal courant.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param evidence_record Preuve empruntée, ou NULL.
|
||||
*/
|
||||
void workspace_set_selected_evidence(
|
||||
Workspace *workspace,
|
||||
const EvidenceRecord *evidence_record
|
||||
);
|
||||
void workspace_set_identity_ocr_runs(
|
||||
Workspace *workspace, GPtrArray *owned_records);
|
||||
void workspace_set_identity_ocr_callback(
|
||||
Workspace *workspace, WorkspaceIdentityOcrCallback callback,
|
||||
gpointer user_data);
|
||||
void workspace_set_evidence_observations(Workspace *workspace,
|
||||
const GPtrArray *observations);
|
||||
void workspace_set_observation_remove_callback(Workspace *workspace,
|
||||
WorkspaceObservationRemoveCallback callback, gpointer user_data);
|
||||
void workspace_set_eml_analysis_available(
|
||||
Workspace *workspace,
|
||||
gboolean available
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche l'aperçu local d'un fichier image ou vidéo.
|
||||
*
|
||||
* file_path doit être un chemin absolu déjà validé par l'application.
|
||||
* Passer NULL libère l'aperçu courant.
|
||||
*/
|
||||
void workspace_set_evidence_preview(Workspace *workspace,
|
||||
const char *file_path, const char *display_name);
|
||||
void workspace_show_evidence_preview(Workspace *workspace,
|
||||
const char *investigation_root, const EvidenceRecord *record);
|
||||
|
||||
/**
|
||||
* @brief Sélectionne une entité dans le graphe affiché.
|
||||
*
|
||||
* La sélection actualise également le volet de détails et le contexte OSINT.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param entity_identifier UUID de l'entité à sélectionner.
|
||||
*
|
||||
* @return TRUE lorsque l'entité est trouvée et sélectionnée.
|
||||
*/
|
||||
gboolean workspace_select_graph_entity(
|
||||
Workspace *workspace,
|
||||
const char *entity_identifier
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Sélectionne une relation dans le graphe affiché.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param relation_identifier UUID de la relation à sélectionner.
|
||||
*
|
||||
* @return TRUE lorsque la relation est trouvée et sélectionnée.
|
||||
*/
|
||||
gboolean workspace_select_graph_relation(
|
||||
Workspace *workspace,
|
||||
const char *relation_identifier
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Actualise l'état d'un outil requis par les actions OSINT.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param tool_identifier Identifiant stable de l'outil.
|
||||
* @param state État traduit par la couche applicative.
|
||||
* @param version Version détectée facultative.
|
||||
*/
|
||||
void workspace_set_osint_tool_state(
|
||||
Workspace *workspace,
|
||||
const char *tool_identifier,
|
||||
OsintActionToolState state,
|
||||
const char *version
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de déclenchement des actions OSINT.
|
||||
*
|
||||
* Le Workspace ne lance aucun outil externe. Il relaie uniquement l'action
|
||||
* et la valeur du contexte courant vers la couche applicative.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_osint_action_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceOsintActionCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de modification de la preuve sélectionnée.
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_edit_evidence_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceEditEvidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback d'analyse locale d'une preuve EML. */
|
||||
void workspace_set_analyze_eml_callback(Workspace *workspace,
|
||||
WorkspaceAnalyzeEmlCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback d'analyse OCR d'un RIB. */
|
||||
void workspace_set_analyze_rib_callback(Workspace *workspace,
|
||||
WorkspaceAnalyzeRibCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback d'extraction locale des métadonnées. */
|
||||
void workspace_set_extract_metadata_callback(Workspace *workspace,
|
||||
WorkspaceExtractMetadataCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de récupération d'un mot de passe PDF. */
|
||||
void workspace_set_recover_pdf_password_callback(Workspace *workspace,
|
||||
WorkspaceRecoverPdfPasswordCallback callback, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de vérification de la preuve affichée.
|
||||
*
|
||||
* Le Workspace emprunte callback et user_data.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback à utiliser, ou NULL.
|
||||
* @param user_data Données empruntées du callback.
|
||||
*/
|
||||
void workspace_set_verify_evidence_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceVerifyEvidenceCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de fin de déplacement d'un nœud.
|
||||
*
|
||||
* Le Workspace relaie uniquement l'événement provenant de la vue graphique.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_graph_node_moved_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceGraphNodeMovedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
void workspace_set_graph_transform_changed_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceGraphTransformChangedCallback callback,
|
||||
gpointer user_data);
|
||||
|
||||
void workspace_set_extraction_drop_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceExtractionDropCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||
*
|
||||
* Le Workspace emprunte callback et user_data.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_reset_graph_layout_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceResetGraphLayoutCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de demande d'ajout d'une relation.
|
||||
*
|
||||
* Le Workspace relaie uniquement l'événement du volet de détails.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_add_relation_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceAddRelationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/** @brief Définit le callback de modification d'une relation. */
|
||||
void workspace_set_edit_relation_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceEditRelationCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
/** @brief Définit le callback de suppression d'une relation. */
|
||||
void workspace_set_delete_relation_callback(Workspace *workspace,
|
||||
WorkspaceDeleteRelationCallback callback, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Définit le callback de sélection d'une relation.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param callback Callback facultatif.
|
||||
* @param user_data Données empruntées transmises au callback.
|
||||
*/
|
||||
void workspace_set_relation_selected_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceRelationSelectedCallback callback,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Affiche les preuves associées à la relation sélectionnée.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param evidence_records Tableau emprunté de EvidenceRecord, ou NULL.
|
||||
*/
|
||||
void workspace_set_relation_evidences(
|
||||
Workspace *workspace,
|
||||
const GPtrArray *evidence_records
|
||||
);
|
||||
|
||||
/** @brief Définit le callback de catégorisation d'une personne. */
|
||||
void workspace_set_person_role_callback(Workspace *workspace,
|
||||
WorkspacePersonRoleCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de confiance d'une personne. */
|
||||
void workspace_set_person_confidence_callback(Workspace *workspace,
|
||||
WorkspacePersonConfidenceCallback callback, gpointer user_data);
|
||||
|
||||
/**
|
||||
* @brief Affiche l'état de chargement du graphe.
|
||||
*
|
||||
* L'ancien graphe emprunté est détaché.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
*/
|
||||
void workspace_set_graph_loading(
|
||||
Workspace *workspace
|
||||
);
|
||||
/** @brief Définit le callback de modification du nom affiché. */
|
||||
void workspace_set_person_name_callback(Workspace *workspace,
|
||||
WorkspacePersonNameCallback callback, gpointer user_data);
|
||||
/** @brief Définit le callback de gestion des preuves d'une personne. */
|
||||
void workspace_set_person_evidence_callback(Workspace *workspace,
|
||||
WorkspacePersonEvidenceCallback callback, gpointer user_data);
|
||||
void workspace_set_evidence_activated_callback(
|
||||
Workspace *workspace,
|
||||
WorkspaceEvidenceActivatedCallback callback,
|
||||
gpointer user_data);
|
||||
/** @brief Définit le callback de sélection d'une entité. */
|
||||
void workspace_set_entity_selected_callback(Workspace *workspace,
|
||||
WorkspaceEntitySelectedCallback callback, gpointer user_data);
|
||||
/** @brief Affiche les preuves de la personne sélectionnée. */
|
||||
void workspace_set_person_evidences(Workspace *workspace,
|
||||
const GPtrArray *records);
|
||||
void workspace_set_person_factual_relations(Workspace *workspace,
|
||||
const GPtrArray *relations, const GPtrArray *evidence_records,const GHashTable *profile_fields);
|
||||
|
||||
/**
|
||||
* @brief Affiche le graphe chargé avec sa disposition persistée.
|
||||
*
|
||||
* Le Workspace emprunte graph_model et graph_layout et ne les libère jamais.
|
||||
*
|
||||
* Passer NULL pour graph_model équivaut à workspace_clear_graph().
|
||||
* Passer NULL pour graph_layout conserve le placement automatique.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param graph_model Graphe emprunté.
|
||||
* @param graph_layout Disposition empruntée, ou NULL.
|
||||
*/
|
||||
void workspace_set_graph(
|
||||
Workspace *workspace,
|
||||
const InvestigationGraphModel *graph_model,
|
||||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean workspace_get_graph_view_transform(
|
||||
Workspace *workspace, double *zoom, double *offset_x, double *offset_y);
|
||||
void workspace_set_graph_view_transform(
|
||||
Workspace *workspace, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/**
|
||||
* @brief Affiche une erreur de chargement du graphe.
|
||||
*
|
||||
* Le texte du message est immédiatement copié par GTK.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
* @param message Détail de l'erreur, ou NULL.
|
||||
*/
|
||||
void workspace_set_graph_error(
|
||||
Workspace *workspace,
|
||||
const char *message
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Détache le graphe et restaure l'état sans enquête.
|
||||
*
|
||||
* Le graphe précédemment emprunté n'est jamais libéré par le Workspace.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
*/
|
||||
void workspace_clear_graph(
|
||||
Workspace *workspace
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Reconstruit visuellement la disposition automatique du graphe.
|
||||
*
|
||||
* Cette fonction ne touche ni SQLite ni InvestigationGraphLayout. Elle doit
|
||||
* être appelée par la couche applicative uniquement après la suppression
|
||||
* réussie des positions persistées.
|
||||
*
|
||||
* Le zoom et le déplacement global du canvas sont conservés.
|
||||
*
|
||||
* @param workspace Zone de travail.
|
||||
*/
|
||||
void workspace_reset_graph_layout(
|
||||
Workspace *workspace
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère la structure Workspace.
|
||||
*
|
||||
* Cette fonction accepte NULL. Elle ne libère jamais le graphe emprunté.
|
||||
*
|
||||
* @param workspace Zone de travail à libérer.
|
||||
*/
|
||||
void workspace_free(
|
||||
Workspace *workspace
|
||||
);
|
||||
|
||||
#endif
|
||||
233
prototypes/web-graph/public/app.js
vendored
233
prototypes/web-graph/public/app.js
vendored
|
|
@ -111,6 +111,7 @@ let navigationHistory = [];
|
|||
let collapsedGroups = new Set();
|
||||
let filters = {};
|
||||
let scale = 1;
|
||||
let graph_scale_initialized = false;
|
||||
let offset = { x: 0, y: 0 };
|
||||
let keyboardIndex = -1;
|
||||
let eventSource = null;
|
||||
|
|
@ -126,6 +127,7 @@ let correlationsRefreshing = false;
|
|||
let correlationRevision = null;
|
||||
let plannerRevision = null;
|
||||
let plannerValue = null;
|
||||
let researchValue = null;
|
||||
let specializedView = "graph";
|
||||
const pinnedPositions = new Map();
|
||||
const reportSelection = new Set();
|
||||
|
|
@ -156,6 +158,7 @@ function invalidateWorkspaceContext() {
|
|||
jobsRefreshing = false;
|
||||
graphRefreshing = false;
|
||||
correlationsRefreshing = false;
|
||||
researchValue = null;
|
||||
}
|
||||
|
||||
function beginWorkspaceContext(workspaceId, generation) {
|
||||
|
|
@ -734,6 +737,148 @@ async function refreshPlanner() {
|
|||
}
|
||||
}
|
||||
|
||||
function researchDecision(actionId) {
|
||||
return document.querySelector(
|
||||
`input[name="research-${CSS.escape(actionId)}"]:checked`,
|
||||
)?.value ?? "defer";
|
||||
}
|
||||
|
||||
function renderResearch() {
|
||||
// INVARIANT: toutes les cartes proviennent du snapshot C ; aucune action ou
|
||||
// adresse de fournisseur n'est construite ni conservée comme vérité JS.
|
||||
const panel = byId("research-plan");
|
||||
panel.replaceChildren();
|
||||
if (!researchValue || researchValue.state === "EMPTY") {
|
||||
byId("research-launch").disabled = true;
|
||||
byId("research-revoke").disabled = true;
|
||||
return;
|
||||
}
|
||||
const heading = document.createElement("h4");
|
||||
heading.textContent = `Question : ${researchValue.question}`;
|
||||
panel.append(heading);
|
||||
const waveOneDone = researchValue.actions.some(
|
||||
(action) => action.wave === 1 && action.contacted,
|
||||
);
|
||||
for (const action of researchValue.actions) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "research-card";
|
||||
const title = document.createElement("h5");
|
||||
title.textContent = `Vague ${action.wave} · ${action.capability_id}`;
|
||||
const explanation = document.createElement("p");
|
||||
explanation.textContent = action.wave === 1
|
||||
? "Gain attendu : obtenir un fait sourcé. Incertitude : le fournisseur peut ne rien connaître."
|
||||
: action.wave === 2
|
||||
? "Nouvelle action non contactée : vérifier ou contredire le premier résultat."
|
||||
: "Action optionnelle distincte ; aucun contact sans autorisation explicite.";
|
||||
const provenance = document.createElement("p");
|
||||
provenance.textContent = `Source/provenance : sélection du graphe · sujet exact : ${action.subject}`;
|
||||
const disclosure = document.createElement("p");
|
||||
disclosure.textContent = `Fournisseur : ${action.provider_id} · contact ${action.contact} · ${action.disclosure}`;
|
||||
const budget = document.createElement("p");
|
||||
budget.textContent = `Budget maximal : ${action.max_requests} requête · ${action.max_response_bytes} octets · ${action.max_active_ms} ms.`;
|
||||
const status = document.createElement("p");
|
||||
status.className = action.contacted ? "research-contacted" : "research-uncontacted";
|
||||
status.textContent = action.contacted
|
||||
? `Résultat : ${action.result_status || "MISSING"}`
|
||||
: "Non contactée — aucune donnée fournisseur reçue.";
|
||||
const choices = document.createElement("fieldset");
|
||||
choices.disabled = action.contacted || (action.wave === 2 && !waveOneDone);
|
||||
const legend = document.createElement("legend");
|
||||
legend.textContent = "Décision humaine";
|
||||
choices.append(legend);
|
||||
for (const [value, label] of [["authorize", "Autoriser"],
|
||||
["defer", "Différer"], ["refuse", "Refuser"]]) {
|
||||
const choice = document.createElement("label");
|
||||
const input = document.createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = `research-${action.action_id}`;
|
||||
input.value = value;
|
||||
input.checked = value === (action.decision?.toLowerCase() ?? "defer");
|
||||
input.addEventListener("change", () => {
|
||||
byId("research-launch").disabled = !researchValue.actions.some(
|
||||
(item) => !item.contacted && researchDecision(item.action_id) === "authorize",
|
||||
);
|
||||
});
|
||||
choice.append(input, ` ${label}`);
|
||||
choices.append(choice);
|
||||
}
|
||||
card.append(title, explanation, provenance, disclosure, budget, status, choices);
|
||||
panel.append(card);
|
||||
}
|
||||
byId("research-launch").disabled = true;
|
||||
byId("research-revoke").disabled = !researchValue.grants.some(
|
||||
(grant) => !grant.revoked_at,
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshResearch() {
|
||||
if (!operationalMode) return;
|
||||
const response = await workspaceFetch("/api/v1/research", { cache: "no-store" });
|
||||
const value = await workspaceJson(response);
|
||||
if (!response.ok || value.contract !== "labfy.research.snapshot.v1")
|
||||
throw new Error(value.message ?? "Recherche indisponible");
|
||||
researchValue = value;
|
||||
renderResearch();
|
||||
}
|
||||
|
||||
async function prepareResearch() {
|
||||
const node = selectedNode();
|
||||
if (!node) throw new Error("Sélectionnez d’abord une preuve ou une entité.");
|
||||
const exclusions = byId("research-exclusions").value.split("\n")
|
||||
.map((item) => item.trim()).filter(Boolean);
|
||||
researchValue = await postCommand("/api/v1/research/prepare", {
|
||||
selection_ids: [node.id],
|
||||
question: byId("research-question").value.trim(),
|
||||
exclusions,
|
||||
idempotency_key: crypto.randomUUID(),
|
||||
});
|
||||
byId("research-note").textContent =
|
||||
"Plan préparé par le cœur C. Choisissez chaque action avant le lancement final.";
|
||||
renderResearch();
|
||||
}
|
||||
|
||||
async function launchResearch() {
|
||||
const actionIds = researchValue.actions.filter((action) =>
|
||||
!action.contacted && researchDecision(action.action_id) === "authorize")
|
||||
.map((action) => action.action_id);
|
||||
if (!actionIds.length) return;
|
||||
const exclusions = byId("research-exclusions").value.split("\n")
|
||||
.map((item) => item.trim()).filter(Boolean);
|
||||
const decisions = researchValue.actions.map((action) => ({
|
||||
action_id: action.action_id,
|
||||
decision: researchDecision(action.action_id).toUpperCase(),
|
||||
}));
|
||||
const grant = await postCommand("/api/v1/research/grants", {
|
||||
plan_id: researchValue.plan_id,
|
||||
input_revision: researchValue.input_revision,
|
||||
selected_action_ids: actionIds,
|
||||
decisions,
|
||||
exclusions,
|
||||
idempotency_key: crypto.randomUUID(),
|
||||
});
|
||||
const grantId = grant.grants[0]?.grant_id;
|
||||
if (!grantId) throw new Error("Autorisation durable absente.");
|
||||
researchValue = await postCommand("/api/v1/research/campaigns", {
|
||||
grant_id: grantId,
|
||||
input_revision: grant.input_revision,
|
||||
action_ids: actionIds,
|
||||
idempotency_key: crypto.randomUUID(),
|
||||
});
|
||||
byId("research-note").textContent =
|
||||
"Campagne terminée ; résultats sourcés et actions non contactées affichés.";
|
||||
renderResearch();
|
||||
}
|
||||
|
||||
async function revokeResearch() {
|
||||
const grant = [...researchValue.grants].reverse().find((item) => !item.revoked_at);
|
||||
if (!grant) return;
|
||||
researchValue = await postCommand("/api/v1/research/revoke", {
|
||||
grant_id: grant.grant_id,
|
||||
});
|
||||
byId("research-note").textContent = "Autorisation révoquée durablement.";
|
||||
renderResearch();
|
||||
}
|
||||
|
||||
const renderer = new GraphRenderer({
|
||||
svg,
|
||||
viewport: byId("viewport"),
|
||||
|
|
@ -1233,7 +1378,9 @@ function resetGlobalView() {
|
|||
focusNodeId = null;
|
||||
filters = {};
|
||||
collapsedGroups.clear();
|
||||
scale = 1;
|
||||
// CONTRACT: « Réinitialiser » revient à la vue globale visible. Une échelle
|
||||
// fixe à 1 ferait sortir le snapshot cœur sous les panneaux persistants.
|
||||
scale = snapshot.nodes.length > 0 ? fitGraphScale() : 1;
|
||||
offset = { x: 0, y: 0 };
|
||||
keyboardIndex = -1;
|
||||
specializedView = "graph";
|
||||
|
|
@ -1338,6 +1485,24 @@ function updateProjectionControls() {
|
|||
}
|
||||
|
||||
function configureControls() {
|
||||
// CONTRACT: la disposition est une préférence locale par origine ; elle ne
|
||||
// transporte ni l'enquête, ni un secret, ni un état métier vers le navigateur.
|
||||
for (const button of document.querySelectorAll(".pane-toggle")) {
|
||||
const pane = byId(button.dataset.pane);
|
||||
const key = `labfy-pane:${button.dataset.pane}`;
|
||||
const apply = (collapsed) => {
|
||||
pane.classList.toggle("pane-collapsed", collapsed);
|
||||
button.setAttribute("aria-expanded", String(!collapsed));
|
||||
button.textContent = collapsed ? "Déployer" : "Réduire";
|
||||
requestAnimationFrame(() => renderer.updateViewport(scale, offset));
|
||||
};
|
||||
apply(localStorage.getItem(key) === "collapsed");
|
||||
button.addEventListener("click", () => {
|
||||
const collapsed = !pane.classList.contains("pane-collapsed");
|
||||
localStorage.setItem(key, collapsed ? "collapsed" : "expanded");
|
||||
apply(collapsed);
|
||||
});
|
||||
}
|
||||
for (const tab of document.querySelectorAll("[data-panel]")) {
|
||||
tab.addEventListener("click", () =>
|
||||
activateInspectorPanel(tab.dataset.panel),
|
||||
|
|
@ -1359,6 +1524,7 @@ function configureControls() {
|
|||
.classList.toggle("drawer-collapsed", collapsed);
|
||||
event.currentTarget.setAttribute("aria-expanded", String(!collapsed));
|
||||
event.currentTarget.textContent = collapsed ? "Déployer" : "Réduire";
|
||||
localStorage.setItem("labfy-pane:drawer", collapsed ? "collapsed" : "expanded");
|
||||
requestAnimationFrame(() => renderer.updateViewport(scale, offset));
|
||||
});
|
||||
byId("view-evidence").addEventListener("click", () => {
|
||||
|
|
@ -1458,6 +1624,23 @@ function openNodeContextMenu(nodeId, clientX, clientY) {
|
|||
}
|
||||
|
||||
function configureGraphInteractions() {
|
||||
// WHY: les panneaux persistants modifient réellement la largeur SVG. Le
|
||||
// renderer doit recaler son viewport avant le prochain drag, sans muter les
|
||||
// coordonnées épinglées qui restent un état de présentation local.
|
||||
new ResizeObserver(() => requestAnimationFrame(() => {
|
||||
const panel = byId("graph-panel");
|
||||
if (!graph_scale_initialized && snapshot.nodes.length > 0 &&
|
||||
panel.clientWidth > 100 && panel.clientHeight > 100) {
|
||||
// WHY: le snapshot peut arriver pendant que la coque tri-panneaux passe
|
||||
// de hidden à sa largeur finale. L'auto-fit unique évite que des nœuds
|
||||
// SVG débordent sous Activité, tout en préservant ensuite zoom/pins.
|
||||
scale = fitGraphScale();
|
||||
graph_scale_initialized = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
renderer.updateViewport(scale, offset);
|
||||
})).observe(byId("graph-panel"));
|
||||
let pan = null;
|
||||
svg.addEventListener("pointerdown", (event) => {
|
||||
if (event.target !== svg && event.target.id !== "graph-background") return;
|
||||
|
|
@ -1624,6 +1807,7 @@ function resetWorkspacePresentation() {
|
|||
plannerValue = null;
|
||||
preparedUploads.clear();
|
||||
pinnedPositions.clear();
|
||||
graph_scale_initialized = false;
|
||||
resetGlobalView();
|
||||
}
|
||||
|
||||
|
|
@ -1646,6 +1830,9 @@ async function activateWorkspace(opened) {
|
|||
snapshot = prepareSnapshot(value);
|
||||
acceptedSnapshotFingerprint = snapshotFingerprint(value);
|
||||
coreMode = snapshot.origin === "core";
|
||||
// CONTRACT: la mesure suit la publication de la coque visible ; elle ne
|
||||
// repose pas sur la largeur héritée lorsque le shell était hidden.
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
scale = fitGraphScale();
|
||||
syncTypeFilterOptions();
|
||||
restoreReportDraft();
|
||||
|
|
@ -1653,7 +1840,8 @@ async function activateWorkspace(opened) {
|
|||
renderTimeline();
|
||||
renderReportSelection();
|
||||
byId("connection").textContent = `Ouverte · génération ${opened.generation}`;
|
||||
await Promise.all([refreshJobs(), refreshCorrelations(), refreshPlanner()]);
|
||||
await Promise.all([refreshJobs(), refreshCorrelations(), refreshPlanner(),
|
||||
refreshResearch()]);
|
||||
for (const [callback, delay] of [[refreshJobs, 500],
|
||||
[refreshOperationalGraph, 700], [refreshCorrelations, 900], [refreshPlanner, 1100]])
|
||||
refreshTimers.add(setInterval(callback, delay));
|
||||
|
|
@ -1684,6 +1872,9 @@ async function activateLegacyWorkspace(session) {
|
|||
snapshot = prepareSnapshot(value);
|
||||
acceptedSnapshotFingerprint = snapshotFingerprint(value);
|
||||
coreMode = snapshot.origin === "core";
|
||||
// CONTRACT: le snapshot cœur doit être ajusté après le layout Agent |
|
||||
// Graphe | Activité, sinon ses coordonnées SVG débordent sous Activité.
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
byId("mode-badge").textContent = operationalMode
|
||||
? "Poste local · contrôles locaux"
|
||||
: coreMode ? "Snapshot du cœur C · lecture seule" : "Démonstration locale · lecture seule";
|
||||
|
|
@ -1700,7 +1891,8 @@ async function activateLegacyWorkspace(session) {
|
|||
byId("graph-state").textContent = `Snapshot cœur indisponible : ${snapshot._loadError}`;
|
||||
if (!coreMode) connectEvents();
|
||||
if (operationalMode) {
|
||||
await Promise.all([refreshJobs(), refreshCorrelations(), refreshPlanner()]);
|
||||
await Promise.all([refreshJobs(), refreshCorrelations(), refreshPlanner(),
|
||||
refreshResearch()]);
|
||||
for (const [callback, delay] of [[refreshJobs, 500],
|
||||
[refreshOperationalGraph, 700], [refreshCorrelations, 900], [refreshPlanner, 1100]])
|
||||
refreshTimers.add(setInterval(callback, delay));
|
||||
|
|
@ -1751,6 +1943,20 @@ function configureApplication() {
|
|||
configureControls();
|
||||
configureGraphInteractions();
|
||||
configureImport();
|
||||
const drawerCollapsed = localStorage.getItem("labfy-pane:drawer") === "collapsed";
|
||||
if (drawerCollapsed && !byId("work-panel").classList.contains("collapsed"))
|
||||
byId("work-panel-toggle").click();
|
||||
byId("agent-send").addEventListener("click", () => {
|
||||
const prompt = byId("agent-prompt").value.trim();
|
||||
if (!prompt) { byId("agent-prompt").focus(); return; }
|
||||
const card = document.createElement("li");
|
||||
card.className = "agent-card user";
|
||||
const label = document.createElement("b"); label.textContent = "USER";
|
||||
const text = document.createElement("p"); text.textContent = prompt;
|
||||
card.append(label, text);
|
||||
document.querySelector(".agent-conversation").append(card);
|
||||
byId("agent-prompt").value = "";
|
||||
});
|
||||
byId("home-button").addEventListener("click", () => void showLibrary({ focus: true }));
|
||||
byId("workspace-create-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -1788,6 +1994,27 @@ function configureApplication() {
|
|||
} finally { button.disabled = false; }
|
||||
});
|
||||
byId("jobs-note").textContent = "Contrôle local authentifié.";
|
||||
byId("research-form").addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
byId("research-note").textContent = "Préparation…";
|
||||
void prepareResearch().catch((error) => {
|
||||
if (error.name !== "AbortError")
|
||||
byId("research-note").textContent = error.message;
|
||||
});
|
||||
});
|
||||
byId("research-launch").addEventListener("click", () => {
|
||||
byId("research-note").textContent = "Admission et campagne…";
|
||||
void launchResearch().catch((error) => {
|
||||
if (error.name !== "AbortError")
|
||||
byId("research-note").textContent = error.message;
|
||||
});
|
||||
});
|
||||
byId("research-revoke").addEventListener("click", () =>
|
||||
void revokeResearch().catch((error) => {
|
||||
if (error.name !== "AbortError")
|
||||
byId("research-note").textContent = error.message;
|
||||
}),
|
||||
);
|
||||
for (const [id, command] of [
|
||||
["queue-pause", "pause"],
|
||||
["queue-resume", "resume"],
|
||||
|
|
|
|||
|
|
@ -91,6 +91,20 @@
|
|||
</div>
|
||||
</nav>
|
||||
<main class="workbench">
|
||||
<aside id="agent-panel" class="agent-panel" aria-label="Agent IA local">
|
||||
<div class="panel-heading"><div><span class="eyebrow">Agent IA · préparation</span><h2>Mission</h2></div><button class="pane-toggle" data-pane="agent-panel" aria-expanded="true">Réduire</button></div>
|
||||
<div class="agent-scroll">
|
||||
<p class="agent-mission">Cartographier les relations autour de <strong>example.test</strong> en respectant les autorisations explicites.</p>
|
||||
<ol class="agent-conversation" aria-label="Conversation SPECIMEN">
|
||||
<li class="agent-card user"><b>USER</b><p>Montre les pivots passifs disponibles.</p></li>
|
||||
<li class="agent-card plan"><b>PLAN</b><p>1. Lire les observations · 2. Proposer les capabilities · 3. Attendre l’accord.</p></li>
|
||||
<li class="agent-card hypothesis"><b>HYPOTHESIS</b><p>Le domaine et l’adresse contact peuvent partager une provenance, sans confirmer une identité.</p></li>
|
||||
<li class="agent-card authorization"><b>AUTHORIZATION</b><p><strong>rdap.lookup_domain</strong><br/>Cible : example.test · Provider : SPECIMEN loopback<br/>Sortie : nom de domaine · Exposition : passive · Budget : 1 requête<br/><button type="button">Examiner l’autorisation</button></p></li>
|
||||
<li class="agent-card result"><b>RESULT</b><p>7 observations reçues ; aucune relation n’est promue automatiquement.</p></li>
|
||||
</ol>
|
||||
<label class="agent-prompt"><span>Prompt à l’agent</span><textarea id="agent-prompt" maxlength="2000" placeholder="Décrivez l’intention d’enquête…"></textarea><button type="button" id="agent-send">Préparer une demande</button></label>
|
||||
</div>
|
||||
</aside>
|
||||
<section id="graph-panel" aria-label="Graphe interactif">
|
||||
<div id="graph-state" role="alert"></div>
|
||||
<div class="graph-legend" aria-label="Légende">
|
||||
|
|
@ -147,10 +161,17 @@
|
|||
<aside id="inspector" aria-label="Inspecteur contextuel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Objet sélectionné</span>
|
||||
<h2>Inspecteur</h2>
|
||||
<span class="eyebrow">Activité · outils · jobs</span>
|
||||
<h2>Activité</h2>
|
||||
</div>
|
||||
<button class="pane-toggle" data-pane="inspector" aria-expanded="true">Réduire</button>
|
||||
</div>
|
||||
<section class="activity-stream" aria-label="Timeline technique SPECIMEN">
|
||||
<div class="activity-entry"><time>15:42:12</time><b>TOOL</b><span>rdap.lookup_domain<br/><code>target = example.test</code></span></div>
|
||||
<div class="activity-entry policy"><time>15:42:12</time><b>POLICY</b><span>Autorisé · budget restant 18/20</span></div>
|
||||
<div class="activity-entry result"><time>15:42:13</time><b>RESULT</b><span>1 artefact · 7 observations</span></div>
|
||||
</section>
|
||||
<details class="system-card"><summary>Système · ressources réelles</summary><dl><dt>CPU</dt><dd id="system-cpu">Disponible via backend</dd><dt>RAM</dt><dd id="system-ram">Disponible via backend</dd><dt>Jobs actifs</dt><dd id="system-jobs">Lecture JobStore</dd><dt>Instance Web</dt><dd id="system-instance">Loopback</dd><dt>Providers</dt><dd id="system-providers">Selon capabilities</dd></dl><p class="future-slots">Prévu : modèle · quantification · GPU AMD · KV cache · réserve RAM · swap · scratch IA.</p></details>
|
||||
<div class="tablist" role="tablist">
|
||||
<button role="tab" data-panel="details-panel" aria-selected="true">
|
||||
Détails</button
|
||||
|
|
@ -202,6 +223,7 @@
|
|||
><button role="tab" data-work-panel="next-panel">
|
||||
Prochaines actions</button
|
||||
><button role="tab" data-work-panel="plans-panel">Plans</button
|
||||
><button role="tab" data-work-panel="research-panel">Recherches</button
|
||||
><button role="tab" data-work-panel="reports-panel">
|
||||
Rapports
|
||||
</button>
|
||||
|
|
@ -237,6 +259,25 @@
|
|||
<h3>Plans approuvés</h3>
|
||||
<ol id="plans-list"></ol>
|
||||
</section>
|
||||
<section id="research-panel" class="work-tab-panel" hidden>
|
||||
<h3>Recherches assistées</h3>
|
||||
<p id="research-note" role="status">
|
||||
Sélectionnez une preuve ou une entité dans le graphe.
|
||||
</p>
|
||||
<form id="research-form" class="research-form">
|
||||
<label>Question
|
||||
<textarea id="research-question" maxlength="512" required></textarea>
|
||||
</label>
|
||||
<label>Exclusions explicites
|
||||
<input id="research-exclusions" maxlength="512"
|
||||
placeholder="Un sujet exact par ligne" />
|
||||
</label>
|
||||
<button id="research-prepare" type="submit">Préparer le plan</button>
|
||||
</form>
|
||||
<div id="research-plan" aria-live="polite"></div>
|
||||
<button id="research-launch" disabled>Autoriser et lancer la sélection</button>
|
||||
<button id="research-revoke" disabled>Révoquer l’autorisation</button>
|
||||
</section>
|
||||
<section id="reports-panel" class="work-tab-panel" hidden>
|
||||
<div id="report-panel">
|
||||
<h3 id="report-title">Préparer le rapport</h3>
|
||||
|
|
|
|||
|
|
@ -699,7 +699,48 @@ body.login .workbench {
|
|||
.observation-actions input {
|
||||
min-width: 12rem;
|
||||
}
|
||||
.research-form {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.research-form label,
|
||||
.research-card {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.research-card {
|
||||
padding: 0.8rem;
|
||||
margin: 0.7rem 0;
|
||||
border: 1px solid var(--surface1);
|
||||
border-radius: 0.65rem;
|
||||
background: var(--mantle);
|
||||
}
|
||||
.research-card h5,
|
||||
.research-card p {
|
||||
margin: 0;
|
||||
}
|
||||
.research-card fieldset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
border-color: var(--surface1);
|
||||
}
|
||||
.research-contacted {
|
||||
color: var(--green);
|
||||
}
|
||||
.research-uncontacted {
|
||||
color: var(--yellow);
|
||||
}
|
||||
/* CONTRACT: la coque finale garde le graphe dominant ; les trois surfaces
|
||||
* sont des vues de la même enquête, jamais des stores concurrents. */
|
||||
.workbench { grid-template-columns: minmax(15rem,20rem) minmax(0,1fr) minmax(18rem,23rem); grid-template-areas: "agent graph inspector" "work work work"; }
|
||||
.agent-panel { grid-area: agent; min-width:0; min-height:0; display:grid; grid-template-rows:auto minmax(0,1fr); background:linear-gradient(180deg,#181825,#11111b); border-right:1px solid var(--border); }
|
||||
.agent-scroll { min-height:0; overflow:auto; padding:.75rem; }.agent-mission { margin:0 0 .8rem; color:var(--muted); line-height:1.45; }.agent-conversation { display:grid; gap:.55rem; margin:0; padding:0; list-style:none; }.agent-card { border:1px solid var(--border); border-left:3px solid var(--blue); border-radius:.65rem; padding:.6rem .7rem; background:#1e1e2ecc; }.agent-card b { font-size:.68rem; letter-spacing:.08em; color:var(--muted); }.agent-card p { margin:.3rem 0 0; line-height:1.4; }.agent-card.plan { border-left-color:var(--accent); }.agent-card.hypothesis { border-left-color:var(--yellow); }.agent-card.authorization { border-left-color:#cba6f7; }.agent-card.result { border-left-color:var(--green); }.agent-prompt { display:grid; gap:.4rem; margin-top:.8rem; }.agent-prompt textarea { min-height:5.5rem; resize:vertical; }.pane-toggle { padding:.28rem .48rem; font-size:.78rem; }
|
||||
.activity-stream { display:grid; gap:.35rem; margin-bottom:.75rem; padding-bottom:.75rem; border-bottom:1px solid var(--border); }.activity-entry { display:grid; grid-template-columns:4.4rem 4.2rem minmax(0,1fr); gap:.3rem; padding:.42rem; border-radius:.42rem; background:#11111b; font-size:.78rem; }.activity-entry time { color:var(--muted); }.activity-entry b { color:var(--blue); font-size:.7rem; }.activity-entry.policy b { color:var(--yellow); }.activity-entry.result b { color:var(--green); }.activity-entry code { color:var(--muted); overflow-wrap:anywhere; }.system-card { margin-bottom:.75rem; padding:.55rem; border:1px solid var(--border); border-radius:.55rem; background:#11111b; }.system-card summary { color:var(--accent); cursor:pointer; }.system-card dl { display:grid; grid-template-columns:1fr auto; gap:.3rem .6rem; margin:.7rem 0; font-size:.8rem; }.system-card dd { margin:0; color:var(--muted); text-align:right; }.future-slots { margin:0; color:var(--muted); font-size:.75rem; line-height:1.35; }
|
||||
.agent-panel.pane-collapsed { grid-template-rows: auto; }.agent-panel.pane-collapsed .agent-scroll { display:none; }.agent-panel.pane-collapsed { min-width: 5.5rem; }.agent-panel.pane-collapsed h2 { display:none; }#inspector.pane-collapsed > :not(.panel-heading) { display:none; }#inspector.pane-collapsed { min-width:5.5rem; }#inspector.pane-collapsed h2 { display:none; }
|
||||
@media (max-width: 900px) {
|
||||
.workbench { grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(18rem,1fr) auto var(--drawer); grid-template-areas:"agent" "graph" "inspector" "work"; }.agent-panel { max-height:19rem; border-right:0; border-bottom:1px solid var(--border); }
|
||||
.workbench.drawer-collapsed {
|
||||
grid-template-rows: minmax(18rem, 1fr) auto auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ export const scenarios = Object.freeze([
|
|||
// de harness isolée ; il démontre l'arrêt du polling et la reconnexion sans
|
||||
// rejouer une mutation dont l'admission n'est pas connue.
|
||||
"test_session_lifetime_browser.mjs",
|
||||
// CONTRACT: deux vagues de recherche SPECIMEN approuvées sont visibles ; une
|
||||
// branche refusée demeure hors transport et ne peut donc produire de résultat.
|
||||
"test_research_browser.mjs",
|
||||
]);
|
||||
|
||||
const tests_directory = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
|
|
|||
59
prototypes/web-graph/tests/test_automatic_session.py
Normal file
59
prototypes/web-graph/tests/test_automatic_session.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""Régressions de session sans code visible du lanceur Web-only."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from http.client import HTTPConnection
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from workspace_server import Handler, WorkspaceServer
|
||||
|
||||
|
||||
class AutomaticSessionTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(prefix="labfy-auto-SPECIMEN-")
|
||||
root = Path(self.temporary.name)
|
||||
self.server = WorkspaceServer(("127.0.0.1", 0), Handler,
|
||||
workspace=root, bridge=root / "missing-bridge", bootstrap="unused",
|
||||
automatic_session=True)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
self.authority = f"127.0.0.1:{self.server.server_port}"
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown(); self.server.server_close(); self.thread.join()
|
||||
self.temporary.cleanup()
|
||||
|
||||
def request(self, method, path, headers=None, body=None):
|
||||
connection = HTTPConnection("127.0.0.1", self.server.server_port, timeout=3)
|
||||
connection.request(method, path, body=body, headers=headers or {"Host": self.authority})
|
||||
response = connection.getresponse(); value = response.read()
|
||||
answer = response.status, dict(response.getheaders()), value
|
||||
connection.close(); return answer
|
||||
|
||||
def test_root_establishes_http_only_session_without_code_or_url_token(self):
|
||||
status, headers, _ = self.request("GET", "/")
|
||||
self.assertEqual(status, 303)
|
||||
self.assertEqual(headers["Location"], "/")
|
||||
self.assertIn("HttpOnly", headers["Set-Cookie"])
|
||||
self.assertIn("SameSite=Strict", headers["Set-Cookie"])
|
||||
self.assertNotIn("unused", headers["Set-Cookie"])
|
||||
cookie = headers["Set-Cookie"].split(";", 1)[0]
|
||||
status, _, body = self.request("GET", "/api/v1/session", {"Host": self.authority, "Cookie": cookie})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn("csrf", json.loads(body))
|
||||
|
||||
def test_third_party_origin_cannot_mutate_auto_session(self):
|
||||
_, headers, _ = self.request("GET", "/")
|
||||
cookie = headers["Set-Cookie"].split(";", 1)[0]
|
||||
status, _, _ = self.request("POST", "/api/v1/queue/pause", {
|
||||
"Host": self.authority, "Cookie": cookie, "Origin": "https://evil.test",
|
||||
"Content-Type": "application/json", "Content-Length": "2"}, "{}")
|
||||
self.assertEqual(status, 403)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -239,7 +239,7 @@ async function testProjectionKeyboardAndResets(page) {
|
|||
await page.setViewport({ width: 700, height: 900 });
|
||||
const narrowLayout = await page.evaluate(() => ({
|
||||
columns: getComputedStyle(document.querySelector("main")).gridTemplateColumns,
|
||||
asideBorderTop: getComputedStyle(document.querySelector("aside")).borderTopStyle,
|
||||
asideBorderTop: getComputedStyle(document.querySelector("#inspector")).borderTopStyle,
|
||||
}));
|
||||
assert.equal(narrowLayout.columns.split(" ").length, 1);
|
||||
assert.equal(narrowLayout.asideBorderTop, "solid");
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ async function clickNode(page, id) {
|
|||
});
|
||||
}
|
||||
|
||||
async function dragNode(page, id, deltaX, deltaY) {
|
||||
const node = await page.$(`[data-id="${id}"]`);
|
||||
const box = await node.boundingBox();
|
||||
assert.ok(box, `boîte du nœud ${id}`);
|
||||
const x = box.x + box.width / 2, y = box.y + box.height / 2;
|
||||
await page.mouse.move(x, y); await page.mouse.down();
|
||||
// Firefox livre les PointerEvents intermédiaires de manière fiable ; un seul
|
||||
// saut ne prouve pas le contrat de position épinglée du renderer SVG.
|
||||
await page.mouse.move(x + deltaX / 2, y + deltaY / 2, { steps: 3 });
|
||||
await page.mouse.move(x + deltaX, y + deltaY, { steps: 3 }); await page.mouse.up();
|
||||
}
|
||||
|
||||
async function inspectCoreMode(page) {
|
||||
await page.waitForSelector(`[data-id="${PERSON}"]`);
|
||||
const initial = await page.evaluate(() => window.__LABFY_TEST__.getState());
|
||||
|
|
@ -91,13 +103,7 @@ async function inspectCoreMode(page) {
|
|||
});
|
||||
assert.equal((await page.evaluate(() => window.__LABFY_TEST__.getState())).visibleNodeIds.length, 0);
|
||||
await page.$eval("#reset", (item) => item.click());
|
||||
|
||||
const person = await page.$(`[data-id="${PERSON}"]`);
|
||||
const box = await person.boundingBox();
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width / 2 + 70, box.y + box.height / 2 + 45, { steps: 4 });
|
||||
await page.mouse.up();
|
||||
await dragNode(page, PERSON, 70, 45);
|
||||
assert.ok((await page.evaluate(() => window.__LABFY_TEST__.getState())).pinned[PERSON]);
|
||||
|
||||
await page.setViewport({ width: 700, height: 900 });
|
||||
|
|
|
|||
124
prototypes/web-graph/tests/test_research_browser.mjs
Normal file
124
prototypes/web-graph/tests/test_research_browser.mjs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import assert from "node:assert/strict";
|
||||
import http from "node:http";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const REPOSITORY = new URL("../../..", import.meta.url);
|
||||
const PROTOTYPE = new URL("..", import.meta.url);
|
||||
const FIREFOX = process.env.FIREFOX_PATH ?? "/usr/bin/firefox";
|
||||
|
||||
function ready(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let text = "";
|
||||
const timer = setTimeout(() => reject(new Error("démarrage recherche expiré")), 8000);
|
||||
server.stdout.on("data", (chunk) => {
|
||||
text += chunk;
|
||||
const url = text.match(/http:\/\/127\.0\.0\.1:(\d+)\//);
|
||||
const code = text.match(/Code de session éphémère : ([^\s]+)/);
|
||||
if (url && code) {
|
||||
clearTimeout(timer);
|
||||
resolve({ url: url[0], code: code[1] });
|
||||
}
|
||||
});
|
||||
server.once("exit", (code) => reject(new Error(`serveur arrêté (${code})`)));
|
||||
});
|
||||
}
|
||||
|
||||
function start(workspace, authority) {
|
||||
return spawn("python3", ["workspace_server.py", "--workspace", workspace,
|
||||
"--bridge", "../../tools/local-jobs", "--port", "0"], {
|
||||
cwd: PROTOTYPE,
|
||||
env: { ...process.env, PYTHONUNBUFFERED: "1",
|
||||
LABFY_RESEARCH_FIXTURE_AUTHORITY: authority },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
async function stop(server) {
|
||||
server.kill("SIGINT");
|
||||
await new Promise((resolve) => server.once("exit", resolve));
|
||||
}
|
||||
|
||||
async function login(page, session) {
|
||||
await page.goto(session.url, { waitUntil: "domcontentloaded" });
|
||||
await page.type("#bootstrap-code", session.code);
|
||||
await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }),
|
||||
page.click("#login-form button")]);
|
||||
}
|
||||
|
||||
function init(workspace) {
|
||||
const result = spawnSync("./tools/local-jobs",
|
||||
["init-j7-specimen", "--workspace", workspace],
|
||||
{ cwd: REPOSITORY, encoding: "utf8", timeout: 30000 });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(spawnSync("./tools/local-jobs",
|
||||
["export", "--workspace", workspace],
|
||||
{ cwd: REPOSITORY, timeout: 15000 }).status, 0);
|
||||
}
|
||||
|
||||
const contacts = [];
|
||||
const provider = http.createServer((request, response) => {
|
||||
contacts.push(request.url);
|
||||
const body = JSON.stringify({ contract: "labfy.fixture.provider.v1",
|
||||
path: request.url });
|
||||
response.writeHead(200, { "Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body) });
|
||||
response.end(body);
|
||||
});
|
||||
await new Promise((resolve) => provider.listen(0, "127.0.0.1", resolve));
|
||||
const authority = `127.0.0.1:${provider.address().port}`;
|
||||
const workspaceA = await mkdtemp(join(tmpdir(), "labfy-research-A-"));
|
||||
const workspaceB = await mkdtemp(join(tmpdir(), "labfy-research-B-"));
|
||||
const profile = await mkdtemp(join(tmpdir(), "labfy-research-firefox-"));
|
||||
let browser, server;
|
||||
try {
|
||||
init(workspaceA); init(workspaceB);
|
||||
server = start(workspaceA, authority); const sessionA = await ready(server);
|
||||
browser = await puppeteer.launch({ browser: "firefox", executablePath: FIREFOX,
|
||||
headless: true, userDataDir: profile, args: ["--no-remote"] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 900 });
|
||||
await login(page, sessionA);
|
||||
await page.waitForSelector("#object-list button");
|
||||
await page.click("#object-list button");
|
||||
await page.click('[data-work-panel="research-panel"]');
|
||||
await page.type("#research-question", "Que confirme cette source SPECIMEN ?");
|
||||
await page.click("#research-prepare");
|
||||
await page.waitForSelector(".research-card input[value=authorize]");
|
||||
const cards = await page.$$(".research-card");
|
||||
assert.equal(cards.length, 3);
|
||||
await (await cards[0].$("input[value=authorize]")).click();
|
||||
await (await cards[2].$("input[value=refuse]")).click();
|
||||
await page.click("#research-launch");
|
||||
await page.waitForFunction(() => document.querySelector("#research-plan")
|
||||
?.textContent.includes("Résultat : NEW"));
|
||||
assert.deepEqual(contacts, ["/specimen/wave-1"]);
|
||||
assert.match(await page.$eval("#research-plan", (node) => node.textContent),
|
||||
/Nouvelle action non contactée/);
|
||||
const secondWave = (await page.$$(".research-card"))[1];
|
||||
await (await secondWave.$("input[value=authorize]")).click();
|
||||
await page.click("#research-launch");
|
||||
await page.waitForFunction(() => document.querySelector("#research-plan")
|
||||
?.textContent.includes("Résultat : CONTRADICTION"));
|
||||
assert.deepEqual(contacts, ["/specimen/wave-1", "/specimen/wave-2"]);
|
||||
assert.doesNotMatch(contacts.join(" "), /refused/);
|
||||
|
||||
await stop(server); server = start(workspaceB, authority);
|
||||
const sessionB = await ready(server); const pageB = await browser.newPage();
|
||||
await login(pageB, sessionB);
|
||||
await pageB.click('[data-work-panel="research-panel"]');
|
||||
assert.equal(await pageB.$$eval(".research-card", (nodes) => nodes.length), 0);
|
||||
assert.match(await pageB.$eval("#research-note", (node) => node.textContent),
|
||||
/Sélectionnez/);
|
||||
console.log("PASS recherche Firefox : deux vagues A, refus sans contact, B isolé");
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (server?.exitCode === null) await stop(server);
|
||||
await new Promise((resolve) => provider.close(resolve));
|
||||
await rm(workspaceA, { recursive: true, force: true });
|
||||
await rm(workspaceB, { recursive: true, force: true });
|
||||
await rm(profile, { recursive: true, force: true });
|
||||
}
|
||||
345
prototypes/web-graph/tests/test_research_web.py
Normal file
345
prototypes/web-graph/tests/test_research_web.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.client import HTTPConnection
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from workspace_server import Handler, WorkspaceServer
|
||||
|
||||
|
||||
class Provider(BaseHTTPRequestHandler):
|
||||
requests = []
|
||||
bodies = {}
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self):
|
||||
type(self).requests.append((self.path, self.headers.get("X-Labfy-Action")))
|
||||
body = type(self).bodies.get(self.path)
|
||||
if body is None:
|
||||
body = json.dumps({"contract": "labfy.fixture.provider.v1",
|
||||
"path": self.path}).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *_args):
|
||||
pass
|
||||
|
||||
|
||||
class ResearchWebTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.repository = Path(__file__).resolve().parents[3]
|
||||
cls.bridge = cls.repository / "tools/local-jobs"
|
||||
cls.temporary = tempfile.TemporaryDirectory(prefix="labfy-research-web-")
|
||||
cls.provider = ThreadingHTTPServer(("127.0.0.1", 0), Provider)
|
||||
cls.provider_thread = threading.Thread(
|
||||
target=cls.provider.serve_forever, daemon=True)
|
||||
cls.provider_thread.start()
|
||||
cls.instances = []
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
for server, thread in cls.instances:
|
||||
server.shutdown(); server.server_close(); thread.join()
|
||||
cls.provider.shutdown(); cls.provider.server_close()
|
||||
cls.provider_thread.join(); cls.temporary.cleanup()
|
||||
|
||||
@classmethod
|
||||
def start_workspace(cls, name):
|
||||
workspace = Path(cls.temporary.name) / name
|
||||
subprocess.run([str(cls.bridge), "init-j7-specimen", "--workspace",
|
||||
str(workspace)], cwd=cls.repository, check=True,
|
||||
capture_output=True, timeout=30)
|
||||
subprocess.run([str(cls.bridge), "export", "--workspace", str(workspace)],
|
||||
cwd=cls.repository, check=True, timeout=15)
|
||||
server = WorkspaceServer(("127.0.0.1", 0), Handler,
|
||||
workspace=workspace, bridge=cls.bridge, bootstrap="SPECIMEN-RESEARCH",
|
||||
research_fixture_authority=f"127.0.0.1:{cls.provider.server_port}")
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start(); cls.instances.append((server, thread))
|
||||
return workspace, server
|
||||
|
||||
@staticmethod
|
||||
def request(server, method, path, value=None, *, cookie=None, csrf=None,
|
||||
origin=None, host=None):
|
||||
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=10)
|
||||
headers = {"Host": host or server.authority}
|
||||
if cookie: headers["Cookie"] = cookie
|
||||
if csrf: headers["X-Labfy-CSRF"] = csrf
|
||||
if origin: headers["Origin"] = origin
|
||||
body = None
|
||||
if value is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
body = json.dumps(value)
|
||||
connection.request(method, path, body=body, headers=headers)
|
||||
response = connection.getresponse(); data = response.read()
|
||||
result = response.status, dict(response.getheaders()), data
|
||||
connection.close(); return result
|
||||
|
||||
def login(self, server):
|
||||
status, headers, _ = self.request(server, "POST", "/api/v1/session",
|
||||
{"bootstrap_code": "SPECIMEN-RESEARCH"}, origin=server.origin)
|
||||
self.assertEqual(status, 200)
|
||||
cookie = headers["Set-Cookie"].split(";", 1)[0]
|
||||
status, _, body = self.request(server, "GET", "/api/v1/session",
|
||||
cookie=cookie)
|
||||
self.assertEqual(status, 200)
|
||||
return cookie, json.loads(body)["csrf"]
|
||||
|
||||
def mutate(self, server, cookie, csrf, path, value):
|
||||
time.sleep(0.04)
|
||||
return self.request(server, "POST", path, value, cookie=cookie,
|
||||
csrf=csrf, origin=server.origin)
|
||||
|
||||
def test_two_waves_refusal_never_contacts_and_workspace_b_isolated(self):
|
||||
Provider.requests.clear()
|
||||
workspace_a, server_a = self.start_workspace("A")
|
||||
cookie_a, csrf_a = self.login(server_a)
|
||||
graph = json.loads((workspace_a / "core-snapshot.json").read_text())
|
||||
seed = graph["nodes"][0]["id"]
|
||||
prepared = {"selection_ids": [seed],
|
||||
"question": "Que confirme cette source SPECIMEN ?",
|
||||
"exclusions": [],
|
||||
"idempotency_key": "95000000-0000-4000-8000-000000000001"}
|
||||
status, _, body = self.mutate(server_a, cookie_a, csrf_a,
|
||||
"/api/v1/research/prepare", prepared)
|
||||
self.assertEqual(status, 200, body); snapshot = json.loads(body)
|
||||
first, second, refused = snapshot["actions"]
|
||||
|
||||
def campaign(action, suffix):
|
||||
decisions = [{"action_id": item["action_id"],
|
||||
"decision": ("REFUSE" if item == refused else
|
||||
"AUTHORIZE" if item == first or item == action
|
||||
else "DEFER")}
|
||||
for item in snapshot["actions"]]
|
||||
status, _, grant_body = self.mutate(server_a, cookie_a, csrf_a,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [action["action_id"]], "exclusions": [],
|
||||
"decisions": decisions,
|
||||
"idempotency_key": f"95000000-0000-4000-8000-0000000000{suffix}"})
|
||||
self.assertEqual(status, 200, grant_body); grant = json.loads(grant_body)
|
||||
grant_id = grant["grants"][0]["grant_id"]
|
||||
status, _, result_body = self.mutate(server_a, cookie_a, csrf_a,
|
||||
"/api/v1/research/campaigns", {"grant_id": grant_id,
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"action_ids": [action["action_id"]],
|
||||
"idempotency_key": f"96000000-0000-4000-8000-0000000000{suffix}"})
|
||||
self.assertEqual(status, 200, result_body)
|
||||
return json.loads(result_body)
|
||||
|
||||
premature = [{"action_id": item["action_id"],
|
||||
"decision": "AUTHORIZE" if item == second else "DEFER"}
|
||||
for item in snapshot["actions"]]
|
||||
status, _, body = self.mutate(server_a, cookie_a, csrf_a,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [second["action_id"]], "exclusions": [],
|
||||
"decisions": premature,
|
||||
"idempotency_key": "95000000-0000-4000-8000-000000000009"})
|
||||
self.assertEqual(status, 409, body)
|
||||
|
||||
previous_proxy = os.environ.get("http_proxy")
|
||||
os.environ["http_proxy"] = "http://127.0.0.1:1"
|
||||
try:
|
||||
after_first = campaign(first, "11")
|
||||
finally:
|
||||
if previous_proxy is None: os.environ.pop("http_proxy", None)
|
||||
else: os.environ["http_proxy"] = previous_proxy
|
||||
self.assertEqual([item[0] for item in Provider.requests],
|
||||
["/specimen/wave-1"])
|
||||
second_view = next(item for item in after_first["actions"]
|
||||
if item["action_id"] == second["action_id"])
|
||||
self.assertFalse(second_view["contacted"])
|
||||
self.assertEqual(next(item for item in after_first["actions"]
|
||||
if item["action_id"] == first["action_id"])["result_status"], "NEW")
|
||||
|
||||
retry_refused = [{"action_id": refused["action_id"],
|
||||
"decision": "AUTHORIZE"}]
|
||||
status, _, body = self.mutate(server_a, cookie_a, csrf_a,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [refused["action_id"]], "exclusions": [],
|
||||
"decisions": retry_refused,
|
||||
"idempotency_key": "95000000-0000-4000-8000-000000000019"})
|
||||
self.assertEqual(status, 409, body)
|
||||
|
||||
after_second = campaign(second, "12")
|
||||
self.assertEqual([item[0] for item in Provider.requests],
|
||||
["/specimen/wave-1", "/specimen/wave-2"])
|
||||
self.assertEqual(next(item for item in after_second["actions"]
|
||||
if item["action_id"] == second["action_id"])["result_status"],
|
||||
"CONTRADICTION")
|
||||
self.assertFalse(next(item for item in after_second["actions"]
|
||||
if item["action_id"] == refused["action_id"])["contacted"])
|
||||
with sqlite3.connect(workspace_a / ".labfy/runtime/jobs.sqlite") as connection:
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_results").fetchone()[0], 2)
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_receipts").fetchone()[0], 2)
|
||||
self.assertEqual(len(list((workspace_a / ".labfy/research").glob("*.txt"))), 2)
|
||||
|
||||
_, server_b = self.start_workspace("B")
|
||||
cookie_b, _ = self.login(server_b)
|
||||
status, _, body = self.request(server_b, "GET", "/api/v1/research",
|
||||
cookie=cookie_b)
|
||||
self.assertEqual(status, 200); self.assertEqual(json.loads(body)["state"], "EMPTY")
|
||||
self.assertNotEqual(server_a.context()["investigation_id"],
|
||||
server_b.context()["investigation_id"])
|
||||
self.assertNotIn("/specimen/refused", [item[0] for item in Provider.requests])
|
||||
|
||||
def test_result_failure_compensates_final_artifact(self):
|
||||
Provider.requests.clear()
|
||||
workspace, server = self.start_workspace("artifact-failure")
|
||||
cookie, csrf = self.login(server)
|
||||
graph = json.loads((workspace / "core-snapshot.json").read_text())
|
||||
prepared = {"selection_ids": [graph["nodes"][0]["id"]],
|
||||
"question": "Faute SQLite SPECIMEN ?", "exclusions": [],
|
||||
"idempotency_key": "98000000-0000-4000-8000-000000000001"}
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/prepare", prepared)
|
||||
self.assertEqual(status, 200, body); snapshot = json.loads(body)
|
||||
first = snapshot["actions"][0]
|
||||
decisions = [{"action_id": item["action_id"],
|
||||
"decision": "AUTHORIZE" if item == first else "DEFER"}
|
||||
for item in snapshot["actions"]]
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [first["action_id"]], "decisions": decisions,
|
||||
"exclusions": [],
|
||||
"idempotency_key": "98000000-0000-4000-8000-000000000002"})
|
||||
self.assertEqual(status, 200, body); grant = json.loads(body)
|
||||
database = workspace / ".labfy/runtime/jobs.sqlite"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.execute("CREATE TRIGGER specimen_fail_result BEFORE INSERT ON research_results BEGIN SELECT RAISE(FAIL, 'SPECIMEN'); END")
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/campaigns", {"grant_id": grant["grants"][0]["grant_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"action_ids": [first["action_id"]],
|
||||
"idempotency_key": "98000000-0000-4000-8000-000000000003"})
|
||||
self.assertEqual(status, 409, body)
|
||||
artifact_dir = workspace / ".labfy/research"
|
||||
self.assertEqual(list(artifact_dir.glob("*.txt")), [])
|
||||
with sqlite3.connect(database) as connection:
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_results").fetchone()[0], 0)
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_receipts").fetchone()[0], 0)
|
||||
|
||||
def test_response_over_action_limit_publishes_nothing(self):
|
||||
Provider.requests.clear()
|
||||
Provider.bodies["/specimen/wave-1"] = b"x" * 4097
|
||||
try:
|
||||
workspace, server = self.start_workspace("response-limit")
|
||||
cookie, csrf = self.login(server)
|
||||
graph = json.loads((workspace / "core-snapshot.json").read_text())
|
||||
prepared = {"selection_ids": [graph["nodes"][0]["id"]],
|
||||
"question": "Borne corps SPECIMEN ?", "exclusions": [],
|
||||
"idempotency_key": "98100000-0000-4000-8000-000000000001"}
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/prepare", prepared)
|
||||
self.assertEqual(status, 200, body); snapshot = json.loads(body)
|
||||
first = snapshot["actions"][0]
|
||||
decisions = [{"action_id": item["action_id"],
|
||||
"decision": "AUTHORIZE" if item == first else "DEFER"}
|
||||
for item in snapshot["actions"]]
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [first["action_id"]],
|
||||
"decisions": decisions, "exclusions": [],
|
||||
"idempotency_key": "98100000-0000-4000-8000-000000000002"})
|
||||
self.assertEqual(status, 200, body); grant = json.loads(body)
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/campaigns", {
|
||||
"grant_id": grant["grants"][0]["grant_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"action_ids": [first["action_id"]],
|
||||
"idempotency_key": "98100000-0000-4000-8000-000000000003"})
|
||||
self.assertEqual(status, 409, body)
|
||||
self.assertEqual(list((workspace / ".labfy/research").glob("*.txt")), [])
|
||||
with sqlite3.connect(workspace / ".labfy/runtime/jobs.sqlite") as connection:
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_results").fetchone()[0], 0)
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_receipts").fetchone()[0], 0)
|
||||
finally:
|
||||
Provider.bodies.pop("/specimen/wave-1", None)
|
||||
|
||||
def test_crash_after_rename_reconciles_without_recontact(self):
|
||||
Provider.requests.clear()
|
||||
workspace, server = self.start_workspace("crash-reconcile")
|
||||
cookie, csrf = self.login(server)
|
||||
graph = json.loads((workspace / "core-snapshot.json").read_text())
|
||||
prepared = {"selection_ids": [graph["nodes"][0]["id"]],
|
||||
"question": "Reprise publication SPECIMEN ?", "exclusions": [],
|
||||
"idempotency_key": "98200000-0000-4000-8000-000000000001"}
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/prepare", prepared)
|
||||
self.assertEqual(status, 200, body); snapshot = json.loads(body)
|
||||
first = snapshot["actions"][0]
|
||||
decisions = [{"action_id": item["action_id"],
|
||||
"decision": "AUTHORIZE" if item == first else "DEFER"}
|
||||
for item in snapshot["actions"]]
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/grants", {"plan_id": snapshot["plan_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"selected_action_ids": [first["action_id"]],
|
||||
"decisions": decisions, "exclusions": [],
|
||||
"idempotency_key": "98200000-0000-4000-8000-000000000002"})
|
||||
self.assertEqual(status, 200, body); grant = json.loads(body)
|
||||
campaign = {"grant_id": grant["grants"][0]["grant_id"],
|
||||
"input_revision": snapshot["input_revision"],
|
||||
"action_ids": [first["action_id"]],
|
||||
"idempotency_key": "98200000-0000-4000-8000-000000000003"}
|
||||
os.environ["LABFY_TEST_RESEARCH_CRASH_AFTER_RENAME"] = "1"
|
||||
try:
|
||||
status, _, _ = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/campaigns", campaign)
|
||||
finally:
|
||||
os.environ.pop("LABFY_TEST_RESEARCH_CRASH_AFTER_RENAME", None)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertEqual(len(Provider.requests), 1)
|
||||
artifact_dir = workspace / ".labfy/research"
|
||||
self.assertEqual(len(list(artifact_dir.glob("*.txt"))), 1)
|
||||
self.assertEqual(len(list(artifact_dir.glob(".publishing-*"))), 1)
|
||||
status, _, body = self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/campaigns", campaign)
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(len(Provider.requests), 1)
|
||||
self.assertEqual(list(artifact_dir.glob(".publishing-*")), [])
|
||||
with sqlite3.connect(workspace / ".labfy/runtime/jobs.sqlite") as connection:
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_results").fetchone()[0], 1)
|
||||
self.assertEqual(connection.execute(
|
||||
"SELECT count(*) FROM research_receipts").fetchone()[0], 1)
|
||||
|
||||
def test_research_routes_keep_host_origin_csrf_and_shape_guards(self):
|
||||
_, server = self.start_workspace("guards")
|
||||
cookie, csrf = self.login(server)
|
||||
self.assertEqual(self.request(server, "GET", "/api/v1/research",
|
||||
cookie=cookie, host="evil.test")[0], 403)
|
||||
value = {"selection_ids": ["x"], "question": "SPECIMEN",
|
||||
"exclusions": [],
|
||||
"idempotency_key": "97000000-0000-4000-8000-000000000001"}
|
||||
self.assertEqual(self.request(server, "POST", "/api/v1/research/prepare",
|
||||
value, cookie=cookie, csrf=csrf, origin="http://evil.test")[0], 403)
|
||||
malformed = dict(value, endpoint="http://127.0.0.1/")
|
||||
self.assertEqual(self.mutate(server, cookie, csrf,
|
||||
"/api/v1/research/prepare", malformed)[0], 400)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -16,6 +16,7 @@ import subprocess
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
from web_library import WebLibrary
|
||||
|
|
@ -188,7 +189,8 @@ def serve(args):
|
|||
try:
|
||||
server = WorkspaceServer(("127.0.0.1", args.port), Handler,
|
||||
workspace=workspace, bridge=args.bridge, bootstrap=bootstrap,
|
||||
library=library, instance_id=instance_id, config_id=config_id)
|
||||
library=library, instance_id=instance_id, config_id=config_id,
|
||||
automatic_session=args.automatic_session)
|
||||
except OSError as error:
|
||||
_handshake(args.handshake_fd, {"ok": False,
|
||||
"error": f"Port indisponible : {error}"})
|
||||
|
|
@ -209,12 +211,13 @@ def serve(args):
|
|||
os.write(descriptor, (bootstrap + "\n").encode())
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
answer = {"ok": True, "origin": server.origin, "bootstrap_code": bootstrap,
|
||||
answer = {"ok": True, "origin": server.origin,
|
||||
"instance_id": instance_id}
|
||||
_handshake(args.handshake_fd, answer)
|
||||
if args.handshake_fd is None:
|
||||
print(f"Labfy Web : {server.origin}/", flush=True)
|
||||
print(f"Code de session éphémère : {bootstrap}", flush=True)
|
||||
if not args.automatic_session:
|
||||
print(f"Code de session éphémère : {bootstrap}", flush=True)
|
||||
# INVARIANT: SIGTERM n'arrête que cette instance déjà authentifiée
|
||||
# par le lanceur ; shutdown s'exécute hors du gestionnaire de signal.
|
||||
def request_shutdown(_signum, _frame):
|
||||
|
|
@ -243,14 +246,18 @@ def start(args):
|
|||
except (OSError, RuntimeError, json.JSONDecodeError):
|
||||
current = None
|
||||
if current is not None and _identity_valid(current):
|
||||
print(f"Instance déjà active sur http://127.0.0.1:{current['port']}/",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
origin = f"http://127.0.0.1:{current['port']}/"
|
||||
if args.open_browser:
|
||||
webbrowser.open(origin)
|
||||
print(f"Instance déjà active sur {origin}", file=sys.stderr)
|
||||
return 0
|
||||
files.clear_stale()
|
||||
read_fd, write_fd = os.pipe()
|
||||
command = [sys.executable, str(Path(__file__).resolve()), "serve",
|
||||
"--port", str(args.port), "--bridge", str(args.bridge),
|
||||
"--handshake-fd", str(write_fd)]
|
||||
if args.automatic_session:
|
||||
command.append("--automatic-session")
|
||||
mode, path = _selection(args)
|
||||
command.extend((f"--{mode}", str(path)))
|
||||
process = subprocess.Popen(command, stdin=subprocess.DEVNULL,
|
||||
|
|
@ -274,7 +281,8 @@ def start(args):
|
|||
print(response.get("error", "Démarrage refusé"), file=sys.stderr)
|
||||
return 1
|
||||
print(f"Labfy Web : {response['origin']}/")
|
||||
print(f"Code de session éphémère : {response['bootstrap_code']}")
|
||||
if args.open_browser:
|
||||
webbrowser.open(f"{response['origin']}/")
|
||||
return 0
|
||||
finally:
|
||||
os.close(lifecycle)
|
||||
|
|
@ -347,6 +355,11 @@ def build_parser():
|
|||
targets.add_argument("--workspace", type=Path)
|
||||
command.add_argument("--port", type=valid_port, default=DEFAULT_PORT)
|
||||
command.add_argument("--bridge", type=Path, default=repository / "tools/local-jobs")
|
||||
command.add_argument("--automatic-session", action="store_true",
|
||||
help="établit une session locale sans code visible")
|
||||
if name == "start":
|
||||
command.add_argument("--open-browser", action="store_true",
|
||||
help="ouvre l'interface locale après vérification")
|
||||
if name == "serve":
|
||||
command.add_argument("--handshake-fd", type=int, help=argparse.SUPPRESS)
|
||||
command.set_defaults(function=function)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class WorkspaceServer(ThreadingHTTPServer):
|
|||
|
||||
def __init__(self, address, handler, *, workspace: Path, bridge: Path,
|
||||
bootstrap: str, library=None, instance_id=None, config_id=None,
|
||||
session_ttl_seconds=SESSION_TTL_SECONDS):
|
||||
session_ttl_seconds=SESSION_TTL_SECONDS,
|
||||
research_fixture_authority=None, automatic_session=False):
|
||||
if (not isinstance(session_ttl_seconds, int) or
|
||||
isinstance(session_ttl_seconds, bool) or session_ttl_seconds <= 0):
|
||||
raise ValueError("Durée de session invalide")
|
||||
|
|
@ -59,6 +60,9 @@ class WorkspaceServer(ThreadingHTTPServer):
|
|||
self.authority = f"127.0.0.1:{self.server_port}"
|
||||
self.origin = f"http://{self.authority}"
|
||||
self.bootstrap = bootstrap
|
||||
# CONTRACT: ce mode est activé seulement par le lanceur possédé. Il
|
||||
# émet un cookie HttpOnly local mais ne relâche jamais Origin/CSRF.
|
||||
self.automatic_session = automatic_session
|
||||
self.library = library
|
||||
self.active_workspace_id = None
|
||||
self.library_generation = (library.snapshot(None)["generation"]
|
||||
|
|
@ -89,6 +93,9 @@ class WorkspaceServer(ThreadingHTTPServer):
|
|||
self.active_uploads = 0
|
||||
self.reserved_upload_bytes = 0
|
||||
self.upload_mutexes = {}
|
||||
# CONTRACT: cette autorité de laboratoire n'est ni une donnée UI ni un
|
||||
# réglage persistant. Le bridge C applique encore sa propre whitelist.
|
||||
self.research_fixture_authority = research_fixture_authority
|
||||
|
||||
def library_snapshot(self):
|
||||
if self.library is None:
|
||||
|
|
@ -160,7 +167,12 @@ class WorkspaceServer(ThreadingHTTPServer):
|
|||
def bridge_call(self, arguments, timeout=8):
|
||||
command = [str(self.bridge), arguments[0], "--workspace",
|
||||
str(self.workspace), *arguments[1:]]
|
||||
environment = os.environ.copy()
|
||||
if self.research_fixture_authority is not None:
|
||||
environment["LABFY_RESEARCH_FIXTURE_AUTHORITY"] = \
|
||||
self.research_fixture_authority
|
||||
result = subprocess.run(command, cwd=self.bridge.parents[1], text=True,
|
||||
env=environment,
|
||||
capture_output=True, timeout=timeout, check=False)
|
||||
if result.returncode != 0:
|
||||
message = result.stderr.strip().splitlines()[-1] if result.stderr.strip() else "Commande C refusée"
|
||||
|
|
@ -406,6 +418,69 @@ class Handler(BaseHTTPRequestHandler):
|
|||
raise TypeError("Champ de plan invalide")
|
||||
return value
|
||||
|
||||
def _research_body(self, kind):
|
||||
value = self._json_body()
|
||||
shapes = {
|
||||
"prepare": {"selection_ids", "question", "exclusions",
|
||||
"idempotency_key"},
|
||||
"grant": {"plan_id", "input_revision", "selected_action_ids",
|
||||
"decisions", "exclusions", "idempotency_key"},
|
||||
"campaign": {"grant_id", "input_revision", "action_ids",
|
||||
"idempotency_key"},
|
||||
"revoke": {"grant_id"},
|
||||
}
|
||||
if not isinstance(value, dict) or set(value) != shapes[kind]:
|
||||
raise TypeError("Champs de recherche inattendus")
|
||||
if kind == "grant":
|
||||
decisions = value["decisions"]
|
||||
if (not isinstance(decisions, list) or not decisions or
|
||||
len(decisions) > 8 or any(not isinstance(item, dict) or
|
||||
set(item) != {"action_id", "decision"}
|
||||
for item in decisions)):
|
||||
raise TypeError("Décisions de recherche invalides")
|
||||
decision_ids = []
|
||||
for item in decisions:
|
||||
if (not isinstance(item["action_id"], str) or
|
||||
not isinstance(item["decision"], str) or
|
||||
item["decision"] not in {"AUTHORIZE", "DEFER", "REFUSE"}):
|
||||
raise TypeError("Décision de recherche invalide")
|
||||
try:
|
||||
uuid.UUID(item["action_id"])
|
||||
except ValueError as error:
|
||||
raise TypeError("Identifiant de décision invalide") from error
|
||||
decision_ids.append(item["action_id"])
|
||||
if len(set(decision_ids)) != len(decision_ids):
|
||||
raise TypeError("Décision de recherche dupliquée")
|
||||
authorized = {item["action_id"] for item in decisions
|
||||
if item["decision"] == "AUTHORIZE"}
|
||||
if not set(value["selected_action_ids"]) <= authorized:
|
||||
raise TypeError("Action sélectionnée sans autorisation")
|
||||
list_names = shapes[kind] & {
|
||||
"selection_ids", "exclusions", "selected_action_ids", "action_ids"}
|
||||
for name in list_names:
|
||||
items = value[name]
|
||||
if (not isinstance(items, list) or len(items) > 8 or
|
||||
(name != "exclusions" and not items) or
|
||||
len(set(items)) != len(items) or
|
||||
any(not isinstance(item, str) or not item or
|
||||
len(item) > 160 or "," in item for item in items)):
|
||||
raise TypeError("Liste de recherche invalide")
|
||||
for name, item in value.items():
|
||||
if name in list_names or name == "decisions":
|
||||
continue
|
||||
if name == "input_revision":
|
||||
if not isinstance(item, int) or isinstance(item, bool) or item < 0:
|
||||
raise TypeError("Révision de recherche invalide")
|
||||
elif (not isinstance(item, str) or not item or len(item) > 512 or
|
||||
"\x00" in item):
|
||||
raise TypeError("Champ de recherche invalide")
|
||||
for name in shapes[kind] & {"plan_id", "grant_id", "idempotency_key"}:
|
||||
try:
|
||||
uuid.UUID(value[name])
|
||||
except ValueError as error:
|
||||
raise TypeError("Identifiant de recherche invalide") from error
|
||||
return value
|
||||
|
||||
def _report_preview_body(self):
|
||||
value = self._json_body()
|
||||
if not isinstance(value, dict) or set(value) != {"object_ids", "title",
|
||||
|
|
@ -487,6 +562,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||
"config_id": self.server.config_id})
|
||||
return
|
||||
if not self._authenticated():
|
||||
if path == "/" and self.server.automatic_session:
|
||||
self._open_automatic_session()
|
||||
return
|
||||
if path in {"/", "/login.js", "/styles.css"}:
|
||||
self._serve_static("/login.html" if path == "/" else path)
|
||||
return
|
||||
|
|
@ -547,6 +625,14 @@ class Handler(BaseHTTPRequestHandler):
|
|||
elif path == "/api/v1/planner":
|
||||
self._serve_export("planner-snapshot.json",
|
||||
"labfy.local_planner.snapshot.v1")
|
||||
elif path == "/api/v1/research":
|
||||
try:
|
||||
value = json.loads(self.server.bridge_call(
|
||||
["research-snapshot-json"]))
|
||||
self._json(HTTPStatus.OK, value)
|
||||
except (ValueError, json.JSONDecodeError,
|
||||
subprocess.TimeoutExpired) as error:
|
||||
self._error(HTTPStatus.CONFLICT, "research_unavailable", str(error))
|
||||
elif path.startswith("/api/v1/reports/"):
|
||||
parts=path.split("/")
|
||||
report_id=parts[4] if len(parts)>4 else ""
|
||||
|
|
@ -709,6 +795,45 @@ class Handler(BaseHTTPRequestHandler):
|
|||
self.server.start_worker()
|
||||
self._json(HTTPStatus.ACCEPTED, json.loads(output))
|
||||
return
|
||||
if path == "/api/v1/research/prepare":
|
||||
value = self._research_body("prepare")
|
||||
graph = json.loads((self.server.workspace /
|
||||
"core-snapshot.json").read_text())
|
||||
revision = graph.get("revision")
|
||||
if (not isinstance(revision, int) or isinstance(revision, bool)
|
||||
or revision < 0):
|
||||
raise ValueError("Révision du snapshot cœur invalide")
|
||||
output = self.server.bridge_call(["research-prepare-json",
|
||||
"--selection", ",".join(value["selection_ids"]),
|
||||
"--question", value["question"], "--exclusions",
|
||||
",".join(value["exclusions"]), "--revision",
|
||||
str(revision), "--key",
|
||||
value["idempotency_key"]], timeout=15)
|
||||
self._json(HTTPStatus.OK, json.loads(output)); return
|
||||
if path == "/api/v1/research/grants":
|
||||
value = self._research_body("grant")
|
||||
output = self.server.bridge_call(["research-grant-json",
|
||||
"--plan", value["plan_id"], "--revision",
|
||||
str(value["input_revision"]), "--actions",
|
||||
",".join(value["selected_action_ids"]), "--decisions",
|
||||
",".join(f'{item["action_id"]}={item["decision"]}'
|
||||
for item in value["decisions"]), "--exclusions",
|
||||
",".join(value["exclusions"]), "--key",
|
||||
value["idempotency_key"]], timeout=15)
|
||||
self._json(HTTPStatus.OK, json.loads(output)); return
|
||||
if path == "/api/v1/research/campaigns":
|
||||
value = self._research_body("campaign")
|
||||
output = self.server.bridge_call(["research-campaign-json",
|
||||
"--grant", value["grant_id"], "--revision",
|
||||
str(value["input_revision"]), "--actions",
|
||||
",".join(value["action_ids"]), "--key",
|
||||
value["idempotency_key"]], timeout=20)
|
||||
self._json(HTTPStatus.OK, json.loads(output)); return
|
||||
if path == "/api/v1/research/revoke":
|
||||
value = self._research_body("revoke")
|
||||
output = self.server.bridge_call(["research-revoke-json",
|
||||
"--grant", value["grant_id"]])
|
||||
self._json(HTTPStatus.OK, json.loads(output)); return
|
||||
if path == "/api/v1/reports/preview":
|
||||
value=self._report_preview_body();generated=time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime())
|
||||
output=self.server.bridge_call(["prepare-report-json","--objects",",".join(value["object_ids"]),"--title",value["title"],"--comment",value["comment"],"--generated-at",generated,"--sections",",".join(value["sections"])],timeout=15)
|
||||
|
|
@ -1018,6 +1143,24 @@ class Handler(BaseHTTPRequestHandler):
|
|||
self._json(HTTPStatus.OK, {"contract": "labfy.workspace.session.v1",
|
||||
"authenticated": True}, cookie=cookie)
|
||||
|
||||
def _open_automatic_session(self):
|
||||
with self.server.session_lock:
|
||||
self.server.session = secrets.token_urlsafe(32)
|
||||
self.server.csrf = secrets.token_urlsafe(32)
|
||||
self.server.session_deadline = (time.monotonic() +
|
||||
self.server.session_ttl_seconds)
|
||||
cookie = (f"{self.server.cookie_name}={self.server.session}; "
|
||||
"HttpOnly; SameSite=Strict; "
|
||||
f"Path=/; Max-Age={self.server.session_ttl_seconds}")
|
||||
# INVARIANT: aucun secret n'est inclus dans l'URL ou dans le document;
|
||||
# l'application récupère seulement le CSRF depuis la même origine.
|
||||
self.send_response(HTTPStatus.SEE_OTHER)
|
||||
self._headers()
|
||||
self.send_header("Set-Cookie", cookie)
|
||||
self.send_header("Location", "/")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def _serve_export(self, name, contract):
|
||||
path = self.server.workspace / name
|
||||
try:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,9 @@
|
|||
/******************************************************************************
|
||||
* @file local_job_store.c
|
||||
* @brief JobStore SQLite V3, distinct de la base métier V20.
|
||||
* @brief JobStore SQLite V4, distinct de la base métier V20.
|
||||
******************************************************************************/
|
||||
#include "core/local_job_store.h"
|
||||
#include "core/research_store.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
|
@ -79,6 +80,263 @@ static const char *v2_to_v3_sql =
|
|||
"CREATE UNIQUE INDEX idx_jobs_active_analysis ON jobs(source_evidence_id,capability_id) "
|
||||
"WHERE state IN('QUEUED','RUNNING','RETRY_WAIT');";
|
||||
|
||||
static const char *research_schema_sql =
|
||||
"CREATE TABLE research_plans("
|
||||
"plan_id TEXT PRIMARY KEY,investigation_id TEXT NOT NULL,"
|
||||
"idempotency_key TEXT NOT NULL,content_fingerprint TEXT NOT NULL,"
|
||||
"input_fingerprint TEXT NOT NULL,input_revision INTEGER NOT NULL CHECK(input_revision>0),"
|
||||
"created_at TEXT NOT NULL,UNIQUE(investigation_id,idempotency_key));"
|
||||
"CREATE TABLE research_seeds("
|
||||
"plan_id TEXT NOT NULL REFERENCES research_plans(plan_id) ON DELETE RESTRICT,"
|
||||
"rank INTEGER NOT NULL,kind TEXT NOT NULL,subject TEXT NOT NULL,PRIMARY KEY(plan_id,rank));"
|
||||
"CREATE TABLE research_actions("
|
||||
"action_id TEXT PRIMARY KEY,plan_id TEXT NOT NULL REFERENCES research_plans(plan_id) ON DELETE RESTRICT,"
|
||||
"rank INTEGER NOT NULL,capability_id TEXT NOT NULL,provider_id TEXT NOT NULL,"
|
||||
"endpoint TEXT NOT NULL,subject TEXT NOT NULL,contact_class TEXT NOT NULL,"
|
||||
"disclosure TEXT NOT NULL,max_requests INTEGER NOT NULL CHECK(max_requests>0),"
|
||||
"max_response_bytes INTEGER NOT NULL CHECK(max_response_bytes>0),"
|
||||
"max_active_ms INTEGER NOT NULL CHECK(max_active_ms>0),UNIQUE(plan_id,rank));"
|
||||
"CREATE TABLE scope_grants("
|
||||
"grant_id TEXT PRIMARY KEY,investigation_id TEXT NOT NULL,idempotency_key TEXT NOT NULL,"
|
||||
"content_fingerprint TEXT NOT NULL,plan_fingerprint TEXT NOT NULL,created_at TEXT NOT NULL,"
|
||||
"expires_at TEXT NOT NULL,revoked_at TEXT,max_requests INTEGER NOT NULL CHECK(max_requests>0),"
|
||||
"max_response_bytes INTEGER NOT NULL CHECK(max_response_bytes>0),"
|
||||
"max_active_ms INTEGER NOT NULL CHECK(max_active_ms>0),"
|
||||
"UNIQUE(investigation_id,idempotency_key),"
|
||||
"FOREIGN KEY(investigation_id,plan_fingerprint) REFERENCES research_plans(investigation_id,content_fingerprint));"
|
||||
"CREATE TABLE scope_grant_actions("
|
||||
"grant_id TEXT NOT NULL REFERENCES scope_grants(grant_id) ON DELETE RESTRICT,"
|
||||
"action_id TEXT NOT NULL REFERENCES research_actions(action_id) ON DELETE RESTRICT,"
|
||||
"subject TEXT NOT NULL,provider_id TEXT NOT NULL,endpoint TEXT NOT NULL,"
|
||||
"PRIMARY KEY(grant_id,action_id));"
|
||||
"CREATE TABLE scope_grant_exclusions("
|
||||
"grant_id TEXT NOT NULL REFERENCES scope_grants(grant_id) ON DELETE RESTRICT,"
|
||||
"subject TEXT NOT NULL,PRIMARY KEY(grant_id,subject));"
|
||||
"CREATE TABLE research_action_decisions("
|
||||
"action_id TEXT PRIMARY KEY REFERENCES research_actions(action_id) ON DELETE RESTRICT,"
|
||||
"plan_id TEXT NOT NULL REFERENCES research_plans(plan_id) ON DELETE RESTRICT,"
|
||||
"decision_code TEXT NOT NULL CHECK(decision_code IN('AUTHORIZE','DEFER','REFUSE')),"
|
||||
"decided_at TEXT NOT NULL);"
|
||||
"CREATE TABLE research_campaigns("
|
||||
"campaign_id TEXT PRIMARY KEY,investigation_id TEXT NOT NULL,"
|
||||
"grant_id TEXT NOT NULL REFERENCES scope_grants(grant_id) ON DELETE RESTRICT,"
|
||||
"idempotency_key TEXT NOT NULL,content_fingerprint TEXT NOT NULL,"
|
||||
"created_at TEXT NOT NULL,state TEXT NOT NULL DEFAULT 'ADMITTED',"
|
||||
"UNIQUE(investigation_id,idempotency_key));"
|
||||
"CREATE TABLE research_results("
|
||||
"result_id TEXT PRIMARY KEY,campaign_id TEXT NOT NULL REFERENCES research_campaigns(campaign_id) ON DELETE RESTRICT,"
|
||||
"action_id TEXT NOT NULL REFERENCES research_actions(action_id) ON DELETE RESTRICT,"
|
||||
"raw_artifact_relative_path TEXT NOT NULL,content_sha256 TEXT NOT NULL,status TEXT NOT NULL);"
|
||||
"CREATE TABLE research_receipts("
|
||||
"receipt_id TEXT PRIMARY KEY,result_id TEXT NOT NULL UNIQUE REFERENCES research_results(result_id) ON DELETE RESTRICT,"
|
||||
"campaign_id TEXT NOT NULL REFERENCES research_campaigns(campaign_id) ON DELETE RESTRICT,"
|
||||
"grant_id TEXT NOT NULL,action_id TEXT NOT NULL,decided_at TEXT NOT NULL,decision_code TEXT NOT NULL,"
|
||||
"FOREIGN KEY(grant_id,action_id) REFERENCES scope_grant_actions(grant_id,action_id));"
|
||||
"CREATE UNIQUE INDEX idx_research_plan_owner_fingerprint "
|
||||
"ON research_plans(investigation_id,content_fingerprint);";
|
||||
|
||||
/* V4 n'est pas publiée : cette extension additive permet aussi d'ouvrir une
|
||||
* base V4 créée par une build intermédiaire de la tranche OSINT. */
|
||||
static const char *research_decision_extension_sql =
|
||||
"CREATE TABLE IF NOT EXISTS research_action_decisions("
|
||||
"action_id TEXT PRIMARY KEY REFERENCES research_actions(action_id) ON DELETE RESTRICT,"
|
||||
"plan_id TEXT NOT NULL REFERENCES research_plans(plan_id) ON DELETE RESTRICT,"
|
||||
"decision_code TEXT NOT NULL CHECK(decision_code IN('AUTHORIZE','DEFER','REFUSE')),"
|
||||
"decided_at TEXT NOT NULL);";
|
||||
|
||||
/* Le Makefile historique lie ce fichier seul dans plusieurs outils. Ces
|
||||
* gardes internes maintiennent cette compatibilité sans rendre le JobStore
|
||||
* dépendant d'un nouveau module à l'édition de liens. */
|
||||
static gboolean research_text(const char *value) {
|
||||
return value != NULL && value[0] != '\0';
|
||||
}
|
||||
|
||||
static gboolean research_hash(const char *value) {
|
||||
if (value == NULL || strlen(value) != 64)
|
||||
return FALSE;
|
||||
for (gsize i = 0; i < 64; i++)
|
||||
if (!g_ascii_isxdigit(value[i]))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean research_timestamp(const char *value) {
|
||||
GDateTime *parsed = value != NULL
|
||||
? g_date_time_new_from_iso8601(value, NULL)
|
||||
: NULL;
|
||||
if (parsed == NULL)
|
||||
return FALSE;
|
||||
g_date_time_unref(parsed);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean research_relative_artifact(const char *value) {
|
||||
if (!research_text(value) || g_path_is_absolute(value) || strchr(value, '\\'))
|
||||
return FALSE;
|
||||
char **parts = g_strsplit(value, "/", -1);
|
||||
gboolean valid = TRUE;
|
||||
for (gsize i = 0; valid && parts[i] != NULL; i++)
|
||||
valid = parts[i][0] != '\0' && g_strcmp0(parts[i], ".") != 0 &&
|
||||
g_strcmp0(parts[i], "..") != 0;
|
||||
g_strfreev(parts);
|
||||
return valid;
|
||||
}
|
||||
|
||||
static const char *research_seed_code(ResearchSeedKind kind) {
|
||||
static const char *const codes[] = {"DOMAIN", "IP", "HTTP_URL", "EMAIL",
|
||||
"SEARCH_TERM"};
|
||||
return kind <= RESEARCH_SEED_SEARCH_TERM ? codes[kind] : NULL;
|
||||
}
|
||||
|
||||
static const char *research_contact_code(ResearchContactClass contact) {
|
||||
static const char *const codes[] = {"NONE", "THIRD_PARTY", "TARGET"};
|
||||
return contact <= RESEARCH_CONTACT_TARGET ? codes[contact] : NULL;
|
||||
}
|
||||
|
||||
static const char *research_decision_code(ResearchActionDecisionCode decision) {
|
||||
static const char *const codes[] = {"AUTHORIZE", "DEFER", "REFUSE"};
|
||||
return decision <= RESEARCH_ACTION_REFUSE ? codes[decision] : NULL;
|
||||
}
|
||||
|
||||
static gboolean bind_text(sqlite3_stmt *statement, int index,
|
||||
const char *value);
|
||||
|
||||
static gboolean research_action_guard(const ResearchAction *action) {
|
||||
return action != NULL && g_uuid_string_is_valid(action->action_id) &&
|
||||
research_text(action->capability_id) && research_text(action->provider_id) &&
|
||||
research_text(action->endpoint) && research_text(action->subject) &&
|
||||
research_contact_code(action->contact) != NULL &&
|
||||
research_text(action->disclosure) && action->max_requests > 0 &&
|
||||
action->max_response_bytes > 0 &&
|
||||
action->max_response_bytes <= G_MAXINT64 && action->max_active_ms > 0;
|
||||
}
|
||||
|
||||
static gboolean research_plan_guard(const ResearchPlan *plan) {
|
||||
if (plan == NULL || g_strcmp0(plan->contract, RESEARCH_PLAN_CONTRACT) != 0 ||
|
||||
!g_uuid_string_is_valid(plan->plan_id) ||
|
||||
!research_text(plan->idempotency_key) ||
|
||||
!research_hash(plan->content_fingerprint) ||
|
||||
!research_hash(plan->input_fingerprint) || plan->input_revision == 0 ||
|
||||
!research_timestamp(plan->created_at) || plan->seeds == NULL ||
|
||||
plan->seed_count == 0 || plan->actions == NULL || plan->action_count == 0)
|
||||
return FALSE;
|
||||
GHashTable *ids = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
gboolean valid = TRUE;
|
||||
for (gsize i = 0; valid && i < plan->seed_count; i++)
|
||||
valid = research_seed_code(plan->seeds[i].kind) != NULL &&
|
||||
research_text(plan->seeds[i].subject);
|
||||
for (gsize i = 0; valid && i < plan->action_count; i++)
|
||||
valid = research_action_guard(&plan->actions[i]) &&
|
||||
g_hash_table_add(ids, (gpointer)plan->actions[i].action_id);
|
||||
g_hash_table_unref(ids);
|
||||
return valid;
|
||||
}
|
||||
|
||||
static gboolean research_grant_guard(const ScopeGrant *grant,
|
||||
const ResearchActionDecision *decisions, gsize decision_count,
|
||||
const char *decided_at) {
|
||||
if (grant == NULL || g_strcmp0(grant->contract, RESEARCH_GRANT_CONTRACT) != 0 ||
|
||||
!g_uuid_string_is_valid(grant->grant_id) ||
|
||||
!g_uuid_string_is_valid(grant->investigation_id) ||
|
||||
!research_text(grant->idempotency_key) ||
|
||||
!research_hash(grant->content_fingerprint) ||
|
||||
!research_hash(grant->plan_fingerprint) ||
|
||||
!research_timestamp(grant->created_at) ||
|
||||
!research_timestamp(grant->expires_at) ||
|
||||
grant->selected_action_ids == NULL || grant->selected_action_count == 0 ||
|
||||
decisions == NULL || decision_count == 0 ||
|
||||
!research_timestamp(decided_at) ||
|
||||
grant->max_requests == 0 || grant->max_response_bytes == 0 ||
|
||||
grant->max_response_bytes > G_MAXINT64 ||
|
||||
grant->max_active_ms == 0)
|
||||
return FALSE;
|
||||
GHashTable *ids = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
for (gsize i = 0; i < grant->selected_action_count; i++)
|
||||
if (!g_uuid_string_is_valid(grant->selected_action_ids[i]))
|
||||
goto invalid;
|
||||
for (gsize i = 0; i < decision_count; i++)
|
||||
if (!g_uuid_string_is_valid(decisions[i].action_id) ||
|
||||
research_decision_code(decisions[i].code) == NULL ||
|
||||
!g_hash_table_add(ids, (gpointer)decisions[i].action_id))
|
||||
goto invalid;
|
||||
for (gsize i = 0; i < grant->excluded_subject_count; i++)
|
||||
if (!research_text(grant->excluded_subjects[i]))
|
||||
goto invalid;
|
||||
g_hash_table_unref(ids);
|
||||
return TRUE;
|
||||
invalid:
|
||||
g_hash_table_unref(ids);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* CONTRACT: chaque admission conserve une décision humaine pour exactement
|
||||
* chaque action du ResearchPlan référencé. WHY: une projection partielle ne
|
||||
* permettrait pas d'auditer les actions DEFER ou REFUSE. */
|
||||
static gboolean research_grant_decisions_cover_plan(sqlite3 *db,
|
||||
const ScopeGrant *grant, const ResearchActionDecision *decisions,
|
||||
gsize decision_count) {
|
||||
sqlite3_stmt *count = NULL;
|
||||
gboolean ok = sqlite3_prepare_v2(db,
|
||||
"SELECT count(*) FROM research_actions a JOIN research_plans p "
|
||||
"ON p.plan_id=a.plan_id WHERE p.investigation_id=?1 "
|
||||
"AND p.content_fingerprint=?2;", -1, &count, NULL) == SQLITE_OK &&
|
||||
bind_text(count, 1, grant->investigation_id) &&
|
||||
bind_text(count, 2, grant->plan_fingerprint) &&
|
||||
sqlite3_step(count) == SQLITE_ROW &&
|
||||
sqlite3_column_int64(count, 0) == (sqlite3_int64)decision_count;
|
||||
sqlite3_finalize(count);
|
||||
/* INVARIANT: les UUID sont déjà uniques via research_grant_guard(); avec
|
||||
* la même cardinalité, leur appartenance au plan rend la couverture
|
||||
* bijective, sans migration du schéma V4. */
|
||||
for (gsize i = 0; ok && i < decision_count; i++) {
|
||||
sqlite3_stmt *member = NULL;
|
||||
ok = sqlite3_prepare_v2(db,
|
||||
"SELECT EXISTS(SELECT 1 FROM research_actions a "
|
||||
"JOIN research_plans p ON p.plan_id=a.plan_id "
|
||||
"WHERE a.action_id=?1 AND p.investigation_id=?2 "
|
||||
"AND p.content_fingerprint=?3);", -1, &member, NULL) == SQLITE_OK &&
|
||||
bind_text(member, 1, decisions[i].action_id) &&
|
||||
bind_text(member, 2, grant->investigation_id) &&
|
||||
bind_text(member, 3, grant->plan_fingerprint) &&
|
||||
sqlite3_step(member) == SQLITE_ROW && sqlite3_column_int(member, 0) == 1;
|
||||
sqlite3_finalize(member);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static gboolean research_campaign_guard(const ResearchCampaign *campaign) {
|
||||
return campaign != NULL &&
|
||||
g_strcmp0(campaign->contract, RESEARCH_CAMPAIGN_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(campaign->campaign_id) &&
|
||||
g_uuid_string_is_valid(campaign->investigation_id) &&
|
||||
g_uuid_string_is_valid(campaign->grant_id) &&
|
||||
research_text(campaign->idempotency_key) &&
|
||||
research_hash(campaign->content_fingerprint) &&
|
||||
research_timestamp(campaign->created_at);
|
||||
}
|
||||
|
||||
static gboolean research_result_guard(const ResearchResult *result) {
|
||||
return result != NULL &&
|
||||
g_strcmp0(result->contract, RESEARCH_RESULT_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(result->result_id) &&
|
||||
g_uuid_string_is_valid(result->campaign_id) &&
|
||||
g_uuid_string_is_valid(result->action_id) &&
|
||||
research_relative_artifact(result->raw_artifact_relative_path) &&
|
||||
research_hash(result->content_sha256) && research_text(result->status);
|
||||
}
|
||||
|
||||
static gboolean research_receipt_guard(const ResearchReceipt *receipt) {
|
||||
return receipt != NULL &&
|
||||
g_strcmp0(receipt->contract, RESEARCH_RECEIPT_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(receipt->receipt_id) &&
|
||||
g_uuid_string_is_valid(receipt->result_id) &&
|
||||
g_uuid_string_is_valid(receipt->campaign_id) &&
|
||||
g_uuid_string_is_valid(receipt->grant_id) &&
|
||||
g_uuid_string_is_valid(receipt->action_id) &&
|
||||
research_timestamp(receipt->decided_at) &&
|
||||
research_text(receipt->decision_code);
|
||||
}
|
||||
|
||||
#ifdef LOCAL_JOB_STORE_ENABLE_TEST_HOOKS
|
||||
static gboolean fail_migration_after_schema = FALSE;
|
||||
void local_job_store_test_fail_migration_after_schema(gboolean enabled) {
|
||||
|
|
@ -173,11 +431,11 @@ LocalJobStore *local_job_store_create(const char *path,
|
|||
return NULL;
|
||||
char *sql = g_strdup_printf(
|
||||
"BEGIN IMMEDIATE;%s"
|
||||
"%sINSERT INTO metadata VALUES('schema_version','3');"
|
||||
"%s%sINSERT INTO metadata VALUES('schema_version','4');"
|
||||
"INSERT INTO metadata VALUES('investigation_id','%s');"
|
||||
"INSERT INTO metadata VALUES('revision','0');"
|
||||
"INSERT INTO controls VALUES(1,0,0,'1970-01-01T00:00:00Z');COMMIT;",
|
||||
schema_sql, plan_schema_sql, investigation_id);
|
||||
schema_sql, plan_schema_sql, research_schema_sql, investigation_id);
|
||||
gboolean success =
|
||||
exec_sql(store->db, sql, error) && g_chmod(path, 0600) == 0;
|
||||
g_free(sql);
|
||||
|
|
@ -216,26 +474,28 @@ LocalJobStore *local_job_store_open(const char *path,
|
|||
owner = g_strcmp0(value, investigation_id) == 0;
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
if ((schema_version < 1 || schema_version > 3) || !owner) {
|
||||
if ((schema_version < 1 || schema_version > 4) || !owner) {
|
||||
job_error(error, G_IO_ERROR_INVALID_DATA,
|
||||
schema_version < 1 || schema_version > 3
|
||||
schema_version < 1 || schema_version > 4
|
||||
? "Version JobStore inconnue ; aucune recréation automatique."
|
||||
: "Le JobStore appartient à une autre enquête.");
|
||||
local_job_store_close(store);
|
||||
return NULL;
|
||||
}
|
||||
/* WHY: V2 ne touche que l'ordonnanceur local. Le lecteur read-only conserve
|
||||
* la compatibilité V1 et ne migre jamais; seul un ouvreur exclusif en écriture
|
||||
* applique atomiquement l'ajout non destructif des plans et budgets. */
|
||||
if (schema_version < 3 && !read_only) {
|
||||
/* WHY: V4 reste une base opérationnelle distincte de V20/V21. Le lecteur
|
||||
* read-only conserve les versions historiques sans les migrer; seul un
|
||||
* ouvreur en écriture applique les ajouts dans une transaction unique. */
|
||||
if (schema_version < 4 && !read_only) {
|
||||
/* CONTRACT: schéma, données et version sont une seule transaction. La
|
||||
* séparation des appels rend testable un crash logique après mutation,
|
||||
* sans affaiblir l'atomicité SQLite. */
|
||||
gboolean migrated = exec_sql(store->db, "BEGIN IMMEDIATE;", error);
|
||||
if (migrated && schema_version == 1)
|
||||
migrated = exec_sql(store->db, plan_schema_sql, error);
|
||||
if (migrated)
|
||||
if (migrated && schema_version < 3)
|
||||
migrated = exec_sql(store->db, v2_to_v3_sql, error);
|
||||
if (migrated)
|
||||
migrated = exec_sql(store->db, research_schema_sql, error);
|
||||
#ifdef LOCAL_JOB_STORE_ENABLE_TEST_HOOKS
|
||||
if (migrated && fail_migration_after_schema) {
|
||||
job_error(error, G_IO_ERROR_FAILED,
|
||||
|
|
@ -245,7 +505,7 @@ LocalJobStore *local_job_store_open(const char *path,
|
|||
#endif
|
||||
if (migrated)
|
||||
migrated = exec_sql(store->db,
|
||||
"UPDATE metadata SET value='3' WHERE key='schema_version';", error);
|
||||
"UPDATE metadata SET value='4' WHERE key='schema_version';", error);
|
||||
if (migrated)
|
||||
migrated = exec_sql(store->db, "COMMIT;", error);
|
||||
if (!migrated) {
|
||||
|
|
@ -254,6 +514,16 @@ LocalJobStore *local_job_store_open(const char *path,
|
|||
return NULL;
|
||||
}
|
||||
}
|
||||
if (schema_version == 4 && !read_only) {
|
||||
gboolean extended = exec_sql(store->db, "BEGIN IMMEDIATE;", error) &&
|
||||
exec_sql(store->db, research_decision_extension_sql, error) &&
|
||||
exec_sql(store->db, "COMMIT;", error);
|
||||
if (!extended) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
local_job_store_close(store);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return store;
|
||||
failure:
|
||||
sqlite3_finalize(statement);
|
||||
|
|
@ -1004,6 +1274,494 @@ gboolean local_job_store_integrity(LocalJobStore *s, GError **error) {
|
|||
return ok;
|
||||
}
|
||||
|
||||
static gboolean research_begin(LocalJobStore *store, GError **error) {
|
||||
if (store == NULL) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT, "ResearchStore absent.");
|
||||
return FALSE;
|
||||
}
|
||||
return exec_sql(store->db, "BEGIN IMMEDIATE;", error);
|
||||
}
|
||||
|
||||
static gboolean research_existing(sqlite3 *db, const char *table,
|
||||
const char *investigation_id,
|
||||
const char *idempotency_key,
|
||||
const char *content_fingerprint,
|
||||
gboolean *out_found, gboolean *out_same) {
|
||||
char *sql = g_strdup_printf(
|
||||
"SELECT content_fingerprint FROM %s WHERE investigation_id=?1 AND "
|
||||
"idempotency_key=?2;", table);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
gboolean ok = sqlite3_prepare_v2(db, sql, -1, &statement, NULL) == SQLITE_OK &&
|
||||
bind_text(statement, 1, investigation_id) &&
|
||||
bind_text(statement, 2, idempotency_key);
|
||||
g_free(sql);
|
||||
if (!ok) {
|
||||
sqlite3_finalize(statement);
|
||||
return FALSE;
|
||||
}
|
||||
int step = sqlite3_step(statement);
|
||||
*out_found = step == SQLITE_ROW;
|
||||
*out_same = *out_found &&
|
||||
g_strcmp0((const char *)sqlite3_column_text(statement, 0),
|
||||
content_fingerprint) == 0;
|
||||
ok = step == SQLITE_ROW || step == SQLITE_DONE;
|
||||
sqlite3_finalize(statement);
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_admit_plan(LocalJobStore *store,
|
||||
const ResearchPlan *plan,
|
||||
gboolean *out_reused, GError **error) {
|
||||
if (out_reused != NULL)
|
||||
*out_reused = FALSE;
|
||||
if (store == NULL || !research_plan_guard(plan)) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Contrat ResearchPlan V1 invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!research_begin(store, error))
|
||||
return FALSE;
|
||||
gboolean found = FALSE, same = FALSE;
|
||||
gboolean ok = research_existing(store->db, "research_plans",
|
||||
store->investigation_id, plan->idempotency_key,
|
||||
plan->content_fingerprint, &found, &same);
|
||||
if (ok && found) {
|
||||
if (!same) {
|
||||
job_error(error, G_IO_ERROR_EXISTS,
|
||||
"Clé ResearchPlan réutilisée avec un contenu différent.");
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
return FALSE;
|
||||
}
|
||||
if (out_reused != NULL)
|
||||
*out_reused = TRUE;
|
||||
return exec_sql(store->db, "COMMIT;", error);
|
||||
}
|
||||
sqlite3_stmt *insert = NULL;
|
||||
ok = ok && sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_plans VALUES(?1,?2,?3,?4,?5,?6,?7);",
|
||||
-1, &insert, NULL) == SQLITE_OK &&
|
||||
bind_text(insert, 1, plan->plan_id) &&
|
||||
bind_text(insert, 2, store->investigation_id) &&
|
||||
bind_text(insert, 3, plan->idempotency_key) &&
|
||||
bind_text(insert, 4, plan->content_fingerprint) &&
|
||||
bind_text(insert, 5, plan->input_fingerprint) &&
|
||||
sqlite3_bind_int64(insert, 6, (sqlite3_int64)plan->input_revision) ==
|
||||
SQLITE_OK &&
|
||||
bind_text(insert, 7, plan->created_at) &&
|
||||
sqlite3_step(insert) == SQLITE_DONE;
|
||||
sqlite3_finalize(insert);
|
||||
for (gsize i = 0; ok && i < plan->seed_count; i++) {
|
||||
sqlite3_stmt *seed = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_seeds VALUES(?1,?2,?3,?4);", -1, &seed,
|
||||
NULL) == SQLITE_OK && bind_text(seed, 1, plan->plan_id) &&
|
||||
sqlite3_bind_int64(seed, 2, (sqlite3_int64)i) == SQLITE_OK &&
|
||||
bind_text(seed, 3, research_seed_code(plan->seeds[i].kind)) &&
|
||||
bind_text(seed, 4, plan->seeds[i].subject) &&
|
||||
sqlite3_step(seed) == SQLITE_DONE;
|
||||
sqlite3_finalize(seed);
|
||||
}
|
||||
for (gsize i = 0; ok && i < plan->action_count; i++) {
|
||||
const ResearchAction *action = &plan->actions[i];
|
||||
sqlite3_stmt *item = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_actions VALUES("
|
||||
"?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12);", -1, &item,
|
||||
NULL) == SQLITE_OK && bind_text(item, 1, action->action_id) &&
|
||||
bind_text(item, 2, plan->plan_id) &&
|
||||
sqlite3_bind_int64(item, 3, (sqlite3_int64)i) == SQLITE_OK &&
|
||||
bind_text(item, 4, action->capability_id) &&
|
||||
bind_text(item, 5, action->provider_id) &&
|
||||
bind_text(item, 6, action->endpoint) &&
|
||||
bind_text(item, 7, action->subject) &&
|
||||
bind_text(item, 8, research_contact_code(action->contact)) &&
|
||||
bind_text(item, 9, action->disclosure) &&
|
||||
sqlite3_bind_int64(item, 10, action->max_requests) == SQLITE_OK &&
|
||||
sqlite3_bind_int64(item, 11,
|
||||
(sqlite3_int64)action->max_response_bytes) ==
|
||||
SQLITE_OK &&
|
||||
sqlite3_bind_int64(item, 12, action->max_active_ms) == SQLITE_OK &&
|
||||
sqlite3_step(item) == SQLITE_DONE;
|
||||
sqlite3_finalize(item);
|
||||
}
|
||||
if (ok)
|
||||
ok = exec_sql(store->db, "COMMIT;", error);
|
||||
if (!ok) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
job_error(error, G_IO_ERROR_FAILED,
|
||||
"Admission atomique du ResearchPlan impossible.");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_admit_grant(LocalJobStore *store,
|
||||
const ScopeGrant *grant,
|
||||
const ResearchActionDecision *decisions,
|
||||
gsize decision_count,
|
||||
const char *decided_at,
|
||||
gboolean *out_reused, GError **error) {
|
||||
if (out_reused != NULL)
|
||||
*out_reused = FALSE;
|
||||
if (store == NULL || !research_grant_guard(grant, decisions,
|
||||
decision_count, decided_at)) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Contrat ScopeGrant V1 invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
if (g_strcmp0(store->investigation_id, grant->investigation_id) != 0) {
|
||||
job_error(error, G_IO_ERROR_PERMISSION_DENIED,
|
||||
"Grant refusé pour une autre enquête.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!research_begin(store, error))
|
||||
return FALSE;
|
||||
if (!research_grant_decisions_cover_plan(store->db, grant, decisions,
|
||||
decision_count)) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Les décisions ne couvrent pas bijectivement le ResearchPlan.");
|
||||
return FALSE;
|
||||
}
|
||||
gboolean found = FALSE, same = FALSE;
|
||||
gboolean ok = research_existing(store->db, "scope_grants",
|
||||
grant->investigation_id, grant->idempotency_key,
|
||||
grant->content_fingerprint, &found, &same);
|
||||
if (ok && found) {
|
||||
if (!same) {
|
||||
job_error(error, G_IO_ERROR_EXISTS,
|
||||
"Clé ScopeGrant réutilisée avec un contenu différent.");
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
return FALSE;
|
||||
}
|
||||
if (out_reused != NULL)
|
||||
*out_reused = TRUE;
|
||||
return exec_sql(store->db, "COMMIT;", error);
|
||||
}
|
||||
sqlite3_stmt *insert = NULL;
|
||||
ok = ok && sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO scope_grants VALUES("
|
||||
"?1,?2,?3,?4,?5,?6,?7,NULL,?8,?9,?10);", -1, &insert,
|
||||
NULL) == SQLITE_OK && bind_text(insert, 1, grant->grant_id) &&
|
||||
bind_text(insert, 2, grant->investigation_id) &&
|
||||
bind_text(insert, 3, grant->idempotency_key) &&
|
||||
bind_text(insert, 4, grant->content_fingerprint) &&
|
||||
bind_text(insert, 5, grant->plan_fingerprint) &&
|
||||
bind_text(insert, 6, grant->created_at) &&
|
||||
bind_text(insert, 7, grant->expires_at) &&
|
||||
sqlite3_bind_int64(insert, 8, grant->max_requests) == SQLITE_OK &&
|
||||
sqlite3_bind_int64(insert, 9,
|
||||
(sqlite3_int64)grant->max_response_bytes) ==
|
||||
SQLITE_OK &&
|
||||
sqlite3_bind_int64(insert, 10, grant->max_active_ms) == SQLITE_OK &&
|
||||
sqlite3_step(insert) == SQLITE_DONE;
|
||||
sqlite3_finalize(insert);
|
||||
/* CONTRACT: la décision humaine est persistée avant de matérialiser le
|
||||
* grant. REFUSE est terminal : une requête ultérieure ne peut ni l'effacer
|
||||
* ni réadmettre l'action sous une nouvelle clé de grant. */
|
||||
for (gsize i = 0; ok && i < decision_count; i++) {
|
||||
sqlite3_stmt *decision = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_action_decisions(action_id,plan_id,decision_code,decided_at) "
|
||||
"SELECT ?1,p.plan_id,?2,?3 FROM research_actions a "
|
||||
"JOIN research_plans p ON p.plan_id=a.plan_id "
|
||||
"WHERE a.action_id=?1 AND p.investigation_id=?4 "
|
||||
"AND p.content_fingerprint=?5 "
|
||||
"ON CONFLICT(action_id) DO UPDATE SET decision_code=excluded.decision_code,"
|
||||
"decided_at=excluded.decided_at WHERE "
|
||||
"research_action_decisions.decision_code!='REFUSE' "
|
||||
"OR excluded.decision_code='REFUSE';", -1, &decision, NULL) == SQLITE_OK &&
|
||||
bind_text(decision, 1, decisions[i].action_id) &&
|
||||
bind_text(decision, 2,
|
||||
research_decision_code(decisions[i].code)) &&
|
||||
bind_text(decision, 3, decided_at) &&
|
||||
bind_text(decision, 4, grant->investigation_id) &&
|
||||
bind_text(decision, 5, grant->plan_fingerprint) &&
|
||||
sqlite3_step(decision) == SQLITE_DONE &&
|
||||
sqlite3_changes(store->db) == 1;
|
||||
sqlite3_finalize(decision);
|
||||
}
|
||||
for (gsize i = 0; ok && i < grant->selected_action_count; i++) {
|
||||
sqlite3_stmt *selected = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO scope_grant_actions(grant_id,action_id,subject,provider_id,endpoint) "
|
||||
"SELECT ?1,a.action_id,a.subject,a.provider_id,a.endpoint FROM research_actions a "
|
||||
"JOIN research_plans p ON p.plan_id=a.plan_id WHERE a.action_id=?2 "
|
||||
"AND p.investigation_id=?3 AND p.content_fingerprint=?4 "
|
||||
"AND EXISTS(SELECT 1 FROM research_action_decisions d "
|
||||
"WHERE d.action_id=a.action_id AND d.decision_code='AUTHORIZE') "
|
||||
"AND (a.rank!=1 OR EXISTS(SELECT 1 FROM research_actions first "
|
||||
"JOIN research_action_decisions fd ON fd.action_id=first.action_id "
|
||||
"JOIN research_results rr ON rr.action_id=first.action_id "
|
||||
"JOIN research_receipts rc ON rc.result_id=rr.result_id "
|
||||
"WHERE first.plan_id=a.plan_id AND first.rank=0 "
|
||||
"AND fd.decision_code='AUTHORIZE'));", -1,
|
||||
&selected, NULL) == SQLITE_OK &&
|
||||
bind_text(selected, 1, grant->grant_id) &&
|
||||
bind_text(selected, 2, grant->selected_action_ids[i]) &&
|
||||
bind_text(selected, 3, grant->investigation_id) &&
|
||||
bind_text(selected, 4, grant->plan_fingerprint) &&
|
||||
sqlite3_step(selected) == SQLITE_DONE && sqlite3_changes(store->db) == 1;
|
||||
sqlite3_finalize(selected);
|
||||
}
|
||||
for (gsize i = 0; ok && i < grant->excluded_subject_count; i++) {
|
||||
sqlite3_stmt *excluded = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO scope_grant_exclusions VALUES(?1,?2);", -1, &excluded,
|
||||
NULL) == SQLITE_OK && bind_text(excluded, 1, grant->grant_id) &&
|
||||
bind_text(excluded, 2, grant->excluded_subjects[i]) &&
|
||||
sqlite3_step(excluded) == SQLITE_DONE;
|
||||
sqlite3_finalize(excluded);
|
||||
}
|
||||
if (ok)
|
||||
ok = exec_sql(store->db, "COMMIT;", error);
|
||||
if (!ok) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
job_error(error, G_IO_ERROR_FAILED,
|
||||
"Admission atomique du ScopeGrant impossible.");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_revoke_grant(LocalJobStore *store,
|
||||
const char *grant_id,
|
||||
const char *revoked_at,
|
||||
GError **error) {
|
||||
sqlite3_stmt *statement = NULL;
|
||||
gboolean ok = store != NULL && g_uuid_string_is_valid(grant_id) &&
|
||||
revoked_at != NULL && sqlite3_prepare_v2(store->db,
|
||||
"UPDATE scope_grants SET revoked_at=COALESCE(revoked_at,?2) "
|
||||
"WHERE grant_id=?1 AND investigation_id=?3;", -1, &statement,
|
||||
NULL) == SQLITE_OK && bind_text(statement, 1, grant_id) &&
|
||||
bind_text(statement, 2, revoked_at) &&
|
||||
bind_text(statement, 3, store->investigation_id) &&
|
||||
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->db) == 1;
|
||||
sqlite3_finalize(statement);
|
||||
if (!ok)
|
||||
job_error(error, G_IO_ERROR_NOT_FOUND, "ScopeGrant à révoquer introuvable.");
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_admit_campaign(LocalJobStore *store,
|
||||
const ResearchCampaign *campaign,
|
||||
gboolean *out_reused, GError **error) {
|
||||
if (out_reused != NULL)
|
||||
*out_reused = FALSE;
|
||||
if (store == NULL || !research_campaign_guard(campaign)) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Contrat ResearchCampaign V1 invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
if (g_strcmp0(store->investigation_id, campaign->investigation_id) != 0) {
|
||||
job_error(error, G_IO_ERROR_PERMISSION_DENIED,
|
||||
"Campagne refusée pour une autre enquête.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!research_begin(store, error))
|
||||
return FALSE;
|
||||
gboolean found = FALSE, same = FALSE;
|
||||
gboolean ok = research_existing(store->db, "research_campaigns",
|
||||
campaign->investigation_id, campaign->idempotency_key,
|
||||
campaign->content_fingerprint, &found, &same);
|
||||
if (ok && found) {
|
||||
if (!same) {
|
||||
job_error(error, G_IO_ERROR_EXISTS,
|
||||
"Clé ResearchCampaign réutilisée avec un contenu différent.");
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
return FALSE;
|
||||
}
|
||||
if (out_reused != NULL)
|
||||
*out_reused = TRUE;
|
||||
return exec_sql(store->db, "COMMIT;", error);
|
||||
}
|
||||
sqlite3_stmt *insert = NULL;
|
||||
ok = ok && sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_campaigns(campaign_id,investigation_id,grant_id,"
|
||||
"idempotency_key,content_fingerprint,created_at) "
|
||||
"SELECT ?1,?2,g.grant_id,?4,?5,?6 FROM scope_grants g "
|
||||
"WHERE g.grant_id=?3 AND g.investigation_id=?2;", -1, &insert,
|
||||
NULL) == SQLITE_OK && bind_text(insert, 1, campaign->campaign_id) &&
|
||||
bind_text(insert, 2, campaign->investigation_id) &&
|
||||
bind_text(insert, 3, campaign->grant_id) &&
|
||||
bind_text(insert, 4, campaign->idempotency_key) &&
|
||||
bind_text(insert, 5, campaign->content_fingerprint) &&
|
||||
bind_text(insert, 6, campaign->created_at) &&
|
||||
sqlite3_step(insert) == SQLITE_DONE && sqlite3_changes(store->db) == 1;
|
||||
sqlite3_finalize(insert);
|
||||
if (ok)
|
||||
ok = exec_sql(store->db, "COMMIT;", error);
|
||||
if (!ok) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
job_error(error, G_IO_ERROR_FAILED,
|
||||
"Admission atomique de la ResearchCampaign impossible.");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_record_result(LocalJobStore *store,
|
||||
const ResearchResult *result,
|
||||
const ResearchReceipt *receipt,
|
||||
GError **error) {
|
||||
if (store == NULL || !research_result_guard(result) ||
|
||||
!research_receipt_guard(receipt)) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Contrat résultat ou reçu de recherche invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
if (g_strcmp0(result->result_id, receipt->result_id) != 0 ||
|
||||
g_strcmp0(result->campaign_id, receipt->campaign_id) != 0 ||
|
||||
g_strcmp0(result->action_id, receipt->action_id) != 0) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Résultat et reçu de recherche incohérents.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!research_begin(store, error))
|
||||
return FALSE;
|
||||
sqlite3_stmt *item = NULL;
|
||||
gboolean ok = sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_results VALUES(?1,?2,?3,?4,?5,?6);", -1,
|
||||
&item, NULL) == SQLITE_OK && bind_text(item, 1, result->result_id) &&
|
||||
bind_text(item, 2, result->campaign_id) &&
|
||||
bind_text(item, 3, result->action_id) &&
|
||||
bind_text(item, 4, result->raw_artifact_relative_path) &&
|
||||
bind_text(item, 5, result->content_sha256) &&
|
||||
bind_text(item, 6, result->status) && sqlite3_step(item) == SQLITE_DONE;
|
||||
sqlite3_finalize(item);
|
||||
sqlite3_stmt *proof = NULL;
|
||||
ok = ok && sqlite3_prepare_v2(store->db,
|
||||
"INSERT INTO research_receipts "
|
||||
"SELECT ?1,?2,c.campaign_id,c.grant_id,?4,?5,?6 "
|
||||
"FROM research_campaigns c JOIN scope_grant_actions ga "
|
||||
"ON ga.grant_id=c.grant_id AND ga.action_id=?4 "
|
||||
"WHERE c.campaign_id=?3 AND c.investigation_id=?7 AND c.grant_id=?8;",
|
||||
-1, &proof, NULL) == SQLITE_OK &&
|
||||
bind_text(proof, 1, receipt->receipt_id) &&
|
||||
bind_text(proof, 2, receipt->result_id) &&
|
||||
bind_text(proof, 3, receipt->campaign_id) &&
|
||||
bind_text(proof, 4, receipt->action_id) &&
|
||||
bind_text(proof, 5, receipt->decided_at) &&
|
||||
bind_text(proof, 6, receipt->decision_code) &&
|
||||
bind_text(proof, 7, store->investigation_id) &&
|
||||
bind_text(proof, 8, receipt->grant_id) &&
|
||||
sqlite3_step(proof) == SQLITE_DONE && sqlite3_changes(store->db) == 1;
|
||||
sqlite3_finalize(proof);
|
||||
if (ok)
|
||||
ok = exec_sql(store->db, "COMMIT;", error);
|
||||
if (!ok) {
|
||||
exec_sql(store->db, "ROLLBACK;", NULL);
|
||||
job_error(error, G_IO_ERROR_FAILED,
|
||||
"Publication atomique du résultat et du reçu impossible.");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
gboolean research_store_policy_decide(LocalJobStore *store,
|
||||
const char *grant_id,
|
||||
const ResearchPolicyRequest *request,
|
||||
ResearchPolicyDecision *out_decision,
|
||||
GError **error) {
|
||||
if (store == NULL || !g_uuid_string_is_valid(grant_id) || request == NULL ||
|
||||
!research_action_guard(request->action) || out_decision == NULL ||
|
||||
!research_timestamp(request->now)) {
|
||||
job_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Requête de policy de recherche invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
sqlite3_stmt *query = NULL;
|
||||
gboolean ok = sqlite3_prepare_v2(store->db,
|
||||
"SELECT g.investigation_id,g.plan_fingerprint,g.expires_at,g.revoked_at,"
|
||||
"g.max_requests,g.max_response_bytes,g.max_active_ms,"
|
||||
"EXISTS(SELECT 1 FROM scope_grant_exclusions e WHERE e.grant_id=g.grant_id AND e.subject=?2) "
|
||||
"FROM scope_grants g WHERE g.grant_id=?1;", -1, &query, NULL) ==
|
||||
SQLITE_OK && bind_text(query, 1, grant_id) &&
|
||||
bind_text(query, 2, request->action->subject) &&
|
||||
sqlite3_step(query) == SQLITE_ROW;
|
||||
if (!ok) {
|
||||
sqlite3_finalize(query);
|
||||
job_error(error, G_IO_ERROR_NOT_FOUND, "ScopeGrant de policy introuvable.");
|
||||
return FALSE;
|
||||
}
|
||||
const char *owner = (const char *)sqlite3_column_text(query, 0);
|
||||
const char *plan = (const char *)sqlite3_column_text(query, 1);
|
||||
const char *expires = (const char *)sqlite3_column_text(query, 2);
|
||||
GDateTime *now_value = g_date_time_new_from_iso8601(request->now, NULL);
|
||||
GDateTime *expires_value = g_date_time_new_from_iso8601(expires, NULL);
|
||||
if (now_value == NULL || expires_value == NULL) {
|
||||
if (now_value != NULL)
|
||||
g_date_time_unref(now_value);
|
||||
if (expires_value != NULL)
|
||||
g_date_time_unref(expires_value);
|
||||
sqlite3_finalize(query);
|
||||
job_error(error, G_IO_ERROR_INVALID_DATA,
|
||||
"Horodatage persistant du ScopeGrant invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
gboolean expired = g_date_time_compare(now_value, expires_value) >= 0;
|
||||
g_date_time_unref(now_value);
|
||||
g_date_time_unref(expires_value);
|
||||
gboolean revoked = sqlite3_column_type(query, 3) != SQLITE_NULL;
|
||||
guint64 max_requests = (guint64)sqlite3_column_int64(query, 4);
|
||||
guint64 max_bytes = (guint64)sqlite3_column_int64(query, 5);
|
||||
guint64 max_ms = (guint64)sqlite3_column_int64(query, 6);
|
||||
gboolean excluded = sqlite3_column_int(query, 7) != 0;
|
||||
ResearchPolicyDecision decision = RESEARCH_POLICY_ALLOW;
|
||||
if (excluded)
|
||||
decision = RESEARCH_POLICY_DENY_EXCLUDED;
|
||||
else if (revoked)
|
||||
decision = RESEARCH_POLICY_DENY_REVOKED;
|
||||
else if (expired)
|
||||
decision = RESEARCH_POLICY_DENY_EXPIRED;
|
||||
else if (g_strcmp0(owner, store->investigation_id) != 0 ||
|
||||
g_strcmp0(owner, request->investigation_id) != 0)
|
||||
decision = RESEARCH_POLICY_DENY_INVESTIGATION;
|
||||
else if (g_strcmp0(plan, request->plan_fingerprint) != 0)
|
||||
decision = RESEARCH_POLICY_DENY_PLAN;
|
||||
else if (request->requested_requests > max_requests ||
|
||||
request->requested_response_bytes > max_bytes ||
|
||||
request->requested_active_ms > max_ms)
|
||||
decision = RESEARCH_POLICY_DENY_BUDGET;
|
||||
sqlite3_finalize(query);
|
||||
if (decision == RESEARCH_POLICY_ALLOW) {
|
||||
const ResearchAction *action = request->action;
|
||||
sqlite3_stmt *selected = NULL;
|
||||
ok = sqlite3_prepare_v2(store->db,
|
||||
"SELECT a.capability_id,a.contact_class,a.disclosure FROM scope_grant_actions ga "
|
||||
"JOIN research_actions a ON a.action_id=ga.action_id WHERE ga.grant_id=?1 "
|
||||
"AND ga.action_id=?2 AND ga.subject=?3 AND ga.provider_id=?4 AND ga.endpoint=?5 "
|
||||
"AND EXISTS(SELECT 1 FROM research_action_decisions d "
|
||||
"WHERE d.action_id=a.action_id AND d.decision_code='AUTHORIZE') "
|
||||
"AND (a.rank!=1 OR EXISTS(SELECT 1 FROM research_actions first "
|
||||
"JOIN research_action_decisions fd ON fd.action_id=first.action_id "
|
||||
"JOIN research_results rr ON rr.action_id=first.action_id "
|
||||
"JOIN research_receipts rc ON rc.result_id=rr.result_id "
|
||||
"WHERE first.plan_id=a.plan_id AND first.rank=0 "
|
||||
"AND fd.decision_code='AUTHORIZE'));",
|
||||
-1, &selected, NULL) == SQLITE_OK && bind_text(selected, 1, grant_id) &&
|
||||
bind_text(selected, 2, action->action_id) &&
|
||||
bind_text(selected, 3, action->subject) &&
|
||||
bind_text(selected, 4, action->provider_id) &&
|
||||
bind_text(selected, 5, action->endpoint);
|
||||
int step = ok ? sqlite3_step(selected) : SQLITE_ERROR;
|
||||
if (step == SQLITE_DONE)
|
||||
decision = RESEARCH_POLICY_DENY_ACTION;
|
||||
else if (step != SQLITE_ROW)
|
||||
ok = FALSE;
|
||||
else if (g_strcmp0((const char *)sqlite3_column_text(selected, 0),
|
||||
action->capability_id) != 0 ||
|
||||
g_strcmp0((const char *)sqlite3_column_text(selected, 1),
|
||||
research_contact_code(action->contact)) != 0 ||
|
||||
g_strcmp0((const char *)sqlite3_column_text(selected, 2),
|
||||
action->disclosure) != 0)
|
||||
decision = RESEARCH_POLICY_DENY_ACTION_CONTENT;
|
||||
sqlite3_finalize(selected);
|
||||
}
|
||||
if (!ok) {
|
||||
job_error(error, G_IO_ERROR_FAILED, "Évaluation de policy impossible.");
|
||||
return FALSE;
|
||||
}
|
||||
*out_decision = decision;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean local_job_store_export_atomic(LocalJobStore *s, const char *path,
|
||||
GError **error) {
|
||||
GPtrArray *jobs = local_job_store_list(s, error);
|
||||
|
|
|
|||
26
src/core/research_adapter_cdx.c
Normal file
26
src/core/research_adapter_cdx.c
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#include "core/research_adapter_cdx.h"
|
||||
#include <string.h>
|
||||
static gboolean timestamp(const char *s){if(!s||strlen(s)!=14)return FALSE;for(guint i=0;i<14;i++)if(!g_ascii_isdigit(s[i]))return FALSE;return TRUE;}
|
||||
static const char *host_of(const char *url,GUri **owned){*owned=g_uri_parse(url,G_URI_FLAGS_NONE,NULL);return *owned?g_uri_get_host(*owned):NULL;}
|
||||
gboolean research_adapter_cdx_normalize(const ResearchAction *a,ResearchCdxMatch match,const ResearchCdxFixture *f,ResearchCdxResult *out,GError **error){
|
||||
if (!a || !a->subject || !f || !out || match > RESEARCH_CDX_EXACT_HOST ||
|
||||
(f->row_count > 0 && f->rows == NULL)) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Réponse CDX invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
ResearchCdxResult r={.captures=g_ptr_array_new_with_free_func(g_free)};
|
||||
GUri *subject_uri=NULL;const char *subject_host=match==RESEARCH_CDX_EXACT_HOST?host_of(a->subject,&subject_uri):NULL;
|
||||
if(match==RESEARCH_CDX_EXACT_HOST&&!subject_host)subject_host=a->subject;
|
||||
for(gsize i=0;i<f->row_count;i++){const ResearchCdxRow *row=&f->rows[i];GUri *u=NULL;const char *h=host_of(row->original_url,&u);
|
||||
gboolean exact=match==RESEARCH_CDX_EXACT_URL?g_strcmp0(row->original_url,a->subject)==0:(h&&g_ascii_strcasecmp(h,subject_host)==0);
|
||||
if(!exact||!timestamp(row->timestamp)||!row->digest){if(u)g_uri_unref(u);if(subject_uri)g_uri_unref(subject_uri);research_cdx_result_clear(&r);g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_INVALID_DATA,"Capture CDX hors sujet ou date invalide.");return FALSE;}
|
||||
g_ptr_array_add(r.captures,g_strdup_printf("%s\t%s\t%u\t%s",row->timestamp,row->original_url,row->status_code,row->digest));if(u)g_uri_unref(u);}
|
||||
if (subject_uri)
|
||||
g_uri_unref(subject_uri);
|
||||
r.next_cursor = g_strdup(f->next_cursor);
|
||||
r.next_piste = r.captures->len > 0;
|
||||
*out = r;
|
||||
return TRUE;
|
||||
}
|
||||
void research_cdx_result_clear(ResearchCdxResult *r){if(!r)return;g_clear_pointer(&r->captures,g_ptr_array_unref);g_free(r->next_cursor);*r=(ResearchCdxResult){0};}
|
||||
48
src/core/research_adapter_dns.c
Normal file
48
src/core/research_adapter_dns.c
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#include "core/research_adapter_dns.h"
|
||||
#include <string.h>
|
||||
static gboolean dns_name(const char *s) {
|
||||
if (!s || !*s || strlen(s) > 253 || strchr(s, '\\') || strchr(s, '@')) return FALSE;
|
||||
for (const unsigned char *p=(const unsigned char *)s; *p; p++)
|
||||
if (*p < 0x21 || *p == 0x7f) return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
static char *record_string(const ResearchDnsRecord *r) {
|
||||
return g_strdup_printf("%s\t%u\t%u\t%s", r->owner, r->ttl,
|
||||
r->preference, r->value);
|
||||
}
|
||||
gboolean research_adapter_dns_normalize(const ResearchAction *action,
|
||||
ResearchDnsType requested_type, const ResearchDnsFixture *fixture,
|
||||
ResearchDnsResult *out, GError **error) {
|
||||
if (!action || !dns_name(action->subject) || !fixture || !out ||
|
||||
(fixture->record_count > 0 && fixture->records == NULL) ||
|
||||
requested_type > RESEARCH_DNS_PTR || fixture->status > RESEARCH_DNS_STATUS_TIMEOUT) {
|
||||
g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_INVALID_ARGUMENT,"Réponse DNS typée invalide."); return FALSE;
|
||||
}
|
||||
ResearchDnsResult result={.status=fixture->status,
|
||||
.records=g_ptr_array_new_with_free_func(g_free)};
|
||||
GHashTable *edges=g_hash_table_new_full(g_str_hash,g_str_equal,g_free,g_free);
|
||||
for (gsize i=0;i<fixture->record_count;i++) {
|
||||
const ResearchDnsRecord *r=&fixture->records[i];
|
||||
if (r->type != requested_type || !dns_name(r->owner) || !r->value || !*r->value) {
|
||||
g_hash_table_unref(edges); research_dns_result_clear(&result);
|
||||
g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_INVALID_DATA,"Enregistrement DNS incohérent."); return FALSE;
|
||||
}
|
||||
g_ptr_array_add(result.records,record_string(r));
|
||||
if (r->type == RESEARCH_DNS_CNAME) g_hash_table_replace(edges,g_ascii_strdown(r->owner,-1),g_ascii_strdown(r->value,-1));
|
||||
}
|
||||
if (requested_type == RESEARCH_DNS_CNAME) {
|
||||
GHashTableIter iter; gpointer start;
|
||||
g_hash_table_iter_init(&iter,edges);
|
||||
while (g_hash_table_iter_next(&iter,&start,NULL)) {
|
||||
GHashTable *seen=g_hash_table_new(g_str_hash,g_str_equal); const char *cur=start;
|
||||
while (cur && g_hash_table_add(seen,(gpointer)cur)) cur=g_hash_table_lookup(edges,cur);
|
||||
if (cur) result.cname_cycle=TRUE;
|
||||
g_hash_table_unref(seen);
|
||||
}
|
||||
}
|
||||
g_hash_table_unref(edges);
|
||||
/* INVARIANT: une cible CNAME est une piste, jamais une propagation de scope. */
|
||||
result.next_piste=fixture->status==RESEARCH_DNS_STATUS_OK && result.records->len>0 && !result.cname_cycle;
|
||||
*out=result; return TRUE;
|
||||
}
|
||||
void research_dns_result_clear(ResearchDnsResult *r) { if (!r) return; g_clear_pointer(&r->records,g_ptr_array_unref); *r=(ResearchDnsResult){0}; }
|
||||
19
src/core/research_adapter_page.c
Normal file
19
src/core/research_adapter_page.c
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include "core/research_adapter_page.h"
|
||||
#include <string.h>
|
||||
static gboolean append_bounded(GString *s,char c,gsize max){if(s->len>=max)return FALSE;g_string_append_c(s,c);return TRUE;}
|
||||
gboolean research_adapter_page_normalize(const ResearchAction *a,const ResearchPageFixture *f,gsize max,ResearchPageResult *out,GError **error){
|
||||
if(!a||!a->subject||!f||!f->content_type||!f->body||!max||f->body_size>a->max_response_bytes||!g_utf8_validate((const char*)f->body,f->body_size,NULL)){
|
||||
g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_INVALID_ARGUMENT,"Page absente, trop grande ou non UTF-8.");return FALSE;}
|
||||
gboolean html=g_ascii_strncasecmp(f->content_type,"text/html",9)==0,text=g_ascii_strncasecmp(f->content_type,"text/plain",10)==0;
|
||||
if(!html&&!text){g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_NOT_SUPPORTED,"Type de page non inerte refusé.");return FALSE;}
|
||||
GString *s=g_string_new(NULL);gboolean tag=FALSE,space=FALSE;
|
||||
for(gsize i=0;i<f->body_size;i++){char c=(char)f->body[i];if(html&&c=='<'){tag=TRUE;space=TRUE;continue;}if(html&&tag){if(c=='>')tag=FALSE;continue;}if(g_ascii_isspace(c)){if(!space&&s->len&&!append_bounded(s,' ',max))goto large;space=TRUE;}else{if(!append_bounded(s,c,max))goto large;space=FALSE;}}
|
||||
g_strstrip(s->str);
|
||||
s->len = strlen(s->str);
|
||||
gboolean has_text = s->len > 0;
|
||||
char *inert_text = g_string_free(s, FALSE);
|
||||
*out = (ResearchPageResult){inert_text, html, TRUE, has_text};
|
||||
return TRUE;
|
||||
large:g_string_free(s,TRUE);g_set_error_literal(error,G_IO_ERROR,G_IO_ERROR_NO_SPACE,"Texte de page hors limite.");return FALSE;
|
||||
}
|
||||
void research_page_result_clear(ResearchPageResult *r){if(!r)return;g_free(r->inert_text);*r=(ResearchPageResult){0};}
|
||||
43
src/core/research_adapter_rdap.c
Normal file
43
src/core/research_adapter_rdap.c
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include "core/research_adapter_rdap.h"
|
||||
#include <string.h>
|
||||
static gboolean domain_suffix(const char *subject,const char *suffix) {
|
||||
gsize a=strlen(subject),b=strlen(suffix); if (!b || b>a) return FALSE;
|
||||
return g_ascii_strcasecmp(subject+a-b,suffix)==0 && (a==b || subject[a-b-1]=='.');
|
||||
}
|
||||
static gboolean ip_prefix(const char *subject,const char *prefix,guint *bits) {
|
||||
char **p=g_strsplit(prefix,"/",2); GInetAddress *a=g_inet_address_new_from_string(subject),*n=g_inet_address_new_from_string(p[0]);
|
||||
char *end=NULL; guint64 count=p[1]?g_ascii_strtoull(p[1],&end,10):999;
|
||||
gboolean ok=a&&n&&g_inet_address_get_family(a)==g_inet_address_get_family(n);
|
||||
guint max=ok&&g_inet_address_get_family(a)==G_SOCKET_FAMILY_IPV4?32:128;
|
||||
ok=ok&&p[1]&&end&&*end=='\0'&&count<=max;
|
||||
if(ok){const guint8 *x=g_inet_address_to_bytes(a),*y=g_inet_address_to_bytes(n); guint full=count/8,rem=count%8; ok=memcmp(x,y,full)==0 && (!rem || (x[full]>>(8-rem))==(y[full]>>(8-rem)));}
|
||||
if (ok)
|
||||
*bits = (guint)count;
|
||||
g_clear_object(&a);
|
||||
g_clear_object(&n);
|
||||
g_strfreev(p);
|
||||
return ok;
|
||||
}
|
||||
gboolean research_adapter_rdap_select(const ResearchAction *action,
|
||||
ResearchRdapServiceKind kind,const ResearchRdapBootstrap *b,ResearchRdapResult *out,GError **error){
|
||||
if (!action || !action->subject || !b || !out || !b->fetched_at ||
|
||||
!b->expires_at || (b->entry_count > 0 && b->entries == NULL)) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Bootstrap RDAP invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
const ResearchRdapBootstrapEntry *best=NULL;guint score=0;
|
||||
for(gsize i=0;i<b->entry_count;i++){const ResearchRdapBootstrapEntry *e=&b->entries[i];guint s=0;gboolean match=FALSE;
|
||||
if(e->kind!=kind||!e->prefix||!e->base_url)continue;
|
||||
if(kind==RESEARCH_RDAP_DOMAIN_SERVICE){match=domain_suffix(action->subject,e->prefix);s=(guint)strlen(e->prefix)*8;}else match=ip_prefix(action->subject,e->prefix,&s);
|
||||
if(match&&(!best||s>score)){best=e;score=s;}}
|
||||
if (!best) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
||||
"Service RDAP bootstrap introuvable.");
|
||||
return FALSE;
|
||||
}
|
||||
*out = (ResearchRdapResult){g_strdup(best->base_url), score,
|
||||
g_strdup(b->fetched_at), g_strdup(b->expires_at), g_strdup(b->etag), TRUE};
|
||||
return TRUE;
|
||||
}
|
||||
void research_rdap_result_clear(ResearchRdapResult *r){if(!r)return;g_free(r->service_url);g_free(r->cache_fetched_at);g_free(r->cache_expires_at);g_free(r->cache_etag);*r=(ResearchRdapResult){0};}
|
||||
34
src/core/research_adapter_search.c
Normal file
34
src/core/research_adapter_search.c
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#include "core/research_adapter_search.h"
|
||||
ResearchSearchAvailability research_adapter_search_availability(const ResearchSearchConfig *c){
|
||||
if(!c||!c->endpoint||!*c->endpoint||(c->provider==RESEARCH_SEARCH_BRAVE&&(!c->secret_reference||!*c->secret_reference)))return RESEARCH_SEARCH_UNAVAILABLE_CONFIG;
|
||||
if(!c->storage_authorized)return RESEARCH_SEARCH_UNAVAILABLE_STORAGE_AUTHORIZATION;
|
||||
return RESEARCH_SEARCH_AVAILABLE;
|
||||
}
|
||||
gboolean research_adapter_search_normalize(const ResearchAction *a,const ResearchSearchConfig *c,const ResearchSearchHit *hits,gsize n,ResearchSearchResult *out,GError **error){
|
||||
if (!a || !a->subject || !out || (n > 0 && hits == NULL)) {
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Recherche invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
ResearchSearchAvailability available=research_adapter_search_availability(c);
|
||||
ResearchSearchResult r={.availability=available,.hits=g_ptr_array_new_with_free_func(g_free)};
|
||||
if(available!=RESEARCH_SEARCH_AVAILABLE){*out=r;return TRUE;}
|
||||
for(gsize i=0;i<n;i++){
|
||||
GUri *uri=hits&&hits[i].url?g_uri_parse(hits[i].url,G_URI_FLAGS_NONE,NULL):NULL;
|
||||
const char *scheme = uri != NULL ? g_uri_get_scheme(uri) : NULL;
|
||||
if (!uri || (g_strcmp0(scheme, "http") != 0 &&
|
||||
g_strcmp0(scheme, "https") != 0) ||
|
||||
!hits[i].title || !hits[i].snippet) {
|
||||
if (uri)
|
||||
g_uri_unref(uri);
|
||||
research_search_result_clear(&r);
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
||||
"Résultat de recherche invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
/* INVARIANT: l'URL reste une piste textuelle, jamais une nouvelle portée. */
|
||||
g_ptr_array_add(r.hits,g_strdup_printf("%s\t%s\t%s",hits[i].title,hits[i].url,hits[i].snippet));g_uri_unref(uri);
|
||||
}
|
||||
r.next_piste=r.hits->len>0;*out=r;return TRUE;
|
||||
}
|
||||
void research_search_result_clear(ResearchSearchResult *r){if(!r)return;g_clear_pointer(&r->hits,g_ptr_array_unref);*r=(ResearchSearchResult){0};}
|
||||
189
src/core/research_contracts.c
Normal file
189
src/core/research_contracts.c
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/******************************************************************************
|
||||
* @file research_contracts.c
|
||||
* @brief Validation des contrats de recherche assistée V1.
|
||||
******************************************************************************/
|
||||
#include "core/research_contracts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static void contract_error(GError **error, const char *message) {
|
||||
if (error != NULL && *error == NULL)
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, message);
|
||||
}
|
||||
|
||||
static gboolean nonempty(const char *value) {
|
||||
return value != NULL && value[0] != '\0';
|
||||
}
|
||||
|
||||
static gboolean fingerprint_valid(const char *value) {
|
||||
if (value == NULL || strlen(value) != 64)
|
||||
return FALSE;
|
||||
for (gsize i = 0; i < 64; i++)
|
||||
if (!g_ascii_isxdigit(value[i]))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean timestamp_valid(const char *value) {
|
||||
GDateTime *parsed = value != NULL
|
||||
? g_date_time_new_from_iso8601(value, NULL)
|
||||
: NULL;
|
||||
if (parsed == NULL)
|
||||
return FALSE;
|
||||
g_date_time_unref(parsed);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean relative_artifact_valid(const char *value) {
|
||||
if (!nonempty(value) || g_path_is_absolute(value) || strchr(value, '\\'))
|
||||
return FALSE;
|
||||
char **parts = g_strsplit(value, "/", -1);
|
||||
gboolean valid = TRUE;
|
||||
for (gsize i = 0; valid && parts[i] != NULL; i++)
|
||||
valid = parts[i][0] != '\0' && g_strcmp0(parts[i], ".") != 0 &&
|
||||
g_strcmp0(parts[i], "..") != 0;
|
||||
g_strfreev(parts);
|
||||
return valid;
|
||||
}
|
||||
|
||||
const char *research_seed_kind_code(ResearchSeedKind kind) {
|
||||
static const char *const codes[] = {"DOMAIN", "IP", "HTTP_URL", "EMAIL",
|
||||
"SEARCH_TERM"};
|
||||
return kind <= RESEARCH_SEED_SEARCH_TERM ? codes[kind] : NULL;
|
||||
}
|
||||
|
||||
const char *research_contact_class_code(ResearchContactClass contact) {
|
||||
static const char *const codes[] = {"NONE", "THIRD_PARTY", "TARGET"};
|
||||
return contact <= RESEARCH_CONTACT_TARGET ? codes[contact] : NULL;
|
||||
}
|
||||
|
||||
const char *research_policy_decision_code(ResearchPolicyDecision decision) {
|
||||
static const char *const codes[] = {
|
||||
"ALLOW", "DENY_EXCLUDED", "DENY_REVOKED",
|
||||
"DENY_EXPIRED", "DENY_INVESTIGATION", "DENY_PLAN",
|
||||
"DENY_ACTION", "DENY_ACTION_CONTENT", "DENY_BUDGET"};
|
||||
return decision <= RESEARCH_POLICY_DENY_BUDGET ? codes[decision]
|
||||
: "DENY_ACTION";
|
||||
}
|
||||
|
||||
const char *research_action_decision_code(ResearchActionDecisionCode decision) {
|
||||
static const char *const codes[] = {"AUTHORIZE", "DEFER", "REFUSE"};
|
||||
return decision <= RESEARCH_ACTION_REFUSE ? codes[decision] : NULL;
|
||||
}
|
||||
|
||||
static gboolean action_valid(const ResearchAction *action) {
|
||||
return action != NULL && g_uuid_string_is_valid(action->action_id) &&
|
||||
nonempty(action->capability_id) && nonempty(action->provider_id) &&
|
||||
nonempty(action->endpoint) && nonempty(action->subject) &&
|
||||
research_contact_class_code(action->contact) != NULL &&
|
||||
nonempty(action->disclosure) && action->max_requests > 0 &&
|
||||
action->max_response_bytes > 0 &&
|
||||
action->max_response_bytes <= G_MAXINT64 && action->max_active_ms > 0;
|
||||
}
|
||||
|
||||
gboolean research_plan_validate(const ResearchPlan *plan, GError **error) {
|
||||
if (plan == NULL || g_strcmp0(plan->contract, RESEARCH_PLAN_CONTRACT) != 0 ||
|
||||
!g_uuid_string_is_valid(plan->plan_id) ||
|
||||
!nonempty(plan->idempotency_key) ||
|
||||
!fingerprint_valid(plan->content_fingerprint) ||
|
||||
!fingerprint_valid(plan->input_fingerprint) || plan->input_revision == 0 ||
|
||||
!timestamp_valid(plan->created_at) || plan->seeds == NULL ||
|
||||
plan->seed_count == 0 || plan->actions == NULL ||
|
||||
plan->action_count == 0) {
|
||||
contract_error(error, "Contrat ResearchPlan V1 invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
GHashTable *identities = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
gboolean valid = TRUE;
|
||||
for (gsize i = 0; valid && i < plan->seed_count; i++)
|
||||
valid = research_seed_kind_code(plan->seeds[i].kind) != NULL &&
|
||||
nonempty(plan->seeds[i].subject);
|
||||
for (gsize i = 0; valid && i < plan->action_count; i++) {
|
||||
const ResearchAction *action = &plan->actions[i];
|
||||
valid = action_valid(action) &&
|
||||
g_hash_table_add(identities, (gpointer)action->action_id);
|
||||
}
|
||||
g_hash_table_unref(identities);
|
||||
if (!valid)
|
||||
contract_error(error, "Seed ou action ResearchPlan invalide ou dupliquée.");
|
||||
return valid;
|
||||
}
|
||||
|
||||
gboolean research_scope_grant_validate(const ScopeGrant *grant,
|
||||
GError **error) {
|
||||
if (grant == NULL ||
|
||||
g_strcmp0(grant->contract, RESEARCH_GRANT_CONTRACT) != 0 ||
|
||||
!g_uuid_string_is_valid(grant->grant_id) ||
|
||||
!g_uuid_string_is_valid(grant->investigation_id) ||
|
||||
!nonempty(grant->idempotency_key) ||
|
||||
!fingerprint_valid(grant->content_fingerprint) ||
|
||||
!fingerprint_valid(grant->plan_fingerprint) ||
|
||||
!timestamp_valid(grant->created_at) ||
|
||||
!timestamp_valid(grant->expires_at) ||
|
||||
grant->selected_action_ids == NULL || grant->selected_action_count == 0 ||
|
||||
grant->max_requests == 0 || grant->max_response_bytes == 0 ||
|
||||
grant->max_response_bytes > G_MAXINT64 ||
|
||||
grant->max_active_ms == 0) {
|
||||
contract_error(error, "Contrat ScopeGrant V1 invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
GHashTable *identities = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
gboolean valid = TRUE;
|
||||
for (gsize i = 0; valid && i < grant->selected_action_count; i++)
|
||||
valid = g_uuid_string_is_valid(grant->selected_action_ids[i]) &&
|
||||
g_hash_table_add(identities,
|
||||
(gpointer)grant->selected_action_ids[i]);
|
||||
g_hash_table_remove_all(identities);
|
||||
for (gsize i = 0; valid && i < grant->excluded_subject_count; i++)
|
||||
valid = nonempty(grant->excluded_subjects[i]) &&
|
||||
g_hash_table_add(identities,
|
||||
(gpointer)grant->excluded_subjects[i]);
|
||||
g_hash_table_unref(identities);
|
||||
if (!valid)
|
||||
contract_error(error, "Sélection ou exclusion ScopeGrant invalide.");
|
||||
return valid;
|
||||
}
|
||||
|
||||
gboolean research_campaign_validate(const ResearchCampaign *campaign,
|
||||
GError **error) {
|
||||
gboolean valid = campaign != NULL &&
|
||||
g_strcmp0(campaign->contract, RESEARCH_CAMPAIGN_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(campaign->campaign_id) &&
|
||||
g_uuid_string_is_valid(campaign->investigation_id) &&
|
||||
g_uuid_string_is_valid(campaign->grant_id) &&
|
||||
nonempty(campaign->idempotency_key) &&
|
||||
fingerprint_valid(campaign->content_fingerprint) &&
|
||||
timestamp_valid(campaign->created_at);
|
||||
if (!valid)
|
||||
contract_error(error, "Contrat ResearchCampaign V1 invalide.");
|
||||
return valid;
|
||||
}
|
||||
|
||||
gboolean research_result_validate(const ResearchResult *result,
|
||||
GError **error) {
|
||||
gboolean valid = result != NULL &&
|
||||
g_strcmp0(result->contract, RESEARCH_RESULT_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(result->result_id) &&
|
||||
g_uuid_string_is_valid(result->campaign_id) &&
|
||||
g_uuid_string_is_valid(result->action_id) &&
|
||||
relative_artifact_valid(result->raw_artifact_relative_path) &&
|
||||
fingerprint_valid(result->content_sha256) && nonempty(result->status);
|
||||
if (!valid)
|
||||
contract_error(error, "Contrat ResearchResult V1 invalide.");
|
||||
return valid;
|
||||
}
|
||||
|
||||
gboolean research_receipt_validate(const ResearchReceipt *receipt,
|
||||
GError **error) {
|
||||
gboolean valid = receipt != NULL &&
|
||||
g_strcmp0(receipt->contract, RESEARCH_RECEIPT_CONTRACT) == 0 &&
|
||||
g_uuid_string_is_valid(receipt->receipt_id) &&
|
||||
g_uuid_string_is_valid(receipt->result_id) &&
|
||||
g_uuid_string_is_valid(receipt->campaign_id) &&
|
||||
g_uuid_string_is_valid(receipt->grant_id) &&
|
||||
g_uuid_string_is_valid(receipt->action_id) &&
|
||||
timestamp_valid(receipt->decided_at) && nonempty(receipt->decision_code);
|
||||
if (!valid)
|
||||
contract_error(error, "Contrat ResearchReceipt V1 invalide.");
|
||||
return valid;
|
||||
}
|
||||
249
src/core/research_http_transport_curl.c
Normal file
249
src/core/research_http_transport_curl.c
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#include "core/research_http_transport_curl.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <curl/curl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
const ResearchTransportRequest *request;
|
||||
ResearchTransportReply *reply;
|
||||
GByteArray *body;
|
||||
guint64 header_bytes;
|
||||
gboolean too_large;
|
||||
gboolean allow_loopback;
|
||||
} CurlContext;
|
||||
|
||||
static gboolean socket_address_public(const struct curl_sockaddr *address,
|
||||
gboolean allow_loopback) {
|
||||
char text[INET6_ADDRSTRLEN] = {0};
|
||||
const void *source = NULL;
|
||||
if (address->family == AF_INET)
|
||||
source = &((const struct sockaddr_in *)&address->addr)->sin_addr;
|
||||
else if (address->family == AF_INET6)
|
||||
source = &((const struct sockaddr_in6 *)&address->addr)->sin6_addr;
|
||||
if (source == NULL || inet_ntop(address->family, source, text, sizeof text) == NULL)
|
||||
return FALSE;
|
||||
GInetAddress *inet = g_inet_address_new_from_string(text);
|
||||
gboolean loopback = inet != NULL && g_inet_address_get_is_loopback(inet);
|
||||
g_clear_object(&inet);
|
||||
return research_policy_address_is_public(text) || (allow_loopback && loopback);
|
||||
}
|
||||
|
||||
static curl_socket_t open_socket(void *data, curlsocktype purpose,
|
||||
struct curl_sockaddr *address) {
|
||||
CurlContext *context = data;
|
||||
if (purpose != CURLSOCKTYPE_IPCXN ||
|
||||
!socket_address_public(address, context->allow_loopback))
|
||||
return CURL_SOCKET_BAD;
|
||||
return socket(address->family, address->socktype, address->protocol);
|
||||
}
|
||||
|
||||
static size_t write_body(char *data, size_t size, size_t count, void *user_data) {
|
||||
CurlContext *context = user_data;
|
||||
gsize length = size * count;
|
||||
if (length > context->request->max_decompressed_bytes - context->body->len) {
|
||||
context->too_large = TRUE;
|
||||
return 0;
|
||||
}
|
||||
g_byte_array_append(context->body, (const guint8 *)data, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
static void store_header(CurlContext *context, const char *line, gsize length) {
|
||||
const char *colon = memchr(line, ':', length);
|
||||
if (colon == NULL)
|
||||
return;
|
||||
gsize name_length = (gsize)(colon - line);
|
||||
const char *value = colon + 1;
|
||||
const char *end = line + length;
|
||||
while (value < end && g_ascii_isspace(*value))
|
||||
value++;
|
||||
while (end > value && g_ascii_isspace(end[-1]))
|
||||
end--;
|
||||
char *name = g_ascii_strdown(line, name_length);
|
||||
char *copy = g_strndup(value, (gsize)(end - value));
|
||||
g_hash_table_replace(context->reply->headers, name, copy);
|
||||
}
|
||||
|
||||
static size_t write_header(char *data, size_t size, size_t count,
|
||||
void *user_data) {
|
||||
CurlContext *context = user_data;
|
||||
gsize length = size * count;
|
||||
if (length > context->request->max_header_bytes - context->header_bytes) {
|
||||
context->too_large = TRUE;
|
||||
return 0;
|
||||
}
|
||||
context->header_bytes += length;
|
||||
store_header(context, data, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
static int progress_cancel(void *data, curl_off_t down_total,
|
||||
curl_off_t down_now, curl_off_t up_total,
|
||||
curl_off_t up_now) {
|
||||
(void)down_total;
|
||||
(void)up_total;
|
||||
(void)up_now;
|
||||
CurlContext *context = data;
|
||||
/* CONTRACT: dlnow est le compteur libcurl des octets téléchargés avant
|
||||
* décompression. Il rend le plafond compressé opposable même sans
|
||||
* Content-Length (notamment en transfert chunked). */
|
||||
if (down_now < 0 || (guint64)down_now >
|
||||
context->request->max_compressed_bytes) {
|
||||
context->too_large = TRUE;
|
||||
return 1;
|
||||
}
|
||||
return context->request->cancellable != NULL &&
|
||||
g_cancellable_is_cancelled(context->request->cancellable);
|
||||
}
|
||||
|
||||
static ResearchTransportState response_state(CURLcode code, long status,
|
||||
const CurlContext *context) {
|
||||
if (context->request->cancellable != NULL &&
|
||||
g_cancellable_is_cancelled(context->request->cancellable))
|
||||
return RESEARCH_TRANSPORT_CANCELLED;
|
||||
if (context->too_large)
|
||||
return RESEARCH_TRANSPORT_TOO_LARGE;
|
||||
if (code == CURLE_FILESIZE_EXCEEDED)
|
||||
return RESEARCH_TRANSPORT_TOO_LARGE;
|
||||
if (code == CURLE_OPERATION_TIMEDOUT)
|
||||
return RESEARCH_TRANSPORT_TIMEOUT;
|
||||
if (code != CURLE_OK)
|
||||
return RESEARCH_TRANSPORT_FAILED;
|
||||
if (status >= 200 && status < 300)
|
||||
return RESEARCH_TRANSPORT_OK;
|
||||
if (status >= 300 && status < 400)
|
||||
return RESEARCH_TRANSPORT_REDIRECT;
|
||||
if (status == 403)
|
||||
return RESEARCH_TRANSPORT_FORBIDDEN;
|
||||
if (status == 429)
|
||||
return RESEARCH_TRANSPORT_RATE_LIMITED;
|
||||
return RESEARCH_TRANSPORT_FAILED;
|
||||
}
|
||||
|
||||
static void parse_reply_headers(ResearchTransportReply *reply) {
|
||||
const char *retry = g_hash_table_lookup(reply->headers, "retry-after");
|
||||
if (retry != NULL) {
|
||||
char *end = NULL;
|
||||
guint64 seconds = g_ascii_strtoull(retry, &end, 10);
|
||||
if (end != retry && *end == '\0' && seconds <= G_MAXUINT) {
|
||||
reply->retry_after_present = TRUE;
|
||||
reply->retry_after_seconds = (guint)seconds;
|
||||
}
|
||||
}
|
||||
const char *location = g_hash_table_lookup(reply->headers, "location");
|
||||
if (reply->state == RESEARCH_TRANSPORT_REDIRECT && location != NULL)
|
||||
reply->redirect_location = g_strdup(location);
|
||||
}
|
||||
|
||||
static gboolean execute_internal(const ResearchTransportRequest *request,
|
||||
const char *resolve_entry,
|
||||
gboolean allow_loopback,
|
||||
ResearchTransportReply *out_reply,
|
||||
GError **error) {
|
||||
ResearchHttpTarget target = {0};
|
||||
if (!research_transport_request_validate(request, &target, error))
|
||||
return FALSE;
|
||||
CURL *curl = curl_easy_init();
|
||||
if (curl == NULL) {
|
||||
research_http_target_clear(&target);
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
||||
"Initialisation libcurl impossible.");
|
||||
return FALSE;
|
||||
}
|
||||
ResearchTransportReply reply = {0};
|
||||
reply.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
CurlContext context = {request, &reply, g_byte_array_new(), 0, FALSE,
|
||||
allow_loopback};
|
||||
struct curl_slist *headers = NULL;
|
||||
for (gsize i = 0; i < request->header_count; i++) {
|
||||
char *line = g_strdup_printf("%s: %s", request->headers[i].name,
|
||||
request->headers[i].value);
|
||||
headers = curl_slist_append(headers, line);
|
||||
g_free(line);
|
||||
}
|
||||
struct curl_slist *resolve = NULL;
|
||||
if (resolve_entry != NULL)
|
||||
resolve = curl_slist_append(resolve, resolve_entry);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request->action->endpoint);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_PROXY, "");
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROXY, "*");
|
||||
curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_IGNORED);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_NONE);
|
||||
curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, NULL);
|
||||
curl_easy_setopt(curl, CURLOPT_COOKIEJAR, NULL);
|
||||
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)request->action->max_active_ms);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE,
|
||||
(curl_off_t)request->max_compressed_bytes);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_cancel);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, open_socket);
|
||||
curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, &context);
|
||||
if (resolve != NULL)
|
||||
curl_easy_setopt(curl, CURLOPT_RESOLVE, resolve);
|
||||
CURLcode code = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
reply.status_code = status >= 0 && status <= G_MAXUINT ? (guint)status : 0;
|
||||
reply.state = response_state(code, status, &context);
|
||||
if (reply.state != RESEARCH_TRANSPORT_TOO_LARGE)
|
||||
reply.body = g_byte_array_free_to_bytes(context.body);
|
||||
else
|
||||
g_byte_array_unref(context.body);
|
||||
parse_reply_headers(&reply);
|
||||
curl_slist_free_all(headers);
|
||||
curl_slist_free_all(resolve);
|
||||
curl_easy_cleanup(curl);
|
||||
research_http_target_clear(&target);
|
||||
*out_reply = reply;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean research_http_transport_curl_execute(
|
||||
const ResearchTransportRequest *request, ResearchTransportReply *out_reply,
|
||||
GError **error) {
|
||||
return execute_internal(request, NULL, FALSE, out_reply, error);
|
||||
}
|
||||
|
||||
#ifdef RESEARCH_TRANSPORT_ENABLE_TEST_HOOKS
|
||||
gboolean research_http_transport_curl_execute_fixture(
|
||||
const ResearchTransportRequest *request, const char *authority,
|
||||
const char *loopback_address, ResearchTransportReply *out_reply,
|
||||
GError **error) {
|
||||
ResearchHttpTarget target = {0};
|
||||
if (authority == NULL || loopback_address == NULL ||
|
||||
!research_transport_request_validate(request, &target, error))
|
||||
return FALSE;
|
||||
GInetAddress *loopback = g_inet_address_new_from_string(loopback_address);
|
||||
gboolean valid = loopback != NULL && g_inet_address_get_is_loopback(loopback) &&
|
||||
g_ascii_strcasecmp(authority, target.host) == 0;
|
||||
g_clear_object(&loopback);
|
||||
if (!valid) {
|
||||
research_http_target_clear(&target);
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED,
|
||||
"Autorité loopback de fixture interdite.");
|
||||
return FALSE;
|
||||
}
|
||||
char *resolve = g_strdup_printf("%s:%u:%s", authority, target.port,
|
||||
loopback_address);
|
||||
research_http_target_clear(&target);
|
||||
gboolean ok = execute_internal(request, resolve, TRUE, out_reply, error);
|
||||
g_free(resolve);
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
226
src/core/research_policy.c
Normal file
226
src/core/research_policy.c
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/******************************************************************************
|
||||
* @file research_policy.c
|
||||
* @brief Validation sans I/O des destinations de recherche assistée.
|
||||
******************************************************************************/
|
||||
#include "core/research_policy.h"
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <string.h>
|
||||
|
||||
static void policy_error(GError **error, const char *message) {
|
||||
if (error != NULL && *error == NULL)
|
||||
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, message);
|
||||
}
|
||||
|
||||
static gboolean clean_text(const char *value) {
|
||||
if (value == NULL || value[0] == '\0' || strchr(value, '\\') != NULL)
|
||||
return FALSE;
|
||||
for (const unsigned char *p = (const unsigned char *)value; *p != '\0'; p++)
|
||||
if (*p < 0x20 || *p == 0x7f)
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean exact_member(const char *value, const char *const *values,
|
||||
gsize count) {
|
||||
if (value == NULL || values == NULL)
|
||||
return FALSE;
|
||||
for (gsize i = 0; i < count; i++)
|
||||
if (g_strcmp0(value, values[i]) == 0)
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean port_allowed(guint16 port, const ResearchNetworkProfile *profile) {
|
||||
if (profile == NULL || profile->allowed_ports == NULL)
|
||||
return FALSE;
|
||||
for (gsize i = 0; i < profile->allowed_port_count; i++)
|
||||
if (port == profile->allowed_ports[i])
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean parse_http_target(const char *text,
|
||||
const ResearchNetworkProfile *profile,
|
||||
gboolean require_endpoint,
|
||||
ResearchHttpTarget *out, GError **error) {
|
||||
if (!clean_text(text) || strchr(text, '@') != NULL || profile == NULL ||
|
||||
out == NULL) {
|
||||
policy_error(error, "Destination HTTP(S) absente ou ambiguë.");
|
||||
return FALSE;
|
||||
}
|
||||
GError *uri_error = NULL;
|
||||
GUri *uri = g_uri_parse(text, G_URI_FLAGS_PARSE_RELAXED, &uri_error);
|
||||
if (uri == NULL) {
|
||||
g_clear_error(&uri_error);
|
||||
policy_error(error, "Destination HTTP(S) invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
const char *scheme = g_uri_get_scheme(uri);
|
||||
const char *host = g_uri_get_host(uri);
|
||||
const char *userinfo = g_uri_get_userinfo(uri);
|
||||
gint parsed_port = g_uri_get_port(uri);
|
||||
guint16 port = parsed_port >= 0 ? (guint16)parsed_port
|
||||
: (g_strcmp0(scheme, "https") == 0 ? 443 : 80);
|
||||
gboolean valid = (g_strcmp0(scheme, "http") == 0 ||
|
||||
g_strcmp0(scheme, "https") == 0) &&
|
||||
clean_text(host) && userinfo == NULL && parsed_port <= 65535 &&
|
||||
port_allowed(port, profile) &&
|
||||
(!require_endpoint ||
|
||||
exact_member(text, profile->allowed_endpoints,
|
||||
profile->allowed_endpoint_count));
|
||||
if (valid) {
|
||||
GInetAddress *literal = g_inet_address_new_from_string(host);
|
||||
if (literal != NULL) {
|
||||
valid = research_policy_address_is_public(host);
|
||||
g_object_unref(literal);
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
g_uri_unref(uri);
|
||||
policy_error(error, "Destination hors profil réseau ou non publique.");
|
||||
return FALSE;
|
||||
}
|
||||
const char *path = g_uri_get_path(uri);
|
||||
const char *query = g_uri_get_query(uri);
|
||||
out->scheme = g_ascii_strdown(scheme, -1);
|
||||
out->host = g_ascii_strdown(host, -1);
|
||||
out->port = port;
|
||||
out->path_and_query = query != NULL
|
||||
? g_strdup_printf("%s?%s", path[0] ? path : "/", query)
|
||||
: g_strdup(path[0] ? path : "/");
|
||||
g_uri_unref(uri);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean subject_matches_host(const char *subject,
|
||||
const ResearchHttpTarget *target) {
|
||||
if (!clean_text(subject))
|
||||
return FALSE;
|
||||
if (g_str_has_prefix(subject, "http://") ||
|
||||
g_str_has_prefix(subject, "https://")) {
|
||||
GUri *uri = g_uri_parse(subject, G_URI_FLAGS_NONE, NULL);
|
||||
const char *host = uri != NULL ? g_uri_get_host(uri) : NULL;
|
||||
gboolean match = host != NULL && g_uri_get_userinfo(uri) == NULL &&
|
||||
g_ascii_strcasecmp(host, target->host) == 0;
|
||||
if (uri != NULL)
|
||||
g_uri_unref(uri);
|
||||
return match;
|
||||
}
|
||||
return strchr(subject, '/') == NULL && strchr(subject, '@') == NULL &&
|
||||
g_ascii_strcasecmp(subject, target->host) == 0;
|
||||
}
|
||||
|
||||
gboolean research_policy_validate_http_action(
|
||||
const ResearchAction *action, const ScopeGrant *grant,
|
||||
const ResearchNetworkProfile *profile, ResearchHttpTarget *out_target,
|
||||
GError **error) {
|
||||
if (action == NULL || grant == NULL || out_target == NULL ||
|
||||
!research_scope_grant_validate(grant, error))
|
||||
return FALSE;
|
||||
gboolean selected = FALSE;
|
||||
for (gsize i = 0; i < grant->selected_action_count; i++)
|
||||
selected |= g_strcmp0(action->action_id, grant->selected_action_ids[i]) == 0;
|
||||
for (gsize i = 0; i < grant->excluded_subject_count; i++)
|
||||
if (g_strcmp0(action->subject, grant->excluded_subjects[i]) == 0) {
|
||||
policy_error(error, "Sujet explicitement exclu du ScopeGrant.");
|
||||
return FALSE;
|
||||
}
|
||||
if (!selected || action->max_requests == 0 ||
|
||||
action->max_requests > grant->max_requests ||
|
||||
action->max_response_bytes == 0 ||
|
||||
action->max_response_bytes > grant->max_response_bytes ||
|
||||
action->max_active_ms == 0 ||
|
||||
action->max_active_ms > grant->max_active_ms) {
|
||||
policy_error(error, "Action absente du grant ou budget hors grant.");
|
||||
return FALSE;
|
||||
}
|
||||
ResearchHttpTarget parsed = {0};
|
||||
if (!parse_http_target(action->endpoint, profile, TRUE, &parsed, error))
|
||||
return FALSE;
|
||||
if (!subject_matches_host(action->subject, &parsed)) {
|
||||
research_http_target_clear(&parsed);
|
||||
policy_error(error, "Le sujet ne correspond pas exactement à la destination.");
|
||||
return FALSE;
|
||||
}
|
||||
*out_target = parsed;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean research_policy_address_is_public(const char *address) {
|
||||
GInetAddress *inet = g_inet_address_new_from_string(address);
|
||||
if (inet == NULL)
|
||||
return FALSE;
|
||||
const guint8 *bytes = g_inet_address_to_bytes(inet);
|
||||
gboolean public = TRUE;
|
||||
if (g_inet_address_get_family(inet) == G_SOCKET_FAMILY_IPV4) {
|
||||
/* CONTRACT: seules les adresses global-unicast sont admissibles. Les
|
||||
* préfixes de documentation et d'essai ne deviennent jamais des cibles
|
||||
* réseau parce qu'ils sont syntaxiquement valides. */
|
||||
public = !(bytes[0] == 0 || bytes[0] == 10 || bytes[0] == 127 ||
|
||||
(bytes[0] == 169 && bytes[1] == 254) ||
|
||||
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) ||
|
||||
(bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 0) ||
|
||||
(bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 2) ||
|
||||
(bytes[0] == 192 && bytes[1] == 88 && bytes[2] == 99) ||
|
||||
(bytes[0] == 192 && bytes[1] == 168) ||
|
||||
(bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127) ||
|
||||
(bytes[0] == 198 && (bytes[1] == 18 || bytes[1] == 19)) ||
|
||||
(bytes[0] == 198 && bytes[1] == 51 && bytes[2] == 100) ||
|
||||
(bytes[0] == 203 && bytes[1] == 0 && bytes[2] == 113) ||
|
||||
bytes[0] >= 224);
|
||||
} else {
|
||||
const struct in6_addr *ipv6 = (const struct in6_addr *)bytes;
|
||||
if (IN6_IS_ADDR_V4MAPPED(ipv6)) {
|
||||
char *mapped = g_strdup_printf("%u.%u.%u.%u", bytes[12], bytes[13],
|
||||
bytes[14], bytes[15]);
|
||||
public = research_policy_address_is_public(mapped);
|
||||
g_free(mapped);
|
||||
} else {
|
||||
public = !(IN6_IS_ADDR_UNSPECIFIED(ipv6) ||
|
||||
IN6_IS_ADDR_LOOPBACK(ipv6) ||
|
||||
(bytes[0] == 0x00 && bytes[1] == 0x64 &&
|
||||
bytes[2] == 0xff && bytes[3] == 0x9b &&
|
||||
bytes[4] == 0x00 && bytes[5] == 0x01) ||
|
||||
(bytes[0] == 0x01 && bytes[1] == 0x00 &&
|
||||
bytes[2] == 0x00 && bytes[3] == 0x00 &&
|
||||
bytes[4] == 0x00 && bytes[5] == 0x00 &&
|
||||
bytes[6] == 0x00 && bytes[7] == 0x00) ||
|
||||
(bytes[0] == 0x20 && bytes[1] == 0x01 &&
|
||||
bytes[2] == 0x00 && bytes[3] == 0x02 &&
|
||||
bytes[4] == 0x00 && bytes[5] == 0x00) ||
|
||||
(bytes[0] == 0x20 && bytes[1] == 0x01 &&
|
||||
bytes[2] == 0x0d && bytes[3] == 0xb8) ||
|
||||
(bytes[0] == 0x20 && bytes[1] == 0x01 &&
|
||||
bytes[2] == 0x00 &&
|
||||
((bytes[3] & 0xf0) == 0x10 ||
|
||||
(bytes[3] & 0xf0) == 0x20)) ||
|
||||
(bytes[0] == 0x20 && bytes[1] == 0x02) ||
|
||||
(bytes[0] == 0x3f && bytes[1] == 0xff &&
|
||||
(bytes[2] & 0xf0) == 0x00) ||
|
||||
(bytes[0] == 0x5f && bytes[1] == 0x00) ||
|
||||
(bytes[0] == 0xfc || bytes[0] == 0xfd) ||
|
||||
(bytes[0] == 0xfe && (bytes[1] & 0x80) == 0x80) ||
|
||||
bytes[0] == 0xff);
|
||||
}
|
||||
}
|
||||
g_object_unref(inet);
|
||||
return public;
|
||||
}
|
||||
|
||||
ResearchRedirectDecision research_policy_check_redirect(
|
||||
const char *location, const ResearchNetworkProfile *profile,
|
||||
ResearchHttpTarget *out_target, GError **error) {
|
||||
if (!parse_http_target(location, profile, FALSE, out_target, error))
|
||||
return RESEARCH_REDIRECT_INVALID;
|
||||
return RESEARCH_REDIRECT_REQUIRES_DECISION;
|
||||
}
|
||||
|
||||
void research_http_target_clear(ResearchHttpTarget *target) {
|
||||
if (target == NULL)
|
||||
return;
|
||||
g_clear_pointer(&target->scheme, g_free);
|
||||
g_clear_pointer(&target->host, g_free);
|
||||
g_clear_pointer(&target->path_and_query, g_free);
|
||||
target->port = 0;
|
||||
}
|
||||
211
src/core/research_transport.c
Normal file
211
src/core/research_transport.c
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/******************************************************************************
|
||||
* @file research_transport.c
|
||||
* @brief Validation et classification d'un transport de recherche borné.
|
||||
******************************************************************************/
|
||||
#include "core/research_transport.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void transport_error(GError **error, GIOErrorEnum code,
|
||||
const char *message) {
|
||||
if (error != NULL && *error == NULL)
|
||||
g_set_error_literal(error, G_IO_ERROR, code, message);
|
||||
}
|
||||
|
||||
static gboolean header_text_valid(const char *value) {
|
||||
if (value == NULL)
|
||||
return FALSE;
|
||||
for (const unsigned char *p = (const unsigned char *)value; *p != '\0'; p++)
|
||||
if (*p < 0x20 || *p == 0x7f)
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean request_header_allowed(const ResearchHeader *header) {
|
||||
static const char *const allowed[] = {
|
||||
"accept", "accept-language", "user-agent", "if-none-match",
|
||||
"if-modified-since"};
|
||||
if (header == NULL || !header_text_valid(header->name) ||
|
||||
!header_text_valid(header->value))
|
||||
return FALSE;
|
||||
for (gsize i = 0; i < G_N_ELEMENTS(allowed); i++)
|
||||
if (g_ascii_strcasecmp(header->name, allowed[i]) == 0)
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean research_transport_request_validate(
|
||||
const ResearchTransportRequest *request, ResearchHttpTarget *out_target,
|
||||
GError **error) {
|
||||
if (request == NULL || request->action == NULL || out_target == NULL ||
|
||||
g_strcmp0(request->method, "GET") != 0 ||
|
||||
request->max_compressed_bytes == 0 ||
|
||||
request->max_decompressed_bytes == 0 ||
|
||||
request->max_header_bytes == 0 ||
|
||||
(request->header_count > 0 && request->headers == NULL) ||
|
||||
request->max_compressed_bytes > request->action->max_response_bytes ||
|
||||
request->max_decompressed_bytes > request->action->max_response_bytes) {
|
||||
transport_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"Requête de transport non bornée ou invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
guint64 header_bytes = 0;
|
||||
for (gsize i = 0; i < request->header_count; i++) {
|
||||
if (!request_header_allowed(&request->headers[i])) {
|
||||
transport_error(error, G_IO_ERROR_INVALID_ARGUMENT,
|
||||
"En-tête de requête interdit.");
|
||||
return FALSE;
|
||||
}
|
||||
header_bytes += strlen(request->headers[i].name) +
|
||||
strlen(request->headers[i].value) + 4;
|
||||
}
|
||||
if (header_bytes > request->max_header_bytes) {
|
||||
transport_error(error, G_IO_ERROR_NO_SPACE, "En-têtes trop volumineux.");
|
||||
return FALSE;
|
||||
}
|
||||
return research_policy_validate_http_action(
|
||||
request->action, request->grant, request->profile, out_target, error);
|
||||
}
|
||||
|
||||
gboolean research_transport_execute(const ResearchTransportRequest *request,
|
||||
ResearchTransportReply *out_reply,
|
||||
GError **error) {
|
||||
ResearchHttpTarget target = {0};
|
||||
if (out_reply == NULL ||
|
||||
!research_transport_request_validate(request, &target, error))
|
||||
return FALSE;
|
||||
research_http_target_clear(&target);
|
||||
*out_reply = (ResearchTransportReply){
|
||||
.state = RESEARCH_TRANSPORT_UNAVAILABLE,
|
||||
.diagnostic = g_strdup("Transport réseau non configuré.")};
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#ifdef RESEARCH_TRANSPORT_ENABLE_TEST_HOOKS
|
||||
static guint retry_after_parse(const char *value, gboolean *present) {
|
||||
if (value == NULL) {
|
||||
*present = FALSE;
|
||||
return 0;
|
||||
}
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
guint64 seconds = g_ascii_strtoull(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' || seconds > G_MAXUINT) {
|
||||
*present = FALSE;
|
||||
return 0;
|
||||
}
|
||||
*present = TRUE;
|
||||
return (guint)seconds;
|
||||
}
|
||||
|
||||
static ResearchTransportState status_state(guint status) {
|
||||
if (status >= 200 && status < 300)
|
||||
return RESEARCH_TRANSPORT_OK;
|
||||
if (status >= 300 && status < 400)
|
||||
return RESEARCH_TRANSPORT_REDIRECT;
|
||||
if (status == 403)
|
||||
return RESEARCH_TRANSPORT_FORBIDDEN;
|
||||
if (status == 429)
|
||||
return RESEARCH_TRANSPORT_RATE_LIMITED;
|
||||
return RESEARCH_TRANSPORT_FAILED;
|
||||
}
|
||||
|
||||
static gboolean fixture_loopback_allowed(const char *host,
|
||||
const char *const *authorities,
|
||||
gsize count) {
|
||||
for (gsize i = 0; i < count; i++)
|
||||
if (g_ascii_strcasecmp(host, authorities[i]) == 0)
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean research_transport_execute_fixture(
|
||||
const ResearchTransportRequest *request,
|
||||
const ResearchTransportFixture *fixture,
|
||||
const char *const *loopback_authorities, gsize authority_count,
|
||||
ResearchTransportReply *out_reply, GError **error) {
|
||||
ResearchHttpTarget target = {0};
|
||||
if (fixture == NULL || out_reply == NULL || fixture->peer_address == NULL ||
|
||||
(fixture->header_count > 0 && fixture->headers == NULL) ||
|
||||
(fixture->body_size > 0 && fixture->body == NULL) ||
|
||||
!research_transport_request_validate(request, &target, error))
|
||||
return FALSE;
|
||||
GInetAddress *peer = g_inet_address_new_from_string(fixture->peer_address);
|
||||
gboolean peer_ok = peer != NULL &&
|
||||
(research_policy_address_is_public(fixture->peer_address) ||
|
||||
(g_inet_address_get_is_loopback(peer) &&
|
||||
fixture_loopback_allowed(target.host, loopback_authorities,
|
||||
authority_count)));
|
||||
g_clear_object(&peer);
|
||||
if (!peer_ok) {
|
||||
research_http_target_clear(&target);
|
||||
transport_error(error, G_IO_ERROR_PERMISSION_DENIED,
|
||||
"Adresse de connexion interdite avant contact.");
|
||||
return FALSE;
|
||||
}
|
||||
ResearchTransportReply reply = {0};
|
||||
reply.status_code = fixture->status_code;
|
||||
reply.state = fixture->unavailable ? RESEARCH_TRANSPORT_UNAVAILABLE
|
||||
: fixture->timed_out ? RESEARCH_TRANSPORT_TIMEOUT
|
||||
: status_state(fixture->status_code);
|
||||
if (request->cancellable != NULL &&
|
||||
g_cancellable_is_cancelled(request->cancellable))
|
||||
reply.state = RESEARCH_TRANSPORT_CANCELLED;
|
||||
guint64 header_bytes = 0;
|
||||
reply.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||
for (gsize i = 0; i < fixture->header_count; i++) {
|
||||
const ResearchHeader *header = &fixture->headers[i];
|
||||
if (!header_text_valid(header->name) || !header_text_valid(header->value)) {
|
||||
reply.state = RESEARCH_TRANSPORT_INVALID_RESPONSE;
|
||||
break;
|
||||
}
|
||||
header_bytes += strlen(header->name) + strlen(header->value) + 4;
|
||||
char *name = g_ascii_strdown(header->name, -1);
|
||||
g_hash_table_replace(reply.headers, name, g_strdup(header->value));
|
||||
}
|
||||
if (header_bytes > request->max_header_bytes ||
|
||||
fixture->body_size > request->max_compressed_bytes ||
|
||||
fixture->decompressed_size > request->max_decompressed_bytes)
|
||||
reply.state = RESEARCH_TRANSPORT_TOO_LARGE;
|
||||
const char *retry = g_hash_table_lookup(reply.headers, "retry-after");
|
||||
reply.retry_after_seconds = retry_after_parse(retry, &reply.retry_after_present);
|
||||
reply.body_is_compressed = fixture->compressed;
|
||||
if (reply.state != RESEARCH_TRANSPORT_TOO_LARGE)
|
||||
reply.body = g_bytes_new(fixture->body, fixture->body_size);
|
||||
if (reply.state == RESEARCH_TRANSPORT_REDIRECT) {
|
||||
ResearchHttpTarget redirected = {0};
|
||||
if (fixture->redirect_location == NULL ||
|
||||
research_policy_check_redirect(fixture->redirect_location,
|
||||
request->profile, &redirected,
|
||||
NULL) !=
|
||||
RESEARCH_REDIRECT_REQUIRES_DECISION)
|
||||
reply.state = RESEARCH_TRANSPORT_INVALID_RESPONSE;
|
||||
else
|
||||
reply.redirect_location = g_strdup(fixture->redirect_location);
|
||||
research_http_target_clear(&redirected);
|
||||
}
|
||||
research_http_target_clear(&target);
|
||||
*out_reply = reply;
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
void research_transport_reply_clear(ResearchTransportReply *reply) {
|
||||
if (reply == NULL)
|
||||
return;
|
||||
g_clear_pointer(&reply->body, g_bytes_unref);
|
||||
g_clear_pointer(&reply->headers, g_hash_table_unref);
|
||||
g_clear_pointer(&reply->redirect_location, g_free);
|
||||
g_clear_pointer(&reply->diagnostic, g_free);
|
||||
*reply = (ResearchTransportReply){0};
|
||||
}
|
||||
|
||||
const char *research_transport_state_code(ResearchTransportState state) {
|
||||
static const char *const codes[] = {
|
||||
"OK", "UNAVAILABLE", "CANCELLED", "TIMEOUT", "RATE_LIMITED",
|
||||
"FORBIDDEN", "REDIRECT_REQUIRES_DECISION", "TOO_LARGE",
|
||||
"INVALID_RESPONSE", "FAILED"};
|
||||
return state <= RESEARCH_TRANSPORT_FAILED ? codes[state] : "FAILED";
|
||||
}
|
||||
27
src/main.c
27
src/main.c
|
|
@ -1,27 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file main.c
|
||||
* @brief Point d'entrée de Labfy Investigation.
|
||||
******************************************************************************/
|
||||
|
||||
#include "core/application.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
Application *application = NULL;
|
||||
int exit_status = EXIT_FAILURE;
|
||||
|
||||
application = application_new();
|
||||
|
||||
if (application == NULL)
|
||||
{
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
exit_status = application_run(application, argc, argv);
|
||||
|
||||
application_free(application);
|
||||
|
||||
return exit_status;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,864 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_investigation_dialog.c
|
||||
* @brief Dialogue GTK de création d'une nouvelle enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/create_investigation_dialog.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
|
||||
/**
|
||||
* @brief Largeur initiale du dialogue.
|
||||
*/
|
||||
#define CREATE_INVESTIGATION_DIALOG_WIDTH 620
|
||||
|
||||
/**
|
||||
* @brief Marge extérieure du dialogue.
|
||||
*/
|
||||
#define CREATE_INVESTIGATION_DIALOG_MARGIN 16
|
||||
|
||||
/**
|
||||
* @brief Espacement entre les composants principaux.
|
||||
*/
|
||||
#define CREATE_INVESTIGATION_DIALOG_SPACING 12
|
||||
|
||||
/**
|
||||
* @brief Contexte conservé pendant la durée de vie du dialogue.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
|
||||
GtkWidget *parent_directory_entry;
|
||||
GtkWidget *investigation_name_entry;
|
||||
GtkWidget *create_button;
|
||||
|
||||
char *parent_directory;
|
||||
|
||||
CreateInvestigationDialogCallback callback;
|
||||
gpointer user_data;
|
||||
|
||||
gboolean completed;
|
||||
} CreateInvestigationDialogContext;
|
||||
|
||||
/**
|
||||
* @brief Libère le contexte associé au dialogue.
|
||||
*
|
||||
* Les widgets GTK sont possédés par la fenêtre et ne sont donc pas
|
||||
* libérés directement ici.
|
||||
*
|
||||
* @param context Contexte à libérer.
|
||||
*/
|
||||
static void create_investigation_dialog_context_free(
|
||||
CreateInvestigationDialogContext *context
|
||||
)
|
||||
{
|
||||
if (context == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_free(context->parent_directory);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Valide et normalise un nom d'enquête.
|
||||
*
|
||||
* La fonction :
|
||||
*
|
||||
* - vérifie que la chaîne est un UTF-8 valide ;
|
||||
* - refuse les noms constitués uniquement d'espaces ;
|
||||
* - reconnaît les espaces Unicode ;
|
||||
* - refuse les séparateurs '/' et '\' ;
|
||||
* - retire les espaces placés au début et à la fin.
|
||||
*
|
||||
* Si normalized_name n'est pas NULL, la fonction y place une nouvelle
|
||||
* chaîne allouée que l'appelant devra libérer avec g_free().
|
||||
*
|
||||
* @param investigation_name Nom à vérifier.
|
||||
* @param normalized_name Adresse recevant le nom normalisé, ou NULL.
|
||||
*
|
||||
* @return TRUE si le nom est valide, sinon FALSE.
|
||||
*/
|
||||
static gboolean create_investigation_dialog_normalize_name(
|
||||
const char *investigation_name,
|
||||
char **normalized_name
|
||||
)
|
||||
{
|
||||
const char *cursor = NULL;
|
||||
const char *first_character = NULL;
|
||||
const char *end_after_last_character = NULL;
|
||||
|
||||
if (normalized_name != NULL)
|
||||
{
|
||||
*normalized_name = NULL;
|
||||
}
|
||||
|
||||
if (investigation_name == NULL ||
|
||||
!g_utf8_validate(
|
||||
investigation_name,
|
||||
-1,
|
||||
NULL
|
||||
))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
cursor = investigation_name;
|
||||
|
||||
while (*cursor != '\0')
|
||||
{
|
||||
gunichar character = 0;
|
||||
const char *next_character = NULL;
|
||||
|
||||
character = g_utf8_get_char(
|
||||
cursor
|
||||
);
|
||||
|
||||
next_character = g_utf8_next_char(
|
||||
cursor
|
||||
);
|
||||
|
||||
if (character == '/' ||
|
||||
character == '\\')
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!g_unichar_isspace(character))
|
||||
{
|
||||
if (first_character == NULL)
|
||||
{
|
||||
first_character = cursor;
|
||||
}
|
||||
|
||||
end_after_last_character = next_character;
|
||||
}
|
||||
|
||||
cursor = next_character;
|
||||
}
|
||||
|
||||
/*
|
||||
* Aucun caractère visible n'a été trouvé.
|
||||
* Le nom était vide ou uniquement composé d'espaces.
|
||||
*/
|
||||
if (first_character == NULL ||
|
||||
end_after_last_character == NULL)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (normalized_name != NULL)
|
||||
{
|
||||
*normalized_name = g_strndup(
|
||||
first_character,
|
||||
(gsize) (
|
||||
end_after_last_character -
|
||||
first_character
|
||||
)
|
||||
);
|
||||
|
||||
if (*normalized_name == NULL)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Met à jour l'état du bouton de création.
|
||||
*
|
||||
* Le bouton est actif uniquement lorsqu'un dossier parent valide a été
|
||||
* sélectionné et que le nom saisi est acceptable.
|
||||
*
|
||||
* @param context Contexte du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_update_create_button(
|
||||
CreateInvestigationDialogContext *context
|
||||
)
|
||||
{
|
||||
const char *investigation_name = NULL;
|
||||
|
||||
gboolean has_valid_parent_directory = FALSE;
|
||||
gboolean has_valid_name = FALSE;
|
||||
|
||||
if (context == NULL ||
|
||||
context->create_button == NULL ||
|
||||
context->investigation_name_entry == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
has_valid_parent_directory =
|
||||
context->parent_directory != NULL &&
|
||||
context->parent_directory[0] != '\0' &&
|
||||
g_file_test(
|
||||
context->parent_directory,
|
||||
G_FILE_TEST_IS_DIR
|
||||
);
|
||||
|
||||
investigation_name = gtk_editable_get_text(
|
||||
GTK_EDITABLE(
|
||||
context->investigation_name_entry
|
||||
)
|
||||
);
|
||||
|
||||
has_valid_name =
|
||||
create_investigation_dialog_normalize_name(
|
||||
investigation_name,
|
||||
NULL
|
||||
);
|
||||
|
||||
gtk_widget_set_sensitive(
|
||||
context->create_button,
|
||||
has_valid_parent_directory && has_valid_name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Termine le dialogue et appelle son callback une seule fois.
|
||||
*
|
||||
* @param context Contexte du dialogue.
|
||||
* @param parent_directory Dossier parent ou NULL en cas d'annulation.
|
||||
* @param investigation_name Nom nettoyé ou NULL en cas d'annulation.
|
||||
*/
|
||||
static void create_investigation_dialog_complete(
|
||||
CreateInvestigationDialogContext *context,
|
||||
const char *parent_directory,
|
||||
const char *investigation_name
|
||||
)
|
||||
{
|
||||
if (context == NULL ||
|
||||
context->completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context->completed = TRUE;
|
||||
|
||||
if (context->callback != NULL)
|
||||
{
|
||||
context->callback(
|
||||
parent_directory,
|
||||
investigation_name,
|
||||
context->user_data
|
||||
);
|
||||
}
|
||||
|
||||
if (context->window != NULL)
|
||||
{
|
||||
gtk_window_destroy(
|
||||
context->window
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Traite les modifications du nom de l'enquête.
|
||||
*
|
||||
* @param editable Champ ayant été modifié.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_on_name_changed(
|
||||
GtkEditable *editable,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = user_data;
|
||||
|
||||
(void) editable;
|
||||
|
||||
create_investigation_dialog_update_create_button(
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Traite le résultat du sélecteur de dossier parent.
|
||||
*
|
||||
* La référence passée dans user_data maintient la fenêtre en vie pendant
|
||||
* toute l'opération asynchrone.
|
||||
*
|
||||
* @param source_object GtkFileDialog ayant lancé l'opération.
|
||||
* @param result Résultat asynchrone.
|
||||
* @param user_data Référence vers la fenêtre du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_on_parent_selected(
|
||||
GObject *source_object,
|
||||
GAsyncResult *result,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *file_dialog = NULL;
|
||||
GtkWindow *dialog_window = NULL;
|
||||
|
||||
CreateInvestigationDialogContext *context = NULL;
|
||||
|
||||
GFile *selected_folder = NULL;
|
||||
GError *error = NULL;
|
||||
|
||||
char *selected_path = NULL;
|
||||
|
||||
file_dialog = GTK_FILE_DIALOG(
|
||||
source_object
|
||||
);
|
||||
|
||||
dialog_window = GTK_WINDOW(
|
||||
user_data
|
||||
);
|
||||
|
||||
context = g_object_get_data(
|
||||
G_OBJECT(dialog_window),
|
||||
"create-investigation-dialog-context"
|
||||
);
|
||||
|
||||
selected_folder = gtk_file_dialog_select_folder_finish(
|
||||
file_dialog,
|
||||
result,
|
||||
&error
|
||||
);
|
||||
|
||||
/*
|
||||
* Le dialogue de création a pu être fermé pendant que le sélecteur
|
||||
* asynchrone était encore actif.
|
||||
*/
|
||||
if (context == NULL ||
|
||||
context->completed)
|
||||
{
|
||||
g_clear_error(&error);
|
||||
|
||||
if (selected_folder != NULL)
|
||||
{
|
||||
g_object_unref(selected_folder);
|
||||
}
|
||||
|
||||
g_object_unref(dialog_window);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_folder == NULL)
|
||||
{
|
||||
if (error != NULL &&
|
||||
!g_error_matches(
|
||||
error,
|
||||
GTK_DIALOG_ERROR,
|
||||
GTK_DIALOG_ERROR_DISMISSED
|
||||
))
|
||||
{
|
||||
g_warning(
|
||||
"Impossible de sélectionner le dossier parent : %s",
|
||||
error->message
|
||||
);
|
||||
}
|
||||
|
||||
g_clear_error(&error);
|
||||
g_object_unref(dialog_window);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
selected_path = g_file_get_path(
|
||||
selected_folder
|
||||
);
|
||||
|
||||
if (selected_path == NULL)
|
||||
{
|
||||
g_warning(
|
||||
"Le dossier sélectionné ne possède pas de chemin local."
|
||||
);
|
||||
|
||||
g_object_unref(selected_folder);
|
||||
g_object_unref(dialog_window);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_file_test(
|
||||
selected_path,
|
||||
G_FILE_TEST_IS_DIR
|
||||
))
|
||||
{
|
||||
g_warning(
|
||||
"Le chemin sélectionné n'est pas un dossier existant : '%s'.",
|
||||
selected_path
|
||||
);
|
||||
|
||||
g_free(selected_path);
|
||||
g_object_unref(selected_folder);
|
||||
g_object_unref(dialog_window);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
g_free(context->parent_directory);
|
||||
|
||||
context->parent_directory = selected_path;
|
||||
selected_path = NULL;
|
||||
|
||||
gtk_editable_set_text(
|
||||
GTK_EDITABLE(
|
||||
context->parent_directory_entry
|
||||
),
|
||||
context->parent_directory
|
||||
);
|
||||
|
||||
create_investigation_dialog_update_create_button(
|
||||
context
|
||||
);
|
||||
|
||||
g_object_unref(selected_folder);
|
||||
g_object_unref(dialog_window);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Ouvre le sélecteur du dossier parent.
|
||||
*
|
||||
* @param button Bouton ayant reçu le clic.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_on_browse_clicked(
|
||||
GtkButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = user_data;
|
||||
GtkFileDialog *file_dialog = NULL;
|
||||
|
||||
(void) button;
|
||||
|
||||
if (context == NULL ||
|
||||
context->completed ||
|
||||
context->window == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
file_dialog = gtk_file_dialog_new();
|
||||
|
||||
gtk_file_dialog_set_title(
|
||||
file_dialog,
|
||||
"Sélectionner le dossier parent"
|
||||
);
|
||||
|
||||
gtk_file_dialog_set_modal(
|
||||
file_dialog,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_file_dialog_select_folder(
|
||||
file_dialog,
|
||||
context->window,
|
||||
NULL,
|
||||
create_investigation_dialog_on_parent_selected,
|
||||
g_object_ref(context->window)
|
||||
);
|
||||
|
||||
g_object_unref(file_dialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Annule la création.
|
||||
*
|
||||
* @param button Bouton ayant reçu le clic.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_on_cancel_clicked(
|
||||
GtkButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = user_data;
|
||||
|
||||
(void) button;
|
||||
|
||||
create_investigation_dialog_complete(
|
||||
context,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Valide les informations et termine le dialogue.
|
||||
*
|
||||
* @param button Bouton ayant reçu le clic.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*/
|
||||
static void create_investigation_dialog_on_create_clicked(
|
||||
GtkButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = user_data;
|
||||
|
||||
const char *entry_text = NULL;
|
||||
char *trimmed_name = NULL;
|
||||
|
||||
(void) button;
|
||||
|
||||
if (context == NULL ||
|
||||
context->completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context->parent_directory == NULL ||
|
||||
!g_file_test(
|
||||
context->parent_directory,
|
||||
G_FILE_TEST_IS_DIR
|
||||
))
|
||||
{
|
||||
create_investigation_dialog_update_create_button(
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
entry_text = gtk_editable_get_text(
|
||||
GTK_EDITABLE(
|
||||
context->investigation_name_entry
|
||||
)
|
||||
);
|
||||
|
||||
if (!create_investigation_dialog_normalize_name(
|
||||
entry_text,
|
||||
&trimmed_name
|
||||
))
|
||||
{
|
||||
create_investigation_dialog_update_create_button(
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
create_investigation_dialog_complete(
|
||||
context,
|
||||
context->parent_directory,
|
||||
trimmed_name
|
||||
);
|
||||
|
||||
g_free(trimmed_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Intercepte la fermeture de la fenêtre.
|
||||
*
|
||||
* Une fermeture avec le bouton système est traitée comme une annulation.
|
||||
*
|
||||
* @param window Fenêtre demandant sa fermeture.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*
|
||||
* @return TRUE car la destruction est gérée par le module.
|
||||
*/
|
||||
static gboolean create_investigation_dialog_on_close_request(
|
||||
GtkWindow *window,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = user_data;
|
||||
|
||||
(void) window;
|
||||
|
||||
create_investigation_dialog_complete(
|
||||
context,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void create_investigation_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
CreateInvestigationDialogCallback callback,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
CreateInvestigationDialogContext *context = NULL;
|
||||
|
||||
GtkWidget *main_box = NULL;
|
||||
|
||||
GtkWidget *parent_label = NULL;
|
||||
GtkWidget *parent_row = NULL;
|
||||
GtkWidget *browse_button = NULL;
|
||||
|
||||
GtkWidget *name_label = NULL;
|
||||
|
||||
GtkWidget *action_box = NULL;
|
||||
GtkWidget *cancel_button = NULL;
|
||||
|
||||
context = g_new0(
|
||||
CreateInvestigationDialogContext,
|
||||
1
|
||||
);
|
||||
|
||||
context->callback = callback;
|
||||
context->user_data = user_data;
|
||||
|
||||
context->window = GTK_WINDOW(
|
||||
gtk_window_new()
|
||||
);
|
||||
|
||||
gtk_window_set_title(
|
||||
context->window,
|
||||
"Nouvelle enquête"
|
||||
);
|
||||
|
||||
gtk_window_set_default_size(
|
||||
context->window,
|
||||
CREATE_INVESTIGATION_DIALOG_WIDTH,
|
||||
-1
|
||||
);
|
||||
|
||||
gtk_window_set_modal(
|
||||
context->window,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_window_set_resizable(
|
||||
context->window,
|
||||
FALSE
|
||||
);
|
||||
|
||||
if (parent_window != NULL)
|
||||
{
|
||||
gtk_window_set_transient_for(
|
||||
context->window,
|
||||
parent_window
|
||||
);
|
||||
|
||||
gtk_window_set_destroy_with_parent(
|
||||
context->window,
|
||||
TRUE
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Le contexte est attaché à la fenêtre.
|
||||
*
|
||||
* Il sera libéré lorsque la dernière référence vers la fenêtre
|
||||
* disparaîtra, y compris si un sélecteur asynchrone est encore actif.
|
||||
*/
|
||||
g_object_set_data_full(
|
||||
G_OBJECT(context->window),
|
||||
"create-investigation-dialog-context",
|
||||
context,
|
||||
(GDestroyNotify)
|
||||
create_investigation_dialog_context_free
|
||||
);
|
||||
|
||||
main_box = gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL,
|
||||
CREATE_INVESTIGATION_DIALOG_SPACING
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_start(
|
||||
main_box,
|
||||
CREATE_INVESTIGATION_DIALOG_MARGIN
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_end(
|
||||
main_box,
|
||||
CREATE_INVESTIGATION_DIALOG_MARGIN
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_top(
|
||||
main_box,
|
||||
CREATE_INVESTIGATION_DIALOG_MARGIN
|
||||
);
|
||||
|
||||
gtk_widget_set_margin_bottom(
|
||||
main_box,
|
||||
CREATE_INVESTIGATION_DIALOG_MARGIN
|
||||
);
|
||||
|
||||
parent_label = gtk_label_new(
|
||||
"Dossier parent :"
|
||||
);
|
||||
|
||||
gtk_widget_set_halign(
|
||||
parent_label,
|
||||
GTK_ALIGN_START
|
||||
);
|
||||
|
||||
parent_row = gtk_box_new(
|
||||
GTK_ORIENTATION_HORIZONTAL,
|
||||
8
|
||||
);
|
||||
|
||||
context->parent_directory_entry = gtk_entry_new();
|
||||
|
||||
gtk_editable_set_editable(
|
||||
GTK_EDITABLE(
|
||||
context->parent_directory_entry
|
||||
),
|
||||
FALSE
|
||||
);
|
||||
|
||||
gtk_widget_set_hexpand(
|
||||
context->parent_directory_entry,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_entry_set_placeholder_text(
|
||||
GTK_ENTRY(
|
||||
context->parent_directory_entry
|
||||
),
|
||||
"Aucun dossier sélectionné"
|
||||
);
|
||||
|
||||
browse_button = gtk_button_new_with_label(
|
||||
"Parcourir"
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(parent_row),
|
||||
context->parent_directory_entry
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(parent_row),
|
||||
browse_button
|
||||
);
|
||||
|
||||
name_label = gtk_label_new(
|
||||
"Nom de l'enquête :"
|
||||
);
|
||||
|
||||
gtk_widget_set_halign(
|
||||
name_label,
|
||||
GTK_ALIGN_START
|
||||
);
|
||||
|
||||
context->investigation_name_entry = gtk_entry_new();
|
||||
|
||||
gtk_entry_set_placeholder_text(
|
||||
GTK_ENTRY(
|
||||
context->investigation_name_entry
|
||||
),
|
||||
"Exemple : Arnaque_Billets"
|
||||
);
|
||||
|
||||
action_box = gtk_box_new(
|
||||
GTK_ORIENTATION_HORIZONTAL,
|
||||
8
|
||||
);
|
||||
|
||||
gtk_widget_set_halign(
|
||||
action_box,
|
||||
GTK_ALIGN_END
|
||||
);
|
||||
|
||||
cancel_button = gtk_button_new_with_label(
|
||||
"Annuler"
|
||||
);
|
||||
|
||||
context->create_button = gtk_button_new_with_label(
|
||||
"Créer"
|
||||
);
|
||||
|
||||
gtk_widget_add_css_class(
|
||||
context->create_button,
|
||||
"suggested-action"
|
||||
);
|
||||
|
||||
gtk_widget_set_sensitive(
|
||||
context->create_button,
|
||||
FALSE
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(action_box),
|
||||
cancel_button
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(action_box),
|
||||
context->create_button
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_box),
|
||||
parent_label
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_box),
|
||||
parent_row
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_box),
|
||||
name_label
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_box),
|
||||
context->investigation_name_entry
|
||||
);
|
||||
|
||||
gtk_box_append(
|
||||
GTK_BOX(main_box),
|
||||
action_box
|
||||
);
|
||||
|
||||
gtk_window_set_child(
|
||||
context->window,
|
||||
main_box
|
||||
);
|
||||
|
||||
g_signal_connect(
|
||||
context->window,
|
||||
"close-request",
|
||||
G_CALLBACK(
|
||||
create_investigation_dialog_on_close_request
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
g_signal_connect(
|
||||
browse_button,
|
||||
"clicked",
|
||||
G_CALLBACK(
|
||||
create_investigation_dialog_on_browse_clicked
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
g_signal_connect(
|
||||
context->investigation_name_entry,
|
||||
"changed",
|
||||
G_CALLBACK(
|
||||
create_investigation_dialog_on_name_changed
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
g_signal_connect(
|
||||
cancel_button,
|
||||
"clicked",
|
||||
G_CALLBACK(
|
||||
create_investigation_dialog_on_cancel_clicked
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
g_signal_connect(
|
||||
context->create_button,
|
||||
"clicked",
|
||||
G_CALLBACK(
|
||||
create_investigation_dialog_on_create_clicked
|
||||
),
|
||||
context
|
||||
);
|
||||
|
||||
gtk_window_present(
|
||||
context->window
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,294 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file create_social_account_dialog.c
|
||||
* @brief Dialogue GTK de création d'un compte social observé.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/create_social_account_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
struct CreateSocialAccountDialogResult
|
||||
{
|
||||
char *platform;
|
||||
char *profile_url;
|
||||
char *username;
|
||||
char *platform_identifier;
|
||||
char *first_observed_at;
|
||||
char *account_state;
|
||||
char *notes;
|
||||
char *evidence_identifier;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
GtkDropDown *platform;
|
||||
GtkEntry *url;
|
||||
GtkEntry *username;
|
||||
GtkEntry *platform_identifier;
|
||||
GtkEntry *observed_at;
|
||||
GtkDropDown *state;
|
||||
GtkTextView *notes;
|
||||
GtkDropDown *evidence;
|
||||
GtkLabel *error;
|
||||
GPtrArray *evidence_identifiers;
|
||||
CreateSocialAccountDialogCallback callback;
|
||||
gpointer user_data;
|
||||
gboolean completed;
|
||||
} CreateSocialAccountDialogState;
|
||||
|
||||
static const char *const platform_codes[] = {
|
||||
"tiktok", "instagram", "facebook", "x", "telegram", "other"
|
||||
};
|
||||
static const char *const state_codes[] = {
|
||||
"unknown", "active", "private", "suspended", "deleted"
|
||||
};
|
||||
|
||||
/** @brief Retourne une copie nettoyée, ou NULL pour un texte vide. */
|
||||
static char *create_social_account_dialog_copy(const char *text)
|
||||
{
|
||||
char *copy = text != NULL ? g_strdup(text) : NULL;
|
||||
if (copy == NULL) return NULL;
|
||||
g_strstrip(copy);
|
||||
if (copy[0] == '\0') { g_free(copy); return NULL; }
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** @brief Extrait le texte du champ de notes. */
|
||||
static char *create_social_account_dialog_notes(CreateSocialAccountDialogState *state)
|
||||
{
|
||||
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
|
||||
GtkTextIter start;
|
||||
GtkTextIter end;
|
||||
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||
}
|
||||
|
||||
/** @brief Libère l'état privé attaché à la fenêtre. */
|
||||
static void create_social_account_dialog_state_free(gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
if (state == NULL) return;
|
||||
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
||||
g_free(state);
|
||||
}
|
||||
|
||||
/** @brief Signale une annulation au callback une seule fois. */
|
||||
static void create_social_account_dialog_cancel(CreateSocialAccountDialogState *state)
|
||||
{
|
||||
if (state == NULL || state->completed) return;
|
||||
state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(NULL, state->user_data);
|
||||
}
|
||||
|
||||
/** @brief Traite la fermeture native du dialogue. */
|
||||
static gboolean create_social_account_dialog_on_close(GtkWindow *window, gpointer user_data)
|
||||
{
|
||||
(void) window;
|
||||
create_social_account_dialog_cancel(user_data);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/** @brief Traite le bouton d'annulation. */
|
||||
static void create_social_account_dialog_on_cancel(GtkButton *button, gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
(void) button;
|
||||
create_social_account_dialog_cancel(state);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
|
||||
/** @brief Valide le formulaire et transmet un résultat possédé. */
|
||||
static void create_social_account_dialog_on_create(GtkButton *button, gpointer user_data)
|
||||
{
|
||||
CreateSocialAccountDialogState *state = user_data;
|
||||
CreateSocialAccountDialogResult *result = NULL;
|
||||
GDateTime *date = NULL;
|
||||
char *notes = NULL;
|
||||
guint platform = gtk_drop_down_get_selected(state->platform);
|
||||
guint account_state = gtk_drop_down_get_selected(state->state);
|
||||
guint evidence = gtk_drop_down_get_selected(state->evidence);
|
||||
const char *url = gtk_editable_get_text(GTK_EDITABLE(state->url));
|
||||
const char *username = gtk_editable_get_text(GTK_EDITABLE(state->username));
|
||||
const char *observed_at = gtk_editable_get_text(GTK_EDITABLE(state->observed_at));
|
||||
(void) button;
|
||||
if (platform >= G_N_ELEMENTS(platform_codes) ||
|
||||
account_state >= G_N_ELEMENTS(state_codes))
|
||||
{
|
||||
gtk_label_set_text(state->error, "Sélectionnez une plateforme et un état.");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
if (url == NULL ||
|
||||
(!g_str_has_prefix(url, "https://") && !g_str_has_prefix(url, "http://")))
|
||||
{
|
||||
gtk_label_set_text(state->error,
|
||||
"L'URL du profil doit commencer par https:// ou http://.");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
if (username == NULL || username[0] == '\0')
|
||||
{
|
||||
gtk_label_set_text(state->error,
|
||||
"Le pseudonyme affiché est obligatoire (par exemple @compte).");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
date = g_date_time_new_from_iso8601(observed_at, NULL);
|
||||
if (date == NULL)
|
||||
{
|
||||
gtk_label_set_text(state->error,
|
||||
"La date doit respecter le format UTC AAAA-MM-JJTHH:MM:SSZ.");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
notes = create_social_account_dialog_notes(state);
|
||||
result = g_new0(CreateSocialAccountDialogResult, 1);
|
||||
result->platform = g_strdup(platform_codes[platform]);
|
||||
result->profile_url = create_social_account_dialog_copy(url);
|
||||
result->username = create_social_account_dialog_copy(username);
|
||||
result->platform_identifier = create_social_account_dialog_copy(
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->platform_identifier)));
|
||||
result->first_observed_at = create_social_account_dialog_copy(observed_at);
|
||||
result->account_state = g_strdup(state_codes[account_state]);
|
||||
result->notes = create_social_account_dialog_copy(notes);
|
||||
if (evidence > 0 && evidence - 1 < state->evidence_identifiers->len)
|
||||
result->evidence_identifier = g_strdup(g_ptr_array_index(
|
||||
state->evidence_identifiers, evidence - 1));
|
||||
state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(result, state->user_data);
|
||||
else create_social_account_dialog_result_free(result);
|
||||
g_free(notes);
|
||||
g_date_time_unref(date);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
|
||||
/** @brief Ajoute une ligne libellée au formulaire. */
|
||||
static void create_social_account_dialog_add_row(
|
||||
GtkGrid *grid, int row, const char *label, GtkWidget *field)
|
||||
{
|
||||
GtkWidget *caption = gtk_label_new(label);
|
||||
gtk_label_set_xalign(GTK_LABEL(caption), 0.0f);
|
||||
gtk_widget_set_halign(caption, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(field, TRUE);
|
||||
gtk_grid_attach(grid, caption, 0, row, 1, 1);
|
||||
gtk_grid_attach(grid, field, 1, row, 1, 1);
|
||||
}
|
||||
|
||||
gboolean create_social_account_dialog_present(
|
||||
GtkWindow *parent, const GPtrArray *evidence_records,
|
||||
CreateSocialAccountDialogCallback callback, gpointer user_data)
|
||||
{
|
||||
static const char *const platform_labels[] = {
|
||||
"TikTok", "Instagram", "Facebook", "X", "Telegram", "Autre", NULL
|
||||
};
|
||||
static const char *const state_labels[] = {
|
||||
"Inconnu", "Actif", "Privé", "Suspendu", "Supprimé", NULL
|
||||
};
|
||||
CreateSocialAccountDialogState *state = NULL;
|
||||
GtkWidget *box = NULL;
|
||||
GtkWidget *grid = NULL;
|
||||
GtkWidget *actions = NULL;
|
||||
GtkWidget *cancel = NULL;
|
||||
GtkWidget *create = NULL;
|
||||
GtkStringList *evidence_labels = NULL;
|
||||
GDateTime *now = NULL;
|
||||
char *now_text = NULL;
|
||||
guint index = 0;
|
||||
if (parent == NULL) return FALSE;
|
||||
state = g_new0(CreateSocialAccountDialogState, 1);
|
||||
state->callback = callback;
|
||||
state->user_data = user_data;
|
||||
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Ajouter un compte social");
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
|
||||
grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
state->platform = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(platform_labels));
|
||||
state->url = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->url, "https://www.tiktok.com/@compte");
|
||||
state->username = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->username, "@pseudonyme affiché");
|
||||
state->platform_identifier = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->platform_identifier, "Identifiant stable, si visible");
|
||||
state->observed_at = GTK_ENTRY(gtk_entry_new());
|
||||
now = g_date_time_new_now_utc();
|
||||
now_text = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
|
||||
gtk_editable_set_text(GTK_EDITABLE(state->observed_at), now_text);
|
||||
state->state = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(state_labels));
|
||||
evidence_labels = gtk_string_list_new(NULL);
|
||||
gtk_string_list_append(evidence_labels, "Aucune preuve associée");
|
||||
for (index = 0; evidence_records != NULL && index < evidence_records->len; index++)
|
||||
{
|
||||
EvidenceRecord *record = g_ptr_array_index((GPtrArray *) evidence_records, index);
|
||||
const char *identifier = evidence_record_get_identifier(record);
|
||||
const char *name = evidence_record_get_original_name(record);
|
||||
if (identifier == NULL || name == NULL) continue;
|
||||
gtk_string_list_append(evidence_labels, name);
|
||||
g_ptr_array_add(state->evidence_identifiers, g_strdup(identifier));
|
||||
}
|
||||
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||
G_LIST_MODEL(evidence_labels), NULL));
|
||||
/* gtk_drop_down_new() prend possession de la référence du modèle. */
|
||||
evidence_labels = NULL;
|
||||
state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
|
||||
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 100);
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 0, "Plateforme", GTK_WIDGET(state->platform));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 1, "URL complète", GTK_WIDGET(state->url));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->username));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 3, "ID plateforme", GTK_WIDGET(state->platform_identifier));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 4, "Première observation (UTC)", GTK_WIDGET(state->observed_at));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 5, "État observé", GTK_WIDGET(state->state));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 6, "Preuve associée", GTK_WIDGET(state->evidence));
|
||||
create_social_account_dialog_add_row(GTK_GRID(grid), 7, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||
state->error = GTK_LABEL(gtk_label_new(NULL));
|
||||
gtk_label_set_wrap(state->error, TRUE);
|
||||
gtk_widget_add_css_class(GTK_WIDGET(state->error), "error");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
cancel = gtk_button_new_with_label("Annuler");
|
||||
create = gtk_button_new_with_label("Ajouter au graphe");
|
||||
gtk_widget_add_css_class(create, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(actions), cancel); gtk_box_append(GTK_BOX(actions), create);
|
||||
gtk_box_append(GTK_BOX(box), grid);
|
||||
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
|
||||
gtk_box_append(GTK_BOX(box), actions);
|
||||
gtk_window_set_child(state->window, box);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(create_social_account_dialog_on_close), state);
|
||||
g_signal_connect(cancel, "clicked", G_CALLBACK(create_social_account_dialog_on_cancel), state);
|
||||
g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state,
|
||||
create_social_account_dialog_state_free);
|
||||
labfy_dialog_present(state->window);
|
||||
g_free(now_text); g_date_time_unref(now);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void create_social_account_dialog_result_free(CreateSocialAccountDialogResult *result)
|
||||
{
|
||||
if (result == NULL) return;
|
||||
g_free(result->platform); g_free(result->profile_url); g_free(result->username);
|
||||
g_free(result->platform_identifier); g_free(result->first_observed_at);
|
||||
g_free(result->account_state); g_free(result->notes);
|
||||
g_free(result->evidence_identifier); g_free(result);
|
||||
}
|
||||
|
||||
#define SOCIAL_RESULT_GETTER(name, field) \
|
||||
const char *create_social_account_dialog_result_get_##name( \
|
||||
const CreateSocialAccountDialogResult *result) \
|
||||
{ return result != NULL ? result->field : NULL; }
|
||||
|
||||
SOCIAL_RESULT_GETTER(platform, platform)
|
||||
SOCIAL_RESULT_GETTER(profile_url, profile_url)
|
||||
SOCIAL_RESULT_GETTER(username, username)
|
||||
SOCIAL_RESULT_GETTER(platform_identifier, platform_identifier)
|
||||
SOCIAL_RESULT_GETTER(first_observed_at, first_observed_at)
|
||||
SOCIAL_RESULT_GETTER(account_state, account_state)
|
||||
SOCIAL_RESULT_GETTER(notes, notes)
|
||||
SOCIAL_RESULT_GETTER(evidence_identifier, evidence_identifier)
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
#include "views/dialog_geometry.h"
|
||||
|
||||
#define LABFY_DIALOG_INITIAL_WIDTH 1200
|
||||
#define LABFY_DIALOG_INITIAL_HEIGHT 800
|
||||
#define LABFY_DIALOG_MINIMUM_WIDTH 800
|
||||
#define LABFY_DIALOG_MINIMUM_HEIGHT 600
|
||||
#define LABFY_DIALOG_WORKAREA_FACTOR 0.90
|
||||
|
||||
typedef struct {
|
||||
double ratio;
|
||||
int minimum_start;
|
||||
int minimum_end;
|
||||
int previous_width;
|
||||
guint stable_frames;
|
||||
} LabfyPanedGeometry;
|
||||
|
||||
LabfyDialogGeometry labfy_dialog_geometry_calculate(
|
||||
int workarea_width, int workarea_height)
|
||||
{
|
||||
LabfyDialogGeometry geometry = {0};
|
||||
int usable_width = workarea_width > 0
|
||||
? (workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
|
||||
? workarea_width
|
||||
: MAX(1, (int) (
|
||||
workarea_width * LABFY_DIALOG_WORKAREA_FACTOR)))
|
||||
: LABFY_DIALOG_INITIAL_WIDTH;
|
||||
int usable_height = workarea_height > 0
|
||||
? (workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
|
||||
? workarea_height
|
||||
: MAX(1, (int) (
|
||||
workarea_height * LABFY_DIALOG_WORKAREA_FACTOR)))
|
||||
: LABFY_DIALOG_INITIAL_HEIGHT;
|
||||
geometry.initial_width = MIN(LABFY_DIALOG_INITIAL_WIDTH, usable_width);
|
||||
geometry.initial_height = MIN(LABFY_DIALOG_INITIAL_HEIGHT, usable_height);
|
||||
geometry.minimum_width = workarea_width >= LABFY_DIALOG_MINIMUM_WIDTH
|
||||
? LABFY_DIALOG_MINIMUM_WIDTH : usable_width;
|
||||
geometry.minimum_height = workarea_height >= LABFY_DIALOG_MINIMUM_HEIGHT
|
||||
? LABFY_DIALOG_MINIMUM_HEIGHT : usable_height;
|
||||
geometry.minimum_width = MIN(
|
||||
geometry.minimum_width, geometry.initial_width);
|
||||
geometry.minimum_height = MIN(
|
||||
geometry.minimum_height, geometry.initial_height);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
static GdkMonitor *dialog_geometry_find_monitor(
|
||||
GtkWindow *window, GtkWindow *parent)
|
||||
{
|
||||
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
GtkNative *native = parent != NULL
|
||||
? gtk_widget_get_native(GTK_WIDGET(parent)) : NULL;
|
||||
GdkSurface *surface = native != NULL
|
||||
? gtk_native_get_surface(native) : NULL;
|
||||
if (surface != NULL) {
|
||||
GdkMonitor *monitor =
|
||||
gdk_display_get_monitor_at_surface(display, surface);
|
||||
if (monitor != NULL) return g_object_ref(monitor);
|
||||
}
|
||||
GListModel *monitors = gdk_display_get_monitors(display);
|
||||
return monitors != NULL && g_list_model_get_n_items(monitors) > 0
|
||||
? g_list_model_get_item(monitors, 0) : NULL;
|
||||
}
|
||||
|
||||
void labfy_dialog_apply_standard_geometry(
|
||||
GtkWindow *window, GtkWindow *parent)
|
||||
{
|
||||
GdkRectangle workarea = {0};
|
||||
GdkMonitor *monitor;
|
||||
LabfyDialogGeometry geometry;
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
monitor = dialog_geometry_find_monitor(window, parent);
|
||||
if (monitor != NULL) gdk_monitor_get_geometry(monitor, &workarea);
|
||||
geometry = labfy_dialog_geometry_calculate(
|
||||
workarea.width, workarea.height);
|
||||
gtk_window_set_resizable(window, TRUE);
|
||||
gtk_window_set_default_size(window,
|
||||
geometry.initial_width, geometry.initial_height);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(window),
|
||||
geometry.minimum_width, geometry.minimum_height);
|
||||
g_clear_object(&monitor);
|
||||
}
|
||||
|
||||
void labfy_dialog_prepare(
|
||||
GtkWindow *window,
|
||||
GtkWindow *parent,
|
||||
gboolean modal,
|
||||
gboolean destroy_with_parent)
|
||||
{
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
gtk_window_set_transient_for(window,
|
||||
GTK_IS_WINDOW(parent) ? parent : NULL);
|
||||
gtk_window_set_modal(window, modal);
|
||||
gtk_window_set_destroy_with_parent(window,
|
||||
destroy_with_parent && GTK_IS_WINDOW(parent));
|
||||
}
|
||||
|
||||
void labfy_dialog_present(GtkWindow *window)
|
||||
{
|
||||
GtkWindow *parent;
|
||||
|
||||
if (!GTK_IS_WINDOW(window)) return;
|
||||
parent = gtk_window_get_transient_for(window);
|
||||
labfy_dialog_apply_standard_geometry(window, parent);
|
||||
gtk_window_present(window);
|
||||
}
|
||||
|
||||
static void paned_geometry_apply(
|
||||
GtkPaned *paned, LabfyPanedGeometry *geometry)
|
||||
{
|
||||
int width = gtk_widget_get_width(GTK_WIDGET(paned));
|
||||
int position;
|
||||
if (geometry == NULL ||
|
||||
width < geometry->minimum_start + geometry->minimum_end) return;
|
||||
position = (int) (width * geometry->ratio);
|
||||
position = MAX(geometry->minimum_start, position);
|
||||
position = MIN(position, MAX(0, width - geometry->minimum_end));
|
||||
gtk_paned_set_position(paned, position);
|
||||
}
|
||||
|
||||
static gboolean paned_geometry_tick(GtkWidget *widget, GdkFrameClock *clock,
|
||||
gpointer data)
|
||||
{
|
||||
GtkPaned *paned = GTK_PANED(widget);
|
||||
LabfyPanedGeometry *geometry = g_object_get_data(
|
||||
G_OBJECT(paned), "labfy-paned-initial-geometry");
|
||||
int width;
|
||||
(void) clock;
|
||||
(void) data;
|
||||
if (geometry == NULL) return G_SOURCE_REMOVE;
|
||||
width = gtk_widget_get_width(widget);
|
||||
if (width < geometry->minimum_start + geometry->minimum_end) {
|
||||
geometry->previous_width = width;
|
||||
geometry->stable_frames = 0;
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
if (width != geometry->previous_width) {
|
||||
geometry->previous_width = width;
|
||||
geometry->stable_frames = 0;
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
if (geometry->stable_frames++ < 1) return G_SOURCE_CONTINUE;
|
||||
paned_geometry_apply(paned, geometry);
|
||||
g_object_set_data(G_OBJECT(paned), "labfy-paned-initial-ratio-applied",
|
||||
GINT_TO_POINTER(1));
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean labfy_paned_initial_ratio_applied(GtkPaned *paned)
|
||||
{
|
||||
return GTK_IS_PANED(paned) && g_object_get_data(G_OBJECT(paned),
|
||||
"labfy-paned-initial-ratio-applied") != NULL;
|
||||
}
|
||||
|
||||
void labfy_paned_apply_initial_ratio(
|
||||
GtkPaned *paned, double ratio, int minimum_start, int minimum_end)
|
||||
{
|
||||
LabfyPanedGeometry *geometry;
|
||||
if (!GTK_IS_PANED(paned)) return;
|
||||
if (g_object_get_data(
|
||||
G_OBJECT(paned), "labfy-paned-initial-geometry") != NULL)
|
||||
return;
|
||||
geometry = g_new0(LabfyPanedGeometry, 1);
|
||||
geometry->ratio = CLAMP(ratio, 0.1, 0.9);
|
||||
geometry->minimum_start = MAX(0, minimum_start);
|
||||
geometry->minimum_end = MAX(0, minimum_end);
|
||||
g_object_set_data_full(G_OBJECT(paned),
|
||||
"labfy-paned-initial-geometry", geometry, g_free);
|
||||
gtk_widget_add_tick_callback(GTK_WIDGET(paned), paned_geometry_tick,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#include "views/document_authenticity_editor.h"
|
||||
#include "core/document_authenticity_service.h"
|
||||
#include "dao/identity_ocr_dao.h"
|
||||
|
||||
struct DocumentAuthenticityEditor{Database*database;char*evidence;GtkWidget*root;
|
||||
GtkDropDown*status;GtkStringList*status_labels;GtkDropDown*run;GtkStringList*run_labels;
|
||||
GPtrArray*run_ids;GtkTextBuffer*justification;GtkTextBuffer*note;GtkLabel*error;GtkBox*history;};
|
||||
static void clear_box(GtkBox*b){GtkWidget*c=gtk_widget_get_first_child(GTK_WIDGET(b));while(c){GtkWidget*n=gtk_widget_get_next_sibling(c);gtk_box_remove(b,c);c=n;}}
|
||||
static char *buffer_text(GtkTextBuffer*b){GtkTextIter s,e;gtk_text_buffer_get_bounds(b,&s,&e);char*t=gtk_text_buffer_get_text(b,&s,&e,FALSE);g_strstrip(t);return t;}
|
||||
gboolean document_authenticity_editor_refresh(DocumentAuthenticityEditor*x,GError**error)
|
||||
{
|
||||
if(!x)return FALSE;
|
||||
clear_box(x->history);GPtrArray*h=document_authenticity_service_history(x->database,x->evidence,error);if(!h)return FALSE;
|
||||
if(h->len==0)gtk_box_append(x->history,gtk_label_new("Aucune appréciation humaine enregistrée."));
|
||||
for(gint i=(gint)h->len-1;i>=0;i--){DocumentAuthenticityAssessment*a=g_ptr_array_index(h,(guint)i);
|
||||
const DocumentAuthenticityStatus*s=document_authenticity_service_status(document_authenticity_assessment_get_status(a));
|
||||
const char*j=document_authenticity_assessment_get_justification(a),*n=document_authenticity_assessment_get_technical_note(a),*r=document_authenticity_assessment_get_ocr_run_identifier(a);
|
||||
char*t=g_strdup_printf("%s — %s — origine humaine%s%s%s%s%s%s",s?s->label:"Statut inconnu",
|
||||
document_authenticity_assessment_get_assessed_at(a),r?" — OcrRun : ":"",r?r:"",j?"\nJustification : ":"",j?j:"",n?"\nNote technique : ":"",n?n:"");
|
||||
GtkWidget*l=gtk_label_new(t);gtk_label_set_xalign(GTK_LABEL(l),0);gtk_label_set_wrap(GTK_LABEL(l),TRUE);gtk_box_append(x->history,l);g_free(t);}
|
||||
g_ptr_array_unref(h);return TRUE;
|
||||
}
|
||||
static void save(GtkButton*b,gpointer data)
|
||||
{
|
||||
(void)b;DocumentAuthenticityEditor*x=data;guint si=gtk_drop_down_get_selected(x->status),ri=gtk_drop_down_get_selected(x->run);gsize count=0;
|
||||
const DocumentAuthenticityStatus*s=document_authenticity_service_statuses(&count);if(si>=count)return;
|
||||
char*j=buffer_text(x->justification),*n=buffer_text(x->note);const char*run=ri>0&&ri-1<x->run_ids->len?g_ptr_array_index(x->run_ids,ri-1):NULL;GError*error=NULL;
|
||||
if(!document_authenticity_service_add(x->database,x->evidence,s[si].code,j,
|
||||
n&&*n?n:NULL,run,NULL,&error)){
|
||||
gtk_label_set_text(x->error,error?error->message:"Erreur d’enregistrement.");gtk_widget_set_visible(GTK_WIDGET(x->error),TRUE);g_clear_error(&error);
|
||||
}else{gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_text_buffer_set_text(x->justification,"",-1);gtk_text_buffer_set_text(x->note,"",-1);document_authenticity_editor_refresh(x,NULL);}
|
||||
g_free(j);g_free(n);
|
||||
}
|
||||
DocumentAuthenticityEditor *document_authenticity_editor_new(Database*d,const char*evidence)
|
||||
{
|
||||
if(!d||!g_uuid_string_is_valid(evidence))return NULL;
|
||||
DocumentAuthenticityEditor*x=g_new0(DocumentAuthenticityEditor,1);x->database=d;x->evidence=g_strdup(evidence);
|
||||
x->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,6);gtk_widget_set_name(x->root,"document-authenticity-editor");
|
||||
GtkWidget*title=gtk_label_new("Authenticité documentaire — appréciation humaine");gtk_label_set_xalign(GTK_LABEL(title),0);gtk_box_append(GTK_BOX(x->root),title);
|
||||
x->status_labels=gtk_string_list_new(NULL);gsize count=0;const DocumentAuthenticityStatus*s=document_authenticity_service_statuses(&count);for(guint i=0;i<count;i++)gtk_string_list_append(x->status_labels,s[i].label);
|
||||
x->status=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->status_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->status),"authenticity-status");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->status));
|
||||
x->run_labels=gtk_string_list_new(NULL);gtk_string_list_append(x->run_labels,"Aucune exécution OCR");x->run_ids=g_ptr_array_new_with_free_func(g_free);GPtrArray*runs=document_authenticity_service_list_runs(d,evidence,NULL);
|
||||
if(runs){for(guint i=0;i<runs->len;i++){IdentityOcrRunRecord*r=g_ptr_array_index(runs,i);char*l=g_strdup_printf("%s — %s",r->executed_at,r->engine);gtk_string_list_append(x->run_labels,l);g_ptr_array_add(x->run_ids,g_strdup(r->id));g_free(l);}g_ptr_array_unref(runs);}
|
||||
x->run=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->run_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->run),"authenticity-ocr-run");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->run));
|
||||
GtkWidget*jv=gtk_text_view_new();x->justification=gtk_text_view_get_buffer(GTK_TEXT_VIEW(jv));gtk_widget_set_name(jv,"authenticity-justification");gtk_widget_set_size_request(jv,-1,60);gtk_box_append(GTK_BOX(x->root),gtk_label_new("Justification factuelle"));gtk_box_append(GTK_BOX(x->root),jv);
|
||||
GtkWidget*nv=gtk_text_view_new();x->note=gtk_text_view_get_buffer(GTK_TEXT_VIEW(nv));gtk_widget_set_name(nv,"authenticity-technical-note");gtk_widget_set_size_request(nv,-1,45);gtk_box_append(GTK_BOX(x->root),gtk_label_new("Note technique facultative"));gtk_box_append(GTK_BOX(x->root),nv);
|
||||
x->error=GTK_LABEL(gtk_label_new(""));gtk_widget_set_name(GTK_WIDGET(x->error),"authenticity-error");gtk_widget_add_css_class(GTK_WIDGET(x->error),"error");gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->error));
|
||||
GtkWidget*button=gtk_button_new_with_label("Enregistrer l’appréciation humaine");gtk_widget_set_name(button,"authenticity-save");g_signal_connect(button,"clicked",G_CALLBACK(save),x);gtk_box_append(GTK_BOX(x->root),button);
|
||||
x->history=GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL,4));gtk_widget_set_name(GTK_WIDGET(x->history),"authenticity-history");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->history));document_authenticity_editor_refresh(x,NULL);return x;
|
||||
}
|
||||
GtkWidget *document_authenticity_editor_get_widget(DocumentAuthenticityEditor*x){return x?x->root:NULL;}
|
||||
void document_authenticity_editor_free(DocumentAuthenticityEditor*x){if(!x)return;g_clear_object(&x->status_labels);g_clear_object(&x->run_labels);g_ptr_array_unref(x->run_ids);g_free(x->evidence);g_free(x);}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#include "views/document_identity_misuse_editor.h"
|
||||
#include "core/document_identity_misuse_service.h"
|
||||
#include "dao/identity_ocr_dao.h"
|
||||
struct DocumentIdentityMisuseEditor{Database*database;char*evidence;GtkWidget*root;
|
||||
GtkDropDown*status;GtkStringList*status_labels;GtkDropDown*run;GtkStringList*run_labels;
|
||||
GPtrArray*run_ids;GtkTextBuffer*justification;GtkLabel*error;GtkBox*history;};
|
||||
static void clear_box(GtkBox*b){GtkWidget*c=gtk_widget_get_first_child(GTK_WIDGET(b));while(c){GtkWidget*n=gtk_widget_get_next_sibling(c);gtk_box_remove(b,c);c=n;}}
|
||||
static char *buffer_text(GtkTextBuffer*b){GtkTextIter s,e;gtk_text_buffer_get_bounds(b,&s,&e);char*t=gtk_text_buffer_get_text(b,&s,&e,FALSE);g_strstrip(t);return t;}
|
||||
gboolean document_identity_misuse_editor_refresh(DocumentIdentityMisuseEditor*x,GError**error)
|
||||
{
|
||||
if(!x)return FALSE;
|
||||
clear_box(x->history);GPtrArray*h=document_identity_misuse_service_history(x->database,x->evidence,error);if(!h)return FALSE;
|
||||
if(h->len==0)gtk_box_append(x->history,gtk_label_new("Aucune évaluation humaine enregistrée."));
|
||||
for(gint i=(gint)h->len-1;i>=0;i--){DocumentIdentityMisuseAssessment*a=g_ptr_array_index(h,(guint)i);
|
||||
const DocumentIdentityMisuseStatus*s=document_identity_misuse_service_status(document_identity_misuse_assessment_get_status(a));
|
||||
const char*j=document_identity_misuse_assessment_get_justification(a),*r=document_identity_misuse_assessment_get_ocr_run_identifier(a);
|
||||
char*t=g_strdup_printf("%s — %s\nOrigine : humaine%s%s\nJustification : %s",
|
||||
s?s->label:"Statut inconnu",document_identity_misuse_assessment_get_assessed_at(a),
|
||||
r?" — OcrRun : ":"",r?r:"",j&&*j?j:"Aucune (statut indéterminé)");
|
||||
GtkWidget*l=gtk_label_new(t);gtk_label_set_xalign(GTK_LABEL(l),0);gtk_label_set_wrap(GTK_LABEL(l),TRUE);gtk_box_append(x->history,l);g_free(t);}
|
||||
g_ptr_array_unref(h);return TRUE;
|
||||
}
|
||||
static void save(GtkButton*b,gpointer data)
|
||||
{
|
||||
(void)b;DocumentIdentityMisuseEditor*x=data;guint si=gtk_drop_down_get_selected(x->status),ri=gtk_drop_down_get_selected(x->run);gsize count=0;
|
||||
const DocumentIdentityMisuseStatus*s=document_identity_misuse_service_statuses(&count);if(si>=count)return;
|
||||
char*j=buffer_text(x->justification);const char*run=ri>0&&ri-1<x->run_ids->len?g_ptr_array_index(x->run_ids,ri-1):NULL;GError*error=NULL;
|
||||
if(!document_identity_misuse_service_add(x->database,x->evidence,s[si].code,j,run,NULL,&error)){
|
||||
gtk_label_set_text(x->error,error?error->message:"Erreur d’enregistrement.");gtk_widget_set_visible(GTK_WIDGET(x->error),TRUE);g_clear_error(&error);
|
||||
}else{gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_text_buffer_set_text(x->justification,"",-1);document_identity_misuse_editor_refresh(x,NULL);}g_free(j);
|
||||
}
|
||||
DocumentIdentityMisuseEditor *document_identity_misuse_editor_new(Database*d,const char*evidence)
|
||||
{
|
||||
if(!d||!g_uuid_string_is_valid(evidence))return NULL;
|
||||
DocumentIdentityMisuseEditor*x=g_new0(DocumentIdentityMisuseEditor,1);x->database=d;x->evidence=g_strdup(evidence);
|
||||
x->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,6);gtk_widget_set_name(x->root,"document-identity-misuse-editor");
|
||||
GtkWidget*title=gtk_label_new("Usage de l’identité du document — évaluation humaine distincte");gtk_label_set_xalign(GTK_LABEL(title),0);gtk_box_append(GTK_BOX(x->root),title);
|
||||
x->status_labels=gtk_string_list_new(NULL);gsize count=0;const DocumentIdentityMisuseStatus*s=document_identity_misuse_service_statuses(&count);for(guint i=0;i<count;i++)gtk_string_list_append(x->status_labels,s[i].label);
|
||||
x->status=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->status_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->status),"identity-misuse-status");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->status));
|
||||
x->run_labels=gtk_string_list_new(NULL);gtk_string_list_append(x->run_labels,"Aucune exécution OCR");x->run_ids=g_ptr_array_new_with_free_func(g_free);GPtrArray*runs=document_identity_misuse_service_list_runs(d,evidence,NULL);
|
||||
if(runs){for(guint i=0;i<runs->len;i++){IdentityOcrRunRecord*r=g_ptr_array_index(runs,i);char*l=g_strdup_printf("%s — %s",r->executed_at,r->engine);gtk_string_list_append(x->run_labels,l);g_ptr_array_add(x->run_ids,g_strdup(r->id));g_free(l);}g_ptr_array_unref(runs);}
|
||||
x->run=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->run_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->run),"identity-misuse-ocr-run");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->run));
|
||||
gtk_box_append(GTK_BOX(x->root),gtk_label_new("Justification factuelle"));GtkWidget*jv=gtk_text_view_new();x->justification=gtk_text_view_get_buffer(GTK_TEXT_VIEW(jv));gtk_widget_set_name(jv,"identity-misuse-justification");gtk_widget_set_size_request(jv,-1,60);gtk_box_append(GTK_BOX(x->root),jv);
|
||||
x->error=GTK_LABEL(gtk_label_new(""));gtk_widget_set_name(GTK_WIDGET(x->error),"identity-misuse-error");gtk_widget_add_css_class(GTK_WIDGET(x->error),"error");gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->error));
|
||||
GtkWidget*button=gtk_button_new_with_label("Enregistrer l’évaluation humaine");gtk_widget_set_name(button,"identity-misuse-save");g_signal_connect(button,"clicked",G_CALLBACK(save),x);gtk_box_append(GTK_BOX(x->root),button);
|
||||
x->history=GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL,4));gtk_widget_set_name(GTK_WIDGET(x->history),"identity-misuse-history");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->history));document_identity_misuse_editor_refresh(x,NULL);return x;
|
||||
}
|
||||
GtkWidget *document_identity_misuse_editor_get_widget(DocumentIdentityMisuseEditor*x){return x?x->root:NULL;}
|
||||
void document_identity_misuse_editor_free(DocumentIdentityMisuseEditor*x){if(!x)return;g_clear_object(&x->status_labels);g_clear_object(&x->run_labels);g_ptr_array_unref(x->run_ids);g_free(x->evidence);g_free(x);}
|
||||
|
|
@ -1,545 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file eml_analysis_dialog.c
|
||||
* @brief Présentation en lecture seule d'une analyse EML.
|
||||
******************************************************************************/
|
||||
#include "views/eml_analysis_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "widgets/controlled_vocab_dropdown.h"
|
||||
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
||||
GtkWidget *summary_label;
|
||||
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
|
||||
} EmlAnalysisDialogState;
|
||||
static void eml_analysis_dialog_update_summary(GtkCheckButton *button,
|
||||
gpointer data)
|
||||
{
|
||||
EmlAnalysisDialogState *state = data; guint kept = 0, promoted = 0;
|
||||
(void) button;
|
||||
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
|
||||
row != NULL; row = gtk_widget_get_next_sibling(row))
|
||||
{
|
||||
GtkWidget *keep = g_object_get_data(G_OBJECT(row), "eml-keep");
|
||||
GtkWidget *promote = g_object_get_data(G_OBJECT(row), "eml-promote");
|
||||
gboolean active = keep != NULL &&
|
||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(keep));
|
||||
if (promote != NULL)
|
||||
{
|
||||
gtk_widget_set_sensitive(promote, active);
|
||||
if (!active)
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(promote), FALSE);
|
||||
}
|
||||
if (active) kept++;
|
||||
if (promote != NULL &&
|
||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote))) promoted++;
|
||||
}
|
||||
char *summary = g_strdup_printf(
|
||||
"%u observation(s) seront conservée(s) dans la fiche ; "
|
||||
"%u seront promue(s) en entité(s).", kept, promoted);
|
||||
gtk_label_set_text(GTK_LABEL(state->summary_label), summary);
|
||||
g_free(summary);
|
||||
}
|
||||
static void eml_analysis_dialog_bind_summary(EmlAnalysisDialogState *state,
|
||||
GtkWidget *content)
|
||||
{
|
||||
state->summary_label = gtk_label_new(
|
||||
"0 observation sera conservée dans la fiche ; "
|
||||
"0 sera promue en entité.");
|
||||
gtk_label_set_xalign(GTK_LABEL(state->summary_label), 0.0f);
|
||||
gtk_box_append(GTK_BOX(content), state->summary_label);
|
||||
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
|
||||
row != NULL; row = gtk_widget_get_next_sibling(row))
|
||||
{
|
||||
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-keep"),
|
||||
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
|
||||
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-promote"),
|
||||
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
|
||||
}
|
||||
}
|
||||
/** @brief Libère l'état de révision. */
|
||||
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
|
||||
/** @brief Signale une annulation une seule fois. */
|
||||
static void eml_analysis_dialog_cancel(EmlAnalysisDialogState *state)
|
||||
{ if (state == NULL || state->completed) return; state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(NULL, state->user_data); }
|
||||
/** @brief Traite la fermeture native. */
|
||||
static gboolean eml_analysis_dialog_on_window_close(GtkWindow *window, gpointer data)
|
||||
{ (void) window; eml_analysis_dialog_cancel(data); return FALSE; }
|
||||
/** @brief Ferme la fenêtre de résultat. */
|
||||
static void eml_analysis_dialog_on_close(GtkButton *button, gpointer data)
|
||||
{ EmlAnalysisDialogState *state = data; (void) button;
|
||||
eml_analysis_dialog_cancel(state); gtk_window_close(state->window); }
|
||||
/** @brief Transmet uniquement les propositions explicitement cochées. */
|
||||
static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||
{
|
||||
EmlAnalysisDialogState *state = data;
|
||||
GPtrArray *selected = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) eml_entity_proposal_free);
|
||||
(void) button;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child))
|
||||
{
|
||||
GtkWidget *check = g_object_get_data(G_OBJECT(child), "eml-keep");
|
||||
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
||||
{
|
||||
const char *type = g_object_get_data(G_OBJECT(child), "eml-type");
|
||||
const char *value = g_object_get_data(G_OBJECT(child), "eml-value");
|
||||
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
||||
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
||||
"eml-provenance");
|
||||
const char *raw = g_object_get_data(G_OBJECT(child), "eml-raw");
|
||||
const char *role = g_object_get_data(G_OBJECT(child), "eml-role");
|
||||
const char *header = g_object_get_data(G_OBJECT(child), "eml-header");
|
||||
guint occurrence = GPOINTER_TO_UINT(g_object_get_data(
|
||||
G_OBJECT(child), "eml-occurrence"));
|
||||
EmlEntityProposal *proposal = eml_entity_proposal_new_observation(
|
||||
type, raw != NULL ? raw : value, value,
|
||||
role != NULL ? role : "other",
|
||||
header != NULL ? header : "manual",
|
||||
occurrence > 0 ? occurrence : 1,
|
||||
controlled_vocab_dropdown_get_selected_code(status),
|
||||
controlled_vocab_dropdown_get_selected_code(provenance));
|
||||
GtkWidget *promote = g_object_get_data(G_OBJECT(child),
|
||||
"eml-promote");
|
||||
proposal->promote_to_entity = promote != NULL &&
|
||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote));
|
||||
g_ptr_array_add(selected, proposal);
|
||||
}
|
||||
}
|
||||
if (selected->len == 0)
|
||||
{ g_ptr_array_unref(selected); return; }
|
||||
state->completed = TRUE;
|
||||
if (state->callback != NULL) state->callback(selected, state->user_data);
|
||||
else g_ptr_array_unref(selected);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
static void eml_analysis_dialog_add_observations(GtkWidget *box,
|
||||
const GPtrArray *observations)
|
||||
{
|
||||
for (guint i = 0; observations != NULL && i < observations->len; i++)
|
||||
{
|
||||
const EmlObservation *observation = g_ptr_array_index(
|
||||
(GPtrArray *) observations, i);
|
||||
char *text = g_strdup_printf("%s — %s — rôle : %s — origine : %s #%u",
|
||||
observation->type_identifier, observation->value_normalized,
|
||||
observation->role, observation->source_header,
|
||||
observation->occurrence);
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget *check = gtk_check_button_new_with_label(
|
||||
"Conserver dans la fiche");
|
||||
GtkWidget *value_label = gtk_label_new(text);
|
||||
GtkWidget *promote = gtk_check_button_new_with_label(
|
||||
"Promouvoir en entité");
|
||||
gtk_widget_set_sensitive(promote, FALSE);
|
||||
GtkWidget *status = controlled_vocab_dropdown_new(
|
||||
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
||||
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
||||
CONTROLLED_VOCAB_PROVENANCE_KIND, observation->provenance_kind);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-type",
|
||||
g_strdup(observation->type_identifier), g_free);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-value",
|
||||
g_strdup(observation->value_normalized), g_free);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-raw",
|
||||
g_strdup(observation->value_raw), g_free);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-role",
|
||||
g_strdup(observation->role), g_free);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-header",
|
||||
g_strdup(observation->source_header), g_free);
|
||||
g_object_set_data(G_OBJECT(row), "eml-occurrence",
|
||||
GUINT_TO_POINTER(observation->occurrence));
|
||||
g_object_set_data(G_OBJECT(row), "eml-status", status);
|
||||
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
||||
g_object_set_data(G_OBJECT(row), "eml-keep", check);
|
||||
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
|
||||
gtk_widget_set_hexpand(check, TRUE);
|
||||
gtk_box_append(GTK_BOX(row), check);
|
||||
gtk_box_append(GTK_BOX(row), value_label);
|
||||
gtk_box_append(GTK_BOX(row), promote);
|
||||
gtk_box_append(GTK_BOX(row), status);
|
||||
gtk_box_append(GTK_BOX(row), provenance);
|
||||
gtk_box_append(GTK_BOX(box), row);
|
||||
g_free(text);
|
||||
}
|
||||
}
|
||||
/** @brief Ajoute les propositions d'un type sous forme de cases décochées. */
|
||||
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
||||
const char *type, const char *label, const GPtrArray *values)
|
||||
{
|
||||
for (guint i = 0; values != NULL && i < values->len; i++)
|
||||
{
|
||||
const char *value = g_ptr_array_index((GPtrArray *) values, i);
|
||||
char *text = g_strdup_printf("%s : %s", label, value);
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget *check = gtk_check_button_new_with_label(
|
||||
"Conserver dans la fiche");
|
||||
GtkWidget *value_label = gtk_label_new(text);
|
||||
GtkWidget *promote = gtk_check_button_new_with_label(
|
||||
"Promouvoir en entité");
|
||||
gtk_widget_set_sensitive(promote, FALSE);
|
||||
GtkWidget *status = controlled_vocab_dropdown_new(
|
||||
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
||||
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
||||
CONTROLLED_VOCAB_PROVENANCE_KIND,
|
||||
g_strcmp0(type, "ip_address") == 0 ? "header" : "header");
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-type", g_strdup(type), g_free);
|
||||
g_object_set_data_full(G_OBJECT(row), "eml-value", g_strdup(value), g_free);
|
||||
g_object_set_data(G_OBJECT(row), "eml-status", status);
|
||||
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
||||
g_object_set_data(G_OBJECT(row), "eml-keep", check);
|
||||
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
|
||||
gtk_widget_set_hexpand(check, TRUE);
|
||||
gtk_box_append(GTK_BOX(row), check);
|
||||
gtk_box_append(GTK_BOX(row), value_label);
|
||||
gtk_box_append(GTK_BOX(row), promote);
|
||||
gtk_box_append(GTK_BOX(row), status);
|
||||
gtk_box_append(GTK_BOX(row), provenance);
|
||||
gtk_box_append(GTK_BOX(box), row); g_free(text);
|
||||
}
|
||||
}
|
||||
/** @brief Ajoute une ligne de métadonnée sélectionnable. */
|
||||
static void eml_analysis_dialog_add_field(GtkGrid *grid, int row,
|
||||
const char *title, const char *value)
|
||||
{
|
||||
GtkWidget *name = gtk_label_new(title);
|
||||
GtkWidget *content = gtk_label_new(value != NULL ? value : "Non présent");
|
||||
gtk_label_set_xalign(GTK_LABEL(name), 0.0f);
|
||||
gtk_label_set_xalign(GTK_LABEL(content), 0.0f);
|
||||
gtk_label_set_selectable(GTK_LABEL(content), TRUE);
|
||||
gtk_label_set_wrap(GTK_LABEL(content), TRUE);
|
||||
gtk_widget_set_valign(name, GTK_ALIGN_START);
|
||||
gtk_grid_attach(grid, name, 0, row, 1, 1);
|
||||
gtk_grid_attach(grid, content, 1, row, 1, 1);
|
||||
}
|
||||
/** @brief Concatène un tableau de chaînes pour son affichage. */
|
||||
static char *eml_analysis_dialog_join(const GPtrArray *values)
|
||||
{
|
||||
GString *text = g_string_new(NULL);
|
||||
for (guint i = 0; values != NULL && i < values->len; i++)
|
||||
g_string_append_printf(text, "%s%s", i > 0 ? "\n" : "",
|
||||
(const char *) g_ptr_array_index((GPtrArray *) values, i));
|
||||
return g_string_free(text, FALSE);
|
||||
}
|
||||
void eml_analysis_dialog_present(GtkWindow *parent,
|
||||
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
const EmlAnalysis *analysis = eml_processing_result_get_analysis(result);
|
||||
GtkWindow *window = NULL;
|
||||
GtkWidget *box = NULL, *content = NULL, *scroll = NULL;
|
||||
GtkWidget *grid = NULL, *raw_scroll = NULL, *raw = NULL;
|
||||
GtkWidget *close = NULL, *integrate = NULL, *actions = NULL;
|
||||
EmlAnalysisDialogState *state = NULL;
|
||||
char *received = NULL, *emails = NULL, *domains = NULL;
|
||||
char *sender_ips = NULL, *destination_ips = NULL;
|
||||
if (parent == NULL || analysis == NULL) return;
|
||||
state = g_new0(EmlAnalysisDialogState, 1);
|
||||
state->callback = callback; state->user_data = user_data;
|
||||
window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(window, "Analyse locale du courriel EML");
|
||||
labfy_dialog_prepare(window, parent, TRUE, TRUE);
|
||||
state->window = window;
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(box, 14); gtk_widget_set_margin_end(box, 14);
|
||||
gtk_widget_set_margin_top(box, 14); gtk_widget_set_margin_bottom(box, 14);
|
||||
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||
"Analyse effectuée sur la copie vérifiée de 02_Preuves_Traitees/Extractions."));
|
||||
grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 0, "Copie analysée",
|
||||
eml_processing_result_get_copy_path(result));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 1, "From",
|
||||
eml_analysis_get_first_header(analysis, "from"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 2, "Reply-To",
|
||||
eml_analysis_get_first_header(analysis, "reply-to"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 3, "To",
|
||||
eml_analysis_get_first_header(analysis, "to"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 4, "Sujet",
|
||||
eml_analysis_get_first_header(analysis, "subject"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 5, "Date",
|
||||
eml_analysis_get_first_header(analysis, "date"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 6, "Message-ID",
|
||||
eml_analysis_get_first_header(analysis, "message-id"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 7, "Authentification",
|
||||
eml_analysis_get_first_header(analysis, "authentication-results"));
|
||||
received = eml_analysis_dialog_join(
|
||||
eml_analysis_get_header_values(analysis, "received"));
|
||||
emails = eml_analysis_dialog_join(eml_analysis_get_email_addresses(analysis));
|
||||
domains = eml_analysis_dialog_join(eml_analysis_get_domains(analysis));
|
||||
sender_ips = eml_analysis_dialog_join(
|
||||
eml_analysis_get_sender_ip_addresses(analysis));
|
||||
destination_ips = eml_analysis_dialog_join(
|
||||
eml_analysis_get_destination_ip_addresses(analysis));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 8, "Received (ordre original)", received);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 9, "Emails proposés", emails);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 10, "Domaines proposés", domains);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 11, "IP expéditeur",
|
||||
sender_ips);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 12, "IP destinataire",
|
||||
destination_ips);
|
||||
gtk_box_append(GTK_BOX(content), grid);
|
||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||
"Choisir les observations à conserver ; la promotion vers le graphe "
|
||||
"est une action séparée."));
|
||||
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
eml_analysis_dialog_add_observations(state->proposals_box,
|
||||
eml_analysis_get_observations(analysis));
|
||||
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
||||
eml_analysis_dialog_bind_summary(state, content);
|
||||
gtk_box_append(GTK_BOX(content), gtk_label_new("En-têtes bruts (lecture seule)"));
|
||||
raw = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
|
||||
gtk_text_view_set_monospace(GTK_TEXT_VIEW(raw), TRUE);
|
||||
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(raw), GTK_WRAP_WORD_CHAR);
|
||||
gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(raw)),
|
||||
eml_analysis_get_raw_headers(analysis), -1);
|
||||
raw_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_size_request(raw_scroll, -1, 180);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(raw_scroll), raw);
|
||||
gtk_box_append(GTK_BOX(content), raw_scroll);
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content);
|
||||
gtk_box_append(GTK_BOX(box), scroll);
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
close = gtk_button_new_with_label("Fermer");
|
||||
integrate = gtk_button_new_with_label("Intégrer la sélection");
|
||||
gtk_widget_add_css_class(integrate, "suggested-action");
|
||||
g_signal_connect(close, "clicked", G_CALLBACK(eml_analysis_dialog_on_close), state);
|
||||
g_signal_connect(integrate, "clicked", G_CALLBACK(eml_analysis_dialog_on_integrate), state);
|
||||
gtk_box_append(GTK_BOX(actions), close); gtk_box_append(GTK_BOX(actions), integrate);
|
||||
gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(window, box);
|
||||
g_signal_connect(window, "close-request",
|
||||
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
|
||||
g_object_set_data_full(G_OBJECT(window), "eml-dialog-state", state,
|
||||
eml_analysis_dialog_state_free);
|
||||
labfy_dialog_present(window);
|
||||
g_free(received); g_free(emails); g_free(domains);
|
||||
g_free(sender_ips); g_free(destination_ips);
|
||||
}
|
||||
|
||||
static void eml_analysis_dialog_append_attachment_summary(
|
||||
GString *text,
|
||||
const EmlPipelineResult *result)
|
||||
{
|
||||
for (guint index = 0; result->mime_result != NULL &&
|
||||
result->mime_result->attachments != NULL &&
|
||||
index < result->mime_result->attachments->len; index++)
|
||||
{
|
||||
EmlAttachment *attachment = g_ptr_array_index(
|
||||
result->mime_result->attachments, index);
|
||||
g_string_append_printf(text,
|
||||
"%s → %s | MIME %s / %s | partie %s | %"
|
||||
G_GSIZE_FORMAT " octets | SHA-256 %s%s\n",
|
||||
attachment->declared_filename != NULL
|
||||
? attachment->declared_filename : "(sans nom)",
|
||||
attachment->sanitized_filename != NULL
|
||||
? attachment->sanitized_filename : "(sans nom)",
|
||||
attachment->content_type != NULL
|
||||
? attachment->content_type : "inconnu",
|
||||
attachment->detected_mime != NULL
|
||||
? attachment->detected_mime : "inconnu",
|
||||
attachment->part_index != NULL ? attachment->part_index : "?",
|
||||
attachment->decoded_size,
|
||||
attachment->sha256 != NULL ? attachment->sha256 : "indisponible",
|
||||
attachment->is_truncated ? " | TRONQUÉ" : "");
|
||||
}
|
||||
}
|
||||
|
||||
static void eml_analysis_dialog_append_document_summary(
|
||||
GString *text,
|
||||
GString *metadata,
|
||||
GString *warnings,
|
||||
const EmlPipelineResult *result)
|
||||
{
|
||||
for (guint index = 0; result->document_analyses != NULL &&
|
||||
index < result->document_analyses->len; index++)
|
||||
{
|
||||
DocumentFileAnalysis *document = g_ptr_array_index(
|
||||
result->document_analyses, index);
|
||||
if (document->ocr != NULL && document->ocr->text != NULL)
|
||||
g_string_append_printf(text, "\nOCR (%s)%s\n%s\n",
|
||||
document->ocr->requested_languages,
|
||||
document->ocr->execution->stdout_truncated
|
||||
? " — texte tronqué/partiel" : "",
|
||||
document->ocr->text);
|
||||
if (document->pdf != NULL)
|
||||
for (guint page_index = 0;
|
||||
page_index < document->pdf->pages->len; page_index++)
|
||||
{
|
||||
PdfPageAnalysis *page = g_ptr_array_index(
|
||||
document->pdf->pages, page_index);
|
||||
g_string_append_printf(text,
|
||||
"\nPDF page %u — méthode %s — état %s\n%s\n",
|
||||
page->page_number,
|
||||
page->method == PDF_PAGE_METHOD_NATIVE
|
||||
? "texte natif" : "OCR",
|
||||
document_analysis_state_code(page->state),
|
||||
page->text != NULL ? page->text : "(aucun texte)");
|
||||
}
|
||||
if (document->metadata != NULL)
|
||||
{
|
||||
DocumentToolExecution *execution =
|
||||
document->metadata->execution;
|
||||
if (execution != NULL &&
|
||||
execution->state == DOCUMENT_ANALYSIS_STATE_UNAVAILABLE)
|
||||
g_string_append(warnings, "ExifTool indisponible.\n");
|
||||
for (guint metadata_index = 0;
|
||||
metadata_index < document->metadata->metadata->len;
|
||||
metadata_index++)
|
||||
{
|
||||
DocumentMetadataEntry *entry = g_ptr_array_index(
|
||||
document->metadata->metadata, metadata_index);
|
||||
g_string_append_printf(metadata,
|
||||
"%s | %s:%s | %s | %s%s\n",
|
||||
entry->code, entry->original_group,
|
||||
entry->original_tag, entry->raw_value,
|
||||
execution != NULL && execution->version != NULL
|
||||
? execution->version : "version inconnue",
|
||||
entry->sensitive
|
||||
? " | SENSIBLE — confirmation explicite requise" : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void eml_analysis_dialog_present_pipeline(
|
||||
GtkWindow *parent,
|
||||
const EmlPipelineResult *result,
|
||||
const char *evidence_name,
|
||||
const char *relative_path,
|
||||
const char *source_sha256,
|
||||
EmlAnalysisDialogCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
if (parent == NULL || result == NULL || result->analysis == NULL) return;
|
||||
EmlAnalysisDialogState *state = g_new0(EmlAnalysisDialogState, 1);
|
||||
state->callback = callback; state->user_data = user_data;
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Révision de l’analyse EML");
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(outer, 14);
|
||||
gtk_widget_set_margin_end(outer, 14);
|
||||
gtk_widget_set_margin_top(outer, 14);
|
||||
gtk_widget_set_margin_bottom(outer, 14);
|
||||
GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
GtkWidget *grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 0, "Preuve", evidence_name);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 1, "Chemin relatif", relative_path);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 2, "SHA-256", source_sha256);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 3, "État global",
|
||||
document_analysis_state_code(result->state));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 4, "From",
|
||||
eml_analysis_get_first_header(result->analysis, "from"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 5, "Sender",
|
||||
eml_analysis_get_first_header(result->analysis, "sender"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 6, "Reply-To",
|
||||
eml_analysis_get_first_header(result->analysis, "reply-to"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 7, "Return-Path",
|
||||
eml_analysis_get_first_header(result->analysis, "return-path"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 8, "To",
|
||||
eml_analysis_get_first_header(result->analysis, "to"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 9, "Cc",
|
||||
eml_analysis_get_first_header(result->analysis, "cc"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 10, "Subject",
|
||||
eml_analysis_get_first_header(result->analysis, "subject"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 11, "Date brute",
|
||||
eml_analysis_get_first_header(result->analysis, "date"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 12, "Message-ID",
|
||||
eml_analysis_get_first_header(result->analysis, "message-id"));
|
||||
char *received = eml_analysis_dialog_join(
|
||||
eml_analysis_get_header_values(result->analysis, "received"));
|
||||
eml_analysis_dialog_add_field(GTK_GRID(grid), 13,
|
||||
"Received (ordre original)", received);
|
||||
gtk_box_append(GTK_BOX(content), grid);
|
||||
|
||||
GString *attachments = g_string_new(NULL);
|
||||
GString *texts = g_string_new(NULL);
|
||||
GString *metadata = g_string_new(NULL);
|
||||
GString *warnings = g_string_new(NULL);
|
||||
eml_analysis_dialog_append_attachment_summary(attachments, result);
|
||||
eml_analysis_dialog_append_document_summary(
|
||||
texts, metadata, warnings, result);
|
||||
for (guint index = 0; result->warnings != NULL &&
|
||||
index < result->warnings->len; index++)
|
||||
g_string_append_printf(warnings, "%s\n",
|
||||
(char *) g_ptr_array_index(result->warnings, index));
|
||||
for (guint index = 0; result->bank_proposals != NULL &&
|
||||
index < result->bank_proposals->len; index++)
|
||||
{
|
||||
BankProposal *proposal = g_ptr_array_index(
|
||||
result->bank_proposals, index);
|
||||
if (!proposal->is_iban_valid ||
|
||||
proposal->suggested_ocr_fix != NULL)
|
||||
g_string_append_printf(warnings,
|
||||
"Proposition bancaire invalide ou corrigée, "
|
||||
"désélectionnée : %s\n",
|
||||
proposal->raw_iban != NULL ? proposal->raw_iban : "(vide)");
|
||||
}
|
||||
GtkWidget *details = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(details), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(details), 12);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(details), 0,
|
||||
"Pièces jointes", attachments->str);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(details), 1,
|
||||
"Texte PDF et OCR", texts->str);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(details), 2,
|
||||
"Métadonnées", metadata->str);
|
||||
eml_analysis_dialog_add_field(GTK_GRID(details), 3,
|
||||
"Avertissements", warnings->str);
|
||||
gtk_box_append(GTK_BOX(content), details);
|
||||
|
||||
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
eml_analysis_dialog_add_observations(state->proposals_box,
|
||||
eml_analysis_get_observations(result->analysis));
|
||||
GPtrArray *valid_ibans = g_ptr_array_new();
|
||||
for (guint index = 0; result->bank_proposals != NULL &&
|
||||
index < result->bank_proposals->len; index++)
|
||||
{
|
||||
BankProposal *proposal = g_ptr_array_index(
|
||||
result->bank_proposals, index);
|
||||
if (proposal->is_iban_valid && proposal->normalized_iban != NULL &&
|
||||
proposal->suggested_ocr_fix == NULL)
|
||||
g_ptr_array_add(valid_ibans, proposal->normalized_iban);
|
||||
}
|
||||
eml_analysis_dialog_add_proposals(state->proposals_box, "iban",
|
||||
"IBAN valide", valid_ibans);
|
||||
g_ptr_array_unref(valid_ibans);
|
||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||
"Propositions à intégrer explicitement"));
|
||||
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
||||
eml_analysis_dialog_bind_summary(state, content);
|
||||
|
||||
GtkWidget *scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content);
|
||||
gtk_box_append(GTK_BOX(outer), scroll);
|
||||
GtkWidget *actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
GtkWidget *close = gtk_button_new_with_label("Rejeter et fermer");
|
||||
GtkWidget *integrate = gtk_button_new_with_label(
|
||||
"Intégrer les éléments sélectionnés");
|
||||
gtk_widget_add_css_class(integrate, "suggested-action");
|
||||
g_signal_connect(close, "clicked",
|
||||
G_CALLBACK(eml_analysis_dialog_on_close), state);
|
||||
g_signal_connect(integrate, "clicked",
|
||||
G_CALLBACK(eml_analysis_dialog_on_integrate), state);
|
||||
gtk_box_append(GTK_BOX(actions), close);
|
||||
gtk_box_append(GTK_BOX(actions), integrate);
|
||||
gtk_box_append(GTK_BOX(outer), actions);
|
||||
gtk_window_set_child(state->window, outer);
|
||||
g_signal_connect(state->window, "close-request",
|
||||
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "eml-dialog-state",
|
||||
state, eml_analysis_dialog_state_free);
|
||||
labfy_dialog_present(state->window);
|
||||
g_free(received);
|
||||
g_string_free(attachments, TRUE);
|
||||
g_string_free(texts, TRUE);
|
||||
g_string_free(metadata, TRUE);
|
||||
g_string_free(warnings, TRUE);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,691 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file evidence_metadata_dialog.c
|
||||
* @brief Dialogue de modification des métadonnées éditables d'une preuve.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/evidence_metadata_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "models/evidence_type.h"
|
||||
#include "dao/identity_ocr_dao.h"
|
||||
#include "dao/identity_traceability_dao.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "views/evidence_identity_ocr_dialog.h"
|
||||
#include "views/person_vocabulary_adapter.h"
|
||||
#include "views/document_authenticity_editor.h"
|
||||
#include "views/document_identity_misuse_editor.h"
|
||||
#include "widgets/ocr_provenance_overlay.h"
|
||||
|
||||
struct EvidenceMetadataDialogResult
|
||||
{
|
||||
char *type_identifier;
|
||||
char *source;
|
||||
char *description;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
GtkDropDown *type_dropdown;
|
||||
GtkEditable *source_entry;
|
||||
GtkTextBuffer *description_buffer;
|
||||
GtkStringList *type_labels;
|
||||
GPtrArray *type_identifiers;
|
||||
EvidenceMetadataDialogCallback callback;
|
||||
EvidenceMetadataDialogAnalyzeCallback analyze_callback;
|
||||
char *evidence_identifier;
|
||||
char *investigation_root;
|
||||
GPtrArray *ocr_runs;
|
||||
GPtrArray *ocr_documents;
|
||||
GPtrArray *ocr_run_identifiers;
|
||||
GtkStringList *ocr_run_labels;
|
||||
GtkDropDown *ocr_run_dropdown;
|
||||
GtkLabel *ocr_summary;
|
||||
GtkTextBuffer *ocr_raw_buffer;
|
||||
GtkTextBuffer *ocr_corrected_buffer;
|
||||
GtkBox *ocr_details;
|
||||
GtkButton *ocr_revise_button;
|
||||
OcrProvenanceOverlay *ocr_overlay;
|
||||
gulong ocr_selection_handler;
|
||||
guint64 ocr_generation;
|
||||
gpointer user_data;
|
||||
gboolean completed;
|
||||
DocumentAuthenticityEditor *authenticity_editor;
|
||||
DocumentIdentityMisuseEditor *identity_misuse_editor;
|
||||
} EvidenceMetadataDialogContext;
|
||||
|
||||
static void evidence_metadata_dialog_complete(
|
||||
EvidenceMetadataDialogContext *context,
|
||||
EvidenceMetadataDialogResult *result
|
||||
);
|
||||
|
||||
/** @brief Ferme le dialogue avec Échap sans enregistrer. */
|
||||
static gboolean evidence_metadata_dialog_on_key_pressed(
|
||||
GtkEventControllerKey *controller, guint keyval, guint keycode,
|
||||
GdkModifierType state, gpointer user_data
|
||||
)
|
||||
{
|
||||
(void) controller;
|
||||
(void) keycode;
|
||||
(void) state;
|
||||
if (keyval != GDK_KEY_Escape) return FALSE;
|
||||
evidence_metadata_dialog_complete(user_data, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/** @brief Copie un texte facultatif et transforme une chaîne vide en NULL. */
|
||||
static char *evidence_metadata_dialog_copy_optional(const char *text)
|
||||
{
|
||||
char *copy = text != NULL ? g_strdup(text) : NULL;
|
||||
if (copy != NULL) g_strstrip(copy);
|
||||
if (copy != NULL && copy[0] == '\0') g_clear_pointer(©, g_free);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** @brief Libère le contexte détenu par la fenêtre. */
|
||||
static void evidence_metadata_dialog_context_free(gpointer data)
|
||||
{
|
||||
EvidenceMetadataDialogContext *context = data;
|
||||
if (context == NULL) return;
|
||||
g_clear_pointer(&context->type_identifiers, g_ptr_array_unref);
|
||||
g_clear_object(&context->type_labels);
|
||||
g_clear_pointer(&context->ocr_runs, g_ptr_array_unref);
|
||||
g_clear_pointer(&context->ocr_documents, g_ptr_array_unref);
|
||||
g_clear_pointer(&context->ocr_run_identifiers, g_ptr_array_unref);
|
||||
g_clear_object(&context->ocr_run_labels);
|
||||
document_authenticity_editor_free(context->authenticity_editor);
|
||||
document_identity_misuse_editor_free(context->identity_misuse_editor);
|
||||
g_free(context->investigation_root);
|
||||
g_free(context->evidence_identifier);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
static void evidence_metadata_dialog_on_analyze(
|
||||
GtkButton *button, gpointer user_data)
|
||||
{
|
||||
EvidenceMetadataDialogContext *context = user_data;
|
||||
(void) button;
|
||||
if (context != NULL && context->analyze_callback != NULL) {
|
||||
gboolean revise=GPOINTER_TO_INT(g_object_get_data(
|
||||
G_OBJECT(button),"revise-existing"));
|
||||
const char *run_identifier = NULL;
|
||||
if (revise && context->ocr_run_dropdown != NULL) {
|
||||
guint selected = gtk_drop_down_get_selected(
|
||||
context->ocr_run_dropdown);
|
||||
if (selected < context->ocr_run_identifiers->len)
|
||||
run_identifier = g_ptr_array_index(
|
||||
context->ocr_run_identifiers, selected);
|
||||
}
|
||||
if (!revise || run_identifier != NULL)
|
||||
context->analyze_callback(context->window,
|
||||
context->evidence_identifier, revise, run_identifier,
|
||||
context->user_data);
|
||||
evidence_metadata_dialog_complete(context, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Détruit la fenêtre hors de la pile du signal GTK courant. */
|
||||
static gboolean evidence_metadata_dialog_destroy_idle(gpointer data)
|
||||
{
|
||||
GtkWindow *window = GTK_WINDOW(data);
|
||||
gtk_window_destroy(window);
|
||||
g_object_unref(window);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/** @brief Termine le dialogue et transmet éventuellement un résultat. */
|
||||
static void evidence_metadata_dialog_complete(
|
||||
EvidenceMetadataDialogContext *context,
|
||||
EvidenceMetadataDialogResult *result
|
||||
)
|
||||
{
|
||||
EvidenceMetadataDialogCallback callback = NULL;
|
||||
gpointer callback_data = NULL;
|
||||
if (context == NULL || context->completed) return;
|
||||
context->completed = TRUE;
|
||||
callback = context->callback;
|
||||
callback_data = context->user_data;
|
||||
gtk_widget_set_visible(GTK_WIDGET(context->window), FALSE);
|
||||
g_idle_add(
|
||||
evidence_metadata_dialog_destroy_idle,
|
||||
g_object_ref(context->window)
|
||||
);
|
||||
if (callback != NULL) callback(result, callback_data);
|
||||
else evidence_metadata_dialog_result_free(result);
|
||||
}
|
||||
|
||||
/** @brief Annule la modification. */
|
||||
static void evidence_metadata_dialog_on_cancel(
|
||||
GtkButton *button, gpointer user_data
|
||||
)
|
||||
{
|
||||
(void) button;
|
||||
evidence_metadata_dialog_complete(user_data, NULL);
|
||||
}
|
||||
|
||||
/** @brief Traite la fermeture native comme une annulation explicite. */
|
||||
static gboolean evidence_metadata_dialog_on_close_request(
|
||||
GtkWindow *window, gpointer user_data
|
||||
)
|
||||
{
|
||||
(void) window;
|
||||
evidence_metadata_dialog_complete(user_data, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/** @brief Construit et transmet les valeurs modifiées. */
|
||||
static void evidence_metadata_dialog_on_save(
|
||||
GtkButton *button, gpointer user_data
|
||||
)
|
||||
{
|
||||
EvidenceMetadataDialogContext *context = user_data;
|
||||
EvidenceMetadataDialogResult *result = NULL;
|
||||
GtkTextIter start; GtkTextIter end; char *description = NULL;
|
||||
guint selected = 0U;
|
||||
(void) button;
|
||||
if (context == NULL) return;
|
||||
selected = gtk_drop_down_get_selected(context->type_dropdown);
|
||||
if (selected >= context->type_identifiers->len) return;
|
||||
gtk_text_buffer_get_bounds(context->description_buffer, &start, &end);
|
||||
description = gtk_text_buffer_get_text(
|
||||
context->description_buffer, &start, &end, FALSE);
|
||||
result = g_new0(EvidenceMetadataDialogResult, 1);
|
||||
result->type_identifier = g_strdup(
|
||||
g_ptr_array_index(context->type_identifiers, selected));
|
||||
result->source = evidence_metadata_dialog_copy_optional(
|
||||
gtk_editable_get_text(context->source_entry));
|
||||
result->description = evidence_metadata_dialog_copy_optional(description);
|
||||
g_free(description);
|
||||
evidence_metadata_dialog_complete(context, result);
|
||||
}
|
||||
|
||||
static void evidence_metadata_dialog_clear_box(GtkBox *box)
|
||||
{
|
||||
GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(box));
|
||||
while (child != NULL) {
|
||||
GtkWidget *next = gtk_widget_get_next_sibling(child);
|
||||
gtk_box_remove(box, child);
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
|
||||
static IdentityFieldObservation *evidence_metadata_dialog_field_model(
|
||||
const IdentityFieldObservationRecord *record)
|
||||
{
|
||||
IdentitySourceBox box = {
|
||||
.page = (gint) record->page_number,
|
||||
.x = (gint) record->source_x,
|
||||
.y = (gint) record->source_y,
|
||||
.width = (gint) record->source_width,
|
||||
.height = (gint) record->source_height,
|
||||
.image_width = (gint) record->source_image_width,
|
||||
.image_height = (gint) record->source_image_height,
|
||||
.available = record->has_source_box
|
||||
};
|
||||
IdentityFieldObservation *field = record->raw_value != NULL
|
||||
? identity_field_observation_new(record->field_code,
|
||||
record->raw_value,
|
||||
record->has_confidence ? record->confidence : -1.0,
|
||||
&box, (guint) record->display_order)
|
||||
: identity_field_observation_new_manual(record->field_code,
|
||||
record->corrected_value, (guint) record->display_order);
|
||||
if (field != NULL)
|
||||
identity_field_observation_set_origin(field, record->origin);
|
||||
return field;
|
||||
}
|
||||
|
||||
static void evidence_metadata_dialog_show_region(
|
||||
GtkButton *button, gpointer user_data)
|
||||
{
|
||||
EvidenceMetadataDialogContext *context = user_data;
|
||||
IdentityFieldObservationRecord *record = g_object_get_data(
|
||||
G_OBJECT(button), "ocr-field-record");
|
||||
IdentityFieldObservation *field =
|
||||
evidence_metadata_dialog_field_model(record);
|
||||
context->ocr_generation++;
|
||||
ocr_provenance_overlay_set_field(
|
||||
context->ocr_overlay, field, context->ocr_generation);
|
||||
identity_field_observation_free(field);
|
||||
}
|
||||
|
||||
static void evidence_metadata_dialog_render_ocr(
|
||||
EvidenceMetadataDialogContext *context)
|
||||
{
|
||||
guint selected = gtk_drop_down_get_selected(context->ocr_run_dropdown);
|
||||
evidence_metadata_dialog_clear_box(context->ocr_details);
|
||||
context->ocr_generation++;
|
||||
ocr_provenance_overlay_clear(
|
||||
context->ocr_overlay, context->ocr_generation);
|
||||
gtk_text_buffer_set_text(context->ocr_raw_buffer, "", -1);
|
||||
gtk_text_buffer_set_text(context->ocr_corrected_buffer, "", -1);
|
||||
gtk_label_set_text(context->ocr_summary,
|
||||
"Aucune analyse OCR sélectionnée.");
|
||||
if (context->ocr_revise_button != NULL)
|
||||
gtk_widget_set_sensitive(
|
||||
GTK_WIDGET(context->ocr_revise_button), FALSE);
|
||||
if (selected >= context->ocr_runs->len ||
|
||||
selected >= context->ocr_run_identifiers->len) return;
|
||||
|
||||
IdentityOcrRunRecord *run = g_ptr_array_index(
|
||||
context->ocr_runs, selected);
|
||||
ocr_provenance_overlay_set_page(context->ocr_overlay,
|
||||
(guint) run->page_number, context->ocr_generation);
|
||||
const char *selected_identifier = g_ptr_array_index(
|
||||
context->ocr_run_identifiers, selected);
|
||||
if (g_strcmp0(run->id, selected_identifier) != 0) return;
|
||||
char *summary = g_strdup_printf(
|
||||
"Run actif : %s\nUTC : %s\nMoteur : %s %s\n"
|
||||
"Langues : %s\nDocument : %s — %s — page %" G_GINT64_FORMAT
|
||||
"\nStatut : %s\nSHA-256 preuve : %s\n"
|
||||
"Artefact texte : %s\nSHA-256 texte : %s\n"
|
||||
"Artefact TSV : %s\nSHA-256 TSV : %s",
|
||||
run->id, run->executed_at, run->engine, run->engine_version,
|
||||
run->requested_languages, run->document_type, run->document_side,
|
||||
run->page_number, run->status, run->expected_sha256,
|
||||
run->text_relative_path, run->text_sha256,
|
||||
run->tsv_relative_path, run->tsv_sha256);
|
||||
gtk_label_set_text(context->ocr_summary, summary);
|
||||
g_free(summary);
|
||||
char *raw_path = g_build_filename(context->investigation_root,
|
||||
run->text_relative_path, NULL);
|
||||
char *raw = NULL;
|
||||
if (!g_file_get_contents(raw_path, &raw, NULL, NULL))
|
||||
raw = g_strdup("Artefact OCR brut indisponible");
|
||||
gtk_text_buffer_set_text(context->ocr_raw_buffer, raw, -1);
|
||||
gtk_text_buffer_set_text(context->ocr_corrected_buffer,
|
||||
run->corrected_transcription != NULL
|
||||
? run->corrected_transcription
|
||||
: "Aucune transcription corrigée", -1);
|
||||
g_free(raw_path);
|
||||
g_free(raw);
|
||||
|
||||
IdentityDocumentObservationRecord *document = NULL;
|
||||
for (guint index = 0; index < context->ocr_documents->len; index++) {
|
||||
IdentityDocumentObservationRecord *candidate =
|
||||
g_ptr_array_index(context->ocr_documents, index);
|
||||
if (g_strcmp0(candidate->ocr_run_id, run->id) == 0) {
|
||||
document = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (document == NULL) {
|
||||
gtk_box_append(context->ocr_details,
|
||||
gtk_label_new("Observation OCR persistée indisponible."));
|
||||
return;
|
||||
}
|
||||
char *description = g_strdup_printf(
|
||||
"Observation %s\nPersonne liée : %s\nDocument : %s — %s — "
|
||||
"page %" G_GINT64_FORMAT "\nÉtat : %s\nNotes : %s",
|
||||
document->id,
|
||||
document->person_id != NULL ? document->person_id
|
||||
: "Personne indisponible",
|
||||
document->document_type, document->document_side,
|
||||
document->page_number, document->review_state,
|
||||
document->factual_notes != NULL ? document->factual_notes
|
||||
: "Aucune note");
|
||||
GtkWidget *document_label = gtk_label_new(description);
|
||||
gtk_widget_set_name(document_label, "evidence-ocr-observation");
|
||||
gtk_label_set_wrap(GTK_LABEL(document_label), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(document_label), 0.0f);
|
||||
gtk_box_append(context->ocr_details, document_label);
|
||||
g_free(description);
|
||||
|
||||
IdentityOcrDao *dao = g_object_get_data(
|
||||
G_OBJECT(context->window), "identity-ocr-dao");
|
||||
GError *error = NULL;
|
||||
GPtrArray *fields = identity_ocr_dao_list_fields_by_document(
|
||||
dao, document->id, &error);
|
||||
for (guint index = 0; fields != NULL && index < fields->len; index++) {
|
||||
IdentityFieldObservationRecord *field =
|
||||
g_ptr_array_index(fields, index);
|
||||
char *field_text = g_strdup_printf(
|
||||
"%s — brut : %s — corrigé : %s — statut : %s — "
|
||||
"origine : %s — page %" G_GINT64_FORMAT,
|
||||
field->field_code,
|
||||
field->raw_value != NULL ? field->raw_value : "absent",
|
||||
field->corrected_value != NULL
|
||||
? field->corrected_value : "absente",
|
||||
field->review_status, field->origin, field->page_number);
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget *label = gtk_label_new(field_text);
|
||||
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
gtk_box_append(GTK_BOX(row), label);
|
||||
GtkWidget *show = gtk_button_new_with_label("Voir la zone");
|
||||
gtk_widget_set_sensitive(show, field->has_source_box);
|
||||
g_object_set_data(G_OBJECT(show), "ocr-field-record", field);
|
||||
g_signal_connect(show, "clicked",
|
||||
G_CALLBACK(evidence_metadata_dialog_show_region), context);
|
||||
gtk_box_append(GTK_BOX(row), show);
|
||||
gtk_box_append(context->ocr_details, row);
|
||||
g_free(field_text);
|
||||
}
|
||||
g_object_set_data_full(G_OBJECT(context->ocr_details),
|
||||
"active-ocr-fields", fields, fields != NULL
|
||||
? (GDestroyNotify) g_ptr_array_unref : NULL);
|
||||
g_clear_error(&error);
|
||||
if (context->ocr_revise_button != NULL)
|
||||
gtk_widget_set_sensitive(
|
||||
GTK_WIDGET(context->ocr_revise_button), TRUE);
|
||||
}
|
||||
|
||||
static void evidence_metadata_dialog_ocr_selected(
|
||||
GObject *object, GParamSpec *spec, gpointer user_data)
|
||||
{
|
||||
(void) object;
|
||||
(void) spec;
|
||||
evidence_metadata_dialog_render_ocr(user_data);
|
||||
}
|
||||
|
||||
static GtkWidget *evidence_metadata_dialog_build_ocr(
|
||||
EvidenceMetadataDialogContext *context, Database *database,
|
||||
const char *root, const EvidenceRecord *record)
|
||||
{
|
||||
if (database == NULL || root == NULL) return NULL;
|
||||
GError *error = NULL;
|
||||
IdentityOcrDao *dao = identity_ocr_dao_new(database);
|
||||
context->ocr_runs = identity_ocr_dao_list_runs_by_evidence(
|
||||
dao, evidence_record_get_identifier(record), &error);
|
||||
context->ocr_documents = identity_ocr_dao_list_documents_by_evidence(
|
||||
dao, evidence_record_get_identifier(record), &error);
|
||||
if (context->ocr_runs == NULL || context->ocr_runs->len == 0 ||
|
||||
context->ocr_documents == NULL) {
|
||||
g_clear_error(&error);
|
||||
identity_ocr_dao_free(dao);
|
||||
return NULL;
|
||||
}
|
||||
g_object_set_data_full(G_OBJECT(context->window), "identity-ocr-dao",
|
||||
dao, (GDestroyNotify) identity_ocr_dao_free);
|
||||
context->investigation_root = g_strdup(root);
|
||||
context->ocr_run_identifiers =
|
||||
g_ptr_array_new_with_free_func(g_free);
|
||||
context->ocr_run_labels = gtk_string_list_new(NULL);
|
||||
for (guint index = 0; index < context->ocr_runs->len; index++) {
|
||||
IdentityOcrRunRecord *run =
|
||||
g_ptr_array_index(context->ocr_runs, index);
|
||||
char *short_id = g_strndup(run->id, 8);
|
||||
char *label = g_strdup_printf(
|
||||
"%s — %s — %s — %s/%s — p.%" G_GINT64_FORMAT
|
||||
" — %s — %s",
|
||||
run->executed_at, run->engine, run->requested_languages,
|
||||
run->document_type, run->document_side, run->page_number,
|
||||
run->status, short_id);
|
||||
gtk_string_list_append(context->ocr_run_labels, label);
|
||||
g_ptr_array_add(context->ocr_run_identifiers, g_strdup(run->id));
|
||||
g_free(short_id);
|
||||
g_free(label);
|
||||
}
|
||||
GtkWidget *section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_name(section, "evidence-ocr-readonly");
|
||||
gtk_box_append(GTK_BOX(section), gtk_label_new(
|
||||
"Analyse OCR d’identité"));
|
||||
context->ocr_run_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||
G_LIST_MODEL(g_object_ref(context->ocr_run_labels)), NULL));
|
||||
gtk_widget_set_name(GTK_WIDGET(context->ocr_run_dropdown),
|
||||
"evidence-ocr-run-selector");
|
||||
gtk_box_append(GTK_BOX(section),
|
||||
GTK_WIDGET(context->ocr_run_dropdown));
|
||||
context->ocr_summary = GTK_LABEL(gtk_label_new(NULL));
|
||||
gtk_widget_set_name(GTK_WIDGET(context->ocr_summary),
|
||||
"evidence-ocr-active-run");
|
||||
gtk_label_set_wrap(context->ocr_summary, TRUE);
|
||||
gtk_label_set_xalign(context->ocr_summary, 0.0f);
|
||||
gtk_box_append(GTK_BOX(section), GTK_WIDGET(context->ocr_summary));
|
||||
GtkWidget *raw = gtk_text_view_new();
|
||||
gtk_widget_set_name(raw, "evidence-ocr-raw");
|
||||
gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
|
||||
context->ocr_raw_buffer =
|
||||
gtk_text_view_get_buffer(GTK_TEXT_VIEW(raw));
|
||||
gtk_box_append(GTK_BOX(section), gtk_label_new("Texte OCR brut"));
|
||||
gtk_box_append(GTK_BOX(section), raw);
|
||||
GtkWidget *corrected = gtk_text_view_new();
|
||||
gtk_widget_set_name(corrected, "evidence-ocr-corrected");
|
||||
gtk_text_view_set_editable(GTK_TEXT_VIEW(corrected), FALSE);
|
||||
context->ocr_corrected_buffer =
|
||||
gtk_text_view_get_buffer(GTK_TEXT_VIEW(corrected));
|
||||
gtk_box_append(GTK_BOX(section),
|
||||
gtk_label_new("Transcription corrigée"));
|
||||
gtk_box_append(GTK_BOX(section), corrected);
|
||||
context->ocr_details = GTK_BOX(gtk_box_new(
|
||||
GTK_ORIENTATION_VERTICAL, 6));
|
||||
gtk_widget_set_name(GTK_WIDGET(context->ocr_details),
|
||||
"evidence-ocr-details");
|
||||
gtk_box_append(GTK_BOX(section), GTK_WIDGET(context->ocr_details));
|
||||
context->ocr_overlay = ocr_provenance_overlay_new();
|
||||
g_object_set_data_full(G_OBJECT(section), "ocr-readonly-overlay",
|
||||
context->ocr_overlay, (GDestroyNotify) ocr_provenance_overlay_free);
|
||||
gtk_box_append(GTK_BOX(section),
|
||||
ocr_provenance_overlay_get_widget(context->ocr_overlay));
|
||||
context->ocr_selection_handler = g_signal_connect(
|
||||
context->ocr_run_dropdown, "notify::selected",
|
||||
G_CALLBACK(evidence_metadata_dialog_ocr_selected), context);
|
||||
/*
|
||||
* Politique d'ouverture et de rafraîchissement : le dernier run inséré
|
||||
* devient actif. La révision, elle, lit toujours l'UUID de la sélection
|
||||
* courante et ne déduit jamais un run de cette politique.
|
||||
*/
|
||||
gtk_drop_down_set_selected(context->ocr_run_dropdown,
|
||||
context->ocr_runs->len - 1);
|
||||
return section;
|
||||
}
|
||||
|
||||
gboolean evidence_metadata_dialog_present_with_ocr(
|
||||
GtkWindow *parent, const EvidenceRecord *record,
|
||||
const GPtrArray *evidence_types, Database *database,
|
||||
const char *investigation_root,
|
||||
EvidenceMetadataDialogAnalyzeCallback analyze_callback,
|
||||
EvidenceMetadataDialogCallback callback, gpointer user_data
|
||||
)
|
||||
{
|
||||
EvidenceMetadataDialogContext *context = NULL;
|
||||
GtkStringList *labels = NULL; GtkWidget *main_box = NULL;
|
||||
GtkWidget *description_view = NULL; GtkWidget *description_scroll = NULL;
|
||||
GtkWidget *buttons = NULL; GtkWidget *cancel = NULL; GtkWidget *save = NULL;
|
||||
guint selected = 0U;
|
||||
if (record == NULL || evidence_types == NULL || evidence_types->len == 0U ||
|
||||
callback == NULL) return FALSE;
|
||||
context = g_new0(EvidenceMetadataDialogContext, 1);
|
||||
context->window = GTK_WINDOW(gtk_window_new());
|
||||
context->callback = callback; context->user_data = user_data;
|
||||
context->analyze_callback = analyze_callback;
|
||||
context->evidence_identifier =
|
||||
g_strdup(evidence_record_get_identifier(record));
|
||||
context->type_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||
labels = gtk_string_list_new(NULL);
|
||||
context->type_labels = labels;
|
||||
for (guint index = 0U; index < evidence_types->len; index++)
|
||||
{
|
||||
const EvidenceType *type = g_ptr_array_index((GPtrArray *) evidence_types, index);
|
||||
gtk_string_list_append(labels, evidence_type_get_label(type));
|
||||
g_ptr_array_add(context->type_identifiers,
|
||||
g_strdup(evidence_type_get_code(type)));
|
||||
if (g_strcmp0(evidence_type_get_code(type),
|
||||
evidence_record_get_type_identifier(record)) == 0) selected = index;
|
||||
}
|
||||
gtk_window_set_title(context->window, "Modifier les métadonnées");
|
||||
labfy_dialog_prepare(context->window, parent, TRUE, TRUE);
|
||||
if (parent != NULL) {
|
||||
gtk_window_set_application(context->window,
|
||||
gtk_window_get_application(parent));
|
||||
}
|
||||
g_object_set_data_full(G_OBJECT(context->window), "evidence-metadata-context",
|
||||
context, evidence_metadata_dialog_context_free);
|
||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(main_box, 18); gtk_widget_set_margin_end(main_box, 18);
|
||||
gtk_widget_set_margin_top(main_box, 18); gtk_widget_set_margin_bottom(main_box, 18);
|
||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new(
|
||||
"Le fichier, son UUID, sa taille, sa date d'import et son SHA-256 resteront inchangés."));
|
||||
context->type_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||
G_LIST_MODEL(g_object_ref(labels)), NULL));
|
||||
gtk_drop_down_set_selected(context->type_dropdown, selected);
|
||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Type de preuve"));
|
||||
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->type_dropdown));
|
||||
context->source_entry = GTK_EDITABLE(gtk_entry_new());
|
||||
gtk_editable_set_text(context->source_entry,
|
||||
evidence_record_get_source(record) != NULL
|
||||
? evidence_record_get_source(record) : "");
|
||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Source"));
|
||||
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->source_entry));
|
||||
description_view = gtk_text_view_new();
|
||||
context->description_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(description_view));
|
||||
gtk_text_buffer_set_text(context->description_buffer,
|
||||
evidence_record_get_description(record) != NULL
|
||||
? evidence_record_get_description(record) : "", -1);
|
||||
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(description_view), GTK_WRAP_WORD_CHAR);
|
||||
description_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(description_scroll, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(description_scroll), description_view);
|
||||
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Description"));
|
||||
gtk_box_append(GTK_BOX(main_box), description_scroll);
|
||||
GtkWidget *ocr = evidence_metadata_dialog_build_ocr(
|
||||
context, database, investigation_root, record);
|
||||
if (ocr != NULL) {
|
||||
GtkWidget *ocr_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_size_request(ocr_scroll, -1, 260);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(ocr_scroll), ocr);
|
||||
gtk_box_append(GTK_BOX(main_box), ocr_scroll);
|
||||
}
|
||||
|
||||
if (database != NULL) {
|
||||
context->authenticity_editor = document_authenticity_editor_new(
|
||||
database, evidence_record_get_identifier(record));
|
||||
if (context->authenticity_editor != NULL) {
|
||||
GtkWidget *auth_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_size_request(auth_scroll, -1, 300);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(auth_scroll),
|
||||
document_authenticity_editor_get_widget(context->authenticity_editor));
|
||||
gtk_box_append(GTK_BOX(main_box), auth_scroll);
|
||||
}
|
||||
context->identity_misuse_editor = document_identity_misuse_editor_new(
|
||||
database, evidence_record_get_identifier(record));
|
||||
if (context->identity_misuse_editor != NULL) {
|
||||
GtkWidget *misuse_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_size_request(misuse_scroll, -1, 300);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(misuse_scroll),
|
||||
document_identity_misuse_editor_get_widget(
|
||||
context->identity_misuse_editor));
|
||||
gtk_box_append(GTK_BOX(main_box), misuse_scroll);
|
||||
}
|
||||
IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database);
|
||||
if (trace_dao != NULL) {
|
||||
GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence(
|
||||
trace_dao, evidence_record_get_identifier(record), NULL);
|
||||
if (relations != NULL && relations->len > 0) {
|
||||
GtkWidget *rel_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_box_append(GTK_BOX(rel_box), gtk_label_new("Relations factuelles avec les personnes"));
|
||||
EntityDao *entity_dao = entity_dao_new(database, NULL);
|
||||
for (guint i = 0; i < relations->len; i++) {
|
||||
PersonEvidenceFactualRelation *rel = g_ptr_array_index(relations, i);
|
||||
const char *person_id = person_evidence_factual_relation_get_person_identifier(rel);
|
||||
const char *rel_type = person_vocabulary_adapter_relation_label(
|
||||
person_evidence_factual_relation_get_relation_type(rel));
|
||||
const char *note = person_evidence_factual_relation_get_factual_note(rel);
|
||||
char *designation = NULL;
|
||||
if (entity_dao != NULL) {
|
||||
EntityRecord *person = entity_dao_find_by_identifier(entity_dao, person_id, NULL);
|
||||
if (person != NULL) {
|
||||
const char *label = entity_record_get_label(person);
|
||||
const char *value = entity_record_get_value(person);
|
||||
designation = g_strdup(label && label[0] ? label : (value ? value : ""));
|
||||
entity_record_free(person);
|
||||
}
|
||||
}
|
||||
if (designation == NULL) designation = g_strdup("Personne inconnue");
|
||||
const char *run = person_evidence_factual_relation_get_ocr_run_identifier(rel);
|
||||
char *text = g_strdup_printf("• %s : %s\n Date : %s — Origine : %s%s%s",
|
||||
designation, rel_type,
|
||||
person_evidence_factual_relation_get_observed_at(rel),
|
||||
person_evidence_factual_relation_get_origin(rel),
|
||||
run ? " — OcrRun : " : "", run ? run : "");
|
||||
if (note && note[0]) {
|
||||
char *tmp = g_strdup_printf("%s\n Note : %s", text, note);
|
||||
g_free(text);
|
||||
text = tmp;
|
||||
}
|
||||
GtkWidget *label = gtk_label_new(text);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
gtk_box_append(GTK_BOX(rel_box), label);
|
||||
g_free(text);
|
||||
g_free(designation);
|
||||
}
|
||||
if (entity_dao != NULL) entity_dao_free(entity_dao);
|
||||
GtkWidget *rel_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(rel_scroll), rel_box);
|
||||
gtk_widget_set_size_request(rel_scroll, -1, 100);
|
||||
gtk_box_append(GTK_BOX(main_box), rel_scroll);
|
||||
g_ptr_array_unref(relations);
|
||||
}
|
||||
identity_traceability_dao_free(trace_dao);
|
||||
}
|
||||
}
|
||||
|
||||
buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(buttons, GTK_ALIGN_END);
|
||||
cancel = gtk_button_new_with_label("Annuler");
|
||||
save = gtk_button_new_with_label("Enregistrer");
|
||||
if (analyze_callback != NULL && ocr != NULL) {
|
||||
GtkWidget *revise = gtk_button_new_with_label(
|
||||
"Réviser l’analyse OCR");
|
||||
context->ocr_revise_button = GTK_BUTTON(revise);
|
||||
gtk_widget_set_name(revise, "revise-existing-identity-ocr");
|
||||
g_object_set_data(G_OBJECT(revise),"revise-existing",
|
||||
GINT_TO_POINTER(TRUE));
|
||||
g_signal_connect(revise, "clicked",
|
||||
G_CALLBACK(evidence_metadata_dialog_on_analyze), context);
|
||||
gtk_box_append(GTK_BOX(buttons), revise);
|
||||
evidence_metadata_dialog_render_ocr(context);
|
||||
}
|
||||
if (analyze_callback != NULL &&
|
||||
evidence_identity_ocr_dialog_file_is_compatible(
|
||||
evidence_record_get_original_name(record))) {
|
||||
GtkWidget *analyze = gtk_button_new_with_label(
|
||||
"Relancer une nouvelle analyse OCR");
|
||||
gtk_widget_set_name(analyze, "analyze-existing-identity");
|
||||
g_signal_connect(analyze, "clicked",
|
||||
G_CALLBACK(evidence_metadata_dialog_on_analyze), context);
|
||||
gtk_box_append(GTK_BOX(buttons), analyze);
|
||||
}
|
||||
gtk_widget_add_css_class(save, "suggested-action");
|
||||
g_signal_connect(cancel, "clicked", G_CALLBACK(evidence_metadata_dialog_on_cancel), context);
|
||||
g_signal_connect(save, "clicked", G_CALLBACK(evidence_metadata_dialog_on_save), context);
|
||||
g_signal_connect(context->window, "close-request",
|
||||
G_CALLBACK(evidence_metadata_dialog_on_close_request), context);
|
||||
{
|
||||
GtkEventController *keys = gtk_event_controller_key_new();
|
||||
g_signal_connect(keys, "key-pressed",
|
||||
G_CALLBACK(evidence_metadata_dialog_on_key_pressed), context);
|
||||
gtk_widget_add_controller(GTK_WIDGET(context->window), keys);
|
||||
}
|
||||
gtk_box_append(GTK_BOX(buttons), cancel); gtk_box_append(GTK_BOX(buttons), save);
|
||||
gtk_box_append(GTK_BOX(main_box), buttons);
|
||||
gtk_window_set_child(context->window, main_box);
|
||||
labfy_dialog_present(context->window);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean evidence_metadata_dialog_present(
|
||||
GtkWindow *parent, const EvidenceRecord *record,
|
||||
const GPtrArray *evidence_types, EvidenceMetadataDialogCallback callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
return evidence_metadata_dialog_present_with_ocr(
|
||||
parent, record, evidence_types, NULL, NULL, NULL,
|
||||
callback, user_data);
|
||||
}
|
||||
|
||||
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result)
|
||||
{
|
||||
if (result == NULL) return;
|
||||
g_free(result->type_identifier); g_free(result->source);
|
||||
g_free(result->description); g_free(result);
|
||||
}
|
||||
const char *evidence_metadata_dialog_result_get_type_identifier(
|
||||
const EvidenceMetadataDialogResult *result)
|
||||
{ return result != NULL ? result->type_identifier : NULL; }
|
||||
const char *evidence_metadata_dialog_result_get_source(
|
||||
const EvidenceMetadataDialogResult *result)
|
||||
{ return result != NULL ? result->source : NULL; }
|
||||
const char *evidence_metadata_dialog_result_get_description(
|
||||
const EvidenceMetadataDialogResult *result)
|
||||
{ return result != NULL ? result->description : NULL; }
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file file_dialog.c
|
||||
* @brief Implémentation du dialogue de sélection d’un fichier de preuve.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/file_dialog.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
/**
|
||||
* @brief Contexte conservé pendant l’opération asynchrone.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
FileDialogCallback callback;
|
||||
gpointer user_data;
|
||||
} FileDialogContext;
|
||||
|
||||
/** @brief Contexte conservé pendant une sélection multiple. */
|
||||
typedef struct
|
||||
{
|
||||
FileDialogMultipleCallback callback;
|
||||
gpointer user_data;
|
||||
} FileDialogMultipleContext;
|
||||
|
||||
/**
|
||||
* @brief Libère le contexte du dialogue.
|
||||
*/
|
||||
static void file_dialog_context_free(
|
||||
FileDialogContext *context
|
||||
)
|
||||
{
|
||||
g_free(
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transmet le résultat au code appelant.
|
||||
*/
|
||||
static void file_dialog_emit_result(
|
||||
FileDialogContext *context,
|
||||
const char *file_path,
|
||||
const GError *error
|
||||
)
|
||||
{
|
||||
if (context == NULL ||
|
||||
context->callback == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context->callback(
|
||||
file_path,
|
||||
error,
|
||||
context->user_data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Traite le résultat du sélecteur de fichier.
|
||||
*/
|
||||
static void file_dialog_on_file_selected(
|
||||
GObject *source_object,
|
||||
GAsyncResult *result,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *dialog =
|
||||
GTK_FILE_DIALOG(
|
||||
source_object
|
||||
);
|
||||
|
||||
FileDialogContext *context =
|
||||
user_data;
|
||||
|
||||
GFile *file = NULL;
|
||||
|
||||
char *file_path = NULL;
|
||||
|
||||
GError *error = NULL;
|
||||
GError *path_error = NULL;
|
||||
|
||||
if (context == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
file =
|
||||
gtk_file_dialog_open_finish(
|
||||
dialog,
|
||||
result,
|
||||
&error
|
||||
);
|
||||
|
||||
if (file == NULL)
|
||||
{
|
||||
/*
|
||||
* Fermer volontairement le dialogue n’est pas une erreur.
|
||||
*/
|
||||
if (error != NULL &&
|
||||
g_error_matches(
|
||||
error,
|
||||
GTK_DIALOG_ERROR,
|
||||
GTK_DIALOG_ERROR_DISMISSED
|
||||
))
|
||||
{
|
||||
file_dialog_emit_result(
|
||||
context,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
file_dialog_emit_result(
|
||||
context,
|
||||
NULL,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
g_clear_error(
|
||||
&error
|
||||
);
|
||||
|
||||
file_dialog_context_free(
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
file_path =
|
||||
g_file_get_path(
|
||||
file
|
||||
);
|
||||
|
||||
/*
|
||||
* g_file_get_path() retourne NULL pour une ressource qui ne
|
||||
* correspond pas à un fichier local.
|
||||
*/
|
||||
if (file_path == NULL)
|
||||
{
|
||||
g_set_error_literal(
|
||||
&path_error,
|
||||
G_IO_ERROR,
|
||||
G_IO_ERROR_NOT_SUPPORTED,
|
||||
"Le fichier sélectionné ne possède pas de chemin local."
|
||||
);
|
||||
|
||||
file_dialog_emit_result(
|
||||
context,
|
||||
NULL,
|
||||
path_error
|
||||
);
|
||||
|
||||
g_clear_error(
|
||||
&path_error
|
||||
);
|
||||
|
||||
g_object_unref(
|
||||
file
|
||||
);
|
||||
|
||||
file_dialog_context_free(
|
||||
context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
file_dialog_emit_result(
|
||||
context,
|
||||
file_path,
|
||||
NULL
|
||||
);
|
||||
|
||||
g_free(
|
||||
file_path
|
||||
);
|
||||
|
||||
g_object_unref(
|
||||
file
|
||||
);
|
||||
|
||||
file_dialog_context_free(
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
void file_dialog_select_file(
|
||||
GtkWindow *parent,
|
||||
FileDialogCallback callback,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *dialog = NULL;
|
||||
|
||||
FileDialogContext *context = NULL;
|
||||
|
||||
if (callback == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context =
|
||||
g_new0(
|
||||
FileDialogContext,
|
||||
1
|
||||
);
|
||||
|
||||
context->callback =
|
||||
callback;
|
||||
|
||||
context->user_data =
|
||||
user_data;
|
||||
|
||||
dialog =
|
||||
gtk_file_dialog_new();
|
||||
|
||||
gtk_file_dialog_set_title(
|
||||
dialog,
|
||||
"Sélectionner un fichier de preuve"
|
||||
);
|
||||
|
||||
gtk_file_dialog_set_modal(
|
||||
dialog,
|
||||
TRUE
|
||||
);
|
||||
|
||||
gtk_file_dialog_open(
|
||||
dialog,
|
||||
parent,
|
||||
NULL,
|
||||
file_dialog_on_file_selected,
|
||||
context
|
||||
);
|
||||
|
||||
g_object_unref(
|
||||
dialog
|
||||
);
|
||||
}
|
||||
|
||||
/** @brief Traite le résultat du sélecteur multiple. */
|
||||
static void file_dialog_on_files_selected(
|
||||
GObject *source_object, GAsyncResult *result, gpointer user_data
|
||||
)
|
||||
{
|
||||
FileDialogMultipleContext *context = user_data;
|
||||
GListModel *files = NULL;
|
||||
GPtrArray *paths = NULL;
|
||||
GError *error = NULL;
|
||||
files = gtk_file_dialog_open_multiple_finish(
|
||||
GTK_FILE_DIALOG(source_object), result, &error);
|
||||
if (files == NULL)
|
||||
{
|
||||
if (error != NULL && g_error_matches(
|
||||
error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
|
||||
context->callback(NULL, NULL, context->user_data);
|
||||
else context->callback(NULL, error, context->user_data);
|
||||
g_clear_error(&error); g_free(context); return;
|
||||
}
|
||||
paths = g_ptr_array_new_with_free_func(g_free);
|
||||
for (guint index = 0U; index < g_list_model_get_n_items(files); index++)
|
||||
{
|
||||
GFile *file = g_list_model_get_item(files, index);
|
||||
char *path = g_file_get_path(file);
|
||||
if (path == NULL)
|
||||
{
|
||||
g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
||||
"Un fichier sélectionné ne possède pas de chemin local.");
|
||||
g_object_unref(file); break;
|
||||
}
|
||||
g_ptr_array_add(paths, path); g_object_unref(file);
|
||||
}
|
||||
if (error != NULL) context->callback(NULL, error, context->user_data);
|
||||
else context->callback(paths, NULL, context->user_data);
|
||||
g_clear_error(&error); g_ptr_array_unref(paths); g_object_unref(files);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
void file_dialog_select_files(
|
||||
GtkWindow *parent, FileDialogMultipleCallback callback, gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *dialog = NULL;
|
||||
FileDialogMultipleContext *context = NULL;
|
||||
if (callback == NULL) return;
|
||||
context = g_new0(FileDialogMultipleContext, 1);
|
||||
context->callback = callback; context->user_data = user_data;
|
||||
dialog = gtk_file_dialog_new();
|
||||
gtk_file_dialog_set_title(dialog, "Sélectionner des fichiers de preuve");
|
||||
gtk_file_dialog_set_modal(dialog, TRUE);
|
||||
gtk_file_dialog_open_multiple(dialog, parent, NULL,
|
||||
file_dialog_on_files_selected, context);
|
||||
g_object_unref(dialog);
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file folder_dialog.c
|
||||
* @brief Implémentation du dialogue de sélection d'un dossier d'enquête.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/folder_dialog.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
/**
|
||||
* @struct FolderDialogContext
|
||||
* @brief Conserve les données nécessaires pendant l'opération asynchrone.
|
||||
*
|
||||
* Le dialogue GTK ne se termine pas immédiatement. Ce contexte permet donc
|
||||
* de conserver le callback et ses données jusqu'à la fermeture du dialogue.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
FolderDialogCallback callback;
|
||||
gpointer user_data;
|
||||
} FolderDialogContext;
|
||||
|
||||
/**
|
||||
* @brief Libère le contexte associé au dialogue.
|
||||
*
|
||||
* @param context Contexte à libérer.
|
||||
*/
|
||||
static void folder_dialog_context_free(FolderDialogContext *context)
|
||||
{
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Traite le résultat de la sélection du dossier.
|
||||
*
|
||||
* @param source_object Objet ayant lancé l'opération asynchrone.
|
||||
* @param result Résultat transmis par GTK.
|
||||
* @param user_data Contexte du dialogue.
|
||||
*/
|
||||
static void folder_dialog_on_folder_selected(
|
||||
GObject *source_object,
|
||||
GAsyncResult *result,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *dialog = GTK_FILE_DIALOG(source_object);
|
||||
FolderDialogContext *context = user_data;
|
||||
GError *error = NULL;
|
||||
GFile *folder = NULL;
|
||||
char *folder_path = NULL;
|
||||
|
||||
folder = gtk_file_dialog_select_folder_finish(dialog, result, &error);
|
||||
|
||||
if (folder == NULL)
|
||||
{
|
||||
/*
|
||||
* Une annulation est un comportement normal de l'utilisateur.
|
||||
* Elle est transmise au code appelant sous la forme d'un chemin NULL.
|
||||
*/
|
||||
if (error != NULL &&
|
||||
!g_error_matches(error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
|
||||
{
|
||||
g_warning(
|
||||
"Impossible de sélectionner le dossier : %s",
|
||||
error->message
|
||||
);
|
||||
}
|
||||
|
||||
if (context->callback != NULL)
|
||||
{
|
||||
context->callback(NULL, context->user_data);
|
||||
}
|
||||
|
||||
g_clear_error(&error);
|
||||
folder_dialog_context_free(context);
|
||||
return;
|
||||
}
|
||||
|
||||
folder_path = g_file_get_path(folder);
|
||||
|
||||
if (context->callback != NULL)
|
||||
{
|
||||
context->callback(folder_path, context->user_data);
|
||||
}
|
||||
|
||||
g_free(folder_path);
|
||||
g_object_unref(folder);
|
||||
folder_dialog_context_free(context);
|
||||
}
|
||||
|
||||
void folder_dialog_select_folder(
|
||||
GtkWindow *parent,
|
||||
FolderDialogCallback callback,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
GtkFileDialog *dialog = NULL;
|
||||
FolderDialogContext *context = NULL;
|
||||
|
||||
context = g_new0(FolderDialogContext, 1);
|
||||
context->callback = callback;
|
||||
context->user_data = user_data;
|
||||
|
||||
dialog = gtk_file_dialog_new();
|
||||
|
||||
gtk_file_dialog_set_title(
|
||||
dialog,
|
||||
"Sélectionner un dossier d'enquête"
|
||||
);
|
||||
|
||||
gtk_file_dialog_set_modal(dialog, TRUE);
|
||||
|
||||
gtk_file_dialog_select_folder(
|
||||
dialog,
|
||||
parent,
|
||||
NULL,
|
||||
folder_dialog_on_folder_selected,
|
||||
context
|
||||
);
|
||||
|
||||
g_object_unref(dialog);
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
#include "views/identity_ocr_language_loader.h"
|
||||
#include <gio/gio.h>
|
||||
|
||||
typedef struct {
|
||||
char *executable;
|
||||
GWeakRef window;
|
||||
GWeakRef dropdown;
|
||||
} IdentityLanguageJob;
|
||||
|
||||
static void identity_language_job_free(gpointer data)
|
||||
{
|
||||
IdentityLanguageJob *job = data;
|
||||
if (job != NULL) {
|
||||
g_free(job->executable);
|
||||
g_weak_ref_clear(&job->window);
|
||||
g_weak_ref_clear(&job->dropdown);
|
||||
g_free(job);
|
||||
}
|
||||
}
|
||||
|
||||
static void identity_languages_worker(GTask *task, gpointer source,
|
||||
gpointer task_data, GCancellable *cancellable)
|
||||
{
|
||||
(void)source; (void)cancellable;
|
||||
IdentityLanguageJob *job = task_data;
|
||||
gchar *stdout_text = NULL;
|
||||
gboolean success = g_spawn_command_line_sync(
|
||||
g_strdup_printf("'%s' --list-langs", job->executable),
|
||||
&stdout_text, NULL, NULL, NULL);
|
||||
if (success && stdout_text != NULL) {
|
||||
GPtrArray *langs = g_ptr_array_new_with_free_func(g_free);
|
||||
gchar **lines = g_strsplit(stdout_text, "\n", -1);
|
||||
for (gsize i = 1; lines[i] != NULL; i++) {
|
||||
gchar *lang = g_strstrip(g_strdup(lines[i]));
|
||||
if (lang[0] != '\0') g_ptr_array_add(langs, lang);
|
||||
else g_free(lang);
|
||||
}
|
||||
g_strfreev(lines);
|
||||
g_task_return_pointer(task, langs, (GDestroyNotify)g_ptr_array_unref);
|
||||
} else {
|
||||
g_task_return_pointer(task, NULL, NULL);
|
||||
}
|
||||
g_free(stdout_text);
|
||||
}
|
||||
|
||||
static void identity_languages_completed(GObject *source,
|
||||
GAsyncResult *result, gpointer data)
|
||||
{
|
||||
(void)source;
|
||||
GTask *task = G_TASK(result);
|
||||
IdentityLanguageJob *job = data;
|
||||
GPtrArray *langs = g_task_propagate_pointer(task, NULL);
|
||||
if (langs != NULL) {
|
||||
GtkWindow *window = GTK_WINDOW(g_weak_ref_get(&job->window));
|
||||
GtkDropDown *dropdown = GTK_DROP_DOWN(g_weak_ref_get(&job->dropdown));
|
||||
if (window != NULL && dropdown != NULL) {
|
||||
GtkStringList *list = gtk_string_list_new(NULL);
|
||||
gtk_string_list_append(list, "Fra (français)");
|
||||
for (guint i = 0; i < langs->len; i++) {
|
||||
const char *lang = g_ptr_array_index(langs, i);
|
||||
if (g_strcmp0(lang, "fra") != 0) {
|
||||
gtk_string_list_append(list, lang);
|
||||
}
|
||||
}
|
||||
gtk_drop_down_set_model(dropdown, G_LIST_MODEL(list));
|
||||
}
|
||||
if (window != NULL) g_object_unref(window);
|
||||
if (dropdown != NULL) g_object_unref(dropdown);
|
||||
g_ptr_array_unref(langs);
|
||||
}
|
||||
}
|
||||
|
||||
void identity_ocr_language_loader_start(GtkWindow *window, const char *tesseract_path, GtkDropDown *language_dropdown)
|
||||
{
|
||||
if (tesseract_path == NULL || window == NULL || language_dropdown == NULL) return;
|
||||
IdentityLanguageJob *job = g_new0(IdentityLanguageJob, 1);
|
||||
job->executable = g_strdup(tesseract_path);
|
||||
g_weak_ref_init(&job->window, G_OBJECT(window));
|
||||
g_weak_ref_init(&job->dropdown, G_OBJECT(language_dropdown));
|
||||
GTask *task = g_task_new(NULL, NULL, identity_languages_completed, job);
|
||||
g_task_set_task_data(task, job, identity_language_job_free);
|
||||
g_task_run_in_thread(task, identity_languages_worker);
|
||||
g_object_unref(task);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,198 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file manage_entity_evidence_dialog.c
|
||||
* @brief Dialogue de sélection des preuves associées à une entité.
|
||||
******************************************************************************/
|
||||
#include "views/manage_entity_evidence_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
#include "widgets/evidence_preview_widget.h"
|
||||
#include <gio/gio.h>
|
||||
typedef struct
|
||||
{
|
||||
char *identifier;
|
||||
char *path;
|
||||
char *relative_path;
|
||||
char *sha256;
|
||||
char *mime_type;
|
||||
GtkCheckButton *check;
|
||||
} EvidenceChoice;
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
EvidencePreviewWidget *preview;
|
||||
char *root_path;
|
||||
GPtrArray *choices;
|
||||
ManageEntityEvidenceDialogCallback callback;
|
||||
ManageEntityEvidenceImportCallback import_callback;
|
||||
gpointer callback_data;
|
||||
gboolean completed;
|
||||
} ManageEvidenceState;
|
||||
/** @brief Ferme le gestionnaire puis lance un import pour cette personne. */
|
||||
static void manage_evidence_on_import(GtkButton *button, gpointer data)
|
||||
{
|
||||
ManageEvidenceState *state = data;
|
||||
(void) button;
|
||||
if (state == NULL || state->completed) return;
|
||||
state->completed = TRUE;
|
||||
if (state->import_callback != NULL)
|
||||
state->import_callback(state->callback_data);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
/** @brief Libère une option privée. */
|
||||
static void evidence_choice_free(gpointer data)
|
||||
{
|
||||
EvidenceChoice *choice = data;
|
||||
if (choice == NULL) return;
|
||||
g_free(choice->identifier); g_free(choice->path);
|
||||
g_free(choice->relative_path); g_free(choice->sha256);
|
||||
g_free(choice->mime_type); g_free(choice);
|
||||
}
|
||||
/** @brief Arrête et efface l'aperçu courant. */
|
||||
static void manage_evidence_clear_preview(ManageEvidenceState *state)
|
||||
{
|
||||
if (state == NULL || state->preview == NULL) return;
|
||||
evidence_preview_widget_clear(state->preview);
|
||||
}
|
||||
/** @brief Libère l'état du dialogue. */
|
||||
static void manage_evidence_state_free(gpointer data)
|
||||
{
|
||||
ManageEvidenceState *state = data;
|
||||
if (state == NULL) return;
|
||||
manage_evidence_clear_preview(state);
|
||||
g_clear_pointer(&state->preview, evidence_preview_widget_free);
|
||||
g_ptr_array_unref(state->choices); g_free(state->root_path); g_free(state);
|
||||
}
|
||||
/** @brief Affiche l'aperçu de l'option manipulée. */
|
||||
static void manage_evidence_on_toggled(GtkCheckButton *button, gpointer data)
|
||||
{
|
||||
ManageEvidenceState *state = data;
|
||||
EvidenceChoice *choice = g_object_get_data(G_OBJECT(button), "choice");
|
||||
if (state == NULL || choice == NULL || choice->path == NULL) return;
|
||||
EvidencePreviewRequest *request = evidence_preview_request_new(
|
||||
state->root_path, choice->identifier, choice->relative_path,
|
||||
choice->sha256, choice->mime_type, 1U);
|
||||
evidence_preview_widget_show(state->preview, request,
|
||||
gtk_check_button_get_label(choice->check));
|
||||
evidence_preview_request_free(request);
|
||||
}
|
||||
/** @brief Termine le dialogue en renvoyant la sélection. */
|
||||
static void manage_evidence_on_save(GtkButton *button, gpointer data)
|
||||
{
|
||||
ManageEvidenceState *state = data;
|
||||
GPtrArray *selected = g_ptr_array_new_with_free_func(g_free);
|
||||
(void) button;
|
||||
for (guint index = 0; index < state->choices->len; index++)
|
||||
{
|
||||
EvidenceChoice *choice = g_ptr_array_index(state->choices, index);
|
||||
if (gtk_check_button_get_active(choice->check))
|
||||
g_ptr_array_add(selected, g_strdup(choice->identifier));
|
||||
}
|
||||
state->completed = TRUE; state->callback(selected, state->callback_data);
|
||||
gtk_window_close(state->window);
|
||||
}
|
||||
/** @brief Termine le dialogue par une annulation. */
|
||||
static gboolean manage_evidence_on_close(GtkWindow *window, gpointer data)
|
||||
{
|
||||
ManageEvidenceState *state = data; (void) window;
|
||||
if (!state->completed) state->callback(NULL, state->callback_data);
|
||||
return FALSE;
|
||||
}
|
||||
gboolean manage_entity_evidence_dialog_present(GtkWindow *parent,
|
||||
const GPtrArray *records, const GPtrArray *selected_ids,
|
||||
const char *root_path, TaskManager *task_manager,
|
||||
ManageEntityEvidenceDialogCallback callback,
|
||||
gpointer callback_data, ManageEntityEvidenceImportCallback import_callback,
|
||||
GError **error)
|
||||
{
|
||||
ManageEvidenceState *state = NULL; GtkWidget *root = NULL;
|
||||
GtkWidget *content = NULL; GtkWidget *list = NULL; GtkWidget *scroll = NULL;
|
||||
GtkWidget *save = NULL; GtkWidget *import = NULL; GtkWidget *actions = NULL;
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (!GTK_IS_WINDOW(parent) || records == NULL || root_path == NULL ||
|
||||
task_manager == NULL ||
|
||||
callback == NULL) return FALSE;
|
||||
state = g_new0(ManageEvidenceState, 1);
|
||||
state->choices = g_ptr_array_new_with_free_func(evidence_choice_free);
|
||||
state->callback = callback; state->callback_data = callback_data;
|
||||
state->root_path = g_strdup(root_path);
|
||||
state->import_callback = import_callback;
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_title(state->window, "Pièces jointes de la personne");
|
||||
labfy_dialog_prepare(state->window, parent, TRUE, TRUE);
|
||||
root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(root, 16); gtk_widget_set_margin_bottom(root, 16);
|
||||
gtk_widget_set_margin_start(root, 16); gtk_widget_set_margin_end(root, 16);
|
||||
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
scroll = gtk_scrolled_window_new(); gtk_widget_set_size_request(scroll, 380, 480);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
|
||||
for (guint index = 0; index < records->len; index++)
|
||||
{
|
||||
const EvidenceRecord *record = g_ptr_array_index(records, index);
|
||||
EvidenceChoice *choice = g_new0(EvidenceChoice, 1); gboolean active = FALSE;
|
||||
char *label = g_strdup_printf("%s — %s",
|
||||
evidence_record_get_original_name(record),
|
||||
evidence_record_get_type_identifier(record));
|
||||
char *candidate = g_build_filename(root_path,
|
||||
evidence_record_get_relative_path(record), NULL);
|
||||
char *canonical_root = g_canonicalize_filename(root_path, NULL);
|
||||
char *canonical_path = g_canonicalize_filename(candidate, NULL);
|
||||
choice->identifier = g_strdup(evidence_record_get_identifier(record));
|
||||
choice->relative_path = g_strdup(
|
||||
evidence_record_get_relative_path(record));
|
||||
choice->sha256 = g_strdup(evidence_record_get_sha256(record));
|
||||
{
|
||||
gboolean uncertain = FALSE;
|
||||
char *content_type = g_content_type_guess(
|
||||
choice->path != NULL ? choice->path : candidate,
|
||||
NULL, 0U, &uncertain);
|
||||
choice->mime_type = content_type != NULL
|
||||
? g_content_type_get_mime_type(content_type)
|
||||
: g_strdup("application/octet-stream");
|
||||
g_free(content_type);
|
||||
(void) uncertain;
|
||||
}
|
||||
if (canonical_root != NULL && canonical_path != NULL &&
|
||||
g_str_has_prefix(canonical_path, canonical_root) &&
|
||||
(canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR ||
|
||||
canonical_path[strlen(canonical_root)] == '\0'))
|
||||
choice->path = g_strdup(canonical_path);
|
||||
g_free(canonical_path); g_free(canonical_root); g_free(candidate);
|
||||
choice->check = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(label));
|
||||
g_free(label);
|
||||
for (guint selected = 0; selected_ids != NULL && selected < selected_ids->len; selected++)
|
||||
if (g_strcmp0(choice->identifier, g_ptr_array_index(selected_ids, selected)) == 0) active = TRUE;
|
||||
gtk_check_button_set_active(choice->check, active);
|
||||
g_object_set_data(G_OBJECT(choice->check), "choice", choice);
|
||||
g_signal_connect(choice->check, "toggled", G_CALLBACK(manage_evidence_on_toggled), state);
|
||||
gtk_box_append(GTK_BOX(list), GTK_WIDGET(choice->check));
|
||||
g_ptr_array_add(state->choices, choice);
|
||||
}
|
||||
state->preview = evidence_preview_widget_new(task_manager, NULL, NULL);
|
||||
if (state->preview == NULL) {
|
||||
gtk_window_destroy(state->window);
|
||||
manage_evidence_state_free(state);
|
||||
return FALSE;
|
||||
}
|
||||
content = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_name(content, "manage-evidence-paned");
|
||||
gtk_paned_set_start_child(GTK_PANED(content), scroll);
|
||||
gtk_paned_set_end_child(GTK_PANED(content),
|
||||
evidence_preview_widget_get_widget(state->preview));
|
||||
labfy_paned_apply_initial_ratio(
|
||||
GTK_PANED(content), 2.0 / 3.0, 480, 240);
|
||||
gtk_paned_set_resize_start_child(GTK_PANED(content), FALSE);
|
||||
gtk_paned_set_resize_end_child(GTK_PANED(content), TRUE);
|
||||
save = gtk_button_new_with_label("Enregistrer les pièces jointes");
|
||||
import = gtk_button_new_with_label("Importer une nouvelle preuve");
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_add_css_class(save, "suggested-action"); gtk_widget_set_halign(save, GTK_ALIGN_END);
|
||||
gtk_box_append(GTK_BOX(actions), import);
|
||||
gtk_box_append(GTK_BOX(actions), save);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
gtk_box_append(GTK_BOX(root), content); gtk_box_append(GTK_BOX(root), actions);
|
||||
gtk_window_set_child(state->window, root);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "state", state, manage_evidence_state_free);
|
||||
g_signal_connect(save, "clicked", G_CALLBACK(manage_evidence_on_save), state);
|
||||
g_signal_connect(import, "clicked", G_CALLBACK(manage_evidence_on_import), state);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(manage_evidence_on_close), state);
|
||||
labfy_dialog_present(state->window); return TRUE;
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file metadata_analysis_dialog.c
|
||||
* @brief Fenêtre de consultation des métadonnées extraites.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/metadata_analysis_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
static gboolean metadata_analysis_dialog_on_escape(
|
||||
GtkWidget *widget, GVariant *arguments, gpointer user_data)
|
||||
{
|
||||
(void) widget;
|
||||
(void) arguments;
|
||||
gtk_window_destroy(GTK_WINDOW(user_data));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void metadata_analysis_dialog_present(GtkWindow *parent, const char *summary,
|
||||
const char *json_path, const char *version)
|
||||
{
|
||||
GtkWidget *window = NULL;
|
||||
GtkWidget *content = NULL;
|
||||
GtkWidget *heading = NULL;
|
||||
GtkWidget *scrolled = NULL;
|
||||
GtkWidget *text_view = NULL;
|
||||
GtkTextBuffer *buffer = NULL;
|
||||
GtkWidget *path_label = NULL;
|
||||
GtkWidget *close_button = NULL;
|
||||
char *heading_text = NULL;
|
||||
char *path_text = NULL;
|
||||
|
||||
window = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window), "Métadonnées de la preuve");
|
||||
labfy_dialog_prepare(GTK_WINDOW(window), parent, TRUE, TRUE);
|
||||
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_top(content, 16);
|
||||
gtk_widget_set_margin_bottom(content, 16);
|
||||
gtk_widget_set_margin_start(content, 16);
|
||||
gtk_widget_set_margin_end(content, 16);
|
||||
heading_text = g_strdup_printf("Extraction locale — ExifTool %s",
|
||||
version != NULL ? version : "version inconnue");
|
||||
heading = gtk_label_new(heading_text);
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(content), heading);
|
||||
scrolled = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scrolled, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
text_view = gtk_text_view_new();
|
||||
gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view), FALSE);
|
||||
gtk_text_view_set_monospace(GTK_TEXT_VIEW(text_view), TRUE);
|
||||
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view), GTK_WRAP_WORD_CHAR);
|
||||
buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
|
||||
gtk_text_buffer_set_text(buffer, summary != NULL ? summary :
|
||||
"Aucune métadonnée exploitable.", -1);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled), text_view);
|
||||
gtk_box_append(GTK_BOX(content), scrolled);
|
||||
path_text = g_strdup_printf("Sortie JSON conservée : %s",
|
||||
json_path != NULL ? json_path : "indisponible");
|
||||
path_label = gtk_label_new(path_text);
|
||||
gtk_label_set_wrap(GTK_LABEL(path_label), TRUE);
|
||||
gtk_label_set_selectable(GTK_LABEL(path_label), TRUE);
|
||||
gtk_widget_set_halign(path_label, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(content), path_label);
|
||||
close_button = gtk_button_new_with_label("Fermer");
|
||||
gtk_widget_set_halign(close_button, GTK_ALIGN_END);
|
||||
g_signal_connect_swapped(close_button, "clicked",
|
||||
G_CALLBACK(gtk_window_destroy), window);
|
||||
{
|
||||
GtkShortcutController *shortcuts = GTK_SHORTCUT_CONTROLLER(
|
||||
gtk_shortcut_controller_new());
|
||||
gtk_shortcut_controller_set_scope(
|
||||
shortcuts, GTK_SHORTCUT_SCOPE_LOCAL);
|
||||
gtk_shortcut_controller_add_shortcut(shortcuts,
|
||||
gtk_shortcut_new(
|
||||
gtk_keyval_trigger_new(GDK_KEY_Escape, 0),
|
||||
gtk_callback_action_new(
|
||||
metadata_analysis_dialog_on_escape, window, NULL)));
|
||||
gtk_widget_add_controller(window, GTK_EVENT_CONTROLLER(shortcuts));
|
||||
}
|
||||
gtk_box_append(GTK_BOX(content), close_button);
|
||||
gtk_window_set_child(GTK_WINDOW(window), content);
|
||||
labfy_dialog_present(GTK_WINDOW(window));
|
||||
g_free(heading_text);
|
||||
g_free(path_text);
|
||||
}
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file osint_dns_review_dialog.c
|
||||
* @brief Dialogue de sélection des propositions issues d'une réponse DNS.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/osint_dns_review_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "models/osint_dns_proposal.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
GtkWidget *select_all_button;
|
||||
GtkWidget *confirm_button;
|
||||
GPtrArray *proposals;
|
||||
GPtrArray *selection_buttons;
|
||||
OsintDnsReviewDialogConfirmCallback callback;
|
||||
gpointer user_data;
|
||||
} OsintDnsReviewDialogContext;
|
||||
|
||||
/** @brief Libère le contexte possédé par la fenêtre. */
|
||||
static void osint_dns_review_dialog_context_free(gpointer data)
|
||||
{
|
||||
OsintDnsReviewDialogContext *context = data;
|
||||
if (context == NULL) return;
|
||||
g_clear_pointer(&context->selection_buttons, g_ptr_array_unref);
|
||||
g_clear_pointer(&context->proposals, g_ptr_array_unref);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
/** @brief Ferme le dialogue sans intégrer de proposition. */
|
||||
static void osint_dns_review_dialog_on_cancel_clicked(
|
||||
GtkButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintDnsReviewDialogContext *context = user_data;
|
||||
(void) button;
|
||||
if (context != NULL) gtk_window_destroy(context->window);
|
||||
}
|
||||
|
||||
/** @brief Actualise l'état du bouton de confirmation. */
|
||||
static void osint_dns_review_dialog_update_confirmation(
|
||||
OsintDnsReviewDialogContext *context
|
||||
)
|
||||
{
|
||||
gboolean has_selection = FALSE;
|
||||
for (guint index = 0;
|
||||
context != NULL && index < context->selection_buttons->len;
|
||||
index++)
|
||||
{
|
||||
GtkCheckButton *button = g_ptr_array_index(
|
||||
context->selection_buttons, index
|
||||
);
|
||||
if (gtk_widget_get_sensitive(GTK_WIDGET(button)) &&
|
||||
gtk_check_button_get_active(button))
|
||||
{
|
||||
has_selection = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (context != NULL)
|
||||
gtk_widget_set_sensitive(context->confirm_button, has_selection);
|
||||
}
|
||||
|
||||
/** @brief Réagit à la sélection individuelle d'une proposition. */
|
||||
static void osint_dns_review_dialog_on_proposal_toggled(
|
||||
GtkCheckButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
(void) button;
|
||||
osint_dns_review_dialog_update_confirmation(user_data);
|
||||
}
|
||||
|
||||
/** @brief Sélectionne ou désélectionne toutes les propositions compatibles. */
|
||||
static void osint_dns_review_dialog_on_select_all_toggled(
|
||||
GtkCheckButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintDnsReviewDialogContext *context = user_data;
|
||||
const gboolean selected = gtk_check_button_get_active(button);
|
||||
for (guint index = 0;
|
||||
context != NULL && index < context->selection_buttons->len;
|
||||
index++)
|
||||
{
|
||||
GtkWidget *selection_button = g_ptr_array_index(
|
||||
context->selection_buttons, index
|
||||
);
|
||||
if (gtk_widget_get_sensitive(selection_button))
|
||||
gtk_check_button_set_active(
|
||||
GTK_CHECK_BUTTON(selection_button), selected
|
||||
);
|
||||
}
|
||||
osint_dns_review_dialog_update_confirmation(context);
|
||||
}
|
||||
|
||||
/** @brief Transmet les propositions cochées puis ferme le dialogue. */
|
||||
static void osint_dns_review_dialog_on_confirm_clicked(
|
||||
GtkButton *button,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintDnsReviewDialogContext *context = user_data;
|
||||
GPtrArray *selected_proposals = g_ptr_array_new();
|
||||
(void) button;
|
||||
|
||||
for (guint index = 0;
|
||||
context != NULL && index < context->selection_buttons->len;
|
||||
index++)
|
||||
{
|
||||
GtkCheckButton *selection_button = g_ptr_array_index(
|
||||
context->selection_buttons, index
|
||||
);
|
||||
if (gtk_widget_get_sensitive(GTK_WIDGET(selection_button)) &&
|
||||
gtk_check_button_get_active(selection_button))
|
||||
{
|
||||
g_ptr_array_add(
|
||||
selected_proposals,
|
||||
g_object_get_data(
|
||||
G_OBJECT(selection_button), "osint-dns-proposal"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context != NULL && context->callback != NULL &&
|
||||
selected_proposals->len > 0U)
|
||||
{
|
||||
context->callback(selected_proposals, context->user_data);
|
||||
}
|
||||
g_ptr_array_unref(selected_proposals);
|
||||
if (context != NULL) gtk_window_destroy(context->window);
|
||||
}
|
||||
|
||||
/** @brief Crée une ligne décrivant une proposition DNS. */
|
||||
static GtkWidget *osint_dns_review_dialog_create_row(
|
||||
OsintDnsReviewDialogContext *context,
|
||||
const OsintDnsProposal *proposal
|
||||
)
|
||||
{
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
|
||||
const char *relation_type = osint_dns_proposal_get_relation_type(proposal);
|
||||
char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal);
|
||||
const gboolean compatible = entity_type != NULL && relation_type != NULL &&
|
||||
normalized_value != NULL;
|
||||
char *title = g_strdup_printf(
|
||||
"%s → %s",
|
||||
osint_dns_proposal_get_record_type(proposal),
|
||||
osint_dns_proposal_get_value(proposal)
|
||||
);
|
||||
char *details = g_strdup_printf(
|
||||
"Cible : %s\nPropriétaire : %s\nType Labfy : %s\n"
|
||||
"Relation prévue : %s — %s → %s",
|
||||
osint_dns_proposal_get_target(proposal),
|
||||
osint_dns_proposal_get_owner(proposal),
|
||||
compatible ? entity_type : "non pris en charge",
|
||||
osint_dns_proposal_get_target(proposal),
|
||||
compatible ? relation_type : "non prise en charge",
|
||||
normalized_value != NULL ? normalized_value
|
||||
: osint_dns_proposal_get_value(proposal)
|
||||
);
|
||||
GtkWidget *selection_button = gtk_check_button_new_with_label(title);
|
||||
GtkWidget *details_label = gtk_label_new(details);
|
||||
|
||||
gtk_widget_set_sensitive(selection_button, compatible);
|
||||
gtk_widget_set_margin_start(details_label, 28);
|
||||
gtk_widget_set_halign(details_label, GTK_ALIGN_START);
|
||||
gtk_label_set_xalign(GTK_LABEL(details_label), 0.0F);
|
||||
gtk_label_set_selectable(GTK_LABEL(details_label), TRUE);
|
||||
g_object_set_data(
|
||||
G_OBJECT(selection_button), "osint-dns-proposal", (gpointer) proposal
|
||||
);
|
||||
g_signal_connect(
|
||||
selection_button, "toggled",
|
||||
G_CALLBACK(osint_dns_review_dialog_on_proposal_toggled), context
|
||||
);
|
||||
g_ptr_array_add(context->selection_buttons, selection_button);
|
||||
gtk_box_append(GTK_BOX(row), selection_button);
|
||||
gtk_box_append(GTK_BOX(row), details_label);
|
||||
g_free(normalized_value);
|
||||
g_free(title);
|
||||
g_free(details);
|
||||
return row;
|
||||
}
|
||||
|
||||
void osint_dns_review_dialog_present(
|
||||
GtkWindow *parent_window,
|
||||
GPtrArray *proposals,
|
||||
OsintDnsReviewDialogConfirmCallback callback,
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintDnsReviewDialogContext *context = NULL;
|
||||
GtkWidget *main_box = NULL;
|
||||
GtkWidget *title_label = NULL;
|
||||
GtkWidget *notice_label = NULL;
|
||||
GtkWidget *scrolled_window = NULL;
|
||||
GtkWidget *proposal_box = NULL;
|
||||
GtkWidget *button_box = NULL;
|
||||
GtkWidget *cancel_button = NULL;
|
||||
|
||||
if (proposals == NULL || proposals->len == 0U) return;
|
||||
context = g_new0(OsintDnsReviewDialogContext, 1);
|
||||
context->window = GTK_WINDOW(gtk_window_new());
|
||||
context->proposals = g_ptr_array_ref(proposals);
|
||||
context->selection_buttons = g_ptr_array_new();
|
||||
context->callback = callback;
|
||||
context->user_data = user_data;
|
||||
|
||||
gtk_window_set_title(context->window, "Intégrer les propositions DNS");
|
||||
labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
|
||||
g_object_set_data_full(
|
||||
G_OBJECT(context->window), "osint-dns-review-context", context,
|
||||
osint_dns_review_dialog_context_free
|
||||
);
|
||||
|
||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_start(main_box, 20);
|
||||
gtk_widget_set_margin_end(main_box, 20);
|
||||
gtk_widget_set_margin_top(main_box, 20);
|
||||
gtk_widget_set_margin_bottom(main_box, 20);
|
||||
title_label = gtk_label_new("Sélectionner les propositions à intégrer");
|
||||
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(title_label, "title-2");
|
||||
notice_label = gtk_label_new(
|
||||
"Aucune écriture ne sera effectuée avant confirmation. "
|
||||
"Les types non pris en charge restent visibles mais désactivés."
|
||||
);
|
||||
gtk_label_set_wrap(GTK_LABEL(notice_label), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(notice_label), 0.0F);
|
||||
|
||||
context->select_all_button = gtk_check_button_new_with_label(
|
||||
"Sélectionner toutes les propositions compatibles"
|
||||
);
|
||||
g_signal_connect(
|
||||
context->select_all_button, "toggled",
|
||||
G_CALLBACK(osint_dns_review_dialog_on_select_all_toggled), context
|
||||
);
|
||||
proposal_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
|
||||
for (guint index = 0; index < proposals->len; index++)
|
||||
gtk_box_append(
|
||||
GTK_BOX(proposal_box),
|
||||
osint_dns_review_dialog_create_row(
|
||||
context, g_ptr_array_index(proposals, index)
|
||||
)
|
||||
);
|
||||
scrolled_window = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scrolled_window, TRUE);
|
||||
gtk_scrolled_window_set_child(
|
||||
GTK_SCROLLED_WINDOW(scrolled_window), proposal_box
|
||||
);
|
||||
|
||||
button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(button_box, GTK_ALIGN_END);
|
||||
cancel_button = gtk_button_new_with_label("Annuler");
|
||||
context->confirm_button = gtk_button_new_with_label("Intégrer la sélection");
|
||||
gtk_widget_add_css_class(context->confirm_button, "suggested-action");
|
||||
gtk_widget_set_sensitive(context->confirm_button, FALSE);
|
||||
g_signal_connect(
|
||||
cancel_button, "clicked",
|
||||
G_CALLBACK(osint_dns_review_dialog_on_cancel_clicked), context
|
||||
);
|
||||
g_signal_connect(
|
||||
context->confirm_button, "clicked",
|
||||
G_CALLBACK(osint_dns_review_dialog_on_confirm_clicked), context
|
||||
);
|
||||
gtk_box_append(GTK_BOX(button_box), cancel_button);
|
||||
gtk_box_append(GTK_BOX(button_box), context->confirm_button);
|
||||
gtk_box_append(GTK_BOX(main_box), title_label);
|
||||
gtk_box_append(GTK_BOX(main_box), notice_label);
|
||||
gtk_box_append(GTK_BOX(main_box), context->select_all_button);
|
||||
gtk_box_append(GTK_BOX(main_box), scrolled_window);
|
||||
gtk_box_append(GTK_BOX(main_box), button_box);
|
||||
gtk_window_set_child(context->window, main_box);
|
||||
labfy_dialog_present(context->window);
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
/******************************************************************************
|
||||
* @file osint_execution_history_dialog.c
|
||||
* @brief Consultation en lecture seule de l'historique OSINT.
|
||||
******************************************************************************/
|
||||
|
||||
#include "views/osint_execution_history_dialog.h"
|
||||
#include "views/dialog_geometry.h"
|
||||
|
||||
#include "core/osint_execution_integrity.h"
|
||||
#include "models/osint_execution_record.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWindow *window;
|
||||
GtkTextBuffer *details_buffer;
|
||||
GtkWidget *integrity_label;
|
||||
const OsintExecutionRecord *current_record;
|
||||
GPtrArray *records;
|
||||
GHashTable *linked_objects;
|
||||
} OsintExecutionHistoryDialogContext;
|
||||
|
||||
/** @brief Libère les données conservées par la fenêtre. */
|
||||
static void osint_execution_history_dialog_context_free(gpointer data)
|
||||
{
|
||||
OsintExecutionHistoryDialogContext *context = data;
|
||||
if (context == NULL) return;
|
||||
g_clear_pointer(&context->records, g_ptr_array_unref);
|
||||
g_clear_pointer(&context->linked_objects, g_hash_table_unref);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
/** @brief Convertit une sortie brute en texte UTF-8 affichable. */
|
||||
static char *osint_execution_history_dialog_output_to_text(GBytes *bytes)
|
||||
{
|
||||
gconstpointer data = NULL;
|
||||
gsize size = 0U;
|
||||
if (bytes == NULL) return g_strdup("");
|
||||
data = g_bytes_get_data(bytes, &size);
|
||||
return data != NULL && size > 0U
|
||||
? g_utf8_make_valid(data, (gssize) size) : g_strdup("");
|
||||
}
|
||||
|
||||
/** @brief Construit le détail textuel complet d'une exécution. */
|
||||
static char *osint_execution_history_dialog_build_details(
|
||||
const OsintExecutionRecord *record, GPtrArray *linked_objects
|
||||
)
|
||||
{
|
||||
GBytes *stdout_raw = osint_execution_record_ref_stdout(record);
|
||||
GBytes *stderr_raw = osint_execution_record_ref_stderr(record);
|
||||
char *stdout_text = osint_execution_history_dialog_output_to_text(stdout_raw);
|
||||
char *stderr_text = osint_execution_history_dialog_output_to_text(stderr_raw);
|
||||
GString *details = g_string_new(NULL);
|
||||
g_string_append_printf(details,
|
||||
"Exécution : %s\nOutil : %s%s%s\nAction : %s\nÉtat : %s\n"
|
||||
"Sélection : %s %s\nCible : %s\nDébut : %s\nFin : %s\n"
|
||||
"Code de sortie : ",
|
||||
osint_execution_record_get_identifier(record),
|
||||
osint_execution_record_get_tool_identifier(record),
|
||||
osint_execution_record_get_tool_version(record) != NULL ? " — " : "",
|
||||
osint_execution_record_get_tool_version(record) != NULL
|
||||
? osint_execution_record_get_tool_version(record) : "",
|
||||
osint_execution_record_get_action_identifier(record),
|
||||
osint_execution_record_get_final_state(record),
|
||||
osint_execution_record_get_selection_kind(record),
|
||||
osint_execution_record_get_selection_identifier(record),
|
||||
osint_execution_record_get_target_value(record),
|
||||
osint_execution_record_get_started_at(record),
|
||||
osint_execution_record_get_finished_at(record));
|
||||
if (osint_execution_record_has_exit_code(record))
|
||||
g_string_append_printf(details, "%d", osint_execution_record_get_exit_code(record));
|
||||
else g_string_append(details, "indisponible");
|
||||
g_string_append_printf(details,
|
||||
"\nArguments : %s\nSHA-256 : %s\n\nObjets liés :\n",
|
||||
osint_execution_record_get_arguments(record),
|
||||
osint_execution_record_get_output_sha256(record));
|
||||
if (linked_objects == NULL || linked_objects->len == 0U)
|
||||
g_string_append(details, "Aucun objet intégré.\n");
|
||||
else for (guint index = 0U; index < linked_objects->len; index++)
|
||||
g_string_append_printf(details, "- %s\n",
|
||||
(const char *) g_ptr_array_index(linked_objects, index));
|
||||
g_string_append_printf(details, "\nSortie standard :\n%s\n\nSortie d'erreur :\n%s",
|
||||
stdout_text[0] != '\0' ? stdout_text : "(vide)",
|
||||
stderr_text[0] != '\0' ? stderr_text : "(vide)");
|
||||
g_bytes_unref(stdout_raw); g_bytes_unref(stderr_raw);
|
||||
g_free(stdout_text); g_free(stderr_text);
|
||||
return g_string_free(details, FALSE);
|
||||
}
|
||||
|
||||
/** @brief Affiche le détail correspondant au bouton d'historique activé. */
|
||||
static void osint_execution_history_dialog_on_record_clicked(
|
||||
GtkButton *button, gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintExecutionHistoryDialogContext *context = user_data;
|
||||
guint encoded_index = GPOINTER_TO_UINT(g_object_get_data(
|
||||
G_OBJECT(button), "osint-execution-index"));
|
||||
OsintExecutionRecord *record = NULL;
|
||||
GPtrArray *linked_objects = NULL;
|
||||
char *details = NULL;
|
||||
if (context == NULL || encoded_index == 0U ||
|
||||
encoded_index > context->records->len) return;
|
||||
record = g_ptr_array_index(context->records, encoded_index - 1U);
|
||||
context->current_record = record;
|
||||
gtk_label_set_text(GTK_LABEL(context->integrity_label),
|
||||
"Intégrité : non vérifiée");
|
||||
gtk_widget_remove_css_class(context->integrity_label, "error");
|
||||
linked_objects = g_hash_table_lookup(context->linked_objects,
|
||||
osint_execution_record_get_identifier(record));
|
||||
details = osint_execution_history_dialog_build_details(record, linked_objects);
|
||||
gtk_text_buffer_set_text(context->details_buffer, details, -1);
|
||||
g_free(details);
|
||||
}
|
||||
|
||||
/** @brief Recalcule et affiche l'intégrité de l'exécution sélectionnée. */
|
||||
static void osint_execution_history_dialog_on_verify_clicked(
|
||||
GtkButton *button, gpointer user_data
|
||||
)
|
||||
{
|
||||
OsintExecutionHistoryDialogContext *context = user_data;
|
||||
GBytes *stdout_raw = NULL; GBytes *stderr_raw = NULL;
|
||||
OsintExecutionIntegrityStatus status;
|
||||
const char *status_text = "Intégrité : vérification impossible";
|
||||
(void) button;
|
||||
if (context == NULL || context->current_record == NULL) return;
|
||||
stdout_raw = osint_execution_record_ref_stdout(context->current_record);
|
||||
stderr_raw = osint_execution_record_ref_stderr(context->current_record);
|
||||
status = osint_execution_integrity_verify(
|
||||
stdout_raw, stderr_raw,
|
||||
osint_execution_record_get_output_sha256(context->current_record));
|
||||
if (status == OSINT_EXECUTION_INTEGRITY_INTACT)
|
||||
status_text = "Intégrité : intacte";
|
||||
else if (status == OSINT_EXECUTION_INTEGRITY_ALTERED)
|
||||
status_text = "Intégrité : altérée";
|
||||
gtk_label_set_text(GTK_LABEL(context->integrity_label), status_text);
|
||||
if (status == OSINT_EXECUTION_INTEGRITY_INTACT)
|
||||
gtk_widget_remove_css_class(context->integrity_label, "error");
|
||||
else gtk_widget_add_css_class(context->integrity_label, "error");
|
||||
g_bytes_unref(stdout_raw); g_bytes_unref(stderr_raw);
|
||||
}
|
||||
|
||||
void osint_execution_history_dialog_present(
|
||||
GtkWindow *parent_window, GPtrArray *records, GHashTable *linked_objects
|
||||
)
|
||||
{
|
||||
OsintExecutionHistoryDialogContext *context = NULL;
|
||||
GtkWidget *main_box = NULL; GtkWidget *content_box = NULL;
|
||||
GtkWidget *history_scroll = NULL; GtkWidget *history_box = NULL;
|
||||
GtkWidget *details_scroll = NULL; GtkWidget *details_view = NULL;
|
||||
GtkWidget *button_box = NULL; GtkWidget *verify_button = NULL;
|
||||
GtkWidget *close_button = NULL;
|
||||
if (records == NULL || records->len == 0U || linked_objects == NULL) return;
|
||||
context = g_new0(OsintExecutionHistoryDialogContext, 1);
|
||||
context->window = GTK_WINDOW(gtk_window_new());
|
||||
context->records = g_ptr_array_ref(records);
|
||||
context->linked_objects = g_hash_table_ref(linked_objects);
|
||||
gtk_window_set_title(context->window, "Historique OSINT");
|
||||
labfy_dialog_prepare(context->window, parent_window, TRUE, TRUE);
|
||||
g_object_set_data_full(G_OBJECT(context->window), "osint-history-context",
|
||||
context, osint_execution_history_dialog_context_free);
|
||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_start(main_box, 16); gtk_widget_set_margin_end(main_box, 16);
|
||||
gtk_widget_set_margin_top(main_box, 16); gtk_widget_set_margin_bottom(main_box, 16);
|
||||
content_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
gtk_widget_set_vexpand(content_box, TRUE);
|
||||
history_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
history_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_size_request(history_scroll, 300, -1);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(history_scroll), history_box);
|
||||
for (guint index = 0U; index < records->len; index++)
|
||||
{
|
||||
OsintExecutionRecord *record = g_ptr_array_index(records, index);
|
||||
char *label = g_strdup_printf("%s\n%s — %s",
|
||||
osint_execution_record_get_finished_at(record),
|
||||
osint_execution_record_get_tool_identifier(record),
|
||||
osint_execution_record_get_final_state(record));
|
||||
GtkWidget *button = gtk_button_new_with_label(label);
|
||||
gtk_widget_set_halign(button, GTK_ALIGN_FILL);
|
||||
g_object_set_data(G_OBJECT(button), "osint-execution-index",
|
||||
GUINT_TO_POINTER(index + 1U));
|
||||
g_signal_connect(button, "clicked",
|
||||
G_CALLBACK(osint_execution_history_dialog_on_record_clicked), context);
|
||||
gtk_box_append(GTK_BOX(history_box), button);
|
||||
g_free(label);
|
||||
}
|
||||
details_view = gtk_text_view_new();
|
||||
context->details_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(details_view));
|
||||
gtk_text_view_set_editable(GTK_TEXT_VIEW(details_view), FALSE);
|
||||
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(details_view), TRUE);
|
||||
gtk_text_view_set_monospace(GTK_TEXT_VIEW(details_view), TRUE);
|
||||
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(details_view), GTK_WRAP_WORD_CHAR);
|
||||
details_scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_hexpand(details_scroll, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(details_scroll), details_view);
|
||||
gtk_box_append(GTK_BOX(content_box), history_scroll);
|
||||
gtk_box_append(GTK_BOX(content_box), details_scroll);
|
||||
button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(button_box, GTK_ALIGN_END);
|
||||
context->integrity_label = gtk_label_new("Intégrité : non vérifiée");
|
||||
gtk_widget_set_hexpand(context->integrity_label, TRUE);
|
||||
gtk_widget_set_halign(context->integrity_label, GTK_ALIGN_START);
|
||||
verify_button = gtk_button_new_with_label("Vérifier l'intégrité");
|
||||
g_signal_connect(verify_button, "clicked",
|
||||
G_CALLBACK(osint_execution_history_dialog_on_verify_clicked), context);
|
||||
close_button = gtk_button_new_with_label("Fermer");
|
||||
g_signal_connect_swapped(close_button, "clicked",
|
||||
G_CALLBACK(gtk_window_destroy), context->window);
|
||||
gtk_box_append(GTK_BOX(button_box), verify_button);
|
||||
gtk_box_append(GTK_BOX(button_box), close_button);
|
||||
gtk_box_append(GTK_BOX(main_box), content_box);
|
||||
gtk_box_append(GTK_BOX(main_box), context->integrity_label);
|
||||
gtk_box_append(GTK_BOX(main_box), button_box);
|
||||
gtk_window_set_child(context->window, main_box);
|
||||
osint_execution_history_dialog_on_record_clicked(
|
||||
GTK_BUTTON(gtk_widget_get_first_child(history_box)), context);
|
||||
labfy_dialog_present(context->window);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue