9a597cefd05a9550407326cf75ab27e721653fcb
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Mvc / Controller / ActionController.php
1 <?php
2
3 /*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16 namespace TYPO3\CMS\Extbase\Mvc\Controller;
17
18 use Psr\EventDispatcher\EventDispatcherInterface;
19 use Psr\Http\Message\ResponseFactoryInterface;
20 use Psr\Http\Message\ResponseInterface;
21 use TYPO3\CMS\Core\Http\HtmlResponse;
22 use TYPO3\CMS\Core\Http\PropagateResponseException;
23 use TYPO3\CMS\Core\Http\Response;
24 use TYPO3\CMS\Core\Http\Stream;
25 use TYPO3\CMS\Core\Messaging\AbstractMessage;
26 use TYPO3\CMS\Core\Messaging\FlashMessage;
27 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
28 use TYPO3\CMS\Core\Messaging\FlashMessageService;
29 use TYPO3\CMS\Core\Page\PageRenderer;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
33 use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
34 use TYPO3\CMS\Extbase\Http\ForwardResponse;
35 use TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException;
36 use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
37 use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
38 use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
39 use TYPO3\CMS\Extbase\Mvc\RequestInterface;
40 use TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver;
41 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
42 use TYPO3\CMS\Extbase\Mvc\View\NotFoundView;
43 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
44 use TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface;
45 use TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest;
46 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
47 use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
48 use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
49 use TYPO3\CMS\Extbase\Property\PropertyMapper;
50 use TYPO3\CMS\Extbase\Reflection\ReflectionService;
51 use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
52 use TYPO3\CMS\Extbase\Service\ExtensionService;
53 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
54 use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
55 use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
56 use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
57 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
58 use TYPO3Fluid\Fluid\View\TemplateView;
59
60 /**
61 * A multi action controller. This is by far the most common base class for Controllers.
62 */
63 abstract class ActionController implements ControllerInterface
64 {
65 /**
66 * @var ResponseFactoryInterface
67 */
68 protected $responseFactory;
69
70 /**
71 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
72 * @internal only to be used within Extbase, not part of TYPO3 Core API.
73 */
74 protected $reflectionService;
75
76 /**
77 * @var HashService
78 * @internal only to be used within Extbase, not part of TYPO3 Core API.
79 */
80 protected $hashService;
81
82 /**
83 * @var ViewResolverInterface
84 * @internal only to be used within Extbase, not part of TYPO3 Core API.
85 */
86 private $viewResolver;
87
88 /**
89 * The current view, as resolved by resolveView()
90 *
91 * @var ViewInterface
92 */
93 protected $view;
94
95 /**
96 * The default view object to use if none of the resolved views can render
97 * a response for the current request.
98 *
99 * @var string
100 */
101 protected $defaultViewObjectName = \TYPO3\CMS\Fluid\View\TemplateView::class;
102
103 /**
104 * Name of the action method
105 *
106 * @var string
107 * @internal only to be used within Extbase, not part of TYPO3 Core API.
108 */
109 protected $actionMethodName = 'indexAction';
110
111 /**
112 * Name of the special error action method which is called in case of errors
113 *
114 * @var string
115 */
116 protected $errorMethodName = 'errorAction';
117
118 /**
119 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
120 */
121 protected $mvcPropertyMappingConfigurationService;
122
123 /**
124 * @var EventDispatcherInterface
125 */
126 protected $eventDispatcher;
127
128 /**
129 * The current request.
130 *
131 * @var RequestInterface
132 */
133 protected $request;
134
135 /**
136 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
137 * @internal only to be used within Extbase, not part of TYPO3 Core API.
138 */
139 protected $signalSlotDispatcher;
140
141 /**
142 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
143 * @internal only to be used within Extbase, not part of TYPO3 Core API.
144 */
145 protected $objectManager;
146
147 /**
148 * @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
149 */
150 protected $uriBuilder;
151
152 /**
153 * Contains the settings of the current extension
154 *
155 * @var array
156 */
157 protected $settings;
158
159 /**
160 * @var \TYPO3\CMS\Extbase\Validation\ValidatorResolver
161 * @internal only to be used within Extbase, not part of TYPO3 Core API.
162 */
163 protected $validatorResolver;
164
165 /**
166 * @var \TYPO3\CMS\Extbase\Mvc\Controller\Arguments Arguments passed to the controller
167 */
168 protected $arguments;
169
170 /**
171 * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
172 * @internal only to be used within Extbase, not part of TYPO3 Core API.
173 */
174 protected $controllerContext;
175
176 /**
177 * @var ConfigurationManagerInterface
178 * @internal only to be used within Extbase, not part of TYPO3 Core API.
179 */
180 protected $configurationManager;
181
182 /**
183 * @var PropertyMapper
184 * @internal only to be used within Extbase, not part of TYPO3 Core API.
185 */
186 private $propertyMapper;
187
188 /**
189 * @internal only to be used within Extbase, not part of TYPO3 Core API.
190 */
191 private FlashMessageService $internalFlashMessageService;
192
193 /**
194 * @internal only to be used within Extbase, not part of TYPO3 Core API.
195 */
196 private ExtensionService $internalExtensionService;
197
198 final public function injectResponseFactory(ResponseFactoryInterface $responseFactory)
199 {
200 $this->responseFactory = $responseFactory;
201 }
202
203 /**
204 * @param ConfigurationManagerInterface $configurationManager
205 * @internal only to be used within Extbase, not part of TYPO3 Core API.
206 */
207 public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
208 {
209 $this->configurationManager = $configurationManager;
210 $this->settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS) + ['offlineMode' => false];
211 }
212
213 /**
214 * Injects the object manager
215 *
216 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
217 * @internal only to be used within Extbase, not part of TYPO3 Core API.
218 */
219 public function injectObjectManager(ObjectManagerInterface $objectManager)
220 {
221 $this->objectManager = $objectManager;
222 $this->arguments = GeneralUtility::makeInstance(Arguments::class);
223 }
224
225 /**
226 * @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
227 * @internal only to be used within Extbase, not part of TYPO3 Core API.
228 */
229 public function injectSignalSlotDispatcher(Dispatcher $signalSlotDispatcher)
230 {
231 $this->signalSlotDispatcher = $signalSlotDispatcher;
232 }
233
234 /**
235 * @param \TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver
236 * @internal only to be used within Extbase, not part of TYPO3 Core API.
237 */
238 public function injectValidatorResolver(ValidatorResolver $validatorResolver)
239 {
240 $this->validatorResolver = $validatorResolver;
241 }
242
243 /**
244 * @param ViewResolverInterface $viewResolver
245 * @internal only to be used within Extbase, not part of TYPO3 Core API.
246 */
247 public function injectViewResolver(ViewResolverInterface $viewResolver)
248 {
249 $this->viewResolver = $viewResolver;
250 }
251
252 /**
253 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
254 * @internal only to be used within Extbase, not part of TYPO3 Core API.
255 */
256 public function injectReflectionService(ReflectionService $reflectionService)
257 {
258 $this->reflectionService = $reflectionService;
259 }
260
261 /**
262 * @param HashService $hashService
263 * @internal only to be used within Extbase, not part of TYPO3 Core API.
264 */
265 public function injectHashService(HashService $hashService)
266 {
267 $this->hashService = $hashService;
268 }
269
270 /**
271 * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
272 */
273 public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
274 {
275 $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
276 }
277
278 public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
279 {
280 $this->eventDispatcher = $eventDispatcher;
281 }
282
283 /**
284 * @param PropertyMapper $propertyMapper
285 * @internal only to be used within Extbase, not part of TYPO3 Core API.
286 */
287 public function injectPropertyMapper(PropertyMapper $propertyMapper): void
288 {
289 $this->propertyMapper = $propertyMapper;
290 }
291
292 /**
293 * @internal only to be used within Extbase, not part of TYPO3 Core API.
294 */
295 final public function injectInternalFlashMessageService(FlashMessageService $flashMessageService): void
296 {
297 $this->internalFlashMessageService = $flashMessageService;
298 }
299
300 /**
301 * @internal only to be used within Extbase, not part of TYPO3 Core API.
302 */
303 final public function injectInternalExtensionService(ExtensionService $extensionService): void
304 {
305 $this->internalExtensionService = $extensionService;
306 }
307
308 /**
309 * Initializes the view before invoking an action method.
310 *
311 * Override this method to solve assign variables common for all actions
312 * or prepare the view in another way before the action is called.
313 *
314 * @param ViewInterface $view The view to be initialized
315 */
316 protected function initializeView(ViewInterface $view)
317 {
318 }
319
320 /**
321 * Initializes the controller before invoking an action method.
322 *
323 * Override this method to solve tasks which all actions have in
324 * common.
325 */
326 protected function initializeAction()
327 {
328 }
329
330 /**
331 * Implementation of the arguments initialization in the action controller:
332 * Automatically registers arguments of the current action
333 *
334 * Don't override this method - use initializeAction() instead.
335 *
336 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException
337 * @see initializeArguments()
338 *
339 * @internal only to be used within Extbase, not part of TYPO3 Core API.
340 */
341 protected function initializeActionMethodArguments()
342 {
343 $methodParameters = $this->reflectionService
344 ->getClassSchema(static::class)
345 ->getMethod($this->actionMethodName)->getParameters();
346
347 foreach ($methodParameters as $parameterName => $parameter) {
348 $dataType = null;
349 if ($parameter->getType() !== null) {
350 $dataType = $parameter->getType();
351 } elseif ($parameter->isArray()) {
352 $dataType = 'array';
353 }
354 if ($dataType === null) {
355 throw new InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
356 }
357 $defaultValue = $parameter->hasDefaultValue() ? $parameter->getDefaultValue() : null;
358 $this->arguments->addNewArgument($parameterName, $dataType, !$parameter->isOptional(), $defaultValue);
359 }
360 }
361
362 /**
363 * Adds the needed validators to the Arguments:
364 *
365 * - Validators checking the data type from the @param annotation
366 * - Custom validators specified with validate annotations.
367 * - Model-based validators (validate annotations in the model)
368 * - Custom model validator classes
369 *
370 * @internal only to be used within Extbase, not part of TYPO3 Core API.
371 */
372 protected function initializeActionMethodValidators()
373 {
374 if ($this->arguments->count() === 0) {
375 return;
376 }
377
378 $classSchemaMethod = $this->reflectionService->getClassSchema(static::class)
379 ->getMethod($this->actionMethodName);
380
381 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
382 foreach ($this->arguments as $argument) {
383 $classSchemaMethodParameter = $classSchemaMethod->getParameter($argument->getName());
384 /*
385 * At this point validation is skipped if there is an IgnoreValidation annotation.
386 *
387 * todo: IgnoreValidation annotations could be evaluated in the ClassSchema and result in
388 * todo: no validators being applied to the method parameter.
389 */
390 if ($classSchemaMethodParameter->ignoreValidation()) {
391 continue;
392 }
393
394 // todo: It's quite odd that an instance of ConjunctionValidator is created directly here.
395 // todo: \TYPO3\CMS\Extbase\Validation\ValidatorResolver::getBaseValidatorConjunction could/should be used
396 // todo: here, to benefit of the built in 1st level cache of the ValidatorResolver.
397 $validator = $this->objectManager->get(ConjunctionValidator::class);
398
399 foreach ($classSchemaMethodParameter->getValidators() as $validatorDefinition) {
400 /** @var ValidatorInterface $validatorInstance */
401 $validatorInstance = $this->objectManager->get(
402 $validatorDefinition['className'],
403 $validatorDefinition['options']
404 );
405
406 $validator->addValidator(
407 $validatorInstance
408 );
409 }
410
411 $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
412 if ($baseValidatorConjunction->count() > 0) {
413 $validator->addValidator($baseValidatorConjunction);
414 }
415 $argument->setValidator($validator);
416 }
417 }
418
419 /**
420 * Collects the base validators which were defined for the data type of each
421 * controller argument and adds them to the argument's validator chain.
422 *
423 * @internal only to be used within Extbase, not part of TYPO3 Core API.
424 */
425 public function initializeControllerArgumentsBaseValidators()
426 {
427 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
428 foreach ($this->arguments as $argument) {
429 $validator = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
430 if ($validator !== null) {
431 $argument->setValidator($validator);
432 }
433 }
434 }
435
436 /**
437 * Handles an incoming request and returns a response object
438 *
439 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
440 * @return ResponseInterface
441 *
442 * @internal only to be used within Extbase, not part of TYPO3 Core API.
443 */
444 public function processRequest(RequestInterface $request): ResponseInterface
445 {
446 $this->request = $request;
447 $this->request->setDispatched(true);
448 $this->uriBuilder = $this->objectManager->get(UriBuilder::class);
449 $this->uriBuilder->setRequest($request);
450 $this->actionMethodName = $this->resolveActionMethodName();
451 $this->initializeActionMethodArguments();
452 $this->initializeActionMethodValidators();
453 $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
454 $this->initializeAction();
455 $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
456 /** @var callable $callable */
457 $callable = [$this, $actionInitializationMethodName];
458 if (is_callable($callable)) {
459 $callable();
460 }
461 $this->mapRequestArgumentsToControllerArguments();
462 $this->controllerContext = $this->buildControllerContext();
463 $this->view = $this->resolveView();
464 if ($this->view !== null) {
465 $this->initializeView($this->view);
466 }
467 $response = $this->callActionMethod($request);
468 $this->renderAssetsForRequest($request);
469
470 return $response;
471 }
472
473 /**
474 * Method which initializes assets that should be attached to the response
475 * for the given $request, which contains parameters that an override can
476 * use to determine which assets to add via PageRenderer.
477 *
478 * This default implementation will attempt to render the sections "HeaderAssets"
479 * and "FooterAssets" from the template that is being rendered, inserting the
480 * rendered content into either page header or footer, as appropriate. Both
481 * sections are optional and can be used one or both in combination.
482 *
483 * You can add assets with this method without worrying about duplicates, if
484 * for example you do this in a plugin that gets used multiple time on a page.
485 *
486 * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
487 *
488 * @internal only to be used within Extbase, not part of TYPO3 Core API.
489 */
490 protected function renderAssetsForRequest($request)
491 {
492 if (!$this->view instanceof TemplateView) {
493 // Only TemplateView (from Fluid engine, so this includes all TYPO3 Views based
494 // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
495 // declared on ViewInterface - so we must assert a specific class. We silently skip
496 // asset processing if the View doesn't match, so we don't risk breaking custom Views.
497 return;
498 }
499 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
500 $variables = ['request' => $request, 'arguments' => $this->arguments];
501 $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
502 $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
503 if (!empty(trim($headerAssets))) {
504 $pageRenderer->addHeaderData($headerAssets);
505 }
506 if (!empty(trim($footerAssets))) {
507 $pageRenderer->addFooterData($footerAssets);
508 }
509 }
510
511 /**
512 * Resolves and checks the current action method name
513 *
514 * @return string Method name of the current action
515 * @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).
516 *
517 * @internal only to be used within Extbase, not part of TYPO3 Core API.
518 */
519 protected function resolveActionMethodName()
520 {
521 $actionMethodName = $this->request->getControllerActionName() . 'Action';
522 if (!method_exists($this, $actionMethodName)) {
523 throw new NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
524 }
525 return $actionMethodName;
526 }
527
528 /**
529 * Calls the specified action method and passes the arguments.
530 *
531 * If the action returns a string, it is appended to the content in the
532 * response object. If the action doesn't return anything and a valid
533 * view exists, the view is rendered automatically.
534 *
535 * @internal only to be used within Extbase, not part of TYPO3 Core API.
536 */
537 protected function callActionMethod(RequestInterface $request): ResponseInterface
538 {
539 // incoming request is not needed yet but can be passed into the action in the future like in symfony
540 // todo: support this via method-reflection
541
542 $preparedArguments = [];
543 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
544 foreach ($this->arguments as $argument) {
545 $preparedArguments[] = $argument->getValue();
546 }
547 $validationResult = $this->arguments->validate();
548 if (!$validationResult->hasErrors()) {
549 $this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments));
550 $actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
551 } else {
552 $actionResult = $this->{$this->errorMethodName}();
553 }
554
555 if ($actionResult instanceof ResponseInterface) {
556 return $actionResult;
557 }
558
559 trigger_error(
560 sprintf(
561 'Controller action %s does not return an instance of %s which is deprecated.',
562 static::class . '::' . $this->actionMethodName,
563 ResponseInterface::class
564 ),
565 E_USER_DEPRECATED
566 );
567
568 $response = new Response();
569 $body = new Stream('php://temp', 'rw');
570 if ($actionResult === null && $this->view instanceof ViewInterface) {
571 if ($this->view instanceof JsonView) {
572 // this is just a temporary solution until Extbase uses PSR-7 responses and users are forced to return a
573 // response object in their controller actions.
574
575 if (!empty($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
576 /** @var TypoScriptFrontendController $typoScriptFrontendController */
577 $typoScriptFrontendController = $GLOBALS['TSFE'];
578 if (empty($typoScriptFrontendController->config['config']['disableCharsetHeader'])) {
579 // If the charset header is *not* disabled in configuration,
580 // TypoScriptFrontendController will send the header later with the Content-Type which we set here.
581 $typoScriptFrontendController->setContentType('application/json');
582 } else {
583 // Although the charset header is disabled in configuration, we *must* send a Content-Type header here.
584 // Content-Type headers optionally carry charset information at the same time.
585 // Since we have the information about the charset, there is no reason to not include the charset information although disabled in TypoScript.
586 $response = $response->withHeader('Content-Type', 'application/json; charset=' . trim($typoScriptFrontendController->metaCharset));
587 }
588 } else {
589 $response = $response->withHeader('Content-Type', 'application/json');
590 }
591 }
592
593 $body->write($this->view->render());
594 } elseif (is_string($actionResult) && $actionResult !== '') {
595 $body->write($actionResult);
596 } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
597 $body->write((string)$actionResult);
598 }
599
600 $body->rewind();
601 return $response->withBody($body);
602 }
603
604 /**
605 * Prepares a view for the current action.
606 * By default, this method tries to locate a view with a name matching the current action.
607 *
608 * @return ViewInterface
609 *
610 * @internal only to be used within Extbase, not part of TYPO3 Core API.
611 */
612 protected function resolveView()
613 {
614 if ($this->viewResolver instanceof GenericViewResolver) {
615 /*
616 * This setter is not part of the ViewResolverInterface as it's only necessary to set
617 * the default view class from this point when using the generic view resolver which
618 * must respect the possibly overridden property defaultViewObjectName.
619 */
620 $this->viewResolver->setDefaultViewClass($this->defaultViewObjectName);
621 }
622
623 $view = $this->viewResolver->resolve(
624 $this->request->getControllerObjectName(),
625 $this->request->getControllerActionName(),
626 $this->request->getFormat()
627 );
628
629 if ($view instanceof ViewInterface) {
630 $this->setViewConfiguration($view);
631 if ($view->canRender($this->controllerContext) === false) {
632 $view = null;
633 }
634 }
635 if (!isset($view)) {
636 $view = $this->objectManager->get(NotFoundView::class);
637 $view->assign('errorMessage', 'No template was found. View could not be resolved for action "'
638 . $this->request->getControllerActionName() . '" in class "' . $this->request->getControllerObjectName() . '"');
639 }
640 $view->setControllerContext($this->controllerContext);
641 if (method_exists($view, 'injectSettings')) {
642 $view->injectSettings($this->settings);
643 }
644 $view->initializeView();
645 // In TYPO3.Flow, solved through Object Lifecycle methods, we need to call it explicitly
646 $view->assign('settings', $this->settings);
647 // same with settings injection.
648 return $view;
649 }
650
651 /**
652 * @param ViewInterface $view
653 *
654 * @internal only to be used within Extbase, not part of TYPO3 Core API.
655 */
656 protected function setViewConfiguration(ViewInterface $view)
657 {
658 // Template Path Override
659 $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
660 ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
661 );
662
663 // set TemplateRootPaths
664 $viewFunctionName = 'setTemplateRootPaths';
665 if (method_exists($view, $viewFunctionName)) {
666 $setting = 'templateRootPaths';
667 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
668 // no need to bother if there is nothing to set
669 if ($parameter) {
670 $view->$viewFunctionName($parameter);
671 }
672 }
673
674 // set LayoutRootPaths
675 $viewFunctionName = 'setLayoutRootPaths';
676 if (method_exists($view, $viewFunctionName)) {
677 $setting = 'layoutRootPaths';
678 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
679 // no need to bother if there is nothing to set
680 if ($parameter) {
681 $view->$viewFunctionName($parameter);
682 }
683 }
684
685 // set PartialRootPaths
686 $viewFunctionName = 'setPartialRootPaths';
687 if (method_exists($view, $viewFunctionName)) {
688 $setting = 'partialRootPaths';
689 $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
690 // no need to bother if there is nothing to set
691 if ($parameter) {
692 $view->$viewFunctionName($parameter);
693 }
694 }
695 }
696
697 /**
698 * Handles the path resolving for *rootPath(s)
699 *
700 * numerical arrays get ordered by key ascending
701 *
702 * @param array $extbaseFrameworkConfiguration
703 * @param string $setting parameter name from TypoScript
704 *
705 * @return array
706 *
707 * @internal only to be used within Extbase, not part of TYPO3 Core API.
708 */
709 protected function getViewProperty($extbaseFrameworkConfiguration, $setting)
710 {
711 $values = [];
712 if (
713 !empty($extbaseFrameworkConfiguration['view'][$setting])
714 && is_array($extbaseFrameworkConfiguration['view'][$setting])
715 ) {
716 $values = $extbaseFrameworkConfiguration['view'][$setting];
717 }
718
719 return $values;
720 }
721
722 /**
723 * A special action which is called if the originally intended action could
724 * not be called, for example if the arguments were not valid.
725 *
726 * The default implementation sets a flash message, request errors and forwards back
727 * to the originating action. This is suitable for most actions dealing with form input.
728 *
729 * We clear the page cache by default on an error as well, as we need to make sure the
730 * data is re-evaluated when the user changes something.
731 *
732 * @return ResponseInterface
733 */
734 protected function errorAction()
735 {
736 $this->addErrorFlashMessage();
737 if (($response = $this->forwardToReferringRequest()) !== null) {
738 return $response->withStatus(400);
739 }
740
741 $response = $this->htmlResponse($this->getFlattenedValidationErrorMessage());
742 return $response->withStatus(400);
743 }
744
745 /**
746 * If an error occurred during this request, this adds a flash message describing the error to the flash
747 * message container.
748 *
749 * @internal only to be used within Extbase, not part of TYPO3 Core API.
750 */
751 protected function addErrorFlashMessage()
752 {
753 $errorFlashMessage = $this->getErrorFlashMessage();
754 if ($errorFlashMessage !== false) {
755 $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
756 }
757 }
758
759 /**
760 * A template method for displaying custom error flash messages, or to
761 * display no flash message at all on errors. Override this to customize
762 * the flash message in your action controller.
763 *
764 * @return string The flash message or FALSE if no flash message should be set
765 *
766 * @internal only to be used within Extbase, not part of TYPO3 Core API.
767 */
768 protected function getErrorFlashMessage()
769 {
770 return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
771 }
772
773 /**
774 * If information on the request before the current request was sent, this method forwards back
775 * to the originating request. This effectively ends processing of the current request, so do not
776 * call this method before you have finished the necessary business logic!
777 *
778 * @return ResponseInterface|null
779 *
780 * @internal only to be used within Extbase, not part of TYPO3 Core API.
781 */
782 protected function forwardToReferringRequest(): ?ResponseInterface
783 {
784 $referringRequest = null;
785 $referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null;
786 if (is_string($referringRequestArguments['@request'] ?? null)) {
787 $referrerArray = json_decode(
788 $this->hashService->validateAndStripHmac($referringRequestArguments['@request']),
789 true
790 );
791 $arguments = [];
792 if (is_string($referringRequestArguments['arguments'] ?? null)) {
793 $arguments = unserialize(
794 base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments']))
795 );
796 }
797 // todo: Remove ReferringRequest. It's only used here in this context to trigger the logic of
798 // \TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest::setArgument() and its parent method which should then
799 // be extracted from the request class.
800 $referringRequest = new ReferringRequest();
801 $referringRequest->setArguments(array_replace_recursive($arguments, $referrerArray));
802 }
803
804 if ($referringRequest !== null) {
805 return (new ForwardResponse((string)$referringRequest->getControllerActionName()))
806 ->withControllerName((string)$referringRequest->getControllerName())
807 ->withExtensionName((string)$referringRequest->getControllerExtensionName())
808 ->withArguments($referringRequest->getArguments())
809 ->withArgumentsValidationResult($this->arguments->validate())
810 ;
811 }
812
813 return null;
814 }
815
816 /**
817 * Returns a string with a basic error message about validation failure.
818 * We may add all validation error messages to a log file in the future,
819 * but for security reasons (@see #54074) we do not return these here.
820 *
821 * @return string
822 *
823 * @internal only to be used within Extbase, not part of TYPO3 Core API.
824 */
825 protected function getFlattenedValidationErrorMessage()
826 {
827 $outputMessage = 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
828 return $outputMessage;
829 }
830
831 /**
832 * @return ControllerContext
833 */
834 public function getControllerContext()
835 {
836 return $this->controllerContext;
837 }
838
839 /**
840 * Creates a Message object and adds it to the FlashMessageQueue.
841 *
842 * @param string $messageBody The message
843 * @param string $messageTitle Optional message title
844 * @param int $severity Optional severity, must be one of \TYPO3\CMS\Core\Messaging\FlashMessage constants
845 * @param bool $storeInSession Optional, defines whether the message should be stored in the session (default) or not
846 * @throws \InvalidArgumentException if the message body is no string
847 * @see \TYPO3\CMS\Core\Messaging\FlashMessage
848 */
849 public function addFlashMessage($messageBody, $messageTitle = '', $severity = AbstractMessage::OK, $storeInSession = true)
850 {
851 if (!is_string($messageBody)) {
852 throw new \InvalidArgumentException('The message body must be of type string, "' . gettype($messageBody) . '" given.', 1243258395);
853 }
854 /* @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */
855 $flashMessage = GeneralUtility::makeInstance(
856 FlashMessage::class,
857 (string)$messageBody,
858 (string)$messageTitle,
859 $severity,
860 $storeInSession
861 );
862
863 $this->getFlashMessageQueue()->enqueue($flashMessage);
864 }
865
866 /**
867 * todo: As soon as the incoming request contains the compiled plugin namespace, extbase will offer a trait to
868 * create a flash message identifier from the current request. Users then should inject the flash message
869 * service themselves if needed.
870 *
871 * @internal only to be used within Extbase, not part of TYPO3 Core API.
872 */
873 protected function getFlashMessageQueue(string $identifier = null): FlashMessageQueue
874 {
875 if ($identifier === null) {
876 $pluginNamespace = $this->internalExtensionService->getPluginNamespace(
877 $this->request->getControllerExtensionName(),
878 $this->request->getPluginName()
879 );
880 $identifier = 'extbase.flashmessages.' . $pluginNamespace;
881 }
882
883 return $this->internalFlashMessageService->getMessageQueueByIdentifier($identifier);
884 }
885
886 /**
887 * Initialize the controller context
888 *
889 * @return \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext ControllerContext to be passed to the view
890 *
891 * @internal only to be used within Extbase, not part of TYPO3 Core API.
892 */
893 protected function buildControllerContext()
894 {
895 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext */
896 $controllerContext = $this->objectManager->get(ControllerContext::class);
897 $controllerContext->setRequest($this->request);
898 if ($this->arguments !== null) {
899 $controllerContext->setArguments($this->arguments);
900 }
901 $controllerContext->setUriBuilder($this->uriBuilder);
902
903 return $controllerContext;
904 }
905
906 /**
907 * Forwards the request to another action and / or controller.
908 *
909 * Request is directly transferred to the other action / controller
910 * without the need for a new request.
911 *
912 * @param string $actionName Name of the action to forward to
913 * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
914 * @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
915 * @param array|null $arguments Arguments to pass to the target action
916 * @throws StopActionException
917 * @see redirect()
918 * @deprecated since TYPO3 11.0, will be removed in 12.0
919 */
920 public function forward($actionName, $controllerName = null, $extensionName = null, array $arguments = null)
921 {
922 trigger_error(
923 sprintf('Method %s is deprecated. To forward to another action, return a %s instead.', __METHOD__, ForwardResponse::class),
924 E_USER_DEPRECATED
925 );
926
927 $this->request->setDispatched(false);
928 $this->request->setControllerActionName($actionName);
929
930 if ($controllerName !== null) {
931 $this->request->setControllerName($controllerName);
932 }
933
934 if ($extensionName !== null) {
935 $this->request->setControllerExtensionName($extensionName);
936 }
937
938 if ($arguments !== null) {
939 $this->request->setArguments($arguments);
940 }
941 throw new StopActionException('forward', 1476045801);
942 }
943
944 /**
945 * Redirects the request to another action and / or controller.
946 *
947 * Redirect will be sent to the client which then performs another request to the new URI.
948 *
949 * NOTE: This method only supports web requests and will thrown an exception
950 * if used with other request types.
951 *
952 * @param string $actionName Name of the action to forward to
953 * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
954 * @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
955 * @param array|null $arguments Arguments to pass to the target action
956 * @param int|null $pageUid Target page uid. If NULL, the current page uid is used
957 * @param null $_ (optional) Unused
958 * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
959 * @throws StopActionException
960 */
961 protected function redirect($actionName, $controllerName = null, $extensionName = null, array $arguments = null, $pageUid = null, $_ = null, $statusCode = 303)
962 {
963 if ($controllerName === null) {
964 $controllerName = $this->request->getControllerName();
965 }
966 $this->uriBuilder->reset()->setCreateAbsoluteUri(true);
967 if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
968 $this->uriBuilder->setTargetPageUid((int)$pageUid);
969 }
970 if (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
971 $this->uriBuilder->setAbsoluteUriScheme('https');
972 }
973 $uri = $this->uriBuilder->uriFor($actionName, $arguments, $controllerName, $extensionName);
974 $this->redirectToUri($uri, null, $statusCode);
975 }
976
977 /**
978 * Redirects the web request to another uri.
979 *
980 * NOTE: This method only supports web requests and will thrown an exception if used with other request types.
981 *
982 * @param mixed $uri A string representation of a URI
983 * @param null $_ (optional) Unused
984 * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
985 * @throws StopActionException
986 */
987 protected function redirectToUri($uri, $_ = null, $statusCode = 303)
988 {
989 $uri = $this->addBaseUriIfNecessary($uri);
990 $response = new HtmlResponse(
991 '',
992 $statusCode,
993 [
994 'Location' => (string)$uri
995 ]
996 );
997
998 throw new StopActionException('redirectToUri', 1476045828, null, $response);
999 }
1000
1001 /**
1002 * Adds the base uri if not already in place.
1003 *
1004 * @param string $uri The URI
1005 * @return string
1006 *
1007 * @internal only to be used within Extbase, not part of TYPO3 Core API.
1008 */
1009 protected function addBaseUriIfNecessary($uri)
1010 {
1011 return GeneralUtility::locationHeaderUrl((string)$uri);
1012 }
1013
1014 /**
1015 * Sends the specified HTTP status immediately and only stops to run back through the middleware stack.
1016 * Note: If any other plugin or content or hook is used within a frontend request, this is skipped by design.
1017 *
1018 * @param int $statusCode The HTTP status code
1019 * @param string $statusMessage A custom HTTP status message
1020 * @param string $content Body content which further explains the status
1021 * @throws PropagateResponseException
1022 */
1023 public function throwStatus($statusCode, $statusMessage = null, $content = null)
1024 {
1025 if ($content === null) {
1026 $content = $statusCode . ' ' . $statusMessage;
1027 }
1028 $response = $this->responseFactory->createResponse((int)$statusCode, $statusMessage);
1029 $response->getBody()->write($content);
1030 throw new PropagateResponseException($response, 1476045871);
1031 }
1032
1033 /**
1034 * Maps arguments delivered by the request object to the local controller arguments.
1035 *
1036 * @throws Exception\RequiredArgumentMissingException
1037 *
1038 * @internal only to be used within Extbase, not part of TYPO3 Core API.
1039 */
1040 protected function mapRequestArgumentsToControllerArguments()
1041 {
1042 /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
1043 foreach ($this->arguments as $argument) {
1044 $argumentName = $argument->getName();
1045 if ($this->request->hasArgument($argumentName)) {
1046 $this->setArgumentValue($argument, $this->request->getArgument($argumentName));
1047 } elseif ($argument->isRequired()) {
1048 throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
1049 }
1050 }
1051 }
1052
1053 /**
1054 * @param Argument $argument
1055 * @param mixed $rawValue
1056 */
1057 private function setArgumentValue(Argument $argument, $rawValue): void
1058 {
1059 if ($rawValue === null) {
1060 $argument->setValue(null);
1061 return;
1062 }
1063 $dataType = $argument->getDataType();
1064 if (is_object($rawValue) && $rawValue instanceof $dataType) {
1065 $argument->setValue($rawValue);
1066 return;
1067 }
1068 $this->propertyMapper->resetMessages();
1069 try {
1070 $argument->setValue(
1071 $this->propertyMapper->convert(
1072 $rawValue,
1073 $dataType,
1074 $argument->getPropertyMappingConfiguration()
1075 )
1076 );
1077 } catch (TargetNotFoundException $e) {
1078 // for optional arguments no exception is thrown.
1079 if ($argument->isRequired()) {
1080 throw $e;
1081 }
1082 }
1083 $argument->getValidationResults()->merge($this->propertyMapper->getMessages());
1084 }
1085
1086 /**
1087 * Returns a response object with either the given html string or the current rendered view as content.
1088 *
1089 * @param string|null $html
1090 * @return ResponseInterface
1091 */
1092 protected function htmlResponse(string $html = null): ResponseInterface
1093 {
1094 $response = $this->responseFactory->createResponse()
1095 ->withHeader('Content-Type', 'text/html; charset=utf-8');
1096 $response->getBody()->write($html ?? $this->view->render());
1097 return $response;
1098 }
1099 }