1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Core\Imaging;
19
20use TYPO3\CMS\Core\Exception;
21use TYPO3\CMS\Core\Type\File\FileInfo;
22use TYPO3\CMS\Core\Utility\CommandUtility;
23use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25/**
26 * Value object for file to be used for ImageMagick/GraphicsMagick invocation when
27 * being used as input file (implies and requires that file exists for some evaluations).
28 */
29class ImageMagickFile
30{
31    /**
32     * Path to input file to be processed
33     *
34     * @var string
35     */
36    protected $filePath;
37
38    /**
39     * Frame to be used (of multi-page document, e.g. PDF)
40     *
41     * @var int|null
42     */
43    protected $frame;
44
45    /**
46     * Whether file actually exists
47     *
48     * @var bool
49     */
50    protected $fileExists;
51
52    /**
53     * File extension as given in $filePath (e.g. 'file.png' -> 'png')
54     *
55     * @var string
56     */
57    protected $fileExtension;
58
59    /**
60     * Resolved mime-type of file
61     *
62     * @var string
63     */
64    protected $mimeType;
65
66    /**
67     * Resolved extension for mime-type (e.g. 'image/png' -> 'png')
68     * (might be empty if not defined in magic.mime database)
69     *
70     * @var string[]
71     * @see FileInfo::getMimeExtensions()
72     */
73    protected $mimeExtensions = [];
74
75    /**
76     * Result to be used for ImageMagick/GraphicsMagick invocation containing
77     * combination of resolved format prefix, $filePath and frame escaped to be
78     * used as CLI argument (e.g. "'png:file.png'")
79     *
80     * @var string
81     */
82    protected $asArgument;
83
84    /**
85     * File extensions that directly can be used (and are considered to be safe).
86     *
87     * @var string[]
88     */
89    protected $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'tif', 'tiff', 'bmp', 'pcx', 'tga', 'ico'];
90
91    /**
92     * File extensions that never shall be used.
93     *
94     * @var string[]
95     */
96    protected $deniedExtensions = ['epi', 'eps', 'eps2', 'eps3', 'epsf', 'epsi', 'ept', 'ept2', 'ept3', 'msl', 'ps', 'ps2', 'ps3'];
97
98    /**
99     * File mime-types that have to be matching. Adding custom mime-types is possible using
100     * $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']
101     *
102     * @var string[]
103     * @see FileInfo::getMimeExtensions()
104     */
105    protected $mimeTypeExtensionMap = [
106        'image/png' => 'png',
107        'image/jpeg' => 'jpg',
108        'image/gif' => 'gif',
109        'image/heic' => 'heic',
110        'image/heif' => 'heif',
111        'image/webp' => 'webp',
112        'image/svg' => 'svg',
113        'image/svg+xml' => 'svg',
114        'image/tiff' => 'tif',
115        'application/pdf' => 'pdf',
116    ];
117
118    /**
119     * @param string $filePath
120     * @param int|null $frame
121     * @return ImageMagickFile
122     */
123    public static function fromFilePath(string $filePath, int $frame = null): self
124    {
125        return GeneralUtility::makeInstance(
126            static::class,
127            $filePath,
128            $frame
129        );
130    }
131
132    /**
133     * @param string $filePath
134     * @param int|null $frame
135     * @throws Exception
136     */
137    public function __construct(string $filePath, int $frame = null)
138    {
139        $this->frame = $frame;
140        $this->fileExists = file_exists($filePath);
141        $this->filePath = $filePath;
142        $this->fileExtension = pathinfo($filePath, PATHINFO_EXTENSION);
143
144        if ($this->fileExists) {
145            $fileInfo = $this->getFileInfo($filePath);
146            $this->mimeType = $fileInfo->getMimeType();
147            $this->mimeExtensions = $fileInfo->getMimeExtensions();
148        }
149
150        $this->asArgument = $this->escape(
151            $this->resolvePrefix() . $this->filePath
152            . ($this->frame !== null ? '[' . $this->frame . ']' : '')
153        );
154    }
155
156    /**
157     * @return string
158     */
159    public function __toString(): string
160    {
161        return $this->asArgument;
162    }
163
164    /**
165     * Resolves according ImageMagic/GraphicsMagic format (e.g. 'png:', 'jpg:', ...).
166     * + in case mime-type could be resolved and is configured, it takes precedence
167     * + otherwise resolved mime-type extension of mime.magick database is used if available
168     *   (includes custom settings with $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'])
169     * + otherwise "safe" and allowed file extension is used (jpg, png, gif, webp, tif, ...)
170     * + potentially malicious script formats (eps, ps, ...) are not allowed
171     *
172     * @return string
173     * @throws Exception
174     */
175    protected function resolvePrefix(): string
176    {
177        $prefixExtension = null;
178        $fileExtension = strtolower($this->fileExtension);
179        if ($this->mimeType !== null && !empty($this->mimeTypeExtensionMap[$this->mimeType])) {
180            $prefixExtension = $this->mimeTypeExtensionMap[$this->mimeType];
181        } elseif (!empty($this->mimeExtensions) && strpos((string)$this->mimeType, 'image/') === 0) {
182            $prefixExtension = $this->mimeExtensions[0];
183        } elseif ($this->isInAllowedExtensions($fileExtension)) {
184            $prefixExtension = $fileExtension;
185        }
186        if ($prefixExtension !== null && !in_array(strtolower($prefixExtension), $this->deniedExtensions, true)) {
187            return $prefixExtension . ':';
188        }
189        throw new Exception(
190            sprintf(
191                'Unsupported file %s (%s)',
192                basename($this->filePath),
193                $this->mimeType ?? 'unknown'
194            ),
195            1550060977
196        );
197    }
198
199    /**
200     * @param string $value
201     * @return string
202     */
203    protected function escape(string $value): string
204    {
205        return CommandUtility::escapeShellArgument($value);
206    }
207
208    /**
209     * @param string $extension
210     * @return bool
211     */
212    protected function isInAllowedExtensions(string $extension): bool
213    {
214        return in_array($extension, $this->allowedExtensions, true);
215    }
216
217    /**
218     * @param string $filePath
219     * @return FileInfo
220     */
221    protected function getFileInfo(string $filePath): FileInfo
222    {
223        return GeneralUtility::makeInstance(FileInfo::class, $filePath);
224    }
225}
226