feat: Permettre à l'enseignant de rédiger avec un éditeur riche et joindre des fichiers

Les enseignants avaient besoin de consignes plus claires pour les élèves :
le champ description en texte brut ne permettait ni mise en forme ni
partage de documents. Cette limitation obligeait à décrire verbalement
les ressources au lieu de les joindre directement.

L'éditeur WYSIWYG (TipTap) remplace le textarea avec gras, italique,
listes et liens. Le contenu HTML est sanitisé côté backend via
symfony/html-sanitizer pour prévenir les injections XSS. Les pièces
jointes (PDF, JPEG, PNG, max 10 Mo) sont uploadées via une API dédiée
avec validation MIME côté domaine et protection path-traversal sur le
téléchargement. Les descriptions en texte brut existantes restent
lisibles sans migration de données.
This commit is contained in:
2026-03-24 16:08:23 +01:00
parent 93baeb1eaa
commit ab835e5c3d
26 changed files with 2655 additions and 33 deletions

View File

@@ -15,6 +15,7 @@ use App\Scolarite\Application\Port\CurrentCalendarProvider;
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
use App\Scolarite\Application\Port\HomeworkRulesChecker;
use App\Scolarite\Application\Port\HomeworkRulesCheckResult;
use App\Scolarite\Application\Port\HtmlSanitizer;
use App\Scolarite\Application\Port\RuleWarning;
use App\Scolarite\Domain\Exception\DateEcheanceInvalideException;
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
@@ -110,6 +111,46 @@ final class CreateHomeworkHandlerTest extends TestCase
self::assertNull($homework->description);
}
#[Test]
public function itSanitizesHtmlDescription(): void
{
$sanitizer = new class implements HtmlSanitizer {
public function sanitize(string $html): string
{
return strip_tags($html, '<p><strong><em><ul><ol><li><a>');
}
};
$handler = $this->createHandlerWithSanitizer(affecte: true, htmlSanitizer: $sanitizer);
$command = $this->createCommand(description: '<p>Texte <strong>gras</strong></p><script>alert("xss")</script>');
$homework = $handler($command);
self::assertSame('<p>Texte <strong>gras</strong></p>alert("xss")', $homework->description);
}
#[Test]
public function itDoesNotSanitizeNullDescription(): void
{
$sanitizer = new class implements HtmlSanitizer {
public bool $called = false;
public function sanitize(string $html): string
{
$this->called = true;
return $html;
}
};
$handler = $this->createHandlerWithSanitizer(affecte: true, htmlSanitizer: $sanitizer);
$command = $this->createCommand(description: null);
$handler($command);
self::assertFalse($sanitizer->called);
}
#[Test]
public function itThrowsWhenSoftRulesViolatedAndNotAcknowledged(): void
{
@@ -269,12 +310,67 @@ final class CreateHomeworkHandlerTest extends TestCase
}
};
$htmlSanitizer = new class implements HtmlSanitizer {
public function sanitize(string $html): string
{
return $html;
}
};
return new CreateHomeworkHandler(
$this->homeworkRepository,
$affectationChecker,
$calendarProvider,
new DueDateValidator(),
$rulesChecker,
$htmlSanitizer,
$this->clock,
);
}
private function createHandlerWithSanitizer(bool $affecte, HtmlSanitizer $htmlSanitizer, ?HomeworkRulesCheckResult $rulesResult = null): CreateHomeworkHandler
{
$affectationChecker = new class($affecte) implements EnseignantAffectationChecker {
public function __construct(private readonly bool $affecte)
{
}
public function estAffecte(UserId $teacherId, ClassId $classId, SubjectId $subjectId, TenantId $tenantId): bool
{
return $this->affecte;
}
};
$calendarProvider = new class implements CurrentCalendarProvider {
public function forCurrentYear(TenantId $tenantId): SchoolCalendar
{
return SchoolCalendar::reconstitute(
tenantId: $tenantId,
academicYearId: AcademicYearId::fromString('550e8400-e29b-41d4-a716-446655440002'),
zone: null,
entries: [],
);
}
};
$rulesChecker = new class($rulesResult ?? HomeworkRulesCheckResult::ok()) implements HomeworkRulesChecker {
public function __construct(private readonly HomeworkRulesCheckResult $result)
{
}
public function verifier(TenantId $tenantId, DateTimeImmutable $dueDate, DateTimeImmutable $creationDate): HomeworkRulesCheckResult
{
return $this->result;
}
};
return new CreateHomeworkHandler(
$this->homeworkRepository,
$affectationChecker,
$calendarProvider,
new DueDateValidator(),
$rulesChecker,
$htmlSanitizer,
$this->clock,
);
}