Passed
Push — master ( a7d46b...0a8eff )
by
unknown
14:11
created

RequestBuilder::loadDefaultValues()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 6
nop 0
dl 0
loc 25
rs 9.3554
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\Http\ApplicationType;
21
use TYPO3\CMS\Core\Http\NormalizedParams;
22
use TYPO3\CMS\Core\Routing\PageArguments;
23
use TYPO3\CMS\Core\SingletonInterface;
24
use TYPO3\CMS\Core\Utility\ArrayUtility;
25
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
28
use TYPO3\CMS\Extbase\Mvc\Exception as MvcException;
29
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException;
30
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException;
31
use TYPO3\CMS\Extbase\Mvc\Request;
32
use TYPO3\CMS\Extbase\Service\ExtensionService;
33
34
/**
35
 * Builds a web request.
36
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
37
 */
38
class RequestBuilder implements SingletonInterface
39
{
40
    /**
41
     * This is a unique key for a plugin (not the extension key!)
42
     *
43
     * @var string
44
     */
45
    protected $pluginName = 'plugin';
46
47
    /**
48
     * The name of the extension (in UpperCamelCase)
49
     *
50
     * @var string
51
     */
52
    protected $extensionName;
53
54
    /**
55
     * The class name of the default controller
56
     *
57
     * @var string
58
     */
59
    private $defaultControllerClassName;
60
61
    /**
62
     * The default controller name
63
     *
64
     * @var string
65
     */
66
    protected $defaultControllerName = '';
67
68
    /**
69
     * The default format of the response object
70
     *
71
     * @var string
72
     */
73
    protected $defaultFormat = 'html';
74
75
    /**
76
     * The allowed actions of the controller. This actions can be called via $_GET and $_POST.
77
     *
78
     * @var array
79
     */
80
    protected $allowedControllerActions = [];
81
82
    /**
83
     * @var ConfigurationManagerInterface
84
     */
85
    protected $configurationManager;
86
87
    /**
88
     * @var ExtensionService
89
     */
90
    protected $extensionService;
91
92
    /**
93
     * @var array
94
     */
95
    private $controllerAliasToClassMapping = [];
96
97
    /**
98
     * @var array
99
     */
100
    private $controllerClassToAliasMapping = [];
101
102
    /**
103
     * @var array|string[]
104
     */
105
    private $allowedControllerAliases = [];
106
107
    /**
108
     * @param ConfigurationManagerInterface $configurationManager
109
     */
110
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
111
    {
112
        $this->configurationManager = $configurationManager;
113
    }
114
115
    /**
116
     * @param ExtensionService $extensionService
117
     */
118
    public function injectExtensionService(ExtensionService $extensionService)
119
    {
120
        $this->extensionService = $extensionService;
121
    }
122
123
    /**
124
     * @throws MvcException
125
     * @see \TYPO3\CMS\Extbase\Core\Bootstrap::initializeConfiguration
126
     */
127
    protected function loadDefaultValues()
128
    {
129
        // todo: See comment in \TYPO3\CMS\Extbase\Core\Bootstrap::initializeConfiguration for further explanation
130
        // todo: on why we shouldn't use the configuration manager here.
131
        $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
132
        if (empty($configuration['extensionName'])) {
133
            throw new MvcException('"extensionName" is not properly configured. Request can\'t be dispatched!', 1289843275);
134
        }
135
        if (empty($configuration['pluginName'])) {
136
            throw new MvcException('"pluginName" is not properly configured. Request can\'t be dispatched!', 1289843277);
137
        }
138
        $this->extensionName = $configuration['extensionName'];
139
        $this->pluginName = $configuration['pluginName'];
140
        $defaultControllerConfiguration = reset($configuration['controllerConfiguration']) ?? [];
141
        $this->defaultControllerClassName = $defaultControllerConfiguration['className'] ?? null;
142
        $this->defaultControllerName = $defaultControllerConfiguration['alias'] ?? null;
143
        $this->allowedControllerActions = [];
144
        foreach ($configuration['controllerConfiguration'] as $controllerClassName => $controllerConfiguration) {
145
            $this->allowedControllerActions[$controllerClassName] = $controllerConfiguration['actions'] ?? null;
146
            $this->controllerAliasToClassMapping[$controllerConfiguration['alias']] = $controllerConfiguration['className'];
147
            $this->controllerClassToAliasMapping[$controllerConfiguration['className']] = $controllerConfiguration['alias'];
148
            $this->allowedControllerAliases[] = $controllerConfiguration['alias'];
149
        }
150
        if (!empty($configuration['format'])) {
151
            $this->defaultFormat = $configuration['format'];
152
        }
153
    }
154
155
    /**
156
     * Builds a web request object from the raw HTTP information and the configuration
157
     *
158
     * @param ServerRequestInterface $mainRequest
159
     * @return Request The web request as an object
160
     */
161
    public function build(ServerRequestInterface $mainRequest)
162
    {
163
        $this->loadDefaultValues();
164
        $pluginNamespace = $this->extensionService->getPluginNamespace($this->extensionName, $this->pluginName);
165
        /** @var NormalizedParams $normalizedParams */
166
        $normalizedParams = $mainRequest->getAttribute('normalizedParams');
167
        $queryArguments = $mainRequest->getAttribute('routing');
168
        if ($queryArguments instanceof PageArguments) {
169
            $parameters = $queryArguments->get($pluginNamespace) ?? [];
170
        } else {
171
            $parameters = $mainRequest->getQueryParams()[$pluginNamespace] ?? [];
172
        }
173
        if ($mainRequest->getMethod() === 'POST') {
174
            $postParameters = $mainRequest->getParsedBody()[$pluginNamespace] ?? [];
175
            ArrayUtility::mergeRecursiveWithOverrule($parameters, $postParameters);
176
        }
177
178
        $files = $this->untangleFilesArray($_FILES);
179
        if (is_array($files[$pluginNamespace] ?? null)) {
180
            $parameters = array_replace_recursive($parameters, $files[$pluginNamespace]);
181
        }
182
183
        $controllerClassName = $this->resolveControllerClassName($parameters);
184
        $actionName = $this->resolveActionName($controllerClassName, $parameters);
185
186
        $baseUri = $normalizedParams->getSiteUrl();
187
        if (ApplicationType::fromRequest($mainRequest)->isBackend()) {
188
            $baseUri .= TYPO3_mainDir;
189
        }
190
191
        $request = GeneralUtility::makeInstance(Request::class);
192
        $request->setPluginName($this->pluginName);
193
        $request->setControllerExtensionName($this->extensionName);
194
        $request->setControllerAliasToClassNameMapping($this->controllerAliasToClassMapping);
195
        $request->setControllerName($this->controllerClassToAliasMapping[$controllerClassName]);
196
        $request->setControllerActionName($actionName);
197
        $request->setRequestUri($normalizedParams->getRequestUrl());
198
        $request->setBaseUri($baseUri);
199
        $request->setMethod($mainRequest->getMethod());
200
        if (isset($parameters['format']) && is_string($parameters['format']) && $parameters['format'] !== '') {
201
            $request->setFormat(filter_var($parameters['format'], FILTER_SANITIZE_STRING));
202
        } else {
203
            $request->setFormat($this->defaultFormat);
204
        }
205
        foreach ($parameters as $argumentName => $argumentValue) {
206
            $request->setArgument($argumentName, $argumentValue);
207
        }
208
        return $request;
209
    }
210
211
    /**
212
     * Returns the current ControllerName extracted from given $parameters.
213
     * If no controller is specified, the defaultControllerName will be returned.
214
     * If that's not available, an exception is thrown.
215
     *
216
     * @param array $parameters
217
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException
218
     * @throws MvcException if the controller could not be resolved
219
     * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException
220
     * @return string
221
     */
222
    protected function resolveControllerClassName(array $parameters)
223
    {
224
        if (!isset($parameters['controller']) || $parameters['controller'] === '') {
225
            if (empty($this->defaultControllerClassName)) {
226
                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);
227
            }
228
            return $this->defaultControllerClassName;
229
        }
230
        $controllerClassName = $this->controllerAliasToClassMapping[$parameters['controller']] ?? '';
231
        if (!in_array($controllerClassName, array_keys($this->allowedControllerActions))) {
232
            $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
233
            if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
234
                throw new PageNotFoundException('The requested resource was not found', 1313857897);
235
            }
236
            if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
237
                return $this->defaultControllerClassName;
238
            }
