ActionController.php 36.2 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
15

16
17
namespace TYPO3\CMS\Extbase\Mvc\Controller;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Http\Message\ResponseFactoryInterface;
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\StreamFactoryInterface;
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\Messaging\AbstractMessage;
26
use TYPO3\CMS\Core\Messaging\FlashMessage;
27
28
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
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\InvalidArgumentNameException;
37
38
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
39
use TYPO3\CMS\Extbase\Mvc\Request;
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\ViewResolverInterface;
44
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
45
46
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
use TYPO3\CMS\Extbase\Property\PropertyMapper;
47
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
48
use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
49
use TYPO3\CMS\Extbase\Service\ExtensionService;
50
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
51
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
52
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
53
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext;
54
55
use TYPO3\CMS\Fluid\View\TemplateView;
use TYPO3Fluid\Fluid\View\AbstractTemplateView;
56
use TYPO3Fluid\Fluid\View\ViewInterface;
57

58
/**
Sebastian Kurfürst's avatar
Sebastian Kurfürst committed
59
 * A multi action controller. This is by far the most common base class for Controllers.
60
 */
61
abstract class ActionController implements ControllerInterface
62
{
63
64
65
66
67
    /**
     * @var ResponseFactoryInterface
     */
    protected $responseFactory;

68
69
70
71
72
    /**
     * @var StreamFactoryInterface
     */
    protected $streamFactory;

73
74
    /**
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
75
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
76
77
78
     */
    protected $reflectionService;

79
    /**
80
     * @var HashService
81
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
82
83
84
     */
    protected $hashService;

85
86
    /**
     * @var ViewResolverInterface
87
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
88
89
90
     */
    private $viewResolver;

91
92
93
94
95
    /**
     * The current view, as resolved by resolveView()
     *
     * @var ViewInterface
     */
96
    protected $view;
97
98
99
100
101
102
103

    /**
     * The default view object to use if none of the resolved views can render
     * a response for the current request.
     *
     * @var string
     */
104
    protected $defaultViewObjectName = TemplateView::class;
105
106
107
108
109

    /**
     * Name of the action method
     *
     * @var string
110
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
     */
    protected $actionMethodName = 'indexAction';

    /**
     * Name of the special error action method which is called in case of errors
     *
     * @var string
     */
    protected $errorMethodName = 'errorAction';

    /**
     * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
     */
    protected $mvcPropertyMappingConfigurationService;

126
127
128
129
130
    /**
     * @var EventDispatcherInterface
     */
    protected $eventDispatcher;

131
132
133
    /**
     * The current request.
     *
134
135
     * @var Request
     * @todo v12: Change @var to RequestInterface, when RequestInterface extends ServerRequestInterface
136
137
138
     */
    protected $request;

139
140
141
142
143
144
145
146
147
148
149
150
151
152
    /**
     * @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
     */
    protected $uriBuilder;

    /**
     * Contains the settings of the current extension
     *
     * @var array
     */
    protected $settings;

    /**
     * @var \TYPO3\CMS\Extbase\Validation\ValidatorResolver
153
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
154
155
156
157
158
159
160
161
162
163
     */
    protected $validatorResolver;

    /**
     * @var \TYPO3\CMS\Extbase\Mvc\Controller\Arguments Arguments passed to the controller
     */
    protected $arguments;

    /**
     * @var ConfigurationManagerInterface
164
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
165
166
167
     */
    protected $configurationManager;

168
169
    /**
     * @var PropertyMapper
170
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
171
172
173
     */
    private $propertyMapper;

174
175
176
177
178
179
180
181
182
183
    /**
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
    private FlashMessageService $internalFlashMessageService;

    /**
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
    private ExtensionService $internalExtensionService;

184
185
186
187
188
    final public function injectResponseFactory(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

189
190
191
192
193
    final public function injectStreamFactory(StreamFactoryInterface $streamFactory)
    {
        $this->streamFactory = $streamFactory;
    }

194
195
    /**
     * @param ConfigurationManagerInterface $configurationManager
196
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
197
198
199
200
     */
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
    {
        $this->configurationManager = $configurationManager;
201
        $this->settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
202
        $this->arguments = GeneralUtility::makeInstance(Arguments::class);
203
204
205
206
    }

    /**
     * @param \TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver
207
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
208
209
210
211
212
213
     */
    public function injectValidatorResolver(ValidatorResolver $validatorResolver)
    {
        $this->validatorResolver = $validatorResolver;
    }

214
215
    /**
     * @param ViewResolverInterface $viewResolver
216
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
217
218
219
220
221
222
     */
    public function injectViewResolver(ViewResolverInterface $viewResolver)
    {
        $this->viewResolver = $viewResolver;
    }

223
224
    /**
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
225
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
226
     */
227
    public function injectReflectionService(ReflectionService $reflectionService)
228
229
230
231
    {
        $this->reflectionService = $reflectionService;
    }

232
233
    /**
     * @param HashService $hashService
234
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
235
236
237
238
239
240
     */
    public function injectHashService(HashService $hashService)
    {
        $this->hashService = $hashService;
    }

241
242
243
    /**
     * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
     */
244
    public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
245
246
247
248
    {
        $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
    }

249
250
251
252
253
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->eventDispatcher = $eventDispatcher;
    }

