*/ 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 FileSource $fileSource, private SharePathResolver $pathResolver, 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.'); } // Deux modes de création : upload d'un fichier (multipart) ou lien vers un fichier du partage SMB (JSON). $sharePath = $this->extractSharePath($request); $document = null !== $sharePath ? $this->createShareLink($request, $sharePath) : $this->createUpload($request); $document->setCreatedAt(new DateTimeImmutable()); $document->setUploadedBy($this->security->getUser()); $this->entityManager->persist($document); $this->entityManager->flush(); return $document; } private function extractSharePath(Request $request): ?string { // Lien SMB : champ multipart/form OU corps JSON { "sharePath": "..." } $fromForm = $request->request->get('sharePath'); if (is_string($fromForm) && '' !== $fromForm) { return $fromForm; } if (str_contains((string) $request->headers->get('Content-Type'), 'application/json')) { $payload = json_decode($request->getContent() ?: '{}', true); if (is_array($payload) && isset($payload['sharePath']) && is_string($payload['sharePath']) && '' !== $payload['sharePath']) { return $payload['sharePath']; } } return null; } private function createUpload(Request $request): TaskDocument { $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.'); } $task = $this->resolveTask($request->request->get('task', '')); // 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'; $fileName = Uuid::v4()->toRfc4122().'.'.$extension; if (!is_dir($this->uploadDir)) { mkdir($this->uploadDir, 0o775, true); } $file->move($this->uploadDir, $fileName); $document = new TaskDocument(); $document->setTask($task); $document->setOriginalName($originalName); $document->setFileName($fileName); $document->setMimeType($mimeType); $document->setSize($fileSize); return $document; } private function createShareLink(Request $request, string $rawSharePath): TaskDocument { $taskIri = $request->request->get('task'); if (!is_string($taskIri) || '' === $taskIri) { $payload = json_decode($request->getContent() ?: '{}', true); $taskIri = is_array($payload) ? ($payload['task'] ?? '') : ''; } $task = $this->resolveTask((string) $taskIri); try { $path = $this->pathResolver->normalizeRelative($rawSharePath); } catch (InvalidPathException) { throw new BadRequestHttpException('Invalid share path.'); } if ('' === $path) { throw new BadRequestHttpException('A share path is required.'); } $entry = $this->findShareEntry($path); if (null === $entry) { throw new BadRequestHttpException('File not found on the share.'); } if (!in_array($entry->mimeType, self::ALLOWED_MIME_TYPES, true)) { throw new BadRequestHttpException(sprintf('File type "%s" is not allowed.', $entry->mimeType)); } $document = new TaskDocument(); $document->setTask($task); $document->setOriginalName($entry->name); $document->setSharePath($path); $document->setMimeType($entry->mimeType); $document->setSize($entry->size); return $document; } /** * Récupère les métadonnées (taille, type) du fichier sur le partage en listant son dossier parent. */ private function findShareEntry(string $path): ?FileEntry { $parent = dirname($path); $parent = ('.' === $parent || '/' === $parent) ? '' : $parent; $name = basename($path); try { $entries = $this->fileSource->dir($parent); } catch (ShareNotConfiguredException) { throw new BadRequestHttpException('Share not configured.'); } catch (ShareConnectionException) { throw new BadRequestHttpException('Unable to reach the share.'); } foreach ($entries as $entry) { if (!$entry->isDir && $entry->name === $name) { return $entry; } } return null; } private function resolveTask(string $taskIri): Task { if ('' === $taskIri) { throw new BadRequestHttpException('A task IRI is required.'); } $task = $this->entityManager->getRepository(Task::class)->find((int) basename($taskIri)); if (null === $task) { throw new BadRequestHttpException('Task not found.'); } return $task; } }