Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Media
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 7
2256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 upload
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 image
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
552
 file
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 validateUploadedFile
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
156
 sendFile
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getAssets
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller;
6
7use Celemas\Core\Factory\Factory;
8use Celemas\Core\Request;
9use Celemas\Core\Response;
10use Cosray\Assets\Assets;
11use Cosray\Assets\ResizeMode;
12use Cosray\Assets\Size;
13use Cosray\Config;
14use Cosray\Exception\RuntimeException;
15use Cosray\Middleware\Permission;
16use Gumlet\ImageResize;
17
18class Media
19{
20    public function __construct(
21        protected readonly Factory $factory,
22        protected readonly Request $request,
23        protected readonly Config $config,
24    ) {}
25
26    /**
27     * TODO: sanitize filename.
28     */
29    #[Permission('panel')]
30    public function upload(string $mediatype, string $doctype, string $uid): Response
31    {
32        $response = Response::create($this->factory);
33        $file = $_FILES['file'] ?? null;
34
35        $result = $this->validateUploadedFile($mediatype, $file);
36
37        if (!$result['ok']) {
38            return $response->json($result, 400);
39        }
40
41        $public = $this->config->path->public;
42        $assets = $this->config->path->assets;
43        $dir = "{$public}{$assets}/{$doctype}/{$uid}";
44
45        if (!is_dir($dir)) {
46            mkdir($dir, 0o755, true);
47        }
48
49        move_uploaded_file($file['tmp_name'], "{$dir}/{$result['file']}");
50
51        return $response->json($result);
52    }
53
54    public function image(string $slug): Response
55    {
56        $image = $this->getAssets()->image($slug);
57        $qs = $this->request->params();
58
59        if ($qs['resize'] ?? null) {
60            [$size, $mode] = match ($qs['resize']) {
61                ResizeMode::Width->value => [new Size((int) $qs['w']), ResizeMode::Width],
62                ResizeMode::Height->value => [new Size((int) $qs['h']), ResizeMode::Height],
63                ResizeMode::LongSide->value => [new Size((int) $qs['size']), ResizeMode::LongSide],
64                ResizeMode::ShortSide->value => [new Size((int) $qs['size']), ResizeMode::ShortSide],
65                ResizeMode::Fit->value => [new Size((int) $qs['w'], (int) $qs['h']), ResizeMode::Fit],
66                ResizeMode::Resize->value => [new Size((int) $qs['w'], (int) $qs['h']), ResizeMode::Resize],
67                ResizeMode::FreeCrop->value => [new Size((int) $qs['w'], (int) $qs['h'], [
68                        'x' => $qs['x'] ? (int) $qs['x'] : false,
69                        'y' => $qs['y'] ? (int) $qs['y'] : false,
70                    ]), ResizeMode::FreeCrop],
71                ResizeMode::Crop->value => [new Size((int) $qs['w'], (int) $qs['h'], match ($qs['pos']) {
72                        'top' => ImageResize::CROPTOP,
73                        'centre' => ImageResize::CROPCENTRE,
74                        'center' => ImageResize::CROPCENTER,
75                        'bottom' => ImageResize::CROPBOTTOM,
76                        'left' => ImageResize::CROPLEFT,
77                        'right' => ImageResize::CROPRIGHT,
78                        'topcenter' => ImageResize::CROPTOPCENTER,
79                        default => throw new RuntimeException('Crop position not supported: ' . $qs['pos']),
80                    }), ResizeMode::Crop],
81                default => throw new RuntimeException('Resize mode not supported: ' . $qs['resize']),
82            };
83
84            $quality = $qs['quality'] ?? null ? (int) $qs['quality'] : null;
85            $image->resize($size, $mode, $qs['enlarge'] ?? false, $quality);
86        }
87
88        $fileServer = $this->config->media->fileServer;
89
90        if ($fileServer) {
91            return $this->sendFile($fileServer, $image->path());
92        }
93
94        return Response::create($this->factory)->file($image->path());
95    }
96
97    public function file(string $slug): Response
98    {
99        $file = $this->getAssets()->file($slug);
100        $fileServer = $this->config->media->fileServer;
101
102        if ($fileServer) {
103            return $this->sendFile($fileServer, $file->path());
104        }
105
106        return Response::create($this->factory)->file($file->path());
107    }
108
109    protected function validateUploadedFile(string $mediatype, ?array $file): array
110    {
111        if (!$file) {
112            return [
113                'ok' => false,
114                'error' => _('Upload fehlgeschlagen. Datei konnte am Server nicht verabeitet werden.'),
115                'file' => _(' Dateiname unbekannt'),
116            ];
117        }
118        $upload = $this->config->upload;
119        $mimeTypes = match ($mediatype) {
120            'file' => $upload->file,
121            'image' => $upload->image,
122            'video' => $upload->video,
123            default => throw new RuntimeException('Media type not supported: ' . $mediatype),
124        };
125        $maxSize = $upload->maxSize;
126
127        $tmpFile = $file['tmp_name'];
128        $fileSize = filesize($tmpFile);
129        $fileInfo = finfo_open(FILEINFO_MIME_TYPE);
130        $mimeType = finfo_file($fileInfo, $tmpFile);
131        finfo_close($fileInfo);
132        $fileName = $file['full_path'];
133        $pathInfo = pathinfo($fileName);
134        $ext = $pathInfo['extension'] ?? null;
135        $allowedExtensions = $mimeTypes[$mimeType] ?? null;
136        $result = [
137            'ok' => true,
138            'file' => $fileName,
139            'error' => '',
140            'code' => 0,
141        ];
142
143        if (($file['error'] ?? null === UPLOAD_ERR_INI_SIZE) || $fileSize > $maxSize) {
144            $size = number_format((float) (($fileSize / 1024) / 1024), 2, '.', '');
145            $allowed = number_format((float) (($maxSize / 1024) / 1024), 2, '.', '');
146
147            return array_merge($result, [
148                'ok' => false,
149                'error' => "Die Datei ist zu groß: {$size} MB. Erlaubt sind {$allowed} MB",
150            ]);
151        }
152
153        if ($file['error'] ?? null !== UPLOAD_ERR_OK) {
154            return array_merge($result, [
155                'ok' => false,
156                'error' => _('Der Dateiupload ist aufgrund eines Serverfehlers fehlgeschlagen.'),
157            ]);
158        }
159
160        if (!$allowedExtensions) {
161            return array_merge($result, [
162                'ok' => false,
163                'error' => _("Der Dateityp ist nicht erlaubt: {$mimeType}."),
164            ]);
165        }
166
167        if (!$ext || !in_array(strtolower($ext), $allowedExtensions, true)) {
168            return array_merge($result, [
169                'ok' => false,
170                'error' => _(
171                    "Falsche Dateiendung: {$ext}. Für diesen Dateityp sind folgende Endungen erlaubt: "
172                    . implode(', ', $allowedExtensions)
173                    . '.',
174                ),
175            ]);
176        }
177
178        return $result;
179    }
180
181    protected function sendFile(string $fileServer, string $file): Response
182    {
183        $response = Response::create($this->factory);
184        $response->header('Content-Type', mime_content_type($file));
185
186        switch ($fileServer) {
187            case 'apache':
188                // apt install libapache2-mod-xsendfile
189                // a2enmod xsendfile
190                // Apache config:
191                //    XSendFile On
192                //    XSendFilePath "/path/to/files"
193                $response->header('X-Sendfile', $file);
194                break;
195            case 'nginx':
196                // Nginx config
197                //   location /path/to/files/ {
198                //       internal;
199                //           alias   /some/path/; # note the trailing slash
200                //       }
201                //   }
202
203                $response->header('X-Accel-Redirect', $file);
204                break;
205            default:
206                throw new RuntimeException(
207                    'File server not supported: `' . $fileServer . '`. Supported values `nginx`, `apache`.',
208                );
209        }
210
211        return $response;
212    }
213
214    protected function getAssets(): Assets
215    {
216        static $assets = null;
217
218        if (!$assets) {
219            $assets = new Assets($this->request, $this->config);
220        }
221
222        return $assets;
223    }
224}