254
255
256
257
    /**
     * @param PropertyMapper $propertyMapper
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
258
259
260
261
262
    public function injectPropertyMapper(PropertyMapper $propertyMapper): void
    {
        $this->propertyMapper = $propertyMapper;
    }

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
    /**
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
    final public function injectInternalFlashMessageService(FlashMessageService $flashMessageService): void
    {
        $this->internalFlashMessageService = $flashMessageService;
    }

    /**
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
    final public function injectInternalExtensionService(ExtensionService $extensionService): void
    {
        $this->internalExtensionService = $extensionService;
    }

279
    /**
280
     * Initializes the controller before invoking an action method.
281
     *
282
283
     * Override this method to solve tasks which all actions have in
     * common.
284
     */
285
    protected function initializeAction()
286
    {
287
288
289
    }

    /**
290
     * Implementation of the arguments initialization in the action controller:
291
292
293
294
295
296
     * Automatically registers arguments of the current action
     *
     * Don't override this method - use initializeAction() instead.
     *
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException
     * @see initializeArguments()
297
298
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
299
300
301
     */
    protected function initializeActionMethodArguments()
    {
302
        $methodParameters = $this->reflectionService
303
304
            ->getClassSchema(static::class)
            ->getMethod($this->actionMethodName)->getParameters();
305

306
        foreach ($methodParameters as $parameterName => $parameter) {
307
            $dataType = null;
308
309
310
            if ($parameter->getType() !== null) {
                $dataType = $parameter->getType();
            } elseif ($parameter->isArray()) {
311
312
313
                $dataType = 'array';
            }
            if ($dataType === null) {
314
                throw new InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
315
            }
316
317
            $defaultValue = $parameter->hasDefaultValue() ? $parameter->getDefaultValue() : null;
            $this->arguments->addNewArgument($parameterName, $dataType, !$parameter->isOptional(), $defaultValue);
318
319
320
321
322
323
324
325
326
327
        }
    }

    /**
     * Adds the needed validators to the Arguments:
     *
     * - Validators checking the data type from the @param annotation
     * - Custom validators specified with validate annotations.
     * - Model-based validators (validate annotations in the model)
     * - Custom model validator classes
328
329
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
330
     */
331
    protected function initializeActionMethodValidators(): void
