Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.46% covered (danger)
37.46%
257 / 686
6.25% covered (danger)
6.25%
2 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
37.46% covered (danger)
37.46%
257 / 686
6.25% covered (danger)
6.25%
2 / 32
11203.86
0.00% covered (danger)
0.00%
0 / 1
 apiMediaUpload
65.57% covered (warning)
65.57%
40 / 61
0.00% covered (danger)
0.00%
0 / 1
17.88
 replaceUploadFiles
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 uploadFiles
93.94% covered (success)
93.94%
31 / 33
0.00% covered (danger)
0.00%
0 / 1
9.02
 uploadFilesToDestination
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createMediaPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createDbEntry
91.84% covered (success)
91.84%
45 / 49
0.00% covered (danger)
0.00%
0 / 1
8.03
 loadFileContent
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
342
 normalizeDbPath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 apiMediaUpdate
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
2.21
 validateMediaUpdate
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 updateMediaFromRequest
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
5.02
 apiReferenceCreate
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 createReferenceFromRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 validateReferenceCreate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 apiCollectionAdd
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 apiCollectionCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateCollectionCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 createCollectionFromRequest
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 createMediaCollectionFromMedia
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 createRecursiveMediaCollection
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
42
 apiMediaCreate
90.70% covered (success)
90.70%
39 / 43
0.00% covered (danger)
0.00%
0 / 1
7.04
 apiMediaExport
