Completed
Push — master ( abb405...0346f6 )
by
unknown
18:42 queued 02:27
created

RequestBuilder::untangleFilesArray()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 9
nop 1
dl 0
loc 31
rs 8.6506
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Extbase\Mvc\Web;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
20
use TYPO3\CMS\Core\Routing\PageArguments;
21
use TYPO3\CMS\Core\SingletonInterface;
22
use TYPO3\CMS\Core\Utility\ArrayUtility;
23
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26
use TYPO3\CMS\Extbase\Mvc\Exception as MvcException;
27
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
28
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
29
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
30
use TYPO3\CMS\Extbase\Service\EnvironmentService;
31
use TYPO3\CMS\Extbase\Service\ExtensionService;
32
33
/**
34
 * Builds a web request.
35
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
36
 */
37
class RequestBuilder implements SingletonInterface
38
{
39
    /**
40
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
41
     */
42
    protected $objectManager;
43
44
    /**
45
     * This is a unique key for a plugin (not the extension key!)
46
     *
47
     * @var string
48
     */
49
    protected $pluginName = 'plugin';
50
51
    /**
52
     * The name of the extension (in UpperCamelCase)
53
     *
54
     * @var string
55
     */
56
    protected $extensionName;
57
58
    /**
59
     * The class name of the default controller
60
     *
61
     * @var string
62
     */
63
    private $defaultControllerClassName;
64
65
    /**
66
     * The default controller name
67
     *
68
     * @var string
69
     */
70
    protected $defaultControllerName = '';
71
72
    /**
73
     * The default format of the response object
74
     *
75
     * @var string
76
     */
77
    protected $defaultFormat = 'html';
78
79
    /**
80
     * The allowed actions of the controller. This actions can be called via $_GET and $_POST.
81
     *
82
     * @var array
83
     */
84
    protected $allowedControllerActions = [];
85
86
    /**
87
     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
88
     */
89
    protected $configurationManager;
90
91
    /**
92
     * @var \TYPO3\CMS\Extbase\Service\ExtensionService
93
     */
94
    protected $extensionService;
95
96
    /**
97
     * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
98
     */
99
    protected $environmentService;
100
101
    /**
102
     * @var array
103
     */
104
    private $controllerAliasToClassMapping = [];
105
106
    /**
107
     * @var array
108
     */
109
    private $controllerClassToAliasMapping = [];
110
111
    /**
112
     * @var array|string[]
113
     */
114
    private $allowedControllerAliases = [];
115
116
    /**
117
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
118
     */
119
    public function injectObjectManager(ObjectManagerInterface $objectManager)
120
    {
121
        $this->objectManager = $objectManager;
122
    }
123
124
    /**
125
     * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
126
     */
127
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
128
    {
129
        $this->configurationManager = $configurationManager;
130
    }
131
132
    /**
133
     * @param \TYPO3\CMS\Extbase\Service\ExtensionService $extensionService
134
     */
135
    public function injectExtensionService(ExtensionService $extensionService)
136
    {
137
        $this->extensionService = $extensionService;
138
    }
139
140
    /**
141
     * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
142
     */
143
    public function injectEnvironmentService(EnvironmentService $environmentService)
144
    {
145
        $this->environmentService = $environmentService;
146
    }
147
148
    /**
149
     * @throws MvcException
150
     * @see \TYPO3\CMS\Extbase\Core\Bootstrap::initializeConfiguration
151
     */
152
    protected function loadDefaultValues()
153
    {
154
        // todo: See comment in \TYPO3\CMS\Extbase\Core\Bootstrap::initializeConfiguration for further explanation
155
        // todo: on why we shouldn't use the configuration manager here.
156
        $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
157
        if (empty($configuration['extensionName'])) {
158
            throw new MvcException('"extensionName" is not properly configured. Request can\'t be dispatched!', 1289843275);
159
        }
160
        if (empty($configuration['pluginName'])) {
161
            throw new MvcException('"pluginName" is not properly configured. Request can\'t be dispatched!', 1289843277);
162
        }
163
        $this->extensionName = $configuration['extensionName'];
164
        $this->pluginName = $configuration['pluginName'];
165
        $defaultControllerConfiguration = reset($configuration['controllerConfiguration']) ?? [];
166
        $this->defaultControllerClassName = $defaultControllerConfiguration['className'] ?? null;
167
        $this->defaultControllerName = $defaultControllerConfiguration['alias'] ?? null;
168
        $this->allowedControllerActions = [];
169
        foreach ($configuration['controllerConfiguration'] as $controllerClassName => $controllerConfiguration) {
170
            $this->allowedControllerActions[$controllerClassName] = $controllerConfiguration['actions'] ?? null;
171
            $this->controllerAliasToClassMapping[$controllerConfiguration['alias']] = $controllerConfiguration['className'];
172
            $this->controllerClassToAliasMapping[$controllerConfiguration['className']] = $controllerConfiguration['alias'];
173
            $this->allowedControllerAliases[] = $controllerConfiguration['alias'];
174
        }
175
        if (!empty($configuration['format'])) {
176
            $this->defaultFormat = $configuration['format'];
177
        }
178
    }
179
180
    /**
181
     * Builds a web request object from the raw HTTP information and the configuration
182
     *
183
     * @return \TYPO3\CMS\Extbase\Mvc\Web\Request The web request as an object
184
     */
185
    public function build()
186
    {
187
        $this->loadDefaultValues();
188
        $pluginNamespace = $this->extensionService->getPluginNamespace($this->extensionName, $this->pluginName);
189
        /** @var \TYPO3\CMS\Core\Http\ServerRequest $typo3Request */
190
        $typo3Request = $GLOBALS['TYPO3_REQUEST'] ?? null;
191
        if ($typo3Request instanceof ServerRequestInterface) {
0 ignored issues
show
introduced by
$typo3Request is always a sub-type of Psr\Http\Message\ServerRequestInterface.
Loading history...
192
            $queryArguments = $typo3Request->getAttribute('routing');
193
            if ($queryArguments instanceof PageArguments) {
194
                $getParameters = $queryArguments->get($pluginNamespace) ?? [];
195
            } else {
196
                $getParameters = $typo3Request->getQueryParams()[$pluginNamespace] ?? [];
197
            }
198
            $bodyParameters = $typo3Request->getParsedBody()[$pluginNamespace] ?? [];
199
            $parameters = $getParameters;
200
            ArrayUtility::mergeRecursiveWithOverrule($parameters, $bodyParameters);
201
        } else {
202
            $parameters = GeneralUtility::_GPmerged($pluginNamespace);
203
        }
204
205
        $files = $this->untangleFilesArray($_FILES);
206
        if (is_array($files[$pluginNamespace] ?? null)) {
207
            $parameters = array_replace_recursive($parameters, $files[$pluginNamespace]);
208
        }
209
210
        $controllerClassName = $this->resolveControllerClassName($parameters);
211
        $actionName = $this->resolveActionName($controllerClassName, $parameters);
212
213
        $baseUri = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
214
        if ($this->environmentService->isEnvironmentInBackendMode()) {
215
            $baseUri .= TYPO3_mainDir;
216
        }
217
218
        /** @var \TYPO3\CMS\Extbase\Mvc\Web\Request $request */
219
        $request = $this->objectManager->get(Request::class);
220
        $request->setPluginName($this->pluginName);
221
        $request->setControllerExtensionName($this->extensionName);
222
        $request->setControllerAliasToClassNameMapping($this->controllerAliasToClassMapping);
223
        $request->setControllerName($this->controllerClassToAliasMapping[$controllerClassName]);
224
        $request->setControllerActionName($actionName);
225
        // @todo Use Environment
226
        $request->setRequestUri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
227
        $request->setBaseUri($baseUri);
228
        $request->setMethod($this->getServerRequestMethod($typo3Request));
229
        if (isset($parameters['format']) && is_string($parameters['format']) && $parameters['format'] !== '') {
230
            $request->setFormat(filter_var($parameters['format'], FILTER_SANITIZE_STRING));
231
        } else {
232
            $request->setFormat($this->defaultFormat);
233
        }
234
        foreach ($parameters as $argumentName => $argumentValue) {
235
            $request->setArgument($argumentName, $argumentValue);
236
        }
237
        return $request;
238
    }
239
240
    /**
241
     * Returns the current ControllerName extracted from given $parameters.
242
     * If no controller is specified, the defaultControllerName will be returned.
243
     * If that's not available, an exception is thrown.
244
     *
245
     * @param array $parameters
246
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException
247
     * @throws MvcException if the controller could not be resolved
248
     * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException
249
     * @return string
250
     */
251
    protected function resolveControllerClassName(array $parameters)
252
    {
253
        if (!isset($parameters['controller']) || $parameters['controller'] === '') {
254
            if (empty($this->defaultControllerClassName)) {
255
                throw new MvcException('The default controller for extension "' . $this->extensionName . '" and plugin "' . $this->pluginName . '" can not be determined. Please check for TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.', 1316104317);
256
            }
257
            return $this->defaultControllerClassName;
258
        }
259
        $controllerClassName = $this->controllerAliasToClassMapping[$parameters['controller']] ?? '';
260
        if (!in_array($controllerClassName, array_keys($this->allowedControllerActions))) {
261
            $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
262
            if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
263
                throw new PageNotFoundException('The requested resource was not found', 1313857897);
264
            }
265
            if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
266
                return $this->defaultControllerClassName;
267
            }
268
            throw new InvalidControllerNameException(
269
                'The controller "' . $parameters['controller'] . '" is not allowed by plugin "' . $this->pluginName . '". Please check for TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.',
270
                1313855173
271
            );
272
        }
