[!!!][TASK] Remove view related properties from ActionController
[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\Core\Page\PageRenderer;
19 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
20 use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
21 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
22 use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest;
23 use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
24 use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
25 use TYPO3Fluid\Fluid\View\TemplateView;
26
27 /**
28 * A multi action controller. This is by far the most common base class for Controllers.
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 */
47 protected $view;
48
49 /**
50 * The default view object to use if none of the resolved views can render
51 * a response for the current request.
52 *
53 * @var string
54 */
55 protected $defaultViewObjectName = \TYPO3\CMS\Fluid\View\TemplateView::class;
56
57 /**
58 * Name of the action method
59 *
60 * @var string
61 */
62 protected $actionMethodName = 'indexAction';
63
64 /**
65 * Name of the special error action method which is called in case of errors
66 *
67 * @var string
68 */
69 protected $errorMethodName = 'errorAction';
70
71 /**
72 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
73 */
74 protected $mvcPropertyMappingConfigurationService;
75
76 /**
77 * The current request.
78 *
79 * @var \TYPO3\CMS\Extbase\Mvc\Request
80 */
81 protected $request;
82
83 /**
84 * The response which will be returned by this action controller
85 *
86 * @var \TYPO3\CMS\Extbase\Mvc\Response
87 */
88 protected $response;
89
90 /**
91 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
92 */
93 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
94 {
95 $this->reflectionService = $reflectionService;
96 }
97
98 /**
99 * @param \TYPO3\CMS\Extbase\Service\CacheService $cacheService
100 */
101 public function injectCacheService(\TYPO3\CMS\Extbase\Service\CacheService $cacheService)
102 {
103 $this->cacheService = $cacheService;
104 }
105
106 /**
107 * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
108 */
109 public function injectMvcPropertyMappingConfigurationService(\TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
110 {
111 $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
112 }
113
114 /**
115 * Handles a request. The result output is returned by altering the given response.
116 *
117 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
118 * @param \TYPO3\CMS\Extbase\Mvc\ResponseInterface $response The response, modified by this handler
119 *
120 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException
121 */
122 public function processRequest(\TYPO3\CMS\Extbase\Mvc\RequestInterface $request, \TYPO3\CMS\Extbase\Mvc\ResponseInterface $response)
123 {
124 if (!$this->canProcessRequest($request)) {
125 throw new \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException(static::class . ' does not support requests of type "' . get_class($request) . '". Supported types are: ' . implode(' ', $this->supportedRequestTypes), 1187701131);
126 }
127
128 if ($response instanceof \TYPO3\CMS\Extbase\Mvc\Web\Response && $request instanceof WebRequest) {
129 $response->setRequest($request);
130 }
131 $this->request = $request;
132 $this->request->setDispatched(true);
133 $this->response = $response;
134 $this->uriBuilder = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
135 $this->uriBuilder->setRequest($request);
136 $this->actionMethodName = $this->resolveActionMethodName();
137 $this->initializeActionMethodArguments();
138 $this->initializeActionMethodValidators();
139 $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
140 $this->initializeAction();
141 $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
142 if (method_exists($this, $actionInitializationMethodName)) {
143 call_user_func([$this, $actionInitializationMethodName]);
144 }
145 $this->mapRequestArgumentsToControllerArguments();
146 $this->controllerContext = $this->buildControllerContext();
147 $this->view = $this->resolveView();
148 if ($this->view !== null) {
149 $this->initializeView($this->view);
150 }
151 $this->callActionMethod();
152 $this->renderAssetsForRequest($request);
153 }
154
155 /**
156 * Method which initializes assets that should be attached to the response
157 * for the given $request, which contains parameters that an override can
158 * use to determine which assets to add via PageRenderer.
159 *
160 * This default implementation will attempt to render the sections "HeaderAssets"
161 * and "FooterAssets" from the template that is being rendered, inserting the
162 * rendered content into either page header or footer, as appropriate. Both
163 * sections are optional and can be used one or both in combination.
164 *
165 * You can add assets with this method without worrying about duplicates, if
166 * for example you do this in a plugin that gets used multiple time on a page.
167 *
168 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
169 */
170 protected function renderAssetsForRequest($request)
171 {
172 if (!$this->view instanceof TemplateView) {
173 // Only TemplateView (from Fluid engine, so this includes all TYPO3 Views based
174 // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
175 // declared on ViewInterface - so we must assert a specific class. We silently skip
176 // asset processing if the View doesn't match, so we don't risk breaking custom Views.
177 return;
178 }
179 $pageRenderer = $this->objectManager->get(PageRenderer::class);
180 $variables = ['request' => $request, 'arguments' => $this->arguments];
181 $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
182 $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
183 if (!empty(trim($headerAssets))) {
184 $pageRenderer->addHeaderData($headerAssets);
185 }
186 if (!empty(trim($footerAssets))) {
187 $pageRenderer->addFooterData($footerAssets);
188 }
189 }
190
191 /**
192 * Implementation of the arguments initilization in the action controller:
193 * Automatically registers arguments of the current action
194 *
195 * Don't override this method - use initializeAction() instead.
196 *
197 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException
198 * @see initializeArguments()
199 */
200 protected function initializeActionMethodArguments()
201 {
202 $methodParameters = $this->reflectionService
203 ->getClassSchema(static::class)
204 ->getMethod($this->actionMethodName)['params'] ?? [];
205
206 foreach ($methodParameters as $parameterName => $parameterInfo) {
207 $dataType = null;
208 if (isset($parameterInfo['type'])) {
209 $dataType = $parameterInfo['type'];
210 } elseif ($parameterInfo['array']) {
211 $dataType = 'array';
212 }
213 if ($dataType === null) {
214 throw new \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
215 }
216 $defaultValue = $parameterInfo['hasDefaultValue'] === true ? $parameterInfo['defaultValue'] : null;
217 $this->arguments->addNewArgument($parameterName, $dataType, $parameterInfo['optional'] === false, $defaultValue);
218 }
219 }
220
221 /**
222 * Adds the needed validators to the Arguments:
223 *
224 * - Validators checking the data type from the @param annotation
225 * - Custom validators specified with validate annotations.
226 * - Model-based validators (validate annotations in the model)
227 * - Custom model validator classes
228 */
229 protected function initializeActionMethodValidators()
230 {
231 if ($this->arguments->count() === 0) {
232 return;
233 }
234
235 $classSchema = $this->reflectionService->getClassSchema(static::class);
236
237 $ignoreValidationAnnotations = array_unique(array_flip(
238 $classSchema->getMethod($this->actionMethodName)['tags']['ignorevalidation'] ?? []
239 ));
240
241 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
242 foreach ($this->arguments as $argument) {
243 if (isset($ignoreValidationAnnotations[$argument->getName()])) {
244 continue;
245 }
246
247 $validator = $this->objectManager->get(ConjunctionValidator::class);
248 $validatorDefinitions = $classSchema->getMethod($this->actionMethodName)['params'][$argument->getName()]['validators'] ?? [];
249
250 foreach ($validatorDefinitions as $validatorDefinition) {
251 /** @var ValidatorInterface $validatorInstance */
252 $validatorInstance = $this->objectManager->get(
253 $validatorDefinition['className'],
254 $validatorDefinition['options']
255 );
256
257 $validator->addValidator(
258 $validatorInstance
259 );
260 }
261
262 $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
263 if ($baseValidatorConjunction->count() > 0) {
264 $validator->addValidator($baseValidatorConjunction);
265 }
266 $argument->setValidator($validator);
267 }
268 }
269
270 /**
271 * Resolves and checks the current action method name
272 *
273 * @return string Method name of the current action
274 * @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).
275 */
276 protected function resolveActionMethodName()
277 {
278 $actionMethodName = $this->request->getControllerActionName() . 'Action';
279 if (!method_exists($this, $actionMethodName)) {
280 throw new \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
281 }
282 return $actionMethodName;
283 }
284
285 /**
286 * Calls the specified action method and passes the arguments.
287 *
288 * If the action returns a string, it is appended to the content in the
289 * response object. If the action doesn't return anything and a valid
290 * view exists, the view is rendered automatically.
291 */
292 protected function callActionMethod()
293 {
294 $preparedArguments = [];
295 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
296 foreach ($this->arguments as $argument) {
297 $preparedArguments[] = $argument->getValue();
298 }
299 $validationResult = $this->arguments->validate();
300 if (!$validationResult->hasErrors()) {
301 $this->emitBeforeCallActionMethodSignal($preparedArguments);
302 $actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
303 } else {
304 $actionResult = $this->{$this->errorMethodName}();
305 }
306
307 if ($actionResult === null && $this->view instanceof ViewInterface) {
308 $this->response->appendContent($this->view->render());
309 } elseif (is_string($actionResult) && $actionResult !== '') {
310 $this->response->appendContent($actionResult);
311 } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
312 $this->response->appendContent((string)$actionResult);
313 }
314 }
315
316 /**
317 * Emits a signal before the current action is called
318 *
319 * @param array $preparedArguments
320 */
321 protected function emitBeforeCallActionMethodSignal(array $preparedArguments)
322 {
323 $this->signalSlotDispatcher->dispatch(__CLASS__, 'beforeCallActionMethod', [static::class, $this->actionMethodName, $preparedArguments]);
324 }
325
326 /**
327 * Prepares a view for the current action.
328 * By default, this method tries to locate a view with a name matching the current action.
329 *
330 * @return ViewInterface
331 */
332 protected function resolveView()
333 {
334 if ($this->defaultViewObjectName != '') {
335 /** @var ViewInterface $view */
336 $view = $this->objectManager->get($this->defaultViewObjectName);
337 $this->setViewConfiguration($view);
338 if ($view->canRender($this->controllerContext) === false) {
339 unset($view);
340 }
341 }
342 if (!isset($view)) {
343 $view = $this->objectManager->get(\TYPO3\CMS\Extbase\Mvc\View\NotFoundView::class);
344 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "'
345 . $this->request->getControllerActionName() . '" in class "' . $this->request->getControllerObjectName() . '"');
346 }
347 $view->setControllerContext($this->controllerContext);
348 if (method_exists($view, 'injectSettings')) {
349 $view->injectSettings($this->settings);
350 }
351 $view->initializeView();
352 // In TYPO3.Flow, solved through Object Lifecycle methods, we need to call it explicitly
353 $view->assign('settings', $this->settings);
354 // same with settings injection.
355 return $view;
356 }
357
358 /**
359 * @param ViewInterface $view
360 */
361 protected function setViewConfiguration(ViewInterface $view)
362 {
363 // Template Path Override
364 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
365 ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
366 );
367
368 // set TemplateRootPaths
369 $viewFunctionName = 'setTemplateRootPaths';
370 if (method_exists($view, $viewFunctionName)) {
371 $setting = 'templateRootPaths';
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 LayoutRootPaths
380 $viewFunctionName = 'setLayoutRootPaths';
381 if (method_exists($view, $viewFunctionName)) {
382 $setting = 'layoutRootPaths';
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 // set PartialRootPaths
391 $viewFunctionName = 'setPartialRootPaths';
392 if (method_exists($view, $viewFunctionName)) {
393 $setting = 'partialRootPaths';
394 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
395 // no need to bother if there is nothing to set
396 if ($parameter) {
397 $view->$viewFunctionName($parameter);
398 }
399 }
400 }
401
402 /**
403 * Handles the path resolving for *rootPath(s)
404 *
405 * numerical arrays get ordered by key ascending
406 *
407 * @param array $extbaseFrameworkConfiguration
408 * @param string $setting parameter name from TypoScript
409 *
410 * @return array
411 */
412 protected function getViewProperty($extbaseFrameworkConfiguration, $setting)
413 {
414 $values = [];
415 if (
416 !empty($extbaseFrameworkConfiguration['view'][$setting])
417 && is_array($extbaseFrameworkConfiguration['view'][$setting])
418 ) {
419 $values = $extbaseFrameworkConfiguration['view'][$setting];
420 }
421
422 return $values;
423 }
424
425 /**
426 * Initializes the view before invoking an action method.
427 *
428 * Override this method to solve assign variables common for all actions
429 * or prepare the view in another way before the action is called.
430 *
431 * @param ViewInterface $view The view to be initialized
432 */
433 protected function initializeView(ViewInterface $view)
434 {
435 }
436
437 /**
438 * Initializes the controller before invoking an action method.
439 *
440 * Override this method to solve tasks which all actions have in
441 * common.
442 */
443 protected function initializeAction()
444 {
445 }
446
447 /**
448 * A special action which is called if the originally intended action could
449 * not be called, for example if the arguments were not valid.
450 *
451 * The default implementation sets a flash message, request errors and forwards back
452 * to the originating action. This is suitable for most actions dealing with form input.
453 *
454 * We clear the page cache by default on an error as well, as we need to make sure the
455 * data is re-evaluated when the user changes something.
456 *
457 * @return string
458 */
459 protected function errorAction()
460 {
461 $this->clearCacheOnError();
462 $this->addErrorFlashMessage();
463 $this->forwardToReferringRequest();
464
465 return $this->getFlattenedValidationErrorMessage();
466 }
467
468 /**
469 * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
470 * Better would be just do delete the cache for the error action, but that is not possible right now.
471 */
472 protected function clearCacheOnError()
473 {
474 $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
475 if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
476 if (isset($GLOBALS['TSFE'])) {
477 $pageUid = $GLOBALS['TSFE']->id;
478 $this->cacheService->clearPageCache([$pageUid]);
479 }
480 }
481 }
482
483 /**
484 * If an error occurred during this request, this adds a flash message describing the error to the flash
485 * message container.
486 */
487 protected function addErrorFlashMessage()
488 {
489 $errorFlashMessage = $this->getErrorFlashMessage();
490 if ($errorFlashMessage !== false) {
491 $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
492 }
493 }
494
495 /**
496 * A template method for displaying custom error flash messages, or to
497 * display no flash message at all on errors. Override this to customize
498 * the flash message in your action controller.
499 *
500 * @return string The flash message or FALSE if no flash message should be set
501 */
502 protected function getErrorFlashMessage()
503 {
504 return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
505 }
506
507 /**
508 * If information on the request before the current request was sent, this method forwards back
509 * to the originating request. This effectively ends processing of the current request, so do not
510 * call this method before you have finished the necessary business logic!
511 *
512 * @throws StopActionException
513 */
514 protected function forwardToReferringRequest()
515 {
516 $referringRequest = $this->request->getReferringRequest();
517 if ($referringRequest !== null) {
518 $originalRequest = clone $this->request;
519 $this->request->setOriginalRequest($originalRequest);
520 $this->request->setOriginalRequestMappingResults($this->arguments->validate());
521 $this->forward(
522 $referringRequest->getControllerActionName(),
523 $referringRequest->getControllerName(),
524 $referringRequest->getControllerExtensionName(),
525 $referringRequest->getArguments()
526 );
527 }
528 }
529
530 /**
531 * Returns a string with a basic error message about validation failure.
532 * We may add all validation error messages to a log file in the future,
533 * but for security reasons (@see #54074) we do not return these here.
534 *
535 * @return string
536 */
537 protected function getFlattenedValidationErrorMessage()
538 {
539 $outputMessage = 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
540 return $outputMessage;
541 }
542 }