*/ final readonly class TaskDocumentProcessor implements ProcessorInterface { private const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB private const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/plain', 'text/csv', 'application/zip', 'application/x-rar-compressed', 'application/gzip', 'application/json', 'application/xml', 'text/xml', ]; private const MIME_TO_EXTENSION = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'application/pdf' => 'pdf', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'text/plain' => 'txt', 'text/csv' => 'csv', 'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar', 'application/gzip' => 'gz', 'application/json' => 'json', 'application/xml' => 'xml', 'text/xml' => 'xml', ]; public function __construct( private EntityManagerInterface $entityManager, private Security $security, private RequestStack $requestStack, private string $uploadDir, ) {} /** * @param TaskDocument $data */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): TaskDocument { $request = $this->requestStack->getCurrentRequest(); if (null === $request) { throw new BadRequestHttpException('No request available.'); } $file = $request->files->get('file'); if (null === $file || !$file->isValid()) { throw new BadRequestHttpException('No valid file uploaded.'); } if ($file->getSize() > self::MAX_FILE_SIZE) { throw new BadRequestHttpException('File size exceeds 50 MB limit.'); } $taskIri = $request->request->get('task', ''); $clientTicketIri = $request->request->get('clientTicket', ''); if ('' === $taskIri && '' === $clientTicketIri) { throw new BadRequestHttpException('Either task or clientTicket IRI is required.'); } $task = null; $clientTicket = null; if ('' !== $taskIri) { // ROLE_CLIENT (without ROLE_ADMIN) cannot upload documents directly to tasks if ($this->security->isGranted('ROLE_CLIENT') && !$this->security->isGranted('ROLE_ADMIN')) { throw new AccessDeniedHttpException('Clients can only upload documents to client tickets.'); } $task = $this->entityManager->getRepository(Task::class)->find((int) basename($taskIri)); if (null === $task) { throw new BadRequestHttpException('Task not found.'); } } if ('' !== $clientTicketIri) { $clientTicket = $this->entityManager->getRepository(ClientTicket::class)->find((int) basename($clientTicketIri)); if (null === $clientTicket) { throw new BadRequestHttpException('Client ticket not found.'); } if (!$this->security->isGranted('ROLE_ADMIN') && $clientTicket->getSubmittedBy() !== $this->security->getUser()) { throw new AccessDeniedHttpException('You can only upload documents to your own tickets.'); } } // Use server-detected MIME type (finfo), not the client-supplied one $originalName = $file->getClientOriginalName(); $mimeType = $file->getMimeType() ?: 'application/octet-stream'; $fileSize = $file->getSize(); if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) { throw new BadRequestHttpException(sprintf('File type "%s" is not allowed.', $mimeType)); } $extension = self::MIME_TO_EXTENSION[$mimeType] ?? 'bin'; $uuid = Uuid::v4()->toRfc4122(); $fileName = $uuid.'.'.$extension; if (!is_dir($this->uploadDir)) { mkdir($this->uploadDir, 0o775, true); } $file->move($this->uploadDir, $fileName); $document = new TaskDocument(); $document->setTask($task); $document->setClientTicket($clientTicket); $document->setOriginalName($originalName); $document->setFileName($fileName); $document->setMimeType($mimeType); $document->setSize($fileSize); $document->setCreatedAt(new DateTimeImmutable()); $document->setUploadedBy($this->security->getUser()); $this->entityManager->persist($document); $this->entityManager->flush(); return $document; } }