239
            throw new InvalidControllerNameException(
240
                '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.',
241
                1313855173
242
            );
243
        }
244
        return filter_var($controllerClassName, FILTER_SANITIZE_STRING);
245
    }
246
247
    /**
248
     * Returns the current actionName extracted from given $parameters.
249
     * If no action is specified, the defaultActionName will be returned.
250
     * If that's not available or the specified action is not defined in the current plugin, an exception is thrown.
251
     *
252
     * @param string $controllerClassName
253
     * @param array $parameters
254
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidActionNameException
255
     * @throws MvcException
256
     * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException
257
     * @return string
258
     */
259
    protected function resolveActionName($controllerClassName, array $parameters)
260
    {
261
        $defaultActionName = is_array($this->allowedControllerActions[$controllerClassName]) ? current($this->allowedControllerActions[$controllerClassName]) : '';
262
        if (!isset($parameters['action']) || $parameters['action'] === '') {
263
            if ($defaultActionName === '') {
264
                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);
265
            }
266
            return $defaultActionName;
267
        }
268
        $actionName = $parameters['action'];
269
        $allowedActionNames = $this->allowedControllerActions[$controllerClassName];
270
        if (!in_array($actionName, $allowedActionNames)) {
271
            $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
272
            if (isset($configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) && (bool)$configuration['mvc']['throwPageNotFoundExceptionIfActionCantBeResolved']) {
273
                throw new PageNotFoundException('The requested resource was not found', 1313857898);
274
            }
275
            if (isset($configuration['mvc']['callDefaultActionIfActionCantBeResolved']) && (bool)$configuration['mvc']['callDefaultActionIfActionCantBeResolved']) {
276
                return $defaultActionName;
277
            }
278
            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);
