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