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