Passed
Push — master ( d66510...33b2f6 )
by
unknown
28:10 queued 15:32
created

FileDumpController::isFileValid()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(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
18
namespace TYPO3\CMS\Core\Controller;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use TYPO3\CMS\Core\Http\Response;
23
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
24
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
25
use TYPO3\CMS\Core\Resource\File;
26
use TYPO3\CMS\Core\Resource\FileInterface;
27
use TYPO3\CMS\Core\Resource\FileReference;
28
use TYPO3\CMS\Core\Resource\Hook\FileDumpEIDHookInterface;
29
use TYPO3\CMS\Core\Resource\ProcessedFile;
30
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
31
use TYPO3\CMS\Core\Resource\ResourceFactory;
32
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
/**
36
 * Class FileDumpController
37
 */
38
class FileDumpController
39
{
40
    /**
41
     * @var ResourceFactory
42
     */
43
    protected $resourceFactory;
44
45
    public function __construct(ResourceFactory $resourceFactory)
46
    {
47
        $this->resourceFactory = $resourceFactory;
48
    }
49
50
    /**
51
     * Main method to dump a file
52
     *
53
     * @param ServerRequestInterface $request
54
     * @return ResponseInterface
55
     * @throws \InvalidArgumentException
56
     * @throws \RuntimeException
57
     * @throws FileDoesNotExistException
58
     * @throws \UnexpectedValueException
59
     */
60
    public function dumpAction(ServerRequestInterface $request): ResponseInterface
61
    {
62
        $parameters = $this->buildParametersFromRequest($request);
63
64
        if (!$this->isTokenValid($parameters, $request)) {
65
            return (new Response())->withStatus(403);
66
        }
67
        $file = $this->createFileObjectByParameters($parameters);
68
        if ($file === null) {
69
            return (new Response())->withStatus(404);
70
        }
71
72
        // Hook: allow some other process to do some security/access checks. Hook should return 403 response if access is rejected, void otherwise
73
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['FileDumpEID.php']['checkFileAccess'] ?? [] as $className) {
74
            $hookObject = GeneralUtility::makeInstance($className);
75
            if (!$hookObject instanceof FileDumpEIDHookInterface) {
76
                throw new \UnexpectedValueException($className . ' must implement interface ' . FileDumpEIDHookInterface::class, 1394442417);
77
            }
78
            $response = $hookObject->checkFileAccess($file);
79
            if ($response instanceof ResponseInterface) {
80
                return $response;
81
            }
82
        }
83
84
        $processingInstructions = [];
85
86
        // Apply cropping, if possible
87
        if (!empty($parameters['cv'])) {
88
            $cropVariant = $parameters['cv'];
89
            $cropString = $file instanceof FileReference ? $file->getProperty('crop') : '';
90
            $cropArea = CropVariantCollection::create((string)$cropString)->getCropArea($cropVariant);
91
            $processingInstructions = array_merge(
92
                $processingInstructions,
93
                [
94
                    'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($file),
95
                ]
96
            );
97
        }
98
99
        // Apply width/height, if given
100
        if (!empty($parameters['s'])) {
101
            $size = GeneralUtility::trimExplode(':', $parameters['s']);
102
            $processingInstructions = array_merge(
103
                $processingInstructions,
104
                [
105
                    'width' => $size[0] ?? null,
106
                    'height' => $size[1] ?? null,
107
                    'minWidth' => $size[2] ? (int)$size[2] : null,
108
                    'minHeight' => $size[3] ? (int)$size[3] : null,
109
                    'maxWidth' => $size[4] ? (int)$size[4] : null,
110
                    'maxHeight' => $size[5] ? (int)$size[5] : null
111
                ]
112
            );
113
        }
114
115
        if (!empty($processingInstructions) && !($file instanceof ProcessedFile)) {
116
            if (is_callable([$file, 'getOriginalFile'])) {
117
                // Get the original file from the file reference
118
                $file = $file->getOriginalFile();
0 ignored issues
show
Bug introduced by
The method getOriginalFile() does not exist on TYPO3\CMS\Core\Resource\File. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
                /** @scrutinizer ignore-call */ 
119
                $file = $file->getOriginalFile();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
            }
120
            $file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);
0 ignored issues
show
Bug introduced by
The method process() does not exist on TYPO3\CMS\Core\Resource\ProcessedFile. Did you maybe mean needsReprocessing()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

120
            /** @scrutinizer ignore-call */ 
121
            $file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method process() does not exist on TYPO3\CMS\Core\Resource\FileReference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

120
            /** @scrutinizer ignore-call */ 
121
            $file = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
121
        }
122
123
        return $file->getStorage()->streamFile($file);
124
    }
