Passed
Push — master ( c14a0b...566b5b )
by
unknown
27:12 queued 10:19
created

RequestBuilder::resolveActionName()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 15
nc 12
nop 2
dl 0
loc 22
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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