38.18% covered (danger)
38.18%
21 / 55
0.00% covered (danger)
0.00%
0 / 1
85.27
 prepareEncryptedMedia
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 createView
62.32% covered (warning)
62.32%
43 / 69
0.00% covered (danger)
0.00%
0 / 1
100.54
 setMediaResponseHeader
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
1122
 validateMediaTypeCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiMediaTypeCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createDocTypeFromRequest
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateMediaTypeL11nCreate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 apiMediaTypeL11nCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createMediaTypeL11nFromRequest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 resizeImage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   Modules\Media
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace Modules\Media\Controller;
16
17use Modules\Admin\Models\AccountPermission;
18use Modules\Admin\Models\NullAccount;
19use Modules\Media\Models\Collection;
20use Modules\Media\Models\CollectionMapper;
21use Modules\Media\Models\Media;
22use Modules\Media\Models\MediaContent;
23use Modules\Media\Models\MediaContentMapper;
24use Modules\Media\Models\MediaMapper;
25use Modules\Media\Models\MediaType;
26use Modules\Media\Models\MediaTypeL11nMapper;
27use Modules\Media\Models\MediaTypeMapper;
28use Modules\Media\Models\NullCollection;
29use Modules\Media\Models\NullMedia;
30use Modules\Media\Models\NullMediaType;
31use Modules\Media\Models\PathSettings;
32use Modules\Media\Models\PermissionCategory;
33use Modules\Media\Models\Reference;
34use Modules\Media\Models\ReferenceMapper;
35use Modules\Media\Models\UploadFile;
36use Modules\Media\Models\UploadStatus;
37use Modules\Media\Theme\Backend\Components\Media\ElementView;
38use Modules\Tag\Models\NullTag;
39use phpOMS\Account\PermissionType;
40use phpOMS\Application\ApplicationAbstract;
41use phpOMS\Asset\AssetType;
42use phpOMS\Autoloader;
43use phpOMS\Localization\BaseStringL11n;
44use phpOMS\Message\Http\HttpRequest;
45use phpOMS\Message\Http\HttpResponse;
46use phpOMS\Message\Http\RequestStatusCode;
47use phpOMS\Message\RequestAbstract;
48use phpOMS\Message\ResponseAbstract;
49use phpOMS\Model\Html\Head;
50use phpOMS\Security\Guard;
51use phpOMS\System\File\FileUtils;
52use phpOMS\System\File\Local\Directory;
53use phpOMS\System\MimeType;
54use phpOMS\Utils\ImageUtils;
55use phpOMS\Utils\Parser\Markdown\Markdown;
56use phpOMS\Utils\StringUtils;
57use phpOMS\Views\View;
58
59/**
60 * Media class.
61 *
62 * @package Modules\Media
63 * @license OMS License 2.0
64 * @link    https://jingga.app
65 * @since   1.0.0
66 */
67final class ApiController extends Controller
68{
69    /**
70     * Api method to upload media file.
71     *
72     * @param RequestAbstract  $request  Request
73     * @param ResponseAbstract $response Response
74     * @param array            $data     Generic data
75     *
76     * @return void
77     *
78     * @api
79     *
80     * @since 1.0.0
81     */
82    public function apiMediaUpload(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
83    {
84        $uploads = $this->uploadFiles(
85            names:              $request->getDataList('names'),
86            fileNames:          $request->getDataList('filenames'),
87            files:              $request->files,
88            account:            $request->header->account,
89            basePath:           __DIR__ . '/../../../Modules/Media/Files' . \urldecode($request->getDataString('path') ?? ''),
90            virtualPath:        \urldecode($request->getDataString('virtualpath') ?? ''),
91            password:           $request->getDataString('password') ?? '',
92            encryptionKey:      $request->getDataString('encryption') ?? ($request->getDataBool('isencrypted') === true && !empty($_SERVER['OMS_PRIVATE_KEY_I'] ?? '') ? $_SERVER['OMS_PRIVATE_KEY_I'] : ''),
93            pathSettings:       $request->getDataInt('pathsettings') ?? PathSettings::RANDOM_PATH, // IMPORTANT!!!
94            hasAccountRelation: $request->getDataBool('link_account') ?? false,
95            readContent:        $request->getDataBool('parse_content') ?? false,
96            unit:               $request->getDataInt('unit')
97        );
98
99        $ids = [];
100        foreach ($uploads as $file) {
101            $ids[] = $file->id;
102
103            // add media types
104            if (!empty($types = $request->getDataJson('types'))) {
105                foreach ($types as $type) {
106                    if (!isset($type['id'])) {
107                        $request->setData('name', $type['name'], true);
108                        $request->setData('title', $type['title'], true);
109                        $request->setData('lang', $type['lang'] ?? null, true);
110
111                        $internalResponse = new HttpResponse();
112                        $this->apiMediaTypeCreate($request, $internalResponse);
113
114                        if (!\is_array($data = $internalResponse->get($request->uri->__toString()))) {
115                            continue;
116                        }
117
118                        $file->addMediaType($tId = $data['response']);
119                    } else {
120                        $file->addMediaType(new NullMediaType($tId = (int) $type['id']));
121                    }
122
123                    $this->createModelRelation(
124                        $request->header->account,
125                        $file->id,
126                        $tId,
127                        MediaMapper::class,
128                        'types',
129                        '',
130                        $request->getOrigin()
131                    );
132                }
133            }
134
135            // add tags
136            if (!empty($tags = $request->getDataJson('tags'))) {
137                foreach ($tags as $tag) {
138                    if (!isset($tag['id'])) {
139                        $request->setData('title', $tag['title'], true);
140                        $request->setData('color', $tag['color'], true);
141                        $request->setData('icon', $tag['icon'] ?? null, true);
142                        $request->setData('language', $tag['language'], true);
143
144                        $internalResponse = new HttpResponse();
145                        $this->app->moduleManager->get('Tag')->apiTagCreate($request, $internalResponse);
146
147                        if (!\is_array($data = $internalResponse->get($request->uri->__toString()))) {
148                            continue;
149                        }
150
151                        $file->addTag($tId = $data['response']);
152                    } else {
153                        $file->addTag(new NullTag($tId = (int) $tag['id']));
154                    }
155
156                    $this->createModelRelation(
157                        $request->header->account,
158                        $file->id,
159                        $tId,
160                        MediaMapper::class,
161                        'tags',
162                        '',
163                        $request->getOrigin()
164                    );
165                }
166            }
167        }
168
169        $this->createStandardAddResponse($request, $response, $ids);
170    }
171
172    /**
173     * Upload a media file and replace the existing media file
174     *
175     * @param array $files              Files
176     * @param array $media              Media files to update
177     * @param bool  $sameNameIfPossible use exact same file name as original file name if the extension is the same
178     *
179     * @return Media[]
180     *
181     * @since 1.0.0
182     */
183    public function replaceUploadFiles(
184        array $files,
185        array $media,
186        bool $sameNameIfPossible = false
187    ) : array
188    {
189        if (empty($files) || \count($files) !== \count($media)) {
190            return [];
191        }
192
193        $nCounter = -1;
194        foreach ($files as $file) {
195            ++$nCounter;
196
197            // set output dir same as existing media
198            $outputDir = \dirname($media[$nCounter]->getAbsolutePath());
199
200            // set upload name (either same as old file name or new file name)
201            $mediaFilename  = \basename($media[$nCounter]->getAbsolutePath());
202            $uploadFilename = \basename($file['tmp_name']);
203
204            $splitMediaFilename  = \explode('.', $mediaFilename);
205            $splitUploadFilename = \explode('.', $uploadFilename);
206
207            $mediaExtension  =  ($c = \count($splitMediaFilename)) > 1 ? $splitMediaFilename[$c - 1] : '';
208            $uploadExtension =  ($c = \count($splitUploadFilename)) > 1 ? $splitUploadFilename[$c - 1] : '';
209
210            if ($sameNameIfPossible && $mediaExtension === $uploadExtension) {
211                $uploadFilename = $mediaFilename;
212            }
213
214            // remove old file
215            \unlink($media[$nCounter]->getAbsolutePath());
216
217            // upload file
218            $upload                   = new UploadFile();
219            $upload->outputDir        = $outputDir;
220            $upload->preserveFileName = $sameNameIfPossible;
221
222            $status = $upload->upload([$file], [$uploadFilename], true);
223            $stat   = \reset($status);
224
225            // update media data
226            $media[$nCounter]->setPath(self::normalizeDbPath($stat['path']) . '/' . $stat['filename']);
227            $media[$nCounter]->size      = $stat['size'];
228            $media[$nCounter]->extension = $stat['extension'];
229
230            MediaMapper::update()->execute($media[$nCounter]);
231
232            if (!empty($media[$nCounter]?->content->content)) {
233                $media[$nCounter]->content->content = self::loadFileContent(
234                    $media[$nCounter]->getAbsolutePath(),
235                    $media[$nCounter]->extension
236                );
237
238                MediaContentMapper::update()->execute($media[$nCounter]->content);
239            }
240        }
241
242        return $media;
243    }
244
245    /**
246     * Upload a media file
247     *
248     * @param array  $names              Database names
249     * @param array  $fileNames          FileNames
250     * @param array  $files              Files
251     * @param int    $account            Uploader
252     * @param string $basePath           Base path. The path which is used for the upload.
253     * @param string $virtualPath        virtual path The path which is used to visually structure the files, like directories
254     *                                   The file storage on the system can be different
255     * @param string $password           File password. The password to protect the file (only database)
256     * @param string $encryptionKey      Encryption key. Used to encrypt the file on the local file storage.
257     * @param int    $pathSettings       Settings which describe where the file should be uploaded to (physically)
258     *                                   - RANDOM_PATH = random location in the base path
259     *                                   - FILE_PATH   = combination of base path and virtual path
260     * @param bool   $hasAccountRelation The uploaded files should be related to an account
261     *
262     * @return Media[]
263     *
264     * @since 1.0.0
265     */
266    public function uploadFiles(
267        array $names = [],
268        array $fileNames = [],
269        array $files = [],
270        int $account = 0,
271        string $basePath = '',
272        string $virtualPath = '',
273        string $password = '',
274        string $encryptionKey = '',
275        int $pathSettings = PathSettings::RANDOM_PATH,
276        bool $hasAccountRelation = true,
277        bool $readContent = false,
278        int $unit = null
279    ) : array
280    {
281        if (empty($files)) {
282            return [];
283        }
284
285        $outputDir = '';
286        $absolute  = false;
287
288        if ($pathSettings === PathSettings::RANDOM_PATH) {
289            $outputDir = self::createMediaPath($basePath);
290        } elseif ($pathSettings === PathSettings::FILE_PATH) {
291            $outputDir = \rtrim($basePath, '/\\');
292            $absolute  = true;
293        } else {
294            return [];
295        }
296
297        if (!Guard::isSafePath($outputDir, __DIR__ . '/../../../')) {
298            return [];
299        }
300
301        $upload                   = new UploadFile();
302        $upload->outputDir        = $outputDir;
303        $upload->preserveFileName = empty($fileNames) || \count($fileNames) === \count($files);
304
305        $status = $upload->upload($files, $fileNames, $absolute, $encryptionKey);
306
307        $sameLength = \count($names) === \count($status);
308        $nCounter   = -1;
309
310        $created = [];
311        foreach ($status as &$stat) {
312            ++$nCounter;
313
314            // Possible: name != filename (name = database media name, filename = name on the file system)
315            $stat['name'] = $sameLength ? $names[$nCounter] : $stat['filename'];
316
317            $created[] = self::createDbEntry(
318                $stat,
319                $account,
320                $virtualPath,
321                app: $hasAccountRelation ? $this->app : null,
322                readContent: $readContent,
323                unit: $unit,
324                password: $password,
325                isEncrypted: !empty($encryptionKey)
326            );
327        }
328
329        return $created;
330    }
331
332    /**
333     * Uploads a file to a destination
334     *
335     * @param array  $files            Files to upload
336     * @param array  $fileNames        Names on the directory
337     * @param string $path             Upload path
338     * @param bool   $preserveFileName Preserve file name
339     *
340     * @return array
341     *
342     * @since 1.0.0
343     */
344    public static function uploadFilesToDestination(
345        array $files,
346        array $fileNames = [],
347        string $path = '',
348        bool $preserveFileName = true
349    ) : array
350    {
351        $upload                   = new UploadFile();
352        $upload->outputDir        = $path; //empty($path) ? $upload->outputDir : $path;
353        $upload->preserveFileName = $preserveFileName;
354
355        return $upload->upload($files, $fileNames, true, '');
356    }
357
358    /**
359     * Create a random file path to store media files
360     *
361     * @param string $basePath Base path for file storage
362     *
363     * @return string Random path to store media files
364     *
365     * @since 1.0.0
366     */
367    public static function createMediaPath(string $basePath = '/Modules/Media/Files') : string
368    {
369        $rndPath = \bin2hex(\random_bytes(4));
370        return $basePath . '/_' . $rndPath[0] . $rndPath[1] . $rndPath[2] . $rndPath[3] . '/_' . $rndPath[4] . $rndPath[5] . $rndPath[6] . $rndPath[7];
371    }
372
373    /**
374     * Create db entry for uploaded file
375     *
376     * @param array                    $status      Files
377     * @param int                      $account     Uploader
378     * @param string                   $virtualPath Virtual path (not on the hard-drive)
379     * @param string                   $ip          Ip of the origin
380     * @param null|ApplicationAbstract $app         Should create relation to uploader
381     * @param bool                     $readContent Should the content of the file be stored in the db
382     *
383     * @return Media
384     *
385     * @since 1.0.0
386     */
387    public static function createDbEntry(
388        array $status,
389        int $account,
390        string $virtualPath = '',
391        string $ip = '127.0.0.1',
392        ApplicationAbstract $app = null,
393        bool $readContent = false,
394        int $unit = null,
395        string $password = '',
396        bool $isEncrypted = false
397    ) : Media
398    {
399        if (!isset($status['status']) || $status['status'] !== UploadStatus::OK) {
400            return new NullMedia();
401        }
402
403        $media = new Media();
404
405        $media->setPath(self::normalizeDbPath($status['path']) . '/' . $status['filename']);
406        $media->name      = empty($status['name']) ? $status['filename'] : $status['name'];
407        $media->size      = $status['size'];
408        $media->createdBy = new NullAccount($account);
409        $media->extension = $status['extension'];
410        $media->unit      = $unit;
411        $media->setVirtualPath($virtualPath);
412        $media->setPassword($password);
413        $media->isEncrypted = $isEncrypted;
414
415        // Store text from document in DB for later use e.g. full text search (uses OCR, text extraction etc. if necessary)
416        if ($readContent && \is_file($media->getAbsolutePath())) {
417            $content = self::loadFileContent($media->getAbsolutePath(), $media->extension);
418
419            if (!empty($content)) {
420                $media->content          = new MediaContent();
421                $media->content->content = $content;
422            }
423        }
424
425        if ($app === null) {
426            MediaMapper::create()->execute($media);
427
428            return $media;
429        }
430
431        $app->eventManager->triggerSimilar('PRE:Module:Media-media-create', '', $media);
432        MediaMapper::create()->execute($media);
433        $app->eventManager->triggerSimilar('POST:Module:Media-media-create', '',
434            [
435                $account,
436                null, $media,
437                StringUtils::intHash(MediaMapper::class), 'Media-media-create',
438                self::NAME,
439                (string) $media->id,
440                '',
441                $ip,
442            ]
443        );
444
445        $app->moduleManager->get('Admin', 'Api')->createAccountModelPermission(
446            new AccountPermission(
447                $account,
448                $app->unitId,
449                $app->appId,
450                self::NAME,
451                self::NAME,
452                PermissionCategory::MEDIA,
453                $media->id,
454                null,
455                PermissionType::READ | PermissionType::MODIFY | PermissionType::DELETE | PermissionType::PERMISSION
456            ),
457            $account,
458            $ip
459        );
460
461        return $media;
462    }
463
464    /**
465     * Load the text content of a file
466     *
467     * @param string $path      Path of the file
468     * @param string $extension File extension
469     *
470     * @return string
471     *
472     * @since 1.0.0
473     */
474    public static function loadFileContent(string $path, string $extension, string $output = 'html') : string
475    {
476        switch ($extension) {
477            case 'pdf':
478                return \phpOMS\Utils\Parser\Pdf\PdfParser::pdf2text($path/*, __DIR__ . '/../../../Tools/OCRImageOptimizer/bin/OCRImageOptimizerApp'*/);
479            case 'doc':
480            case 'docx':
481                $include = \realpath(__DIR__ . '/../../../Resources/');
482                if ($include === false) {
483                    return '';
484                }
485
486                if (!Autoloader::inPaths($include)) {
487                    Autoloader::addPath($include);
488                }
489
490                return \phpOMS\Utils\Parser\Document\DocumentParser::parseDocument($path, $output);
491            case 'ppt':
492            case 'pptx':
493                $include = \realpath(__DIR__ . '/../../../Resources/');
494                if ($include === false) {
495                    return '';
496                }
497
498                if (!Autoloader::inPaths($include)) {
499                    Autoloader::addPath($include);
500                }
501
502                return \phpOMS\Utils\Parser\Presentation\PresentationParser::parsePresentation($path, $output);
503            case 'xls':
504            case 'xlsx':
505                $include = \realpath(__DIR__ . '/../../../Resources/');
506                if ($include === false) {
507                    return '';
508                }
509
510                if (!Autoloader::inPaths($include)) {
511                    Autoloader::addPath($include);
512                }
513
514                return \phpOMS\Utils\Parser\Spreadsheet\SpreadsheetParser::parseSpreadsheet($path, $output);
515            case 'txt':
516            case 'md':
517                $contents = \file_get_contents($path);
518
519                return $contents === false ? '' : $contents;
520            default:
521                return '';
522        }
523    }
524
525    /**
526     * Normalize the file path
527     *
528     * @param string $path Path to the file
529     *
530     * @return string
531     *
532     * @throws \Exception
533     *
534     * @since 1.0.0
535     */
536    public static function normalizeDbPath(string $path) : string
537    {
538        $realpath = \realpath(__DIR__ . '/../../../');
539        if ($realpath === false) {
540            throw new \Exception(); // @codeCoverageIgnore
541        }
542
543        return FileUtils::absolute(\str_replace('\\', '/',
544            \str_replace($realpath, '',
545                \rtrim($path, '\\/')
546            )
547        ));
548    }
549
550    /**
551     * Api method to update media.
552     *
553     * @param RequestAbstract  $request  Request
554     * @param ResponseAbstract $response Response
555     * @param array            $data     Generic data
556     *
557     * @return void
558     *
559     * @api
560     *
561     * @since 1.0.0
562     */
563    public function apiMediaUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
564    {
565        if (!empty($val = $this->validateMediaUpdate($request))) {
566            $response->header->status = RequestStatusCode::R_400;
567            $this->createInvalidUpdateResponse($request, $response, $val);
568
569            return;
570        }
571
572        /** @var Media $old */
573        $old = MediaMapper::get()->where('id', (int) $request->getData('id'))->execute();
574        $new = $this->updateMediaFromRequest($request, clone $old);
575
576        $this->updateModel($request->header->account, $old, $new, MediaMapper::class, 'media', $request->getOrigin());
577        $this->createStandardUpdateResponse($request, $response, $new);
578    }
579
580    /**
581     * Validate media update request
582     *
583     * @param RequestAbstract $request Request
584     *
585     * @return array<string, bool> Returns the validation array of the request
586     *
587     * @since 1.0.0
588     */
589    private function validateMediaUpdate(RequestAbstract $request) : array
590    {
591        $val = [];
592        if (($val['id'] = !$request->hasData('id'))) {
593            return $val;
594        }
595
596        return [];
597    }
598
599    /**
600     * Method to update media from request.
601     *
602     * @param RequestAbstract $request Request
603     *
604     * @return Media
605     *
606     * @since 1.0.0
607     */
608    private function updateMediaFromRequest(RequestAbstract $request, Media $new) : Media
609    {
610        $new->name        = $request->getDataString('name') ?? $new->name;
611        $new->description = $request->getDataString('description') ?? $new->description;
612        $new->setPath($request->getDataString('path') ?? $new->getPath());
613        $new->setVirtualPath(\urldecode($request->getDataString('virtualpath') ?? $new->getVirtualPath()));
614
615        if ($new->id === 0
616            || !$this->app->accountManager->get($request->header->account)->hasPermission(
617                PermissionType::MODIFY,
618                $this->app->unitId,
619                $this->app->appId,
620                self::NAME,
621                PermissionCategory::MEDIA,
622                $request->header->account
623            )
624        ) {
625            return $new;
626        }
627
628        // @todo: create test for this content change and the parsed content change
629        if ($request->hasData('content')) {
630            \file_put_contents(
631                $new->isAbsolute
632                    ? $new->getPath()
633                    : __DIR__ . '/../../../' . \ltrim($new->getPath(), '\\/'),
634                $request->getDataString('content') ?? ''
635            );
636
637            $new->size = \strlen($request->getDataString('content') ?? '');
638        }
639
640        return $new;
641    }
642
643    /**
644     * Api method to create a reference.
645     *
646     * @param RequestAbstract  $request  Request
647     * @param ResponseAbstract $response Response
648     * @param array            $data     Generic data
649     *
650     * @return void
651     *
652     * @api
653     *
654     * @since 1.0.0
655     */
656    public function apiReferenceCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
657    {
658        if (!empty($val = $this->validateReferenceCreate($request))) {
659            $response->header->status = RequestStatusCode::R_400;
660            $this->createInvalidCreateResponse($request, $response, $val);
661
662            return;
663        }
664
665        $ref = $this->createReferenceFromRequest($request);
666        $this->createModel($request->header->account, $ref, ReferenceMapper::class, 'media_reference', $request->getOrigin());
667
668        // get parent collection
669        // create relation
670        $parentCollectionId = (int) $request->getData('parent');
671        if ($parentCollectionId === 0) {
672            /** @var Collection $parentCollection */
673            $parentCollection = CollectionMapper::get()
674                ->where('virtualPath', \dirname($request->getDataString('virtualpath') ?? ''))
675                ->where('name', \basename($request->getDataString('virtualpath') ?? ''))
676                ->execute();
677
678            $parentCollectionId = $parentCollection->id;
679        }
680
681        if (!$request->hasData('source')) {
682            /** @var \Modules\Media\Models\Media $child */
683            $child = MediaMapper::get()
684                ->where('virtualPath', \dirname($request->getDataString('child') ?? ''))
685                ->where('name', \basename($request->getDataString('child') ?? ''))
686                ->execute();
687
688            $request->setData('source', $child->id);
689        }
690
691        $this->createModelRelation(
692            $request->header->account,
693            $parentCollectionId,
694            $ref->id,
695            CollectionMapper::class,
696            'sources',
697            '',
698            $request->getOrigin()
699        );
700
701        $this->createStandardCreateResponse($request, $response, $ref);
702    }
703
704    /**
705     * Method to create a reference from request.
706     *
707     * @param RequestAbstract $request Request
708     *
709     * @return Reference Returns the collection from the request
710     *
711     * @since 1.0.0
712     */
713    private function createReferenceFromRequest(RequestAbstract $request) : Reference
714    {
715        $mediaReference            = new Reference();
716        $mediaReference->name      = \basename($request->getDataString('virtualpath') ?? '/');
717        $mediaReference->source    = new NullMedia((int) $request->getData('source'));
718        $mediaReference->createdBy = new NullAccount($request->header->account);
719        $mediaReference->setVirtualPath(\dirname($request->getDataString('virtualpath') ?? '/'));
720
721        return $mediaReference;
722    }
723
724    /**
725     * Validate reference create request
726     *
727     * @param RequestAbstract $request Request
728     *
729     * @return array<string, bool> Returns the validation array of the request
730     *
731     * @since 1.0.0
732     */
733    private function validateReferenceCreate(RequestAbstract $request) : array
734    {
735        $val = [];
736        if (($val['parent'] = (!$request->hasData('parent') && !$request->hasData('virtualpath')))
737            || ($val['source'] = (!$request->hasData('source') && !$request->hasData('child')))
738        ) {
739            return $val;
740        }
741
742        return [];
743    }
744
745    /**
746     * Api method to add an element to a collection.
747     *
748     * Very similar to create Reference
749     * Reference = it's own media element which points to another element (disadvantage = additional step)
750     * Collection add = directly pointing to other media element (disadvantage = we don't know if we are allowed to modify/delete)
751     *
752     * @param RequestAbstract  $request  Request
753     * @param ResponseAbstract $response Response
754     * @param array            $data     Generic data
755     *
756     * @return void
757     *
758     * @api
759     *
760     * @since 1.0.0
761     */
762    public function apiCollectionAdd(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
763    {
764        $collection = (int) $request->getData('collection');
765        $media      = $request->getDataJson('media-list');
766
767        foreach ($media as $file) {
768            $this->createModelRelation(
769                $request->header->account,
770                $collection,
771                $file,
772                CollectionMapper::class,
773                'sources',
774                '',
775                $request->getOrigin()
776            );
777        }
778    }
779
780    /**
781     * Api method to create a collection.
782     *
783     * @param RequestAbstract  $request  Request
784     * @param ResponseAbstract $response Response
785     * @param array            $data     Generic data
786     *
787     * @return void
788     *
789     * @api
790     *
791     * @since 1.0.0
792     */
793    public function apiCollectionCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
794    {
795        if (!empty($val = $this->validateCollectionCreate($request))) {
796            $response->header->status = RequestStatusCode::R_400;
797            $this->createInvalidCreateResponse($request, $response, $val);
798
799            return;
800        }
801
802        $collection = $this->createCollectionFromRequest($request);
803        $this->createModel($request->header->account, $collection, CollectionMapper::class, 'collection', $request->getOrigin());
804        $this->createStandardCreateResponse($request, $response, $collection);
805    }
806
807    /**
808     * Validate collection create request
809     *
810     * @param RequestAbstract $request Request
811     *
812     * @return array<string, bool> Returns the validation array of the request
813     *
814     * @since 1.0.0
815     */
816    private function validateCollectionCreate(RequestAbstract $request) : array
817    {
818        $val = [];
819        if (($val['name'] = !$request->hasData('name'))) {
820            return $val;
821        }
822
823        return [];
824    }
825
826    /**
827     * Method to create collection from request.
828     *
829     * @param RequestAbstract $request Request
830     *
831     * @return Collection Returns the collection from the request
832     *
833     * @since 1.0.0
834     */
835    private function createCollectionFromRequest(RequestAbstract $request) : Collection
836    {
837        $mediaCollection                 = new Collection();
838        $mediaCollection->name           = $request->getDataString('name') ?? '';
839        $mediaCollection->description    = ($description = Markdown::parse($request->getDataString('description') ?? ''));
840        $mediaCollection->descriptionRaw = $description;
841        $mediaCollection->createdBy      = new NullAccount($request->header->account);
842
843        $media = $request->getDataJson('media-list');
844        foreach ($media as $file) {
845            $mediaCollection->addSource(new NullMedia((int) $file));
846        }
847
848        $virtualPath = \urldecode($request->getDataString('virtualpath') ?? '/');
849        $basePath    = __DIR__ . '/../../../Modules/Media/Files';
850        $outputDir   = $request->hasData('path')
851            ? $basePath . '/' . \ltrim($request->getDataString('path') ?? '', '\\/')
852            : self::createMediaPath($basePath);
853
854        $dirPath   = $outputDir . '/' . ($request->getDataString('name') ?? '');
855        $outputDir = \substr($outputDir, \strlen(__DIR__ . '/../../..'));
856
857        $mediaCollection->setVirtualPath($virtualPath);
858        $mediaCollection->setPath($outputDir);
859
860        if (($request->getDataBool('create_directory') ?? false)
861            && !\is_dir($dirPath)
862        ) {
863            \mkdir($dirPath, 0755, true);
864        }
865
866        return $mediaCollection;
867    }
868
869    /**
870     * Method to create media collection from request.
871     *
872     * This doesn't create a database entry only the collection model.
873     *
874     * @param string  $name        Collection name
875     * @param string  $description Description
876     * @param Media[] $media       Media files to create the collection from
877     * @param int     $account     Account Id
878     *
879     * @return Collection
880     *
881     * @since 1.0.0
882     */
883    public function createMediaCollectionFromMedia(string $name, string $description, array $media, int $account) : Collection
884    {
885        if (empty($media)
886            || !$this->app->accountManager->get($account)->hasPermission(
887                PermissionType::CREATE, $this->app->unitId, null, self::NAME, PermissionCategory::COLLECTION, null)
888        ) {
889            return new NullCollection();
890        }
891
892        /* Create collection */
893        $mediaCollection                 = new Collection();
894        $mediaCollection->name           = $name;
895        $mediaCollection->description    = Markdown::parse($description);
896        $mediaCollection->descriptionRaw = $description;
897        $mediaCollection->createdBy      = new NullAccount($account);
898        $mediaCollection->setSources($media);
899        $mediaCollection->setVirtualPath('/');
900        $mediaCollection->setPath('/Modules/Media/Files');
901
902        return $mediaCollection;
903    }
904
905    /**
906     * Create a collection recursively
907     *
908     * The function also creates all parent collections if they don't exist
909     *
910     * @param string $path         Virtual path of the collection
911     * @param int    $account      Account who creates this collection
912     * @param string $physicalPath The physical path where the corresponding directory should be created
913     *
914     * @return Collection
915     *
916     * @since 1.0.0
917     */
918    public function createRecursiveMediaCollection(string $path, int $account, string $physicalPath = '') : Collection
919    {
920        $status = false;
921        if (!empty($physicalPath)) {
922            $status = \is_dir($physicalPath) ? true : \mkdir($physicalPath, 0755, true);
923        }
924
925        $path      = \trim($path, '/');
926        $paths     = \explode('/', $path);
927        $tempPaths = $paths;
928        $length    = \count($paths);
929
930        /** @var Collection $parentCollection */
931        $parentCollection = null;
932
933        $temp = '';
934        for ($i = $length; $i > 0; --$i) {
935            $temp = '/' . \implode('/', $tempPaths);
936
937            /** @var Collection $parentCollection */
938            $parentCollection = CollectionMapper::getParentCollection($temp)->execute();
939            if ($parentCollection->id > 0) {
940                break;
941            }
942
943            \array_pop($tempPaths);
944        }
945
946        for (; $i < $length; ++$i) {
947            /* Create collection */
948            $childCollection            = new Collection();
949            $childCollection->name      = $paths[$i];
950            $childCollection->createdBy = new NullAccount($account);
951            $childCollection->setVirtualPath('/'. \ltrim($temp, '/'));
952            $childCollection->setPath('/Modules/Media/Files' . $temp);
953
954            $this->createModel($account, $childCollection, CollectionMapper::class, 'collection', '127.0.0.1');
955            $this->createModelRelation(
956                $account,
957                $parentCollection->id,
958                $childCollection->id,
959                CollectionMapper::class,
960                'sources',
961                '',
962                '127.0.0.1'
963            );
964
965            $parentCollection = $childCollection;
966            $temp            .= '/' . $paths[$i];
967        }
968
969        return $parentCollection;
970    }
971
972    /**
973     * Api method to create media file.
974     *
975     * @param RequestAbstract  $request  Request
976     * @param ResponseAbstract $response Response
977     * @param array            $data     Generic data
978     *
979     * @return void
980     *
981     * @api
982     *
983     * @throws \Exception
984     *
985     * @since 1.0.0
986     */
987    public function apiMediaCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
988    {
989        $path        = \urldecode($request->getDataString('path') ?? '');
990        $virtualPath = \urldecode($request->getDataString('virtualpath') ?? '/');
991        $fileName    = $request->getDataString('filename') ?? ($request->getDataString('name') ?? '');
992        $fileName   .= \strripos($fileName, '.') === false ? '.txt' : '';
993
994        $outputDir = '';
995        $basePath  = __DIR__ . '/../../../Modules/Media/Files';
996        if (!$request->hasData('path')) {
997            $outputDir = self::createMediaPath($basePath);
998        } elseif (\stripos(
999                FileUtils::absolute($basePath . '/' . \ltrim($path, '\\/')),
1000                FileUtils::absolute(__DIR__ . '/../../../')
1001            ) !== 0) {
1002            $outputDir = self::createMediaPath($basePath);
1003        } else {
1004            $outputDir = $basePath . '/' . \ltrim($path, '\\/');
1005        }
1006
1007        if (!\is_dir($outputDir)) {
1008            $created = Directory::create($outputDir, 0775, true);
1009
1010            if (!$created) {
1011                throw new \Exception('Couldn\'t create outputdir: "' . $outputDir . '"'); // @codeCoverageIgnore
1012            }
1013        }
1014
1015        \file_put_contents($outputDir . '/' . $fileName, $request->getDataString('content') ?? '');
1016        $outputDir = \substr($outputDir, \strlen(__DIR__ . '/../../..'));
1017
1018        $status = [
1019            [
1020                'status'    => UploadStatus::OK,
1021                'path'      => $outputDir,
1022                'filename'  => $fileName,
1023                'name'      => $request->getDataString('name') ?? '',
1024                'size'      => \strlen($request->getDataString('content') ?? ''),
1025                'extension' => \substr($fileName, \strripos($fileName, '.') + 1),
1026            ],
1027        ];
1028
1029        $ids = [];
1030        foreach ($status as $stat) {
1031            $created = self::createDbEntry(
1032                $stat,
1033                $request->header->account,
1034                $virtualPath,
1035                $request->getOrigin(),
1036                $this->app,
1037                unit: $request->getDataInt('unit'),
1038                password: $request->getDataString('password') ?? '',
1039                isEncrypted: $request->getDataBool('isencrypted') ?? $request->hasData('encryption')
1040            );
1041
1042            $ids[] = $created->id;
1043        }
1044
1045        $this->createStandardAddResponse($request, $response, $ids);
1046    }
1047
1048    /**
1049     * Routing end-point for application behaviour.
1050     *
1051     * @param HttpRequest  $request  Request
1052     * @param HttpResponse $response Response
1053     * @param array        $data     Generic data
1054     *
1055     * @return void
1056     *
1057     * @api
1058     *
1059     * @since 1.0.0
1060     */
1061    public function apiMediaExport(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1062    {
1063        $filePath = '';
1064        $media    = null;
1065
1066        if ($request->hasData('id')) {
1067            /** @var Media $media */
1068            $media    = MediaMapper::get()->where('id', (int) $request->getData('id'))->execute();
1069            $filePath = $media->getAbsolutePath();
1070        } else {
1071            $path  = \urldecode($request->getDataString('path') ?? '');
1072            $media = new NullMedia();
1073
1074            if (\is_file($filePath = __DIR__ . '/../../../' . \ltrim($path, '\\/'))) {
1075                $name = \explode('.', \basename($path));
1076
1077                $media->name       = $name[0];
1078                $media->extension  = $name[\count($name) - 1] ?? '';
1079                $media->isAbsolute = false;
1080                $media->setVirtualPath(\dirname($path));
1081                $media->setPath('/' . \ltrim($path, '\\/'));
1082            }
1083        }
1084
1085        if ($media->id > 0) {
1086            if (!($data['ignorePermission'] ?? false)
1087                && $request->header->account !== $media->createdBy->id
1088                && !$this->app->accountManager->get($request->header->account)->hasPermission(
1089                    PermissionType::READ,
1090                    $this->app->unitId,
1091                    $this->app->appId,
1092                    self::NAME,
1093                    PermissionCategory::MEDIA,
1094                    $media->id
1095                )
1096            ) {
1097                $response->header->status = RequestStatusCode::R_403;
1098                $this->createInvalidReturnResponse($request, $response, $media);
1099
1100                return;
1101            }
1102
1103            if (!isset($data['guard'])) {
1104                $data['guard'] = __DIR__ . '/../Files';
1105            }
1106        } elseif (empty($data) || !isset($data['guard'])) {
1107            $response->header->status = RequestStatusCode::R_403;
1108            $this->createInvalidReturnResponse($request, $response, $media);
1109        }
1110
1111        if (!Guard::isSafePath($filePath, $data['guard'] ?? '')) {
1112            $response->header->status = RequestStatusCode::R_403;
1113
1114            return;
1115        }
1116
1117        if ($media->hasPassword()
1118            && !$media->comparePassword($request->getDataString('password') ?? '')
1119        ) {
1120            $view = new View($this->app->l11nManager, $request, $response);
1121            $view->setTemplate('/Modules/Media/Theme/Api/invalidPassword');
1122
1123            return;
1124        }
1125
1126        if ($media->isEncrypted) {
1127            $media = $this->prepareEncryptedMedia($media, $request);
1128
1129            if ($media->id === 0) {
1130                $response->header->status = RequestStatusCode::R_500;
1131                $this->createInvalidReturnResponse($request, $response, $media);
1132
1133                return;
1134            }
1135        }
1136
1137        if ($media->extension !== 'collection' && !\is_file($media->getAbsolutePath())) {
1138            $response->header->status = RequestStatusCode::R_500;
1139            $this->createInvalidReturnResponse($request, $response, $media);
1140
1141            return;
1142        }
1143
1144        $this->setMediaResponseHeader($media, $request, $response);
1145        $view               = $this->createView($media, $request, $response);
1146        $view->data['path'] = __DIR__ . '/../../../';
1147
1148        $response->set('export', $view);
1149    }
1150
1151    /**
1152     * Decrypt an encrypted media element
1153     *
1154     * @param Media           $media   Media model
1155     * @param RequestAbstract $request Request model
1156     *
1157     * @return Media
1158     *
1159     * @since 1.0.0
1160     */
1161    private function prepareEncryptedMedia(Media $media, RequestAbstract $request) : Media
1162    {
1163        $path         = '';
1164        $absolutePath = '';
1165
1166        $counter = 0;
1167        do {
1168            $randomName = \sha1(\random_bytes(32));
1169
1170            $path         =  '../../../Temp/' . $randomName . '.' . $media->getExtension();
1171            $absolutePath = __DIR__ . '/' . $path;
1172
1173            ++$counter;
1174        } while (!\is_file($absolutePath) && $counter < 1000);
1175
1176        if ($counter >= 1000) {
1177            return new NullMedia();
1178        }
1179
1180        $encryptionKey = $request->getDataBool('isencrypted') === true && !empty($_SESSION['OMS_PRIVATE_KEY_I'] ?? '')
1181            ? $_SESSION['OMS_PRIVATE_KEY_I']
1182            : $request->getDataString('encrpkey') ?? '';
1183
1184        $decrypted = $media->decrypt($encryptionKey, $absolutePath);
1185
1186        if (!$decrypted) {
1187            return new NullMedia();
1188        }
1189
1190        $media->path = $media->isAbsolute ? $absolutePath : $path;
1191
1192        return $media;
1193    }
1194
1195    /**
1196     * Routing end-point for application behaviour.
1197     *
1198     * @param Media        $media    Media
1199     * @param HttpRequest  $request  Request
1200     * @param HttpResponse $response Response
1201     *
1202     * @return View
1203     *
1204     * @since 1.0.0
1205     */
1206    public function createView(Media $media, RequestAbstract $request, ResponseAbstract $response) : View
1207    {
1208        $view        = new ElementView($this->app->l11nManager, $request, $response);
1209        $view->media = $media;
1210
1211        if (!\headers_sent()) {
1212            $response->endAllOutputBuffering(); // for large files
1213        }
1214
1215        if (($type = $request->getDataString('type')) === null) {
1216            $view->setTemplate('/Modules/Media/Theme/Api/render');
1217        } elseif ($type === 'html') {
1218            $head = new Head();
1219
1220            $css = '';
1221            if (\is_file(__DIR__ . '/../../../Web/Backend/css/backend-small.css')) {
1222                $css = \file_get_contents(__DIR__ . '/../../../Web/Backend/css/backend-small.css');
1223
1224                if ($css === false) {
1225                    $css = ''; // @codeCoverageIgnore
1226                }
1227            }
1228
1229            $css = \preg_replace('!\s+!', ' ', $css);
1230            $head->setStyle('core', $css ?? '');
1231
1232            $head->addAsset(AssetType::CSS, 'cssOMS/styles.css?v=1.0.0');
1233            $view->data['head'] = $head;
1234
1235            switch (\strtolower($media->extension)) {
1236                case 'jpg':
1237                case 'jpeg':
1238                case 'gif':
1239                case 'png':
1240                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/image_raw');
1241                    break;
1242                case 'pdf':
1243                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/pdf_raw');
1244                    break;
1245                case 'c':
1246                case 'cpp':
1247                case 'h':
1248                case 'php':
1249                case 'js':
1250                case 'css':
1251                case 'csv':
1252                case 'rs':
1253                case 'py':
1254                case 'r':
1255                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/text_raw');
1256                    break;
1257                case 'json':
1258                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/json_raw');
1259                    break;
1260                case 'txt':
1261                case 'cfg':
1262                case 'log':
1263                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/text_raw');
1264                    break;
1265                case 'md':
1266                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/markdown_raw');
1267                    break;
1268                case 'xls':
1269                case 'xlsx':
1270                    $view->setTemplate('/Modules/Media/Theme/Api/spreadsheetAsHtml');
1271                    break;
1272                case 'doc':
1273                case 'docx':
1274                    $view->setTemplate('/Modules/Media/Theme/Api/wordAsHtml');
1275                    break;
1276                case 'mp3':
1277                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/audio_raw');
1278                    break;
1279                case 'mp4':
1280                case 'mpeg':
1281                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/video_raw');
1282                    break;
1283                case 'collection':
1284                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/collection_raw');
1285                    break;
1286                default:
1287                    $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/default');
1288            }
1289        }
1290
1291        return $view;
1292    }
1293
1294    /**
1295     * Set header for report/template
1296     *
1297     * @param Media            $media    Media file
1298     * @param RequestAbstract  $request  Request
1299     * @param ResponseAbstract $response Response
1300     *
1301     * @return void
1302     *
1303     * @since 1.0.0
1304     */
1305    private function setMediaResponseHeader(Media $media, RequestAbstract $request, ResponseAbstract $response) : void
1306    {
1307        switch ($request->getDataString('type') ?? \strtolower($media->extension)) {
1308            case 'htm':
1309            case 'html':
1310                $response->header->set('Content-Type', MimeType::M_HTML, true);
1311                break;
1312            case 'pdf':
1313                $response->header->set('Content-Type', MimeType::M_PDF, true);
1314                break;
1315            case 'c':
1316            case 'cpp':
1317            case 'h':
1318            case 'php':
1319            case 'js':
1320            case 'css':
1321            case 'rs':
1322            case 'py':
1323            case 'r':
1324                $response->header->set('Content-Type', MimeType::M_TXT, true);
1325                break;
1326            case 'txt':
1327            case 'cfg':
1328            case 'log':
1329            case 'md':
1330                $response->header->set('Content-Type', MimeType::M_TXT, true);
1331                break;
1332            case 'csv':
1333            case 'json':
1334                $response->header->set('Content-Type', MimeType::M_CSV, true);
1335                break;
1336            case 'xls':
1337                $response->header->set('Content-Type', MimeType::M_XLS, true);
1338                break;
1339            case 'xlsx':
1340                $response->header->set('Content-Type', MimeType::M_XLSX, true);
1341                break;
1342            case 'doc':
1343                $response->header->set('Content-Type', MimeType::M_DOC, true);
1344                break;
1345            case 'docx':
1346                $response->header->set('Content-Type', MimeType::M_DOCX, true);
1347                break;
1348            case 'ppt':
1349                $response->header->set('Content-Type', MimeType::M_PPT, true);
1350                break;
1351            case 'pptx':
1352                $response->header->set('Content-Type', MimeType::M_PPTX, true);
1353                break;
1354            case 'jpg':
1355            case 'jpeg':
1356                $response->header->set('Content-Type', MimeType::M_JPG, true);
1357                break;
1358            case 'gif':
1359                $response->header->set('Content-Type', MimeType::M_GIF, true);
1360                break;
1361            case 'png':
1362                $response->header->set('Content-Type', MimeType::M_PNG, true);
1363                break;
1364            case 'mp3':
1365                $response->header->set('Content-Type', MimeType::M_MP3, true);
1366                break;
1367            case 'mp4':
1368                $response->header->set('Content-Type', MimeType::M_MP4, true);
1369                break;
1370            case 'mpeg':
1371                $response->header->set('Content-Type', MimeType::M_MPEG, true);
1372                break;
1373            default:
1374                $response->header->set('Content-Type', MimeType::M_BIN, true);
1375                $response->header->set('Content-Disposition', 'attachment; filename="' . \addslashes($media->name) . '"', true);
1376                $response->header->set('Content-Transfer-Encoding', 'binary', true);
1377        }
1378    }
1379
1380    /**
1381     * Validate document create request
1382     *
1383     * @param RequestAbstract $request Request
1384     *
1385     * @return array<string, bool>
1386     *
1387     * @since 1.0.0
1388     */
1389    private function validateMediaTypeCreate(RequestAbstract $request) : array
1390    {
1391        $val = [];
1392        if (($val['name'] = !$request->hasData('name'))
1393        ) {
1394            return $val;
1395        }
1396
1397        return [];
1398    }
1399
1400    /**
1401     * Api method to create document
1402     *
1403     * @param RequestAbstract  $request  Request
1404     * @param ResponseAbstract $response Response
1405     * @param array            $data     Generic data
1406     *
1407     * @return void
1408     *
1409     * @api
1410     *
1411     * @since 1.0.0
1412     */
1413    public function apiMediaTypeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1414    {
1415        if (!empty($val = $this->validateMediaTypeCreate($request))) {
1416            $response->header->status = RequestStatusCode::R_400;
1417            $this->createInvalidCreateResponse($request, $response, $val);
1418
1419            return;
1420        }
1421
1422        $type = $this->createDocTypeFromRequest($request);
1423        $this->createModel($request->header->account, $type, MediaTypeMapper::class, 'doc_type', $request->getOrigin());
1424        $this->createStandardCreateResponse($request, $response, $type);
1425    }
1426
1427    /**
1428     * Method to create task from request.
1429     *
1430     * @param RequestAbstract $request Request
1431     *
1432     * @return MediaType
1433     *
1434     * @since 1.0.0
1435     */
1436    private function createDocTypeFromRequest(RequestAbstract $request) : MediaType
1437    {
1438        $type       = new MediaType();
1439        $type->name = $request->getDataString('name') ?? '';
1440
1441        if ($request->hasData('title')) {
1442            $type->setL11n(
1443                $request->getDataString('title') ?? '',
1444                $request->getDataString('lang') ?? $request->header->l11n->language
1445            );
1446        }
1447
1448        return $type;
1449    }
1450
1451    /**
1452     * Validate l11n create request
1453     *
1454     * @param RequestAbstract $request Request
1455     *
1456     * @return array<string, bool>
1457     *
1458     * @since 1.0.0
1459     */
1460    private function validateMediaTypeL11nCreate(RequestAbstract $request) : array
1461    {
1462        $val = [];
1463        if (($val['title'] = !$request->hasData('title'))
1464            || ($val['type'] = !$request->hasData('type'))
1465        ) {
1466            return $val;
1467        }
1468
1469        return [];
1470    }
1471
1472    /**
1473     * Api method to create tag localization
1474     *
1475     * @param RequestAbstract  $request  Request
1476     * @param ResponseAbstract $response Response
1477     * @param array            $data     Generic data
1478     *
1479     * @return void
1480     *
1481     * @api
1482     *
1483     * @since 1.0.0
1484     */
1485    public function apiMediaTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1486    {
1487        if (!empty($val = $this->validateMediaTypeL11nCreate($request))) {
1488            $response->header->status = RequestStatusCode::R_400;
1489            $this->createInvalidCreateResponse($request, $response, $val);
1490
1491            return;
1492        }
1493
1494        $l11nMediaType = $this->createMediaTypeL11nFromRequest($request);
1495        $this->createModel($request->header->account, $l11nMediaType, MediaTypeL11nMapper::class, 'media_type_l11n', $request->getOrigin());
1496        $this->createStandardCreateResponse($request, $response, $l11nMediaType);
1497    }
1498
1499    /**
1500     * Method to create tag localization from request.
1501     *
1502     * @param RequestAbstract $request Request
1503     *
1504     * @return BaseStringL11n
1505     *
1506     * @since 1.0.0
1507     */
1508    private function createMediaTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n
1509    {
1510        $l11nMediaType          = new BaseStringL11n();
1511        $l11nMediaType->ref     = $request->getDataInt('type') ?? 0;
1512        $l11nMediaType->content = $request->getDataString('title') ?? '';
1513        $l11nMediaType->setLanguage(
1514            $request->getDataString('language') ?? $request->header->l11n->language
1515        );
1516
1517        return $l11nMediaType;
1518    }
1519
1520    /**
1521     * Resize image file
1522     *
1523     * @param Media $media  Media object
1524     * @param int   $width  New width
1525     * @param int   $height New height
1526     * @param bool  $crop   Crop image instead of resizing
1527     *
1528     * @return Media
1529     * @since 1.0.0
1530     */
1531    public function resizeImage(
1532        Media $media,
1533        int $width,
1534        int $height,
1535        bool $crop = false) : Media {
1536        ImageUtils::resize(
1537            $media->getAbsolutePath(),
1538            $media->getAbsolutePath(),
1539            $width,
1540            $height,
1541            $crop
1542        );
1543
1544        $temp        = \filesize($media->getAbsolutePath());
1545        $media->size = $temp === false ? 0 : $temp;
1546
1547        return $media;
1548    }
1549}