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