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.
210 lines
7.7 KiB
PHP
210 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Scolarite\Infrastructure\Api\Controller;
|
|
|
|
use App\Administration\Infrastructure\Security\SecurityUser;
|
|
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
|
use App\Scolarite\Application\Port\StudentClassReader;
|
|
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkHandler;
|
|
use App\Scolarite\Application\Query\GetStudentHomework\GetStudentHomeworkQuery;
|
|
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDetailDto;
|
|
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDto;
|
|
use App\Scolarite\Domain\Model\Homework\Homework;
|
|
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
|
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
|
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
|
use App\Scolarite\Infrastructure\Security\HomeworkStudentVoter;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
|
|
use function array_map;
|
|
use function is_string;
|
|
use function realpath;
|
|
use function str_starts_with;
|
|
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
#[IsGranted(HomeworkStudentVoter::VIEW)]
|
|
final readonly class StudentHomeworkController
|
|
{
|
|
public function __construct(
|
|
private Security $security,
|
|
private GetStudentHomeworkHandler $handler,
|
|
private HomeworkRepository $homeworkRepository,
|
|
private HomeworkAttachmentRepository $attachmentRepository,
|
|
private ScheduleDisplayReader $displayReader,
|
|
private StudentClassReader $studentClassReader,
|
|
#[Autowire('%kernel.project_dir%/var/storage')]
|
|
private string $uploadsDir,
|
|
) {
|
|
}
|
|
|
|
#[Route('/api/me/homework', name: 'api_student_homework_list', methods: ['GET'])]
|
|
public function list(Request $request): JsonResponse
|
|
{
|
|
$user = $this->getSecurityUser();
|
|
|
|
$subjectId = $request->query->get('subjectId');
|
|
|
|
$result = ($this->handler)(new GetStudentHomeworkQuery(
|
|
studentId: $user->userId(),
|
|
tenantId: $user->tenantId(),
|
|
subjectId: is_string($subjectId) && $subjectId !== '' ? $subjectId : null,
|
|
));
|
|
|
|
return new JsonResponse([
|
|
'data' => array_map($this->serializeListItem(...), $result),
|
|
]);
|
|
}
|
|
|
|
#[Route('/api/me/homework/{id}', name: 'api_student_homework_detail', methods: ['GET'])]
|
|
public function detail(string $id): JsonResponse
|
|
{
|
|
$user = $this->getSecurityUser();
|
|
$tenantId = TenantId::fromString($user->tenantId());
|
|
|
|
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($id), $tenantId);
|
|
|
|
if ($homework === null) {
|
|
throw new NotFoundHttpException('Devoir non trouvé.');
|
|
}
|
|
|
|
$this->assertHomeworkBelongsToStudentClass($user, $homework);
|
|
|
|
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
|
|
|
$subjects = $this->displayReader->subjectDisplay($user->tenantId(), (string) $homework->subjectId);
|
|
$teacherNames = $this->displayReader->teacherNames($user->tenantId(), (string) $homework->teacherId);
|
|
|
|
$detail = StudentHomeworkDetailDto::fromDomain(
|
|
$homework,
|
|
$subjects[(string) $homework->subjectId]['name'] ?? '',
|
|
$subjects[(string) $homework->subjectId]['color'] ?? null,
|
|
$teacherNames[(string) $homework->teacherId] ?? '',
|
|
$attachments,
|
|
);
|
|
|
|
return new JsonResponse(['data' => $this->serializeDetail($detail)]);
|
|
}
|
|
|
|
#[Route('/api/me/homework/{homeworkId}/attachments/{attachmentId}', name: 'api_student_homework_attachment', methods: ['GET'])]
|
|
public function downloadAttachment(string $homeworkId, string $attachmentId): BinaryFileResponse
|
|
{
|
|
$user = $this->getSecurityUser();
|
|
$tenantId = TenantId::fromString($user->tenantId());
|
|
|
|
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($homeworkId), $tenantId);
|
|
|
|
if ($homework === null) {
|
|
throw new NotFoundHttpException('Devoir non trouvé.');
|
|
}
|
|
|
|
$this->assertHomeworkBelongsToStudentClass($user, $homework);
|
|
|
|
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
|
|
|
foreach ($attachments as $attachment) {
|
|
if ((string) $attachment->id === $attachmentId) {
|
|
$fullPath = $this->uploadsDir . '/' . $attachment->filePath;
|
|
$realPath = realpath($fullPath);
|
|
$realUploadsDir = realpath($this->uploadsDir);
|
|
|
|
if ($realPath === false || $realUploadsDir === false || !str_starts_with($realPath, $realUploadsDir)) {
|
|
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
|
}
|
|
|
|
$response = new BinaryFileResponse($realPath);
|
|
$response->setContentDisposition(
|
|
ResponseHeaderBag::DISPOSITION_INLINE,
|
|
$attachment->filename,
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
|
}
|
|
|
|
private function assertHomeworkBelongsToStudentClass(SecurityUser $user, Homework $homework): void
|
|
{
|
|
$classId = $this->studentClassReader->currentClassId(
|
|
$user->userId(),
|
|
TenantId::fromString($user->tenantId()),
|
|
);
|
|
|
|
if ($classId === null || (string) $homework->classId !== $classId) {
|
|
throw new NotFoundHttpException('Devoir non trouvé.');
|
|
}
|
|
}
|
|
|
|
private function getSecurityUser(): SecurityUser
|
|
{
|
|
$user = $this->security->getUser();
|
|
|
|
if (!$user instanceof SecurityUser) {
|
|
throw new AccessDeniedHttpException('Authentification requise.');
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function serializeListItem(StudentHomeworkDto $dto): array
|
|
{
|
|
return [
|
|
'id' => $dto->id,
|
|
'subjectId' => $dto->subjectId,
|
|
'subjectName' => $dto->subjectName,
|
|
'subjectColor' => $dto->subjectColor,
|
|
'teacherId' => $dto->teacherId,
|
|
'teacherName' => $dto->teacherName,
|
|
'title' => $dto->title,
|
|
'description' => $dto->description,
|
|
'dueDate' => $dto->dueDate,
|
|
'createdAt' => $dto->createdAt,
|
|
'hasAttachments' => $dto->hasAttachments,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function serializeDetail(StudentHomeworkDetailDto $dto): array
|
|
{
|
|
return [
|
|
'id' => $dto->id,
|
|
'subjectId' => $dto->subjectId,
|
|
'subjectName' => $dto->subjectName,
|
|
'subjectColor' => $dto->subjectColor,
|
|
'teacherId' => $dto->teacherId,
|
|
'teacherName' => $dto->teacherName,
|
|
'title' => $dto->title,
|
|
'description' => $dto->description,
|
|
'dueDate' => $dto->dueDate,
|
|
'createdAt' => $dto->createdAt,
|
|
'attachments' => array_map(
|
|
static fn ($a): array => [
|
|
'id' => $a->id,
|
|
'filename' => $a->filename,
|
|
'fileSize' => $a->fileSize,
|
|
'mimeType' => $a->mimeType,
|
|
],
|
|
$dto->attachments,
|
|
),
|
|
];
|
|
}
|
|
}
|