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