125
126
    protected function buildParametersFromRequest(ServerRequestInterface $request): array
127
    {
128
        $parameters = ['eID' => 'dumpFile'];
129
        $queryParams = $request->getQueryParams();
130
        // Identifier of what to process. f, r or p
131
        // Only needed while hash_equals
132
        $t = (string)($queryParams['t'] ?? '');
133
        if ($t) {
134
            $parameters['t'] = $t;
135
        }
136
        // sys_file
137
        $f = (string)($queryParams['f'] ?? '');
138
        if ($f) {
139
            $parameters['f'] = (int)$f;
140
        }
141
        // sys_file_reference
142
        $r = (string)($queryParams['r'] ?? '');
143
        if ($r) {
144
            $parameters['r'] = (int)$r;
145
        }
146
        // Processed file
147
        $p = (string)($queryParams['p'] ?? '');
148
        if ($p) {
149
            $parameters['p'] = (int)$p;
150
        }
151
        // File's width and height in this order: w:h:minW:minH:maxW:maxH
152
        $s = (string)($queryParams['s'] ?? '');
153
        if ($s) {
154
            $parameters['s'] = $s;
155
        }
156
        // File's crop variant
157
        $v = (string)($queryParams['cv'] ?? '');
158
        if ($v) {
159
            $parameters['cv'] = (string)$v;
160
        }
161
162
        return $parameters;
163
    }
164
165
    protected function isTokenValid(array $parameters, ServerRequestInterface $request): bool
166
    {
167
        return hash_equals(
168
            GeneralUtility::hmac(implode('|', $parameters), 'resourceStorageDumpFile'),
169
            $request->getQueryParams()['token'] ?? ''
170
        );
171
    }
172
173
    /**
174
     * @param array $parameters
175
     * @return File|FileReference|ProcessedFile|null
176
     */
177
    protected function createFileObjectByParameters(array $parameters)
178
    {
179
        $file = null;
180
        if (isset($parameters['f'])) {
181
            try {
182
                $file = $this->resourceFactory->getFileObject($parameters['f']);
183
                if ($file->isDeleted() || $file->isMissing()) {
184
                    $file = null;
185
                }
186
                if (!$this->isFileValid($file)) {
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $file of TYPO3\CMS\Core\Controlle...ntroller::isFileValid() does only seem to accept TYPO3\CMS\Core\Resource\FileInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

186
                if (!$this->isFileValid(/** @scrutinizer ignore-type */ $file)) {
Loading history...
187
                    $file = null;
188
                }
189
            } catch (\Exception $e) {
190
                $file = null;
191
            }
192
        } elseif (isset($parameters['r'])) {
193
            try {
194
                $file = $this->resourceFactory->getFileReferenceObject($parameters['r']);
195
                if ($file->isMissing()) {
196
                    $file = null;
197
                }
198
                if (!$this->isFileValid($file->getOriginalFile())) {
199
                    $file = null;
200
                }
201
            } catch (\Exception $e) {
202
                $file = null;
203
            }
204
        } elseif (isset($parameters['p'])) {
205
            try {
206
                $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
207
                /** @var ProcessedFile|null $file */
208
                $file = $processedFileRepository->findByUid($parameters['p']);
209
                if (!$file || $file->isDeleted()) {
210
                    $file = null;
211
                }
212
                if (!$this->isFileValid($file->getOriginalFile())) {
213
                    $file = null;
214
                }
215
            } catch (\Exception $e) {
216
                $file = null;
217
            }
218
        }
219
        return $file;
220
    }
221
222
    protected function isFileValid(FileInterface $file): bool
223
    {
224
        return $file->getStorage()->getDriverType() !== 'Local'
225
            || GeneralUtility::makeInstance(FileNameValidator::class)
226
                ->isValid(basename($file->getIdentifier()));
227
    }
228
}
229