Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
AttachmentController
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
3 / 3
10
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
 index
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 createDownloadResponse
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Attachment Controller
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Anatoliy Belsky <ab@php.net>
12 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
13 * @copyright 2009-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2009-06-23
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ\Controller\Frontend;
22
23use phpMyFAQ\Attachment\AbstractAttachment;
24use phpMyFAQ\Attachment\AttachmentException;
25use phpMyFAQ\Attachment\AttachmentService;
26use phpMyFAQ\Core\Exception;
27use phpMyFAQ\Faq\Permission;
28use phpMyFAQ\Filter;
29use Symfony\Component\HttpFoundation\Request;
30use Symfony\Component\HttpFoundation\Response;
31use Symfony\Component\HttpFoundation\StreamedResponse;
32use Symfony\Component\Routing\Attribute\Route;
33
34final class AttachmentController extends AbstractFrontController
35{
36    public function __construct(
37        private readonly Permission $faqPermission,
38    ) {
39        parent::__construct();
40    }
41
42    /**
43     * @throws Exception
44     * @throws \Exception
45     */
46    #[Route(
47        path: '/attachment/{attachmentId}',
48        name: 'public.attachment',
49        requirements: ['attachmentId' => '\d+'],
50        methods: ['GET'],
51    )]
52    public function index(Request $request): Response
53    {
54        $id = Filter::filterVar($request->attributes->get('attachmentId'), FILTER_VALIDATE_INT);
55        $attachmentErrors = [];
56        $attachment = null;
57
58        $attachmentService = new AttachmentService($this->configuration, $this->currentUser, $this->faqPermission);
59
60        if ($id === null) {
61            $attachmentErrors[] = $attachmentService->getGenericErrorMessage();
62        }
63        if ($id !== null) {
64            try {
65                $attachment = $attachmentService->getAttachment($id);
66            } catch (AttachmentException $attachmentException) {
67                $attachmentErrors[] = $attachmentService->getAttachmentErrorMessage($attachmentException);
68            }
69        }
70
71        if (
72            $attachment instanceof AbstractAttachment
73            && $attachment->getRecordId() > 0
74            && $attachmentService->canDownloadAttachment($attachment)
75        ) {
76            $this->createDownloadResponse($attachment)->send();
77            return $this->render('attachment.twig', [
78                ...$this->getHeader($request),
79                'attachmentErrors' => $attachmentErrors,
80            ]);
81        }
82        $attachmentErrors[] = $attachmentService->getGenericErrorMessage();
83
84        return $this->render('attachment.twig', [
85            ...$this->getHeader($request),
86            'attachmentErrors' => $attachmentErrors,
87        ]);
88    }
89
90    public function createDownloadResponse(AbstractAttachment $attachment): StreamedResponse
91    {
92        $streamedResponse = new StreamedResponse(static function () use ($attachment): void {
93            $attachment->rawOut();
94        });
95
96        $streamedResponse->headers->set('Content-Type', $attachment->getMimeType());
97        $streamedResponse->headers->set('Content-Length', (string) $attachment->getFilesize());
98        $streamedResponse->headers->set('X-Content-Type-Options', 'nosniff');
99
100        $disposition = $attachment->getMimeType() === 'application/pdf' ? 'inline' : 'attachment';
101        $streamedResponse->headers->set(
102            'Content-Disposition',
103            $disposition . '; filename="' . rawurlencode($attachment->getFilename()) . '"',
104        );
105
106        $streamedResponse->headers->set('Content-MD5', $attachment->getRealHash());
107
108        return $streamedResponse;
109    }
110}