332
    {
333
334
335
        if ($this->arguments->count() === 0) {
            return;
        }
336

337
        $classSchemaMethod = $this->reflectionService->getClassSchema(static::class)->getMethod($this->actionMethodName);
338

339
        /** @var Argument $argument */
340
        foreach ($this->arguments as $argument) {
341
            $classSchemaMethodParameter = $classSchemaMethod->getParameter($argument->getName());
342
343
344
            // At this point validation is skipped if there is an IgnoreValidation annotation.
            // @todo: IgnoreValidation annotations could be evaluated in the ClassSchema and result in
            //        no validators being applied to the method parameter.
345
            if ($classSchemaMethodParameter->ignoreValidation()) {
346
347
                continue;
            }
348
349
            /** @var ConjunctionValidator $validator */
            $validator = $this->validatorResolver->createValidator(ConjunctionValidator::class, []);
350
            foreach ($classSchemaMethodParameter->getValidators() as $validatorDefinition) {
351
                /** @var ValidatorInterface $validatorInstance */
352
                $validatorInstance = $this->validatorResolver->createValidator($validatorDefinition['className'], $validatorDefinition['options']);
353
354
                $validator->addValidator(
                    $validatorInstance
355
356
                );
            }
357
            $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
358
            if ($baseValidatorConjunction->count() > 0) {
359
360
361
362
363
364
                $validator->addValidator($baseValidatorConjunction);
            }
            $argument->setValidator($validator);
        }
    }

365
366
367
    /**
     * Collects the base validators which were defined for the data type of each
     * controller argument and adds them to the argument's validator chain.
368
369
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
370
371
372
     */
    public function initializeControllerArgumentsBaseValidators()
    {
373
        /** @var Argument $argument */
374
375
376
377
378
379
380
381
382
        foreach ($this->arguments as $argument) {
            $validator = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
            if ($validator !== null) {
                $argument->setValidator($validator);
            }
        }
    }

    /**
383
     * Handles an incoming request and returns a response object
384
385
     *
     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
386
     * @return ResponseInterface
387
388
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
389
     */
390
    public function processRequest(RequestInterface $request): ResponseInterface
391
    {
392
        /** @var Request $request */
393
        $this->request = $request;
394
        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
395
396
397
398
399
400
401
        $this->uriBuilder->setRequest($request);
        $this->actionMethodName = $this->resolveActionMethodName();
        $this->initializeActionMethodArguments();
        $this->initializeActionMethodValidators();
        $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
        $this->initializeAction();
        $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
402
403
        /** @var callable $callable */
        $callable = [$this, $actionInitializationMethodName];
404
405
        if (is_callable($callable)) {
            $callable();
406
407
408
        }
        $this->mapRequestArgumentsToControllerArguments();
        $this->view = $this->resolveView();
409
        if ($this->view !== null && method_exists($this, 'initializeView')) {
410
411
            $this->initializeView($this->view);
        }
412
        $response = $this->callActionMethod($request);
413
        $this->renderAssetsForRequest($request);
414

415
        return $response;
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
    }

    /**
     * Method which initializes assets that should be attached to the response
     * for the given $request, which contains parameters that an override can
     * use to determine which assets to add via PageRenderer.
     *
     * This default implementation will attempt to render the sections "HeaderAssets"
     * and "FooterAssets" from the template that is being rendered, inserting the
     * rendered content into either page header or footer, as appropriate. Both
     * sections are optional and can be used one or both in combination.
     *
     * You can add assets with this method without worrying about duplicates, if
     * for example you do this in a plugin that gets used multiple time on a page.
     *
     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
432
433
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
434
435
436
     */
    protected function renderAssetsForRequest($request)
    {
437
438
        if (!$this->view instanceof AbstractTemplateView) {
            // Only AbstractTemplateView (from Fluid engine, so this includes all TYPO3 Views based
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
            // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
            // declared on ViewInterface - so we must assert a specific class. We silently skip
            // asset processing if the View doesn't match, so we don't risk breaking custom Views.
            return;
        }
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $variables = ['request' => $request, 'arguments' => $this->arguments];
        $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
        $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
        if (!empty(trim($headerAssets))) {
            $pageRenderer->addHeaderData($headerAssets);
        }
        if (!empty(trim($footerAssets))) {
            $pageRenderer->addFooterData($footerAssets);
        }
    }

