Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
44 / 44
Attachment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
5 / 5
17
100.00% covered (success)
100.00%
44 / 44
 attachment
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
8 / 8
 attachment_show
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
9 / 9
 attachment_view
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 attachment_download
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 attachment_thumbnail
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
17 / 17
1<?php
2
3// {{{ License
4
5// This file is part of GNU social - https://www.gnu.org/software/social
6//
7// GNU social is free software: you can redistribute it and/or modify
8// it under the terms of the GNU Affero General Public License as published by
9// the Free Software Foundation, either version 3 of the License, or
10// (at your option) any later version.
11//
12// GNU social is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15// GNU Affero General Public License for more details.
16//
17// You should have received a copy of the GNU Affero General Public License
18// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
19
20// }}}
21
22namespace App\Controller;
23
24use App\Core\Controller;
25use App\Core\DB\DB;
26use App\Core\Event;
27use App\Core\GSFile;
28use function App\Core\I18n\_m;
29use App\Core\Log;
30use App\Core\Router\Router;
31use App\Entity\AttachmentThumbnail;
32use App\Util\Common;
33use App\Util\Exception\ClientException;
34use App\Util\Exception\NoSuchFileException;
35use App\Util\Exception\NotFoundException;
36use App\Util\Exception\ServerException;
37use Symfony\Component\HttpFoundation\HeaderUtils;
38use Symfony\Component\HttpFoundation\Request;
39use Symfony\Component\HttpFoundation\Response;
40use Symfony\Component\Mime\MimeTypes;
41
42class Attachment extends Controller
43{
44    /**
45     * Generic function that handles getting a representation for an attachment
46     */
47    private function attachment(int $id, callable $handle)
48    {
49        if ($id <= 0) { // This should never happen coming from the router, but let's bail if it does
50            // @codeCoverageIgnoreStart
51            Log::critical("Attachment controller called with {$id}, which should not be possible");
52            throw new ClientException(_m('No such attachment'), 404);
53        // @codeCoverageIgnoreEnd
54        } else {
55            if (Event::handle('AttachmentFileInfo', [$id, &$res]) != Event::stop) {
56                // If no one else claims this attachment, use the default representation
57                try {
58                    $res = GSFile::getAttachmentFileInfo($id);
59                } catch (NoSuchFileException $e) {
60                    // Continue below
61                }
62            }
63        }
64
65        if (empty($res)) {
66            throw new ClientException(_m('No such attachment'), 404);
67        } else {
68            if (!array_key_exists('filepath', $res)) {
69                // @codeCoverageIgnoreStart
70                throw new ServerException('This attachment is not stored locally.');
71            // @codeCoverageIgnoreEnd
72            } else {
73                return $handle($res);
74            }
75        }
76    }
77
78    /**
79     * The page where the attachment and it's info is shown
80     */
81    public function attachment_show(Request $request, int $id)
82    {
83        try {
84            $attachment = DB::findOneBy('attachment', ['id' => $id]);
85            return $this->attachment($id, function ($res) use ($id, $attachment) {
86                return [
87                    '_template'        => 'attachments/show.html.twig',
88                    'download'         => Router::url('attachment_download', ['id' => $id]),
89                    'attachment'       => $attachment,
90                    'right_panel_vars' => ['attachment_id' => $id],
91                ];
92            });
93        } catch (NotFoundException) {
94            throw new ClientException(_m('No such attachment'), 404);
95        }
96    }
97
98    /**
99     * Display the attachment inline
100     */
101    public function attachment_view(Request $request, int $id)
102    {
103        return $this->attachment($id, fn (array $res) => GSFile::sendFile(
104                                     $res['filepath'], $res['mimetype'],
105                                     GSFile::ensureFilenameWithProperExtension($res['filename'], $res['mimetype']) ?? $res['filename'],
106                                     HeaderUtils::DISPOSITION_INLINE
107                                 )
108        );
109    }
110
111    public function attachment_download(Request $request, int $id)
112    {
113        return $this->attachment($id, fn (array $res) => GSFile::sendFile(
114                                     $res['filepath'], $res['mimetype'],
115                                     GSFile::ensureFilenameWithProperExtension($res['filename'], $res['mimetype']) ?? $res['filename'],
116                                     HeaderUtils::DISPOSITION_ATTACHMENT
117                                 )
118        );
119    }
120
121    /**
122     * Controller to produce a thumbnail for a given attachment id
123     *
124     * @param Request $request
125     * @param int     $id      Attachment ID
126     *
127     * @throws ClientException
128     * @throws NotFoundException
129     * @throws ServerException
130     * @throws \App\Util\Exception\DuplicateFoundException
131     *
132     * @return Response
133     */
134    public function attachment_thumbnail(Request $request, int $id): Response
135    {
136        $attachment = DB::findOneBy('attachment', ['id' => $id]);
137
138        $default_width  = Common::config('thumbnail', 'width');
139        $default_height = Common::config('thumbnail', 'height');
140        $default_crop   = Common::config('thumbnail', 'smart_crop');
141        $width          = $this->int('w') ?: $default_width;
142        $height         = $this->int('h') ?: $default_height;
143        $crop           = $this->bool('c') ?: $default_crop;
144
145        Event::handle('GetAllowedThumbnailSizes', [&$sizes]);
146        if (!in_array(['width' => $width, 'height' => $height], $sizes)) {
147            throw new ClientException(_m('The requested thumbnail dimensions are not allowed'), 400); // 400 Bad Request
148        }
149
150        if (!is_null($attachment->getWidth()) && !is_null($attachment->getHeight())) {
151            [$width, $height] = AttachmentThumbnail::predictScalingValues($attachment->getWidth(), $attachment->getHeight(), $width, $height, $crop);
152        }
153
154        $thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, width: $width, height: $height, crop: $crop);
155
156        $filename = $thumbnail->getFilename();
157        $path     = $thumbnail->getPath();
158        $mimetype = $thumbnail->getMimetype();
159
160        return GSFile::sendFile(filepath: $path, mimetype: $mimetype, output_filename: $filename . '.' . MimeTypes::getDefault()->getExtensions($mimetype)[0], disposition: HeaderUtils::DISPOSITION_INLINE);
161    }
162}