0b7aa14c323d4460cf23dd27495c44cf50395a2f
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Mvc / Controller / ActionController.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Mvc\Controller;
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 TYPO3\CMS\Core\Messaging\FlashMessage;
18 use TYPO3\CMS\Core\Page\PageRenderer;
19 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
20 use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
21 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
22 use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest;
23 use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
24 use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
25 use TYPO3Fluid\Fluid\View\TemplateView;
26
27 /**
28 * A multi action controller. This is by far the most common base class for Controllers.
29 *
30 * @api
31 */
32 class ActionController extends AbstractController
33 {
34 /**
35 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
36 */
37 protected $reflectionService;
38
39 /**
40 * @var \TYPO3\CMS\Extbase\Service\CacheService
41 */
42 protected $cacheService;
43
44 /**
45 * The current view, as resolved by resolveView()
46 *
47 * @var ViewInterface
48 * @api
49 */
50 protected $view = null;
51
52 /**
53 * @var string
54 * @api
55 */
56 protected $namespacesViewObjectNamePattern = '@vendor\@extension\View\@controller\@action@format';
57
58 /**
59 * A list of formats and object names of the views which should render them.
60 *
61 * Example:
62 *
63 * array('html' => 'Tx_MyExtension_View_MyHtmlView', 'json' => 'F3...
64 *
65 * @var array
66 */
67 protected $viewFormatToObjectNameMap = [];
68
69 /**
70 * The default view object to use if none of the resolved views can render
71 * a response for the current request.
72 *
73 * @var string
74 * @api
75 */
76 protected $defaultViewObjectName = \TYPO3\CMS\Fluid\View\TemplateView::class;
77
78 /**
79 * Name of the action method
80 *
81 * @var string
82 * @api
83 */
84 protected $actionMethodName = 'indexAction';
85
86 /**
87 * Name of the special error action method which is called in case of errors
88 *
89 * @var string
90 * @api
91 */
92 protected $errorMethodName = 'errorAction';
93
94 /**
95 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
96 * @api
97 */
98 protected $mvcPropertyMappingConfigurationService;
99
100 /**
101 * The current request.
102 *
103 * @var \TYPO3\CMS\Extbase\Mvc\Request
104 * @api
105 */
106 protected $request;
107
108 /**
109 * The response which will be returned by this action controller
110 *
111 * @var \TYPO3\CMS\Extbase\Mvc\Response
112 * @api
113 */
114 protected $response;
115
116 /**
117 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
118 */
119 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
120 {
121 $this->reflectionService = $reflectionService;
122 }
123
124 /**
125 * @param \TYPO3\CMS\Extbase\Service\CacheService $cacheService
126 */
127 public function injectCacheService(\TYPO3\CMS\Extbase\Service\CacheService $cacheService)
128 {
129 $this->cacheService = $cacheService;
130 }
131
132 /**
133 * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
134 */
135 public function injectMvcPropertyMappingConfigurationService(\TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
136 {
137 $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
138 }
139
140 /**
141 * Handles a request. The result output is returned by altering the given response.
142 *
143 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
144 * @param \TYPO3\CMS\Extbase\Mvc\ResponseInterface $response The response, modified by this handler
145 *
146 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException
147 */
148 public function processRequest(\TYPO3\CMS\Extbase\Mvc\RequestInterface $request, \TYPO3\CMS\Extbase\Mvc\ResponseInterface $response)
149 {
150 if (!$this->canProcessRequest($request)) {
151 throw new \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException(static::class . ' does not support requests of type "' . get_class($request) . '". Supported types are: ' . implode(' ', $this->supportedRequestTypes), 1187701131);
152 }
153
154 if ($response instanceof \TYPO3\CMS\Extbase\Mvc\Web\Response && $request instanceof WebRequest) {
155 $response->setRequest($request);
156 }
157 $this->request = $request;
158 $this->request->setDispatched(true);
159 $this->response = $response;
160 $this->uriBuilder = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
161 $this->uriBuilder->setRequest($request);
162 $this->actionMethodName = $this->resolveActionMethodName();
163 $this->initializeActionMethodArguments();
164 $this->initializeActionMethodValidators();
165 $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
166 $this->initializeAction();
167 $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
168 if (method_exists($this, $actionInitializationMethodName)) {
169 call_user_func([$this, $actionInitializationMethodName]);
170 }
171 $this->mapRequestArgumentsToControllerArguments();
172 $this->controllerContext = $this->buildControllerContext();
173 $this->view = $this->resolveView();
174 if ($this->view !== null) {
175 $this->initializeView($this->view);
176 }
177 $this->callActionMethod();
178 $this->renderAssetsForRequest($request);
179 }
180
181 /**
182 * Method which initializes assets that should be attached to the response
183 * for the given $request, which contains parameters that an override can
184 * use to determine which assets to add via PageRenderer.
185 *
186 * This default implementation will attempt to render the sections "HeaderAssets"
187 * and "FooterAssets" from the template that is being rendered, inserting the
188 * rendered content into either page header or footer, as appropriate. Both
189 * sections are optional and can be used one or both in combination.
190 *
191 * You can add assets with this method without worrying about duplicates, if
192 * for example you do this in a plugin that gets used multiple time on a page.
193 *
194 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
195 */
196 protected function renderAssetsForRequest($request)
197 {
198 if (!$this->view instanceof TemplateView) {
199 // Only TemplateView (from Fluid engine, so this includes all TYPO3 Views based
200 // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
201 // declared on ViewInterface - so we must assert a specific class. We silently skip
202 // asset processing if the View doesn't match, so we don't risk breaking custom Views.
203 return;
204 }
205 $pageRenderer = $this->objectManager->get(PageRenderer::class);
206 $variables = ['request' => $request, 'arguments' => $this->arguments];
207 $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
208 $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
209 if (!empty(trim($headerAssets))) {
210 $pageRenderer->addHeaderData($headerAssets);
211 }
212 if (!empty(trim($footerAssets))) {
213 $pageRenderer->addFooterData($footerAssets);
214 }
215 }
216
217 /**
218 * Implementation of the arguments initilization in the action controller:
219 * Automatically registers arguments of the current action
220 *
221 * Don't override this method - use initializeAction() instead.
222 *
223 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException
224 * @see initializeArguments()
225 */
226 protected function initializeActionMethodArguments()
227 {
228 $methodParameters = $this->reflectionService->getMethodParameters(static::class, $this->actionMethodName);
229 foreach ($methodParameters as $parameterName => $parameterInfo) {
230 $dataType = null;
231 if (isset($parameterInfo['type'])) {
232 $dataType = $parameterInfo['type'];
233 } elseif ($parameterInfo['array']) {
234 $dataType = 'array';
235 }
236 if ($dataType === null) {
237 throw new \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
238 }
239 $defaultValue = $parameterInfo['hasDefaultValue'] === true ? $parameterInfo['defaultValue'] : null;
240 $this->arguments->addNewArgument($parameterName, $dataType, $parameterInfo['optional'] === false, $defaultValue);
241 }
242 }
243
244 /**
245 * Adds the needed validators to the Arguments:
246 *
247 * - Validators checking the data type from the @param annotation
248 * - Custom validators specified with validate annotations.
249 * - Model-based validators (validate annotations in the model)
250 * - Custom model validator classes
251 */
252 protected function initializeActionMethodValidators()
253 {
254 if ($this->arguments->count() === 0) {
255 return;
256 }
257
258 $classSchema = $this->reflectionService->getClassSchema(static::class);
259
260 $ignoreValidationAnnotations = array_unique(array_flip(
261 $classSchema->getMethod($this->actionMethodName)['tags']['ignorevalidation'] ?? []
262 ));
263
264 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
265 foreach ($this->arguments as $argument) {
266 if (isset($ignoreValidationAnnotations[$argument->getName()])) {
267 continue;
268 }
269
270 $validator = $this->objectManager->get(ConjunctionValidator::class);
271 $validatorDefinitions = $classSchema->getMethod($this->actionMethodName)['params'][$argument->getName()]['validators'] ?? [];
272
273 foreach ($validatorDefinitions as $validatorDefinition) {
274 /** @var ValidatorInterface $validatorInstance */
275 $validatorInstance = $this->objectManager->get(
276 $validatorDefinition['className'],
277 $validatorDefinition['options']
278 );
279
280 $validator->addValidator(
281 $validatorInstance
282 );
283 }
284
285 $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
286 if ($baseValidatorConjunction->count() > 0) {
287 $validator->addValidator($baseValidatorConjunction);
288 }
289 $argument->setValidator($validator);
290 }
291 }
292
293 /**
294 * Resolves and checks the current action method name
295 *
296 * @return string Method name of the current action
297 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException if the action specified in the request object does not exist (and if there's no default action either).
298 */
299 protected function resolveActionMethodName()
300 {
301 $actionMethodName = $this->request->getControllerActionName() . 'Action';
302 if (!method_exists($this, $actionMethodName)) {
303 throw new \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
304 }
305 return $actionMethodName;
306 }
307
308 /**
309 * Calls the specified action method and passes the arguments.
310 *
311 * If the action returns a string, it is appended to the content in the
312 * response object. If the action doesn't return anything and a valid
313 * view exists, the view is rendered automatically.
314 *
315 * @api
316 */
317 protected function callActionMethod()
318 {
319 $preparedArguments = [];
320 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
321 foreach ($this->arguments as $argument) {
322 $preparedArguments[] = $argument->getValue();
323 }
324 $validationResult = $this->arguments->validate();
325 if (!$validationResult->hasErrors()) {
326 $this->emitBeforeCallActionMethodSignal($preparedArguments);
327 $actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
328 } else {
329 $actionResult = $this->{$this->errorMethodName}();
330 }
331
332 if ($actionResult === null && $this->view instanceof ViewInterface) {
333 $this->response->appendContent($this->view->render());
334 } elseif (is_string($actionResult) && $actionResult !== '') {
335 $this->response->appendContent($actionResult);
336 } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
337 $this->response->appendContent((string)$actionResult);
338 }
339 }
340
341 /**
342 * Emits a signal before the current action is called
343 *
344 * @param array $preparedArguments
345 */
346 protected function emitBeforeCallActionMethodSignal(array $preparedArguments)
347 {
348 $this->signalSlotDispatcher->dispatch(__CLASS__, 'beforeCallActionMethod', [static::class, $this->actionMethodName, $preparedArguments]);
349 }
350
351 /**
352 * Prepares a view for the current action.
353 * By default, this method tries to locate a view with a name matching the current action.
354 *
355 * @return ViewInterface
356 * @api
357 */
358 protected function resolveView()
359 {
360 $viewObjectName = $this->resolveViewObjectName();
361 if ($viewObjectName !== false) {
362 /** @var $view ViewInterface */
363 $view = $this->objectManager->get($viewObjectName);
364 $this->setViewConfiguration($view);
365 if ($view->canRender($this->controllerContext) === false) {
366 unset($view);
367 }
368 }
369 if (!isset($view) && $this->defaultViewObjectName != '') {
370 /** @var $view ViewInterface */
371 $view = $this->objectManager->get($this->defaultViewObjectName);
372 $this->setViewConfiguration($view);
373 if ($view->canRender($this->controllerContext) === false) {
374 unset($view);
375 }
376 }
377 if (!isset($view)) {
378 $view = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\View\NotFoundView::class);
379 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "'
380 . $this->request->getControllerActionName() . '" in class "' . $this->request->getControllerObjectName() . '"');
381 }
382 $view->setControllerContext($this->controllerContext);
383 if (method_exists($view, 'injectSettings')) {
384 $view->injectSettings($this->settings);
385 }
386 $view->initializeView();
387 // In TYPO3.Flow, solved through Object Lifecycle methods, we need to call it explicitly
388 $view->assign('settings', $this->settings);
389 // same with settings injection.
390 return $view;
391 }
392
393 /**
394 * @param ViewInterface $view
395 */
396 protected function setViewConfiguration(ViewInterface $view)
397 {
398 // Template Path Override
399 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
400 ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
401 );
402
403 // set TemplateRootPaths
404 $viewFunctionName = 'setTemplateRootPaths';
405 if (method_exists($view, $viewFunctionName)) {
406 $setting = 'templateRootPaths';
407 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
408 // no need to bother if there is nothing to set
409 if ($parameter) {
410 $view->$viewFunctionName($parameter);
411 }
412 }
413
414 // set LayoutRootPaths
415 $viewFunctionName = 'setLayoutRootPaths';
416 if (method_exists($view, $viewFunctionName)) {
417 $setting = 'layoutRootPaths';
418 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
419 // no need to bother if there is nothing to set
420 if ($parameter) {
421 $view->$viewFunctionName($parameter);
422 }
423 }
424
425 // set PartialRootPaths
426 $viewFunctionName = 'setPartialRootPaths';
427 if (method_exists($view, $viewFunctionName)) {
428 $setting = 'partialRootPaths';
429 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
430 // no need to bother if there is nothing to set
431 if ($parameter) {
432 $view->$viewFunctionName($parameter);
433 }
434 }
435 }
436
437 /**
438 * Handles the path resolving for *rootPath(s)
439 *
440 * numerical arrays get ordered by key ascending
441 *
442 * @param array $extbaseFrameworkConfiguration
443 * @param string $setting parameter name from TypoScript
444 *
445 * @return array
446 */
447 protected function getViewProperty($extbaseFrameworkConfiguration, $setting)
448 {
449 $values = [];
450 if (
451 !empty($extbaseFrameworkConfiguration['view'][$setting])
452 && is_array($extbaseFrameworkConfiguration['view'][$setting])
453 ) {
454 $values = $extbaseFrameworkConfiguration['view'][$setting];
455 }
456
457 return $values;
458 }
459
460 /**
461 * Determines the fully qualified view object name.
462 *
463 * @return mixed The fully qualified view object name or FALSE if no matching view could be found.
464 * @api
465 */
466 protected function resolveViewObjectName()
467 {
468 $vendorName = $this->request->getControllerVendorName();
469 if ($vendorName === null) {
470 return false;
471 }
472
473 $possibleViewName = str_replace(
474 [
475 '@vendor',
476 '@extension',
477 '@controller',
478 '@action'
479 ],
480 [
481 $vendorName,
482 $this->request->getControllerExtensionName(),
483 $this->request->getControllerName(),
484 ucfirst($this->request->getControllerActionName())
485 ],
486 $this->namespacesViewObjectNamePattern
487 );
488 $format = $this->request->getFormat();
489 $viewObjectName = str_replace('@format', ucfirst($format), $possibleViewName);
490 if (class_exists($viewObjectName) === false) {
491 $viewObjectName = str_replace('@format', '', $possibleViewName);
492 }
493 if (isset($this->viewFormatToObjectNameMap[$format]) && class_exists($viewObjectName) === false) {
494 $viewObjectName = $this->viewFormatToObjectNameMap[$format];
495 }
496 return class_exists($viewObjectName) ? $viewObjectName : false;
497 }
498
499 /**
500 * Initializes the view before invoking an action method.
501 *
502 * Override this method to solve assign variables common for all actions
503 * or prepare the view in another way before the action is called.
504 *
505 * @param ViewInterface $view The view to be initialized
506 *
507 * @api
508 */
509 protected function initializeView(ViewInterface $view)
510 {
511 }
512
513 /**
514 * Initializes the controller before invoking an action method.
515 *
516 * Override this method to solve tasks which all actions have in
517 * common.
518 *
519 * @api
520 */
521 protected function initializeAction()
522 {
523 }
524
525 /**
526 * A special action which is called if the originally intended action could
527 * not be called, for example if the arguments were not valid.
528 *
529 * The default implementation sets a flash message, request errors and forwards back
530 * to the originating action. This is suitable for most actions dealing with form input.
531 *
532 * We clear the page cache by default on an error as well, as we need to make sure the
533 * data is re-evaluated when the user changes something.
534 *
535 * @return string
536 * @api
537 */
538 protected function errorAction()
539 {
540 $this->clearCacheOnError();
541 $this->addErrorFlashMessage();
542 $this->forwardToReferringRequest();
543
544 return $this->getFlattenedValidationErrorMessage();
545 }
546
547 /**
548 * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
549 * Better would be just do delete the cache for the error action, but that is not possible right now.
550 */
551 protected function clearCacheOnError()
552 {
553 $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
554 if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
555 if (isset($GLOBALS['TSFE'])) {
556 $pageUid = $GLOBALS['TSFE']->id;
557 $this->cacheService->clearPageCache([$pageUid]);
558 }
559 }
560 }
561
562 /**
563 * If an error occurred during this request, this adds a flash message describing the error to the flash
564 * message container.
565 */
566 protected function addErrorFlashMessage()
567 {
568 $errorFlashMessage = $this->getErrorFlashMessage();
569 if ($errorFlashMessage !== false) {
570 $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
571 }
572 }
573
574 /**
575 * A template method for displaying custom error flash messages, or to
576 * display no flash message at all on errors. Override this to customize
577 * the flash message in your action controller.
578 *
579 * @return string The flash message or FALSE if no flash message should be set
580 * @api
581 */
582 protected function getErrorFlashMessage()
583 {
584 return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
585 }
586
587 /**
588 * If information on the request before the current request was sent, this method forwards back
589 * to the originating request. This effectively ends processing of the current request, so do not
590 * call this method before you have finished the necessary business logic!
591 *
592 * @throws StopActionException
593 */
594 protected function forwardToReferringRequest()
595 {
596 $referringRequest = $this->request->getReferringRequest();
597 if ($referringRequest !== null) {
598 $originalRequest = clone $this->request;
599 $this->request->setOriginalRequest($originalRequest);
600 $this->request->setOriginalRequestMappingResults($this->arguments->validate());
601 $this->forward(
602 $referringRequest->getControllerActionName(),
603 $referringRequest->getControllerName(),
604 $referringRequest->getControllerExtensionName(),
605 $referringRequest->getArguments()
606 );
607 }
608 }
609
610 /**
611 * Returns a string with a basic error message about validation failure.
612 * We may add all validation error messages to a log file in the future,
613 * but for security reasons (@see #54074) we do not return these here.
614 *
615 * @return string
616 */
617 protected function getFlattenedValidationErrorMessage()
618 {
619 $outputMessage = 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
620 return $outputMessage;
621 }
622
623 /**
624 * Returns a map of action method names and their parameters.
625 *
626 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
627 *
628 * @return array Array of method parameters by action name
629 * @deprecated
630 */
631 public static function getActionMethodParameters($objectManager)
632 {
633 trigger_error(
634 'Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 v10.0.',
635 E_USER_DEPRECATED
636 );
637
638 $reflectionService = $objectManager->get(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class);
639
640 $result = [];
641
642 $className = get_called_class();
643 $methodNames = get_class_methods($className);
644 foreach ($methodNames as $methodName) {
645 if (strlen($methodName) > 6 && strpos($methodName, 'Action', strlen($methodName) - 6) !== false) {
646 $result[$methodName] = $reflectionService->getMethodParameters($className, $methodName);
647 }
648 }
649
650 return $result;
651 }
652 }