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