Add sanitize_plaintext(text: str) -> str to utils/text.py for preparing XMPP plain-text message bodies from untrusted Pronote/AI content. - Strips HTML tags via BeautifulSoup (html.parser) - Strips C0, DEL, and C1 control characters (preserves \t, \n, \r) - Preserves Unicode including emojis (📌📅📚💬📢) - Idempotent: f(f(x)) == f(x) - Addresses SEC-XMPP-06: XMPP injection hardening 37 unit tests covering HTML, entities, control chars, emojis, idempotence. Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid> Co-authored-by: opencode/coder <coder@agents.invalid>
61 lines
2.4 KiB
Python
61 lines
2.4 KiB
Python
"""Utilitaires de normalisation et de traitement du texte.
|
|
|
|
Ce module centralise les transformations de texte partagées par plusieurs
|
|
couches du pipeline ``pronote-sync`` (sources, synchronisation) afin que les
|
|
modules de logique de domaine ne dépendent pas d'adaptateurs concrets.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
__all__ = ["normalize_subject", "sanitize_plaintext"]
|
|
|
|
|
|
def normalize_subject(subject: str) -> str:
|
|
"""Normalise une matière pour le matching déterministe.
|
|
|
|
Applique la normalisation Unicode NFKC, unifie les espaces (y compris
|
|
tabulations et espaces insécables), supprime la ponctuation et met la
|
|
chaîne en minuscules. Deux représentations visuellement identiques d'une
|
|
même matière produisent ainsi la même forme normalisée.
|
|
|
|
:param subject: La matière brute.
|
|
:return: La forme normalisée (NFKC, espaces unifiés, sans ponctuation, minuscule).
|
|
:rtype: str
|
|
"""
|
|
normalized = unicodedata.normalize("NFKC", subject)
|
|
normalized = re.sub(r"\s+", " ", normalized).strip()
|
|
normalized = re.sub(r"[^\w\s]", "", normalized)
|
|
normalized = re.sub(r"\s+", " ", normalized).strip()
|
|
return normalized.lower()
|
|
|
|
|
|
# Pattern des caractères de contrôle ASCII non imprimables (à l'exception
|
|
# des tabulations ``\\t``, des sauts de ligne ``\\n`` et des retours chariot ``\\r``).
|
|
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
|
|
|
|
|
def sanitize_plaintext(text: str) -> str:
|
|
"""Prépare un texte pour le corps de message XMPP en texte brut.
|
|
|
|
Supprime les balises HTML (via ``BeautifulSoup`` avec le parseur
|
|
``html.parser``) puis les caractères de contrôle ASCII non imprimables,
|
|
à l'exception des tabulations (``\\t``), des sauts de ligne (``\\n``) et
|
|
des retours chariot (``\\r``). Les caractères Unicode au-delà de ``\\x1f``,
|
|
notamment les emojis, sont conservés. La transformation est idempotente :
|
|
appliquée deux fois, elle produit le même résultat qu'appliquée une seule
|
|
fois. Une chaîne vide donne une chaîne vide.
|
|
|
|
:param text: Le texte brut ou HTML à assainir.
|
|
:return: Le texte assaini, sans balises HTML ni caractères de contrôle.
|
|
:rtype: str
|
|
"""
|
|
# Étape 1 : suppression des balises HTML.
|
|
plain = BeautifulSoup(text, "html.parser").get_text()
|
|
# Étape 2 : suppression des caractères de contrôle.
|
|
return _CONTROL_CHARS_RE.sub("", plain)
|