273
        return filter_var($controllerClassName, FILTER_SANITIZE_STRING);
274
    }
275
276
    /**
277
     * Returns the current actionName extracted from given $parameters.
278
     * If no action is specified, the defaultActionName will be returned.
279
     * If that's not available or the specified action is not defined in the current plugin, an exception is thrown.
280
     *
281
     * @param string $controllerClassName
282
     * @param array $parameters
283
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException
284
     * @throws MvcException
285
     * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException
286
     * @return string
287
     */
288
    protected function resolveActionName($controllerClassName, array $parameters)
289
    {
290
        $defaultActionName = is_array($this->allowedControllerActions[$controllerClassName]) ? current($this->allowedControllerActions[$controllerClassName]) : '';
291
        if (!isset($parameters['action']) || $parameters['action'] === '') {
292
            if ($defaultActionName === '') {
293
                throw new MvcException('The default action can not be determined for controller "' . $controllerClassName . '". Please check TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php.', 1295479651);
294
            }
295
            return $defaultActionName;
296
        }
297
        $actionName = $parameters['action'];
298
        $allowedActionNames = $this->allowedControllerActions[$controllerClassName];
299
        if (!in_array($actionName, $allowedActionNames)) {
300
            $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
301
            if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
302
                throw new PageNotFoundException('The requested resource was not found', 1313857898);
303
            }
304
            if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
305
                return $defaultActionName;
306
            }
