- Block SVG MIME type in TaskDocumentProcessor upload validation - Serve existing SVG files as attachment (defense-in-depth) in download controller - Block ROLE_CLIENT from uploading documents to tasks (only allowed via portal tickets) - Add Doctrine extension to filter projects by allowedProjects for ROLE_CLIENT Tickets: T-003, T-005, T-006 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
67 lines
2.6 KiB
PHP
67 lines
2.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Entity\TaskDocument;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
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;
|
|
|
|
class TaskDocumentDownloadController extends AbstractController
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $entityManager,
|
|
private readonly Security $security,
|
|
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
|
|
{
|
|
$document = $this->entityManager->getRepository(TaskDocument::class)->find($id);
|
|
|
|
if (null === $document) {
|
|
throw new NotFoundHttpException('Document not found.');
|
|
}
|
|
|
|
// ROLE_CLIENT can only download documents from their own tickets
|
|
if (!$this->security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('ROLE_USER')) {
|
|
$ticket = $document->getClientTicket();
|
|
if (null === $ticket || $ticket->getSubmittedBy() !== $this->security->getUser()) {
|
|
throw new AccessDeniedHttpException('You do not have access to this document.');
|
|
}
|
|
}
|
|
|
|
$filePath = $this->uploadDir.'/'.$document->getFileName();
|
|
|
|
if (!file_exists($filePath)) {
|
|
throw new NotFoundHttpException('File not found on disk.');
|
|
}
|
|
|
|
$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->headers->set('Content-Type', $mimeType);
|
|
|
|
return $response;
|
|
}
|
|
}
|