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