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