279
        }
280
        return filter_var($actionName, FILTER_SANITIZE_STRING);
281
    }
282
283
    /**
284
     * Transforms the convoluted _FILES superglobal into a manageable form.
285
     *
286
     * @param array $convolutedFiles The _FILES superglobal
287
     * @return array Untangled files
288
     */
289
    protected function untangleFilesArray(array $convolutedFiles)
290
    {
291
        $untangledFiles = [];
292
        $fieldPaths = [];
293
        foreach ($convolutedFiles as $firstLevelFieldName => $fieldInformation) {
294
            if (!is_array($fieldInformation['error'])) {
295
                $fieldPaths[] = [$firstLevelFieldName];
296
            } else {
297
                $newFieldPaths = $this->calculateFieldPaths($fieldInformation['error'], $firstLevelFieldName);
298
                array_walk($newFieldPaths, function (&$value, $key) {
299
                    $value = explode('/', $value);
300
                });
301
                $fieldPaths = array_merge($fieldPaths, $newFieldPaths);
302
            }
303
        }
304
        foreach ($fieldPaths as $fieldPath) {
305
            if (count($fieldPath) === 1) {
306
                $fileInformation = $convolutedFiles[$fieldPath[0]];
307
            } else {
308
                $fileInformation = [];
309
                foreach ($convolutedFiles[$fieldPath[0]] as $key => $subStructure) {
310
                    try {
311
                        $fileInformation[$key] = ArrayUtility::getValueByPath($subStructure, array_slice($fieldPath, 1));
312
                    } catch (MissingArrayPathException $e) {
313
                        // do nothing if the path is invalid
314
                    }
315
                }
316
            }
317
            $untangledFiles = ArrayUtility::setValueByPath($untangledFiles, $fieldPath, $fileInformation);
318
        }
319
        return $untangledFiles;
320
    }
321
322
    /**
323
     * Returns an array of all possibles "field paths" for the given array.
324
     *
325
     * @param array $structure The array to walk through
326
     * @param string $firstLevelFieldName
327
     * @return array An array of paths (as strings) in the format "key1/key2/key3" ...
328
     */
329
    protected function calculateFieldPaths(array $structure, $firstLevelFieldName = null)
330
    {
331
        $fieldPaths = [];
332
        if (is_array($structure)) {
0 ignored issues
show
introduced by
The condition is_array($structure) is always true.
Loading history...
333
            foreach ($structure as $key => $subStructure) {
334
                $fieldPath = ($firstLevelFieldName !== null ? $firstLevelFieldName . '/' : '') . $key;
335
                if (is_array($subStructure)) {
336
                    foreach ($this->calculateFieldPaths($subStructure) as $subFieldPath) {
337
                        $fieldPaths[] = $fieldPath . '/' . $subFieldPath;
338
                    }
339
                } else {
340
                    $fieldPaths[] = $fieldPath;
341
                }
342
            }
343
        }
344
        return $fieldPaths;
345
    }
346
}
347