Passed
Push — master ( 21cfa8...7dda91 )
by
unknown
13:30
created

FileDumpController::buildParametersFromRequest()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 47
rs 6.5222
cc 9
nc 256
nop 1
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(
124
            $file,
125
            (bool)($parameters['dl'] ?? false),
126
            $parameters['fn'] ?? null
127
        );
128
    }
129
130
    protected function buildParametersFromRequest(ServerRequestInterface $request): array
131
    {
132
        $parameters = ['eID' => 'dumpFile'];
133
        $queryParams = $request->getQueryParams();
134
        // Identifier of what to process. f, r or p
135
        // Only needed while hash_equals
136
        $t = (string)($queryParams['t'] ?? '');
137
        if ($t) {
138
            $parameters['t'] = $t;
139
        }
140
        // sys_file
141
        $f = (string)($queryParams['f'] ?? '');
142
        if ($f) {
143
            $parameters['f'] = (int)$f;
144
        }
145
        // sys_file_reference
146
        $r = (string)($queryParams['r'] ?? '');
147
        if ($r) {
148
            $parameters['r'] = (int)$r;
149
        }
150
        // Processed file
151
        $p = (string)($queryParams['p'] ?? '');
152
        if ($p) {
153
            $parameters['p'] = (int)$p;
154
        }
155
        // File's width and height in this order: w:h:minW:minH:maxW:maxH
156
        $s = (string)($queryParams['s'] ?? '');
157
        if ($s) {
158
            $parameters['s'] = $s;
159
        }
160
        // File's crop variant
161
        $cv = (string)($queryParams['cv'] ?? '');
162
        if ($cv) {
163
            $parameters['cv'] = $cv;
164
        }
165
        // As download
166
        $dl = (string)($queryParams['dl'] ?? '');
167
        if ($dl) {
168
            $parameters['dl'] = (int)$dl;
169
        }
170
        // Alternative file name
171
        $fn = (string)($queryParams['fn'] ?? '');
172
        if ($fn) {
173
            $parameters['fn'] = $fn;
174
        }
175
176
        return $parameters;
177
    }
178
179
    protected function isTokenValid(array $parameters, ServerRequestInterface $request): bool
180
    {
181
        return hash_equals(
182
            GeneralUtility::hmac(implode('|', $parameters), 'resourceStorageDumpFile'),
183
            $request->getQueryParams()['token'] ?? ''
184
        );
185
    }
186
187
    /**
188
     * @param array $parameters
189
     * @return File|FileReference|ProcessedFile|null
190
     */
191
    protected function createFileObjectByParameters(array $parameters)
192
    {
193
        $file = null;
194
        if (isset($parameters['f'])) {
195
            try {
196
                $file = $this->resourceFactory->getFileObject($parameters['f']);
197
                if ($file->isDeleted() || $file->isMissing() || !$this->isFileValid($file)) {
198
                    $file = null;
199
                }
200
            } catch (\Exception $e) {
201
                $file = null;
202
            }
203
        } elseif (isset($parameters['r'])) {
204
            try {
205
                $file = $this->resourceFactory->getFileReferenceObject($parameters['r']);
206
                if ($file->isMissing() || !$this->isFileValid($file->getOriginalFile())) {
207
                    $file = null;
208
                }
209
            } catch (\Exception $e) {
210
                $file = null;
211
            }
212
        } elseif (isset($parameters['p'])) {
213
            try {
214
                $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
215
                /** @var ProcessedFile|null $file */
216
                $file = $processedFileRepository->findByUid($parameters['p']);
217
                if (!$file || $file->isDeleted() || !$this->isFileValid($file->getOriginalFile())) {
218
                    $file = null;
219
                }
220
            } catch (\Exception $e) {
221
                $file = null;
222
            }
223
        }
224
        return $file;
225
    }
226
227
    protected function isFileValid(FileInterface $file): bool
228
    {
229
        return $file->getStorage()->getDriverType() !== 'Local'
230
            || GeneralUtility::makeInstance(FileNameValidator::class)
231
                ->isValid(basename($file->getIdentifier()));
232
    }
233
}
234