Le corps complet d'un message en file d'attente reste dans la file de modération après sa suppression par l'auteur, sans moyen de le purger

Résumé

Lorsqu’un passage passe par la file d’attente d’approbation, ReviewableQueuedPost.payload["raw"] contient une copie complète du corps soumis. Après que le passage a été approuvé puis supprimé par son auteur, le passage public est remplacé par le stub « (post supprimé par l’auteur) », mais le révisable conserve le corps original aussi longtemps que la ligne du passage existe — ce qui, après une suppression par l’auteur, est indéfiniment — et continue de le servir au personnel sur /review.

Il n’y a aucun chemin de nettoyage (scrubbing) pour cela : Reviewable.scrubbable_types est [ReviewableUser], donc l’action de nettoyage administrateur ajoutée dans #36556 (« les modérateurs peuvent désormais utiliser une action ‘Scrub’ pour supprimer les données personnelles de l’utilisateur ») ne couvre pas les passages en file d’attente. La charge utile n’est supprimée que si quelqu’un détruit définitivement l’enregistrement du passage, ce que la suppression par l’auteur ne fait pas et ce que l’interface utilisateur ne permet pas pour le premier passage d’un sujet.

Reproduit sur v2026.7.1. Le code pertinent n’a pas changé sur main à la date du 25-09-2026.

Version

Discourse v2026.7.1 (auto-hébergé, instance de test locale autorisée). Noyau uniquement — aucun plugin impliqué.

Étapes pour reproduire

  1. En tant qu’administrateur, exiger l’approbation pour les passages d’un utilisateur normal, par exemple en définissant approve post count sur 5 et en retirant trust_level_0 de approve unless allowed groups.

  2. En tant que cet utilisateur normal, créer un sujet avec un marqueur unique dans le corps. La réponse revient sous la forme {"action": "enqueued"}.

  3. En tant que personnel, l’approuver : PUT /review/<reviewable_id>/perform/approve_post?version=0. Le passage est créé.

  4. En tant qu’auteur, le supprimer via l’action normale destinée aux utilisateurs — DELETE /t/<topic_id>.json pour le sujet dont c’est le premier passage.

  5. En tant que personnel, demander GET /review.json?status=all.

Attendu

Une fois que l’auteur supprime le contenu, la copie de modération du même texte devrait suivre le cycle de vie du contenu : elle devrait être supprimée, nettoyée, ou au moins être nettoyable par un administrateur de la même manière qu’un ReviewableUser rejeté.

Réel

L’étape 5 retourne toujours le corps original complet. Mesuré sur l’exécution reproduite :

Observation Résultat
Ligne posts après la suppression par l’auteur user_deleted = t, raw = '(topic deleted by author)'
reviewables.payload après la suppression par l’auteur {"raw":"Queued body DRLGQUEUEDfb08ab5b648f. padding …"} — inchangé
GET /review.json?status=all du personnel contient le corps original
Reviewable.scrubbable_types ["ReviewableUser"]
PUT /review/<id>/scrub.json de l’administrateur HTTP 404 (type non nettoyable)
Après la destruction permanente de l’enregistrement du passage la ligne du révisable est disparue (dependent: :destroy)

La dernière ligne est le seul chemin qui le nettoie, et il n’est pas accessible depuis la suppression par l’auteur : la suppression par l’auteur marque le passage comme user_deleted et, après delete_removed_posts_after heures, le met à la corbeille. Une ligne de passage mise à la corbeille continue d’exister, donc la charge utile continue d’exister.

Origine du problème

  • app/models/reviewable_queued_post.rb construit et lit payload['raw'] ; rien n’indique un objectif ou une durée de rétention pour cela.

  • lib/post_destroyer.rb:562 resolve_reviewables_for_author_deletion ne touche que Reviewable.where(target: @post, status: pending). Un révisable de passage en file d’attente approuvé n’est jamais transitionné ni nettoyé.

  • app/models/post.rb:71 has_many :reviewables, as: :target, dependent: :destroy — se déclenche lors de la destruction de l’enregistrement, et non lors de la suppression douce produite par une suppression par l’auteur.

  • app/models/reviewable.rb:81 scrubbable_types retourne [ReviewableUser] ; app/controllers/reviewables_controller.rb:191 exige en outre status: rejected.

  • Il n’y a pas de nettoyage planifié pour les révisables. app/jobs/scheduled/ contient des tâches clean_up_* / purge_* pour les brouillons, les jetons e-mail, les exports, les uploads, les clés API et plus, mais rien pour les révisables.

Impact

Ce n’est pas une divulgation publique — la file d’attente de révision est réservée au personnel, et je ne prétends pas le contraire. C’est un manque de rétention de données : un texte qu’un utilisateur a supprimé du forum reste dans le magasin de modération indéfiniment, est affiché dans l’interface de la file d’attente de révision, et aucune action d’administrateur ne peut le supprimer sans détruire définitivement l’enregistrement du passage.

Cela compte pour la même raison pour laquelle #36556 a ajouté le nettoyage pour les utilisateurs rejetés : les charges utiles des révisables peuvent contenir des données personnelles, et un site qui reçoit une demande d’effacement n’a pas de moyen pris en charge pour nettoyer une charge utile de passage en file d’attente.

Correction suggérée

L’une ou l’autre de ces options le résout ; faire les deux est mieux :

  1. Étendre resolve_reviewables_for_author_deletion (et le chemin de la corbeille) pour nettoyer payload['raw'] sur les révisables dont le passage cible a été supprimé par l’auteur ou mis à la corbeille.

  2. Ajouter ReviewableQueuedPost à Reviewable.scrubbable_types et permettre à l’action de nettoyage administrateur existante de s’appliquer lorsque le passage cible ne porte plus le contenu, afin que les opérateurs aient un remède pris en charge pour les charges utiles créées avant la correction.

Une spécification de régression devrait affirmer que, après qu’un auteur a supprimé un passage en file d’attente approuvé, la charge utile du révisable ne contient plus le corps soumis.

2 « J'aime »