456
457
458
459
460
    /**
     * Resolves and checks the current action method name
     *
     * @return string Method name of the current action
     * @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).
461
462
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
463
464
465
466
467
     */
    protected function resolveActionMethodName()
    {
        $actionMethodName = $this->request->getControllerActionName() . 'Action';
        if (!method_exists($this, $actionMethodName)) {
468
            throw new NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
469
470
471
472
473
474
475
476
477
478
        }
        return $actionMethodName;
    }

    /**
     * Calls the specified action method and passes the arguments.
     *
     * If the action returns a string, it is appended to the content in the
     * response object. If the action doesn't return anything and a valid
     * view exists, the view is rendered automatically.
479
480
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
481
     */
482
    protected function callActionMethod(RequestInterface $request): ResponseInterface
483
    {
484
485
486
        // incoming request is not needed yet but can be passed into the action in the future like in symfony
        // todo: support this via method-reflection

487
        $preparedArguments = [];
488
        /** @var Argument $argument */
489
490
491
        foreach ($this->arguments as $argument) {
            $preparedArguments[] = $argument->getValue();
        }
492
        $validationResult = $this->arguments->validate();
493
        if (!$validationResult->hasErrors()) {
494
            $this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments));
495
            $actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
496
        } else {
497
            $actionResult = $this->{$this->errorMethodName}();
498
499
        }

500
501
502
        if ($actionResult instanceof ResponseInterface) {
            return $actionResult;
        }
503
        throw new \RuntimeException(
504
            sprintf(
505
                'Controller action %s did not return an instance of %s.',
506
                static::class . '::' . $this->actionMethodName,
507
508
                ResponseInterface::class
            ),
509
            1638554283
510
        );
511
512
513
514
515
516
    }

    /**
     * Prepares a view for the current action.
     * By default, this method tries to locate a view with a name matching the current action.
     *
517
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
518
     */
519
    protected function resolveView(): ViewInterface
520
    {
521
522
523
524
525
526
527
528
529
530
531
532
533
534
        if ($this->viewResolver instanceof GenericViewResolver) {
            /*
             * This setter is not part of the ViewResolverInterface as it's only necessary to set
             * the default view class from this point when using the generic view resolver which
             * must respect the possibly overridden property defaultViewObjectName.
             */
            $this->viewResolver->setDefaultViewClass($this->defaultViewObjectName);
        }

        $view = $this->viewResolver->resolve(
            $this->request->getControllerObjectName(),
            $this->request->getControllerActionName(),
            $this->request->getFormat()
        );
535
        $this->setViewConfiguration($view);
536
537
538
539
540
541
542
543
544
        if ($view instanceof AbstractTemplateView) {
            $renderingContext = $view->getRenderingContext();
            if ($renderingContext instanceof RenderingContext) {
                $renderingContext->setRequest($this->request);
            }
            $templatePaths = $view->getRenderingContext()->getTemplatePaths();
            $templatePaths->fillDefaultsByPackageName($this->request->getControllerExtensionKey());
            $templatePaths->setFormat($this->request->getFormat());
        }
545
546
547
548
549
550
551
552
        if (method_exists($view, 'injectSettings')) {
            $view->injectSettings($this->settings);
        }
        $view->assign('settings', $this->settings);
        return $view;
    }

    /**
553
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
554
555
556
557
558
559
560
561
     */
    protected function setViewConfiguration(ViewInterface $view)
    {
        // Template Path Override
        $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
            ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
        );

562
        if (method_exists($view, 'setTemplateRootPaths')) {
563
564
565
566
            $setting = 'templateRootPaths';
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
            // no need to bother if there is nothing to set
            if ($parameter) {
567
                $view->setTemplateRootPaths($parameter);
568
569
570
            }
        }

571
        if (method_exists($view, 'setLayoutRootPaths')) {
572
573
574
575
            $setting = 'layoutRootPaths';
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
            // no need to bother if there is nothing to set
            if ($parameter) {
576
                $view->setLayoutRootPaths($parameter);
577
578
579
            }
        }

