feat(documents) : bouton reload explorateur + liaison d'un fichier du partage SMB à un ticket

This commit is contained in:
Matthieu
2026-06-12 15:23:56 +02:00
parent 0f1eeeba1c
commit 73a34ef438
12 changed files with 472 additions and 42 deletions
@@ -5,24 +5,33 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\TaskDocument;
use App\Service\Share\Exception\ShareConnectionException;
use App\Service\Share\Exception\ShareNotConfiguredException;
use App\Service\Share\FileSource;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use function is_resource;
class TaskDocumentDownloadController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly FileSource $fileSource,
private readonly string $uploadDir,
) {}
#[Route('/api/task_documents/{id}/download', name: 'task_document_download', methods: ['GET'], priority: 1)]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function __invoke(int $id): BinaryFileResponse
public function __invoke(int $id): Response
{
$document = $this->entityManager->getRepository(TaskDocument::class)->find($id);
@@ -30,6 +39,20 @@ class TaskDocumentDownloadController extends AbstractController
throw new NotFoundHttpException('Document not found.');
}
$mimeType = $document->getMimeType() ?? 'application/octet-stream';
// Inline for images (except SVG) and PDFs, attachment for everything else.
// SVG is always attachment to prevent XSS via embedded JavaScript.
$inline = 'image/svg+xml' !== $mimeType && (str_starts_with($mimeType, 'image/') || 'application/pdf' === $mimeType);
$disposition = $inline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT;
return $document->isShareLink()
? $this->streamFromShare($document, $mimeType, $disposition)
: $this->streamFromDisk($document, $mimeType, $disposition);
}
private function streamFromDisk(TaskDocument $document, string $mimeType, string $disposition): BinaryFileResponse
{
$filePath = $this->uploadDir.'/'.$document->getFileName();
if (!file_exists($filePath)) {
@@ -37,18 +60,32 @@ class TaskDocumentDownloadController extends AbstractController
}
$response = new BinaryFileResponse($filePath);
$mimeType = $document->getMimeType() ?? 'application/octet-stream';
// Inline for images and PDFs, attachment for everything else
// SVG files are always served as attachment to prevent XSS via embedded JavaScript
$disposition = 'image/svg+xml' === $mimeType
? ResponseHeaderBag::DISPOSITION_ATTACHMENT
: (str_starts_with($mimeType, 'image/') || 'application/pdf' === $mimeType
? ResponseHeaderBag::DISPOSITION_INLINE
: ResponseHeaderBag::DISPOSITION_ATTACHMENT);
$response->setContentDisposition($disposition, $document->getOriginalName());
$response->setContentDisposition($disposition, (string) $document->getOriginalName());
$response->headers->set('Content-Type', $mimeType);
$response->headers->set('X-Content-Type-Options', 'nosniff');
return $response;
}
private function streamFromShare(TaskDocument $document, string $mimeType, string $disposition): StreamedResponse
{
try {
$stream = $this->fileSource->read((string) $document->getSharePath());
} catch (ShareNotConfiguredException) {
throw new NotFoundHttpException('Share not configured.');
} catch (ShareConnectionException) {
throw new NotFoundHttpException('File not found on the share.');
}
$response = new StreamedResponse(function () use ($stream): void {
if (is_resource($stream)) {
fpassthru($stream);
fclose($stream);
}
});
$response->headers->set('Content-Type', $mimeType);
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition($disposition, (string) $document->getOriginalName()));
$response->headers->set('X-Content-Type-Options', 'nosniff');
return $response;
}