Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
DownloadUploadAction
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
3 / 3
4
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 sanitiseAttachmentFilename
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Document\Submission\Admin;
6
7use App\Domain\Document\Submission\Service\InvestorDocumentService;
8use Psr\Http\Message\ResponseInterface;
9use Psr\Http\Message\ServerRequestInterface;
10
11/**
12 * Admin-side download. No ownership check (RoleMiddleware gates the
13 * route to admin/super_admin); the file is streamed with the same
14 * defensive headers as the investor-side download so even an admin's
15 * browser cannot render it inline as a same-origin asset.
16 */
17final readonly class DownloadUploadAction
18{
19    public function __construct(
20        private InvestorDocumentService $service,
21    ) {}
22
23    /**
24     * @param array<string, string> $args
25     * @param ServerRequestInterface $request
26     * @param ResponseInterface $response
27     */
28    public function __invoke(
29        ServerRequestInterface $request,
30        ResponseInterface $response,
31        array $args,
32    ): ResponseInterface {
33        $documentId = (int)$args['id'];
34
35        $event = $this->service->getCurrentFileForAdmin($documentId);
36        $storageKey = (string)$event->fileStorageKey;
37        $stream = $this->service->getStream($storageKey);
38
39        $filename = self::sanitiseAttachmentFilename($event->originalFilename ?? 'document');
40        $mimeType = $event->mimeType ?? 'application/octet-stream';
41
42        return $response
43            ->withHeader('Content-Type', $mimeType)
44            ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
45            ->withHeader('Cache-Control', 'private, no-store')
46            ->withHeader('X-Content-Type-Options', 'nosniff')
47            ->withBody($stream);
48    }
49
50    private static function sanitiseAttachmentFilename(string $name): string
51    {
52        $clean = preg_replace('/[^A-Za-z0-9 ._\-]/', '_', $name) ?? 'document';
53        $clean = trim($clean);
54
55        return $clean === '' ? 'document' : $clean;
56    }
57}