diff --git a/TICKET-104.md b/TICKET-104.md new file mode 100644 index 0000000..bfdfc28 --- /dev/null +++ b/TICKET-104.md @@ -0,0 +1,53 @@ +# Ticket 104 — Lier et exploiter les extractions dans l’enquête + +## Objectif + +Rendre les fichiers produits par les outils d’analyse (OSINT, ExifTool, +analyse EML, OCR, récupération PDF, etc.) traçables et directement +exploitables depuis l’interface de l’enquête. + +## Besoins + +- Enregistrer chaque extraction comme une preuve traitée identifiable. +- Conserver la référence de l’entité ou de la preuve à l’origine de + l’extraction. +- Afficher les extractions liées dans la fiche de l’objet source. +- Permettre la prévisualisation des fichiers texte avec défilement. +- Afficher les résultats texte dans les fiches concernées sans ouvrir le + gestionnaire de fichiers. +- Depuis l’onglet **Dossier**, permettre de glisser-déposer une extraction + sur le graphe afin de proposer sa création ou son rattachement à une + entité. +- Préserver l’intégrité et le chemin relatif des fichiers produits dans + `02_Preuves_Traitees/Extractions`. + +## Contraintes d’implémentation + +- Utiliser les associations persistées preuve–entité existantes, sans + dupliquer les fichiers ni les résultats. +- Refuser les chemins sortant du dossier de l’enquête. +- Ne jamais exécuter automatiquement le contenu d’une extraction déposée + dans le graphe. +- Conserver les commentaires Doxygen des nouvelles fonctions publiques. + +## Critères d’acceptation + +1. Une extraction terminée apparaît comme preuve traitée et indique sa + source. +2. La fiche de la source liste les extractions associées. +3. Un fichier `.txt` peut être lu dans une zone défilante depuis sa fiche et + depuis l’onglet Dossier. +4. Un glisser-déposer sur le graphe ouvre une action explicite de création ou + d’association, sans modification silencieuse. +5. Les associations et chemins sont conservés après fermeture et réouverture + de l’enquête. +6. Les tests unitaires couvrent l’association, les chemins invalides et le + transfert Dossier → graphe. + +## Vérification + +- `make -j2` +- `make test` +- Vérification manuelle avec une extraction OSINT et un fichier texte dans + `02_Preuves_Traitees/Extractions`. + diff --git a/reports/report_meline59760_simple.json b/reports/report_meline59760_simple.json new file mode 100644 index 0000000..e7bad57 --- /dev/null +++ b/reports/report_meline59760_simple.json @@ -0,0 +1 @@ +{"Instagram": {"site": {"tags": ["networking", "photo", "social"], "checkType": "message", "urlProbe": "https://www.instagram.com/api/v1/users/web_profile_info/?username={username}", "headers": {"x-ig-app-id": "936619743392459", "referer": "https://www.instagram.com/"}, "presenseStrs": ["\"biography\""], "absenceStrs": ["Sorry, this page isn't available.", "dialog-404"], "errors": {"Login \u2022 Instagram": "Login required", "\"routePath\":\"\\/\"": "Login required (rate-limited or session blocked)", "Please wait a few minutes before you try again.": "Rate limited"}, "alexaRank": 4, "urlMain": "https://www.instagram.com/", "url": "https://www.instagram.com/{username}/", "usernameClaimed": "cristiano", "usernameUnclaimed": "noonewouldeverusethis77777", "protection": ["tls_fingerprint"]}, "username": "meline59760", "keywords": [], "parsing_enabled": true, "url_main": "https://www.instagram.com/", "cookies": null, "url_user": "https://www.instagram.com/meline59760/", "url_probe": "https://www.instagram.com/api/v1/users/web_profile_info/?username=meline59760", "ids_usernames": {"meline59760": "username"}, "ids_links": [], "status": {"username": "meline59760", "site_name": "Instagram", "url": "https://www.instagram.com/meline59760/", "status": "Claimed", "ids": {"username": "meline59760", "fullname": "M\u00e9line Mignon", "id": "23685360083", "image": "https://scontent-cdg4-1.cdninstagram.com/v/t51.82787-19/724092486_18109543660784084_9144824295371353753_n.jpg?stp=dst-jpg_s320x320_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4zMjAuYzIifQ&_nc_ht=scontent-cdg4-1.cdninstagram.com&_nc_cat=108&_nc_oc=Q6cZ2gHFderv5RK6nIlzUJzRyHTOdeaqKM3gUV4Vo2DVognufvoKgRywYw-bMdEu_y4IFC4&_nc_ohc=_hNKnSuKlbUQ7kNvwE9OlKd&_nc_gid=n6RBziFUv7ZEDBwwlMp9Uw&edm=AOQ1c0wBAAAA&ccb=7-5&oh=00_AQD6CBmIB7n96DndM11ZrubcUJ5rJVODRwrNSpuzpDMFRg&oe=6A67B191&_nc_sid=8b3546", "bio": "\ud83d\udc95", "facebook_uid": "17841423522510670", "is_business": "False", "is_joined_recently": "False", "is_private": "True", "is_verified": "False", "follower_count": "104", "following_count": "16", "_extractor": "Instagram GraphQL"}, "tags": ["networking", "photo", "social"], "keywords": [], "keyword_match_status": "No Keywords"}, "http_status": 200, "is_similar": false, "rank": 4}, "ChaturBate": {"site": {"protection": ["ip_reputation", "tls_fingerprint"], "tags": ["porn", "streaming", "video", "webcam"], "checkType": "status_code", "alexaRank": 3611, "urlMain": "https://chaturbate.com", "url": "https://chaturbate.com/{username}/", "usernameClaimed": "cute18cute", "usernameUnclaimed": "noonewouldeverusethis77777", "errors": {"Just a moment": "Cloudflare challenge", "challenges.cloudflare.com": "Cloudflare challenge"}}, "username": "meline59760", "keywords": [], "parsing_enabled": true, "url_main": "https://chaturbate.com", "cookies": null, "url_user": "https://chaturbate.com/meline59760/", "url_probe": "https://chaturbate.com/meline59760/", "status": {"username": "meline59760", "site_name": "ChaturBate", "url": "https://chaturbate.com/meline59760/", "status": "Claimed", "ids": {}, "tags": ["porn", "streaming", "video", "webcam"], "keywords": [], "keyword_match_status": "No Keywords"}, "http_status": 200, "is_similar": false, "rank": 3611}} \ No newline at end of file diff --git a/src/views/main_window.c b/src/views/main_window.c index 53e197d..474b517 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -29,7 +29,7 @@ /** * @brief Position initiale de la séparation verticale. */ -#define MAIN_WINDOW_TASK_PANEL_POSITION 430 +#define MAIN_WINDOW_TASK_PANEL_POSITION 390 /** * @brief Titre par défaut de la fenêtre. diff --git a/src/widgets/task_panel.c b/src/widgets/task_panel.c index c469699..1f349bc 100644 --- a/src/widgets/task_panel.c +++ b/src/widgets/task_panel.c @@ -32,6 +32,7 @@ typedef struct GtkWidget *progress_bar; GtkWidget *detail_label; GtkWidget *cancel_button; + GtkWidget *remove_button; } TaskPanelRow; /** @@ -188,6 +189,19 @@ static void task_panel_on_clear_clicked( ); } +/** @brief Retire uniquement la tâche terminée de cette ligne. */ +static void task_panel_on_remove_clicked(GtkButton *button, gpointer user_data) +{ + TaskPanel *task_panel = user_data; + BackgroundTask *task = NULL; + if (task_panel == NULL || task_panel->task_manager == NULL) return; + task = g_object_get_data(G_OBJECT(button), "labfy-background-task"); + if (task != NULL && background_task_get_state(task) != + BACKGROUND_TASK_STATE_RUNNING && background_task_get_state(task) != + BACKGROUND_TASK_STATE_PENDING) + task_manager_remove(task_panel->task_manager, task); +} + /** * @brief Réagit aux changements structurels du gestionnaire. * @@ -437,6 +451,9 @@ static void task_panel_row_update( task_row->cancel_button, state == BACKGROUND_TASK_STATE_RUNNING ); + gtk_widget_set_visible(task_row->remove_button, + state != BACKGROUND_TASK_STATE_RUNNING && + state != BACKGROUND_TASK_STATE_PENDING); g_clear_error( &error @@ -564,6 +581,7 @@ static TaskPanelRow *task_panel_create_task_row( gtk_button_new_with_label( "Annuler" ); + task_row->remove_button = gtk_button_new_with_label("Nettoyer"); gtk_widget_set_halign( task_row->cancel_button, @@ -593,6 +611,10 @@ static TaskPanelRow *task_panel_create_task_row( ), task_panel ); + g_object_set_data(G_OBJECT(task_row->remove_button), + "labfy-background-task", task_row->task); + g_signal_connect(task_row->remove_button, "clicked", + G_CALLBACK(task_panel_on_remove_clicked), task_panel); gtk_box_append( GTK_BOX(header_box), @@ -608,6 +630,7 @@ static TaskPanelRow *task_panel_create_task_row( GTK_BOX(header_box), task_row->cancel_button ); + gtk_box_append(GTK_BOX(header_box), task_row->remove_button); task_row->progress_bar = gtk_progress_bar_new(); diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 7d410ba..a72ce7f 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -97,6 +97,7 @@ struct Workspace GtkWidget *recover_pdf_password_button; GtkWidget *evidence_preview_stack; GtkWidget *evidence_preview_status; + GtkWidget *evidence_preview_text; GtkWidget *evidence_preview_picture; GtkWidget *evidence_preview_video; GtkWidget *evidence_preview_expand_button; @@ -1425,6 +1426,12 @@ Workspace *workspace_new(void) gtk_video_set_autoplay(GTK_VIDEO(workspace->evidence_preview_video), FALSE); gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), workspace->evidence_preview_status, "status"); + workspace->evidence_preview_text = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(workspace->evidence_preview_text), FALSE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(workspace->evidence_preview_text), GTK_WRAP_WORD_CHAR); + gtk_widget_set_vexpand(workspace->evidence_preview_text, TRUE); + gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), + workspace->evidence_preview_text, "text"); gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), workspace->evidence_preview_picture, "image"); gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack), @@ -2880,7 +2887,25 @@ void workspace_set_evidence_preview(Workspace *workspace, } else { - char *message = g_strdup_printf( + char *message = NULL; + if (mime_type != NULL && g_strcmp0(mime_type, "text/plain") == 0) + { + char *contents = NULL; + gsize length = 0; + if (g_file_get_contents(file_path, &contents, &length, NULL)) + { + GtkTextBuffer *buffer = gtk_text_view_get_buffer( + GTK_TEXT_VIEW(workspace->evidence_preview_text)); + gtk_text_buffer_set_text(buffer, contents, (gint) MIN(length, 200000)); + gtk_stack_set_visible_child_name(GTK_STACK( + workspace->evidence_preview_stack), "text"); + workspace->evidence_preview_is_video = FALSE; + g_free(contents); + goto text_preview_ready; + } + g_free(contents); + } + message = g_strdup_printf( "Aperçu indisponible pour %s%s%s.", display_name != NULL ? display_name : "ce fichier", error != NULL ? " : " : "", @@ -2892,6 +2917,7 @@ void workspace_set_evidence_preview(Workspace *workspace, g_free(message); goto cleanup; } +text_preview_ready: workspace->evidence_preview_path = g_strdup(file_path); gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, workspace->evidence_preview_path != NULL);