307
            throw new InvalidActionNameException('The action "' . $actionName . '" (controller "' . $controllerClassName . '") is not allowed by this plugin / module. Please check TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configurePlugin() in your ext_localconf.php / TYPO3\\CMS\\Extbase\\Utility\\ExtensionUtility::configureModule() in your ext_tables.php.', 1313855175);
308
        }
309
        return filter_var($actionName, FILTER_SANITIZE_STRING);
310
    }
311
312
    /**
313
     * Transforms the convoluted _FILES superglobal into a manageable form.
314
     *
315
     * @param array $convolutedFiles The _FILES superglobal
316
     * @return array Untangled files
317
     */
318
    protected function untangleFilesArray(array $convolutedFiles)
319
    {
320
        $untangledFiles = [];
321
        $fieldPaths = [];
322
        foreach ($convolutedFiles as $firstLevelFieldName => $fieldInformation) {
323
            if (!is_array($fieldInformation['error'])) {
324
                $fieldPaths[] = [$firstLevelFieldName];
325
            } else {
326
                $newFieldPaths = $this->calculateFieldPaths($fieldInformation['error'], $firstLevelFieldName);
327
                array_walk($newFieldPaths, function (&$value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

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

327
                array_walk($newFieldPaths, function (&$value, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
328
                    $value = explode('/', $value);
329
                });
330
                $fieldPaths = array_merge($fieldPaths, $newFieldPaths);
331
            }
332
        }
333
        foreach ($fieldPaths as $fieldPath) {
334
            if (count($fieldPath) === 1) {
335
                $fileInformation = $convolutedFiles[$fieldPath[0]];
336
            } else {
337
                $fileInformation = [];
338
                foreach ($convolutedFiles[$fieldPath[0]] as $key => $subStructure) {
339
                    try {
340
                        $fileInformation[$key] = ArrayUtility::getValueByPath($subStructure, array_slice($fieldPath, 1));
341
                    } catch (MissingArrayPathException $e) {
342
                        // do nothing if the path is invalid
343
                    }
344
                }
345
            }
346
            $untangledFiles = ArrayUtility::setValueByPath($untangledFiles, $fieldPath, $fileInformation);
347
        }
348
        return $untangledFiles;
349
    }
350
351
    /**
352
     * Returns an array of all possibles "field paths" for the given array.
353
     *
354
     * @param array $structure The array to walk through
355
     * @param string $firstLevelFieldName
356
     * @return array An array of paths (as strings) in the format "key1/key2/key3" ...
357
     */
358
    protected function calculateFieldPaths(array $structure, $firstLevelFieldName = null)
359
    {
360
        $fieldPaths = [];
361
        if (is_array($structure)) {
0 ignored issues
show
introduced by
The condition is_array($structure) is always true.
Loading history...
362
            foreach ($structure as $key => $subStructure) {
363
                $fieldPath = ($firstLevelFieldName !== null ? $firstLevelFieldName . '/' : '') . $key;
364
                if (is_array($subStructure)) {
365
                    foreach ($this->calculateFieldPaths($subStructure) as $subFieldPath) {
366
                        $fieldPaths[] = $fieldPath . '/' . $subFieldPath;
367
                    }
368
                } else {
369
                    $fieldPaths[] = $fieldPath;
370
                }
371
            }
372
        }
373
        return $fieldPaths;
374
    }
375
376
    protected function getServerRequestMethod(?ServerRequestInterface $typo3Request): string
377
    {
378
        if ($typo3Request instanceof ServerRequestInterface) {
379
            return $typo3Request->getMethod();
380
        }
381
        return isset($_SERVER['REQUEST_METHOD']) && is_string($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
382
    }
383
}
384