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