580
        if (method_exists($view, 'setPartialRootPaths')) {
581
582
583
584
            $setting = 'partialRootPaths';
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
            // no need to bother if there is nothing to set
            if ($parameter) {
585
                $view->setPartialRootPaths($parameter);
586
587
588
589
590
591
592
593
594
595
596
597
598
            }
        }
    }

    /**
     * Handles the path resolving for *rootPath(s)
     *
     * numerical arrays get ordered by key ascending
     *
     * @param array $extbaseFrameworkConfiguration
     * @param string $setting parameter name from TypoScript
     *
     * @return array
599
600
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
601
602
603
     */
    protected function getViewProperty($extbaseFrameworkConfiguration, $setting)
    {
604
        $values = [];
605
606
607
608
        if (
            !empty($extbaseFrameworkConfiguration['view'][$setting])
            && is_array($extbaseFrameworkConfiguration['view'][$setting])
        ) {
609
            $values = $extbaseFrameworkConfiguration['view'][$setting];
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
        }

        return $values;
    }

    /**
     * A special action which is called if the originally intended action could
     * not be called, for example if the arguments were not valid.
     *
     * The default implementation sets a flash message, request errors and forwards back
     * to the originating action. This is suitable for most actions dealing with form input.
     *
     * We clear the page cache by default on an error as well, as we need to make sure the
     * data is re-evaluated when the user changes something.
     *
625
     * @return ResponseInterface
626
627
628
629
     */
    protected function errorAction()
    {
        $this->addErrorFlashMessage();
630
        if (($response = $this->forwardToReferringRequest()) !== null) {
631
            return $response->withStatus(400);
632
        }
633

634
635
        $response = $this->htmlResponse($this->getFlattenedValidationErrorMessage());
        return $response->withStatus(400);
636
637
638
639
640
    }

    /**
     * If an error occurred during this request, this adds a flash message describing the error to the flash
     * message container.
641
642
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
643
644
645
646
647
648
649
650
651
652
653
654
655
656
     */
    protected function addErrorFlashMessage()
    {
        $errorFlashMessage = $this->getErrorFlashMessage();
        if ($errorFlashMessage !== false) {
            $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
        }
    }

    /**
     * A template method for displaying custom error flash messages, or to
     * display no flash message at all on errors. Override this to customize
     * the flash message in your action controller.
     *
657
     * @return string|bool The flash message or FALSE if no flash message should be set
658
659
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
660
661
662
     */
    protected function getErrorFlashMessage()
    {
663
        return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
664
665
666
667
668
669
670
    }

    /**
     * If information on the request before the current request was sent, this method forwards back
     * to the originating request. This effectively ends processing of the current request, so do not
     * call this method before you have finished the necessary business logic!
     *
671
     * @return ResponseInterface|null
672
673
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
674
     */
675
    protected function forwardToReferringRequest(): ?ResponseInterface
676
    {
677
678
        $referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null;
        if (is_string($referringRequestArguments['@request'] ?? null)) {
679
            $referrerArray = json_decode(
680
                $this->hashService->validateAndStripHmac($referringRequestArguments['@request']),
681
682
683
                true
            );
            $arguments = [];
684
685
686
687
688
            if (is_string($referringRequestArguments['arguments'] ?? null)) {
                $arguments = unserialize(
                    base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments']))
                );
            }
689
690
691
692
693
694
            $replacedArguments = array_replace_recursive($arguments, $referrerArray);
            $nonExtbaseBaseArguments = [];
            foreach ($replacedArguments as $argumentName => $argumentValue) {
                if (!is_string($argumentName) || $argumentName === '') {
                    throw new InvalidArgumentNameException('Invalid argument name.', 1623940985);
                }
695
                if (str_starts_with($argumentName, '__')
696
697
698
699
700
701
702
703
704
705
706
707
                    || in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)
                ) {
                    // Don't handle internalArguments here, not needed for forwardResponse()
                    continue;
                }
                $nonExtbaseBaseArguments[$argumentName] = $argumentValue;
            }
            return (new ForwardResponse((string)($replacedArguments['@action'] ?? 'index')))
                ->withControllerName((string)($replacedArguments['@controller'] ?? 'Standard'))
                ->withExtensionName((string)($replacedArguments['@extension'] ?? ''))
                ->withArguments($nonExtbaseBaseArguments)
                ->withArgumentsValidationResult($this->arguments->validate());
708
        }
