[TASK] Introduce BackendActionController
[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 $actionResult = $this->processActionResult();
270 $content = $this->resolveActionResult($actionResult);
271 $this->appendContent($content);
272 }
273
274 /**
275 * Emits a signal before the current action is called
276 *
277 * @param array $preparedArguments
278 */
279 protected function emitBeforeCallActionMethodSignal(array $preparedArguments) {
280 $this->signalSlotDispatcher->dispatch(__CLASS__, 'beforeCallActionMethod', array(get_class($this), $this->actionMethodName, $preparedArguments));
281 }
282
283 /**
284 * Prepares a view for the current action.
285 * By default, this method tries to locate a view with a name matching the current action.
286 *
287 * @return ViewInterface
288 * @api
289 */
290 protected function resolveView() {
291 $viewObjectName = $this->resolveViewObjectName();
292 if ($viewObjectName !== FALSE) {
293 /** @var $view ViewInterface */
294 $view = $this->objectManager->get($viewObjectName);
295 $this->setViewConfiguration($view);
296 if ($view->canRender($this->controllerContext) === FALSE) {
297 unset($view);
298 }
299 }
300 if (!isset($view) && $this->defaultViewObjectName != '') {
301 /** @var $view ViewInterface */
302 $view = $this->objectManager->get($this->defaultViewObjectName);
303 $this->setViewConfiguration($view);
304 if ($view->canRender($this->controllerContext) === FALSE) {
305 unset($view);
306 }
307 }
308 if (!isset($view)) {
309 $view = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\View\NotFoundView::class);
310 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "'
311 . $this->request->getControllerActionName() . '" in class "' . $this->request->getControllerObjectName() . '"');
312 }
313 $view->setControllerContext($this->controllerContext);
314 if (method_exists($view, 'injectSettings')) {
315 $view->injectSettings($this->settings);
316 }
317 $view->initializeView();
318 // In TYPO3.Flow, solved through Object Lifecycle methods, we need to call it explicitly
319 $view->assign('settings', $this->settings);
320 // same with settings injection.
321 return $view;
322 }
323
324 /**
325 * @param ViewInterface $view
326 *
327 * @return void
328 */
329 protected function setViewConfiguration(ViewInterface $view) {
330 // Template Path Override
331 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
332 ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
333 );
334
335 // set TemplateRootPaths
336 $viewFunctionName = 'setTemplateRootPaths';
337 if (method_exists($view, $viewFunctionName)) {
338 $setting = 'templateRootPaths';
339 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
340 // no need to bother if there is nothing to set
341 if ($parameter) {
342 $view->$viewFunctionName($parameter);
343 }
344 }
345
346 // set LayoutRootPaths
347 $viewFunctionName = 'setLayoutRootPaths';
348 if (method_exists($view, $viewFunctionName)) {
349 $setting = 'layoutRootPaths';
350 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
351 // no need to bother if there is nothing to set
352 if ($parameter) {
353 $view->$viewFunctionName($parameter);
354 }
355 }
356
357 // set PartialRootPaths
358 $viewFunctionName = 'setPartialRootPaths';
359 if (method_exists($view, $viewFunctionName)) {
360 $setting = 'partialRootPaths';
361 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
362 // no need to bother if there is nothing to set
363 if ($parameter) {
364 $view->$viewFunctionName($parameter);
365 }
366 }
367 }
368
369 /**
370 * Handles the path resolving for *rootPath(s)
371 *
372 * numerical arrays get ordered by key ascending
373 *
374 * @param array $extbaseFrameworkConfiguration
375 * @param string $setting parameter name from TypoScript
376 *
377 * @return array
378 */
379 protected function getViewProperty($extbaseFrameworkConfiguration, $setting) {
380 $values = array();
381 if (
382 !empty($extbaseFrameworkConfiguration['view'][$setting])
383 && is_array($extbaseFrameworkConfiguration['view'][$setting])
384 ) {
385 $values = ArrayUtility::sortArrayWithIntegerKeys($extbaseFrameworkConfiguration['view'][$setting]);
386 $values = array_reverse($values, TRUE);
387 }
388
389 return $values;
390 }
391
392 /**
393 * Determines the fully qualified view object name.
394 *
395 * @return mixed The fully qualified view object name or FALSE if no matching view could be found.
396 * @api
397 */
398 protected function resolveViewObjectName() {
399 $vendorName = $this->request->getControllerVendorName();
400 if ($vendorName === NULL) {
401 return FALSE;
402 }
403
404 $possibleViewName = str_replace(
405 array(
406 '@vendor',
407 '@extension',
408 '@controller',
409 '@action'
410 ),
411 array(
412 $vendorName,
413 $this->request->getControllerExtensionName(),
414 $this->request->getControllerName(),
415 ucfirst($this->request->getControllerActionName())
416 ),
417 $this->namespacesViewObjectNamePattern
418 );
419 $format = $this->request->getFormat();
420 $viewObjectName = str_replace('@format', ucfirst($format), $possibleViewName);
421 if (class_exists($viewObjectName) === FALSE) {
422 $viewObjectName = str_replace('@format', '', $possibleViewName);
423 }
424 if (isset($this->viewFormatToObjectNameMap[$format]) && class_exists($viewObjectName) === FALSE) {
425 $viewObjectName = $this->viewFormatToObjectNameMap[$format];
426 }
427 return class_exists($viewObjectName) ? $viewObjectName : FALSE;
428 }
429
430 /**
431 * Initializes the view before invoking an action method.
432 *
433 * Override this method to solve assign variables common for all actions
434 * or prepare the view in another way before the action is called.
435 *
436 * @param ViewInterface $view The view to be initialized
437 *
438 * @return void
439 * @api
440 */
441 protected function initializeView(ViewInterface $view) {
442 }
443
444 /**
445 * Initializes the controller before invoking an action method.
446 *
447 * Override this method to solve tasks which all actions have in
448 * common.
449 *
450 * @return void
451 * @api
452 */
453 protected function initializeAction() {
454 }
455
456 /**
457 * A special action which is called if the originally intended action could
458 * not be called, for example if the arguments were not valid.
459 *
460 * The default implementation sets a flash message, request errors and forwards back
461 * to the originating action. This is suitable for most actions dealing with form input.
462 *
463 * We clear the page cache by default on an error as well, as we need to make sure the
464 * data is re-evaluated when the user changes something.
465 *
466 * @return string
467 * @api
468 */
469 protected function errorAction() {
470 $this->clearCacheOnError();
471 $this->addErrorFlashMessage();
472 $this->forwardToReferringRequest();
473
474 return $this->getFlattenedValidationErrorMessage();
475 }
476
477 /**
478 * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
479 * Better would be just do delete the cache for the error action, but that is not possible right now.
480 *
481 * @return void
482 */
483 protected function clearCacheOnError() {
484 $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
485 if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
486 if (isset($GLOBALS['TSFE'])) {
487 $pageUid = $GLOBALS['TSFE']->id;
488 $this->cacheService->clearPageCache(array($pageUid));
489 }
490 }
491 }
492
493 /**
494 * If an error occurred during this request, this adds a flash message describing the error to the flash
495 * message container.
496 *
497 * @return void
498 */
499 protected function addErrorFlashMessage() {
500 $errorFlashMessage = $this->getErrorFlashMessage();
501 if ($errorFlashMessage !== FALSE) {
502 $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
503 }
504 }
505
506 /**
507 * A template method for displaying custom error flash messages, or to
508 * display no flash message at all on errors. Override this to customize
509 * the flash message in your action controller.
510 *
511 * @return string The flash message or FALSE if no flash message should be set
512 * @api
513 */
514 protected function getErrorFlashMessage() {
515 return 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '()';
516 }
517
518 /**
519 * If information on the request before the current request was sent, this method forwards back
520 * to the originating request. This effectively ends processing of the current request, so do not
521 * call this method before you have finished the necessary business logic!
522 *
523 * @return void
524 * @throws StopActionException
525 */
526 protected function forwardToReferringRequest() {
527 $referringRequest = $this->request->getReferringRequest();
528 if ($referringRequest !== NULL) {
529 $originalRequest = clone $this->request;
530 $this->request->setOriginalRequest($originalRequest);
531 $this->request->setOriginalRequestMappingResults($this->arguments->getValidationResults());
532 $this->forward(
533 $referringRequest->getControllerActionName(),
534 $referringRequest->getControllerName(),
535 $referringRequest->getControllerExtensionName(),
536 $referringRequest->getArguments()
537 );
538 }
539 }
540
541 /**
542 * Returns a string with a basic error message about validation failure.
543 * We may add all validation error messages to a log file in the future,
544 * but for security reasons (@see #54074) we do not return these here.
545 *
546 * @return string
547 */
548 protected function getFlattenedValidationErrorMessage() {
549 $outputMessage = 'Validation failed while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '().' . PHP_EOL;
550 return $outputMessage;
551 }
552
553 /**
554 * Returns a map of action method names and their parameters.
555 *
556 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
557 *
558 * @return array Array of method parameters by action name
559 */
560 static public function getActionMethodParameters($objectManager) {
561 $reflectionService = $objectManager->get(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class);
562
563 $result = array();
564
565 $className = get_called_class();
566 $methodNames = get_class_methods($className);
567 foreach ($methodNames as $methodName) {
568 if (strlen($methodName) > 6 && strpos($methodName, 'Action', strlen($methodName) - 6) !== FALSE) {
569 $result[$methodName] = $reflectionService->getMethodParameters($className, $methodName);
570 }
571 }
572
573 return $result;
574 }
575
576 /**
577 * If the action returns a string, it is appended to the content in the
578 * response object. If the action does not return anything and a valid
579 * view exists, the view is rendered automatically.
580 *
581 * @param string|object $actionResult
582 * @return NULL|string
583 */
584 protected function resolveActionResult($actionResult) {
585 if ($actionResult === NULL && $this->view instanceof ViewInterface) {
586 return $this->view->render();
587 } elseif (is_string($actionResult) && $actionResult !== '') {
588 return $actionResult;
589 } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
590 return (string)$actionResult;
591 }
592 return NULL;
593 }
594
595 /**
596 * Appends content to response object.
597 *
598 * @param string $content
599 */
600 protected function appendContent($content) {
601 $this->response->appendContent($content);
602 }
603
604 /**
605 * Calls the specified action method and passes the arguments.
606 *
607 * @return string|object
608 */
609 protected function processActionResult() {
610 $preparedArguments = array();
611 foreach ($this->arguments as $argument) {
612 $preparedArguments[] = $argument->getValue();
613 }
614 $validationResult = $this->arguments->getValidationResults();
615 if (!$validationResult->hasErrors()) {
616 $this->emitBeforeCallActionMethodSignal($preparedArguments);
617 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
618 return $actionResult;
619 } else {
620 $methodTagsValues = $this->reflectionService->getMethodTagsValues(get_class($this), $this->actionMethodName);
621 $ignoreValidationAnnotations = array();
622 if (isset($methodTagsValues['ignorevalidation'])) {
623 $ignoreValidationAnnotations = $methodTagsValues['ignorevalidation'];
624 }
625 // if there exist errors which are not ignored with @ignorevalidation => call error method
626 // else => call action method
627 $shouldCallActionMethod = TRUE;
628 foreach ($validationResult->getSubResults() as $argumentName => $subValidationResult) {
629 if (!$subValidationResult->hasErrors()) {
630 continue;
631 }
632 if (array_search('$' . $argumentName, $ignoreValidationAnnotations) !== FALSE) {
633 continue;
634 }
635 $shouldCallActionMethod = FALSE;
636 break;
637 }
638 if ($shouldCallActionMethod) {
639 $this->emitBeforeCallActionMethodSignal($preparedArguments);
640 $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
641 return $actionResult;
642 } else {
643 $actionResult = call_user_func(array($this, $this->errorMethodName));
644 return $actionResult;
645 }
646 }
647 }
648 }