709
710

        return null;
711
712
713
714
715
716
717
718
    }

    /**
     * Returns a string with a basic error message about validation failure.
     * We may add all validation error messages to a log file in the future,
     * but for security reasons (@see #54074) we do not return these here.
     *
     * @return string
719
720
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
721
722
723
     */
    protected function getFlattenedValidationErrorMessage()
    {
724
        $outputMessage = 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
725
726
        return $outputMessage;
    }
727
728
729
730
731
732
733
734
735
736
737

    /**
     * Creates a Message object and adds it to the FlashMessageQueue.
     *
     * @param string $messageBody The message
     * @param string $messageTitle Optional message title
     * @param int $severity Optional severity, must be one of \TYPO3\CMS\Core\Messaging\FlashMessage constants
     * @param bool $storeInSession Optional, defines whether the message should be stored in the session (default) or not
     * @throws \InvalidArgumentException if the message body is no string
     * @see \TYPO3\CMS\Core\Messaging\FlashMessage
     */
738
    public function addFlashMessage($messageBody, $messageTitle = '', $severity = AbstractMessage::OK, $storeInSession = true)
739
740
741
742
743
    {
        if (!is_string($messageBody)) {
            throw new \InvalidArgumentException('The message body must be of type string, "' . gettype($messageBody) . '" given.', 1243258395);
        }
        /* @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */
744
745
        $flashMessage = GeneralUtility::makeInstance(
            FlashMessage::class,
746
747
748
749
750
            (string)$messageBody,
            (string)$messageTitle,
            $severity,
            $storeInSession
        );
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772

        $this->getFlashMessageQueue()->enqueue($flashMessage);
    }

    /**
     * todo: As soon as the incoming request contains the compiled plugin namespace, extbase will offer a trait to
     *       create a flash message identifier from the current request. Users then should inject the flash message
     *       service themselves if needed.
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
     */
    protected function getFlashMessageQueue(string $identifier = null): FlashMessageQueue
    {
        if ($identifier === null) {
            $pluginNamespace = $this->internalExtensionService->getPluginNamespace(
                $this->request->getControllerExtensionName(),
                $this->request->getPluginName()
            );
            $identifier = 'extbase.flashmessages.' . $pluginNamespace;
        }

        return $this->internalFlashMessageService->getMessageQueueByIdentifier($identifier);
773
774
775
776
777
778
779
780
781
782
783
784
    }

    /**
     * Redirects the request to another action and / or controller.
     *
     * Redirect will be sent to the client which then performs another request to the new URI.
     *
     * @param string $actionName Name of the action to forward to
     * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
     * @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
     * @param array|null $arguments Arguments to pass to the target action
     * @param int|null $pageUid Target page uid. If NULL, the current page uid is used
785
     * @param null $_ (optional) Unused
786
787
     * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
     */
788
    protected function redirect($actionName, $controllerName = null, $extensionName = null, array $arguments = null, $pageUid = null, $_ = null, $statusCode = 303): ResponseInterface
789
790
791
792
793
794
795
796
    {
        if ($controllerName === null) {
            $controllerName = $this->request->getControllerName();
        }
        $this->uriBuilder->reset()->setCreateAbsoluteUri(true);
        if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
            $this->uriBuilder->setTargetPageUid((int)$pageUid);
        }
797
        if (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
798
799
800
            $this->uriBuilder->setAbsoluteUriScheme('https');
        }
        $uri = $this->uriBuilder->uriFor($actionName, $arguments, $controllerName, $extensionName);
801
        return $this->redirectToUri($uri, null, $statusCode);
802
803
804
805
806
807
    }

    /**
     * Redirects the web request to another uri.
     *
     * @param mixed $uri A string representation of a URI
808
809
     * @param null $_ (optional) Unused
     * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
810
     */
811
    protected function redirectToUri($uri, $_ = null, $statusCode = 303): ResponseInterface
812
813
    {
        $uri = $this->addBaseUriIfNecessary($uri);
814
        return new RedirectResponse($uri, $statusCode);
815
816
817
818
819
820
821
    }

    /**
     * Adds the base uri if not already in place.
     *
     * @param string $uri The URI
     * @return string
822
823
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
824
825
826
     */
    protected function addBaseUriIfNecessary($uri)
    {
827
        return GeneralUtility::locationHeaderUrl((string)$uri);
828
829
830
    }

    /**
831
832
     * Sends the specified HTTP status immediately and only stops to run back through the middleware stack.
     * Note: If any other plugin or content or hook is used within a frontend request, this is skipped by design.
833
834
835
836
     *
     * @param int $statusCode The HTTP status code
     * @param string $statusMessage A custom HTTP status message
     * @param string $content Body content which further explains the status
837
     * @throws PropagateResponseException
838
839
840
     */
    public function throwStatus($statusCode, $statusMessage = null, $content = null)
    {
841
        if ($content === null) {
842
            $content = $statusCode . ' ' . $statusMessage;
843
        }
844
        $response = $this->responseFactory
845
846
            ->createResponse((int)$statusCode, (string)$statusMessage)
            ->withBody($this->streamFactory->createStream((string)$content));
847
        throw new PropagateResponseException($response, 1476045871);
848
849
850
851
852
853
    }

    /**
     * Maps arguments delivered by the request object to the local controller arguments.
     *
     * @throws Exception\RequiredArgumentMissingException
854
855
     *
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
856
857
858
     */
    protected function mapRequestArgumentsToControllerArguments()
    {
859
        /** @var Argument $argument */
860
861
862
        foreach ($this->arguments as $argument) {
            $argumentName = $argument->getName();
            if ($this->request->hasArgument($argumentName)) {
863
                $this->setArgumentValue($argument, $this->request->getArgument($argumentName));
864
            } elseif ($argument->isRequired()) {
865
                throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
866
867
868
            }
        }
    }
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901

    /**
     * @param Argument $argument
     * @param mixed $rawValue
     */
    private function setArgumentValue(Argument $argument, $rawValue): void
    {
        if ($rawValue === null) {
            $argument->setValue(null);
            return;
        }
        $dataType = $argument->getDataType();
        if (is_object($rawValue) && $rawValue instanceof $dataType) {
            $argument->setValue($rawValue);
            return;
        }
        $this->propertyMapper->resetMessages();
        try {
            $argument->setValue(
                $this->propertyMapper->convert(
                    $rawValue,
                    $dataType,
                    $argument->getPropertyMappingConfiguration()
                )
            );
        } catch (TargetNotFoundException $e) {
            // for optional arguments no exception is thrown.
            if ($argument->isRequired()) {
                throw $e;
            }
        }
        $argument->getValidationResults()->merge($this->propertyMapper->getMessages());
    }
902
903
904
905
906
907
908
909
910

    /**
     * Returns a response object with either the given html string or the current rendered view as content.
     *
     * @param string|null $html
     * @return ResponseInterface
     */
    protected function htmlResponse(string $html = null): ResponseInterface
    {
911
912
913
        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withBody($this->streamFactory->createStream($html ?? $this->view->render()));
914
    }
915
916
917
918
919
920
921
922
923
924

    /**
     * Returns a response object with either the given json string or the current rendered
     * view as content. Mainly to be used for actions / controllers using the JsonView.
     *
     * @param string|null $json
     * @return ResponseInterface
     */
    protected function jsonResponse(string $json = null): ResponseInterface
    {
925
926
927
        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json; charset=utf-8')
            ->withBody($this->streamFactory->createStream($json ?? $this->view->render()));
928
    }
929
}