Passed
Push — master ( 028eab...d24489 )
by
unknown
29:02
created

ActionController::getFlashMessageQueue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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\ResponseInterface;
20
use TYPO3\CMS\Core\Http\HtmlResponse;
21
use TYPO3\CMS\Core\Http\Response;
22
use TYPO3\CMS\Core\Http\ResponseFactoryInterface;
23
use TYPO3\CMS\Core\Http\Stream;
24
use TYPO3\CMS\Core\Messaging\AbstractMessage;
25
use TYPO3\CMS\Core\Messaging\FlashMessage;
26
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
27
use TYPO3\CMS\Core\Messaging\FlashMessageService;
28
use TYPO3\CMS\Core\Page\PageRenderer;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
use TYPO3\CMS\Core\Utility\MathUtility;
31
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
32
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
33
use TYPO3\CMS\Extbase\Http\ForwardResponse;
34
use TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException;
35
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
36
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
37
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
38
use TYPO3\CMS\Extbase\Mvc\Request;
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\CacheService;
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\ContentObject\ContentObjectRenderer;
59
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
60
use TYPO3Fluid\Fluid\View\TemplateView;
61
62
/**
63
 * A multi action controller. This is by far the most common base class for Controllers.
64
 */
65
abstract class ActionController implements ControllerInterface
66
{
67
    /**
68
     * @var ResponseFactoryInterface
69
     */
70
    protected $responseFactory;
71
72
    /**
73
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
74
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
75
     */
76
    protected $reflectionService;
77
78
    /**
79
     * @var \TYPO3\CMS\Extbase\Service\CacheService
80
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
81
     */
82
    protected $cacheService;
83
84
    /**
85
     * @var HashService
86
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
87
     */
88
    protected $hashService;
89
90
    /**
91
     * @var ViewResolverInterface
92
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
93
     */
94
    private $viewResolver;
95
96
    /**
97
     * The current view, as resolved by resolveView()
98
     *
99
     * @var ViewInterface
100
     */
101
    protected $view;
102
103
    /**
104
     * The default view object to use if none of the resolved views can render
105
     * a response for the current request.
106
     *
107
     * @var string
108
     */
109
    protected $defaultViewObjectName = \TYPO3\CMS\Fluid\View\TemplateView::class;
110
111
    /**
112
     * Name of the action method
113
     *
114
     * @var string
115
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
116
     */
117
    protected $actionMethodName = 'indexAction';
118
119
    /**
120
     * Name of the special error action method which is called in case of errors
121
     *
122
     * @var string
123
     */
124
    protected $errorMethodName = 'errorAction';
125
126
    /**
127
     * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService
128
     */
129
    protected $mvcPropertyMappingConfigurationService;
130
131
    /**
132
     * @var EventDispatcherInterface
133
     */
134
    protected $eventDispatcher;
135
136
    /**
137
     * The current request.
138
     *
139
     * @var \TYPO3\CMS\Extbase\Mvc\Request
140
     */
141
    protected $request;
142
143
    /**
144
     * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
145
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
146
     */
147
    protected $signalSlotDispatcher;
148
149
    /**
150
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
151
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
152
     */
153
    protected $objectManager;
154
155
    /**
156
     * @var \TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder
157
     */
158
    protected $uriBuilder;
159
160
    /**
161
     * Contains the settings of the current extension
162
     *
163
     * @var array
164
     */
165
    protected $settings;
166
167
    /**
168
     * @var \TYPO3\CMS\Extbase\Validation\ValidatorResolver
169
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
170
     */
171
    protected $validatorResolver;
172
173
    /**
174
     * @var \TYPO3\CMS\Extbase\Mvc\Controller\Arguments Arguments passed to the controller
175
     */
176
    protected $arguments;
177
178
    /**
179
     * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
180
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
181
     */
182
    protected $controllerContext;
183
184
    /**
185
     * @var ConfigurationManagerInterface
186
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
187
     */
188
    protected $configurationManager;
189
190
    /**
191
     * @var PropertyMapper
192
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
193
     */
194
    private $propertyMapper;
195
196
    /**
197
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
198
     */
199
    private FlashMessageService $internalFlashMessageService;
200
201
    /**
202
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
203
     */
204
    private ExtensionService $internalExtensionService;
205
206
    final public function injectResponseFactory(ResponseFactoryInterface $responseFactory)
207
    {
208
        $this->responseFactory = $responseFactory;
209
    }
210
211
    /**
212
     * @param ConfigurationManagerInterface $configurationManager
213
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
214
     */
215
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
216
    {
217
        $this->configurationManager = $configurationManager;
218
        $this->settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
219
    }
220
221
    /**
222
     * Injects the object manager
223
     *
224
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
225
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
226
     */
227
    public function injectObjectManager(ObjectManagerInterface $objectManager)
228
    {
229
        $this->objectManager = $objectManager;
230
        $this->arguments = GeneralUtility::makeInstance(Arguments::class);
231
    }
232
233
    /**
234
     * @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
235
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
236
     */
237
    public function injectSignalSlotDispatcher(Dispatcher $signalSlotDispatcher)
238
    {
239
        $this->signalSlotDispatcher = $signalSlotDispatcher;
240
    }
241
242
    /**
243
     * @param \TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver
244
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
245
     */
246
    public function injectValidatorResolver(ValidatorResolver $validatorResolver)
247
    {
248
        $this->validatorResolver = $validatorResolver;
249
    }
250
251
    /**
252
     * @param ViewResolverInterface $viewResolver
253
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
254
     */
255
    public function injectViewResolver(ViewResolverInterface $viewResolver)
256
    {
257
        $this->viewResolver = $viewResolver;
258
    }
259
260
    /**
261
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
262
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
263
     */
264
    public function injectReflectionService(ReflectionService $reflectionService)
265
    {
266
        $this->reflectionService = $reflectionService;
267
    }
268
269
    /**
270
     * @param \TYPO3\CMS\Extbase\Service\CacheService $cacheService
271
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
272
     */
273
    public function injectCacheService(CacheService $cacheService)
274
    {
275
        $this->cacheService = $cacheService;
276
    }
277
278
    /**
279
     * @param HashService $hashService
280
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
281
     */
282
    public function injectHashService(HashService $hashService)
283
    {
284
        $this->hashService = $hashService;
285
    }
286
287
    /**
288
     * @param \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService
289
     */
290
    public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService)
291
    {
292
        $this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
293
    }
294
295
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
296
    {
297
        $this->eventDispatcher = $eventDispatcher;
298
    }
299
300
    /**
301
     * @param PropertyMapper $propertyMapper
302
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
303
     */
304
    public function injectPropertyMapper(PropertyMapper $propertyMapper): void
305
    {
306
        $this->propertyMapper = $propertyMapper;
307
    }
308
309
    /**
310
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
311
     */
312
    final public function injectInternalFlashMessageService(FlashMessageService $flashMessageService): void
313
    {
314
        $this->internalFlashMessageService = $flashMessageService;
315
    }
316
317
    /**
318
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
319
     */
320
    final public function injectInternalExtensionService(ExtensionService $extensionService): void
321
    {
322
        $this->internalExtensionService = $extensionService;
323
    }
324
325
    /**
326
     * Initializes the view before invoking an action method.
327
     *
328
     * Override this method to solve assign variables common for all actions
329
     * or prepare the view in another way before the action is called.
330
     *
331
     * @param ViewInterface $view The view to be initialized
332
     */
333
    protected function initializeView(ViewInterface $view)
334
    {
335
    }
336
337
    /**
338
     * Initializes the controller before invoking an action method.
339
     *
340
     * Override this method to solve tasks which all actions have in
341
     * common.
342
     */
343
    protected function initializeAction()
344
    {
345
    }
346
347
    /**
348
     * Implementation of the arguments initialization in the action controller:
349
     * Automatically registers arguments of the current action
350
     *
351
     * Don't override this method - use initializeAction() instead.
352
     *
353
     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException
354
     * @see initializeArguments()
355
     *
356
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
357
     */
358
    protected function initializeActionMethodArguments()
359
    {
360
        $methodParameters = $this->reflectionService
361
            ->getClassSchema(static::class)
362
            ->getMethod($this->actionMethodName)->getParameters();
363
364
        foreach ($methodParameters as $parameterName => $parameter) {
365
            $dataType = null;
366
            if ($parameter->getType() !== null) {
367
                $dataType = $parameter->getType();
368
            } elseif ($parameter->isArray()) {
369
                $dataType = 'array';
370
            }
371
            if ($dataType === null) {
372
                throw new InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
373
            }
374
            $defaultValue = $parameter->hasDefaultValue() ? $parameter->getDefaultValue() : null;
375
            $this->arguments->addNewArgument($parameterName, $dataType, !$parameter->isOptional(), $defaultValue);
376
        }
377
    }
378
379
    /**
380
     * Adds the needed validators to the Arguments:
381
     *
382
     * - Validators checking the data type from the @param annotation
383
     * - Custom validators specified with validate annotations.
384
     * - Model-based validators (validate annotations in the model)
385
     * - Custom model validator classes
386
     *
387
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
388
     */
389
    protected function initializeActionMethodValidators()
390
    {
391
        if ($this->arguments->count() === 0) {
392
            return;
393
        }
394
395
        $classSchemaMethod = $this->reflectionService->getClassSchema(static::class)
396
            ->getMethod($this->actionMethodName);
397
398
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
399
        foreach ($this->arguments as $argument) {
400
            $classSchemaMethodParameter = $classSchemaMethod->getParameter($argument->getName());
401
            /*
402
             * At this point validation is skipped if there is an IgnoreValidation annotation.
403
             *
404
             * todo: IgnoreValidation annotations could be evaluated in the ClassSchema and result in
405
             * todo: no validators being applied to the method parameter.
406
             */
407
            if ($classSchemaMethodParameter->ignoreValidation()) {
408
                continue;
409
            }
410
411
            // todo: It's quite odd that an instance of ConjunctionValidator is created directly here.
412
            // todo: \TYPO3\CMS\Extbase\Validation\ValidatorResolver::getBaseValidatorConjunction could/should be used
413
            // todo: here, to benefit of the built in 1st level cache of the ValidatorResolver.
414
            $validator = $this->objectManager->get(ConjunctionValidator::class);
415
416
            foreach ($classSchemaMethodParameter->getValidators() as $validatorDefinition) {
417
                /** @var ValidatorInterface $validatorInstance */
418
                $validatorInstance = $this->objectManager->get(
419
                    $validatorDefinition['className'],
420
                    $validatorDefinition['options']
421
                );
422
423
                $validator->addValidator(
424
                    $validatorInstance
425
                );
426
            }
427
428
            $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
429
            if ($baseValidatorConjunction->count() > 0) {
430
                $validator->addValidator($baseValidatorConjunction);
431
            }
432
            $argument->setValidator($validator);
433
        }
434
    }
435
436
    /**
437
     * Collects the base validators which were defined for the data type of each
438
     * controller argument and adds them to the argument's validator chain.
439
     *
440
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
441
     */
442
    public function initializeControllerArgumentsBaseValidators()
443
    {
444
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
445
        foreach ($this->arguments as $argument) {
446
            $validator = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType());
447
            if ($validator !== null) {
448
                $argument->setValidator($validator);
449
            }
450
        }
451
    }
452
453
    /**
454
     * Handles an incoming request and returns a response object
455
     *
456
     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
457
     * @return ResponseInterface
458
     *
459
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
460
     */
461
    public function processRequest(RequestInterface $request): ResponseInterface
462
    {
463
        $this->request = $request;
0 ignored issues
show
Documentation Bug introduced by
$request is of type TYPO3\CMS\Extbase\Mvc\RequestInterface, but the property $request was declared to be of type TYPO3\CMS\Extbase\Mvc\Request. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
464
        $this->request->setDispatched(true);
465
        $this->uriBuilder = $this->objectManager->get(UriBuilder::class);
466
        $this->uriBuilder->setRequest($request);
467
        $this->actionMethodName = $this->resolveActionMethodName();
468
        $this->initializeActionMethodArguments();
469
        $this->initializeActionMethodValidators();
470
        $this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
471
        $this->initializeAction();
472
        $actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
473
        /** @var callable $callable */
474
        $callable = [$this, $actionInitializationMethodName];
475
        if (method_exists($this, $actionInitializationMethodName)) {
476
            // todo: replace method_exists with is_callable or even both
477
            //       method_exists alone does not guarantee that $callable is actually callable
478
            call_user_func($callable);
479
        }
480
        $this->mapRequestArgumentsToControllerArguments();
481
        $this->controllerContext = $this->buildControllerContext();
482
        $this->view = $this->resolveView();
483
        if ($this->view !== null) {
484
            $this->initializeView($this->view);
485
        }
486
        $response = $this->callActionMethod($request);
487
        $this->renderAssetsForRequest($request);
488
489
        return $response;
490
    }
491
492
    /**
493
     * Method which initializes assets that should be attached to the response
494
     * for the given $request, which contains parameters that an override can
495
     * use to determine which assets to add via PageRenderer.
496
     *
497
     * This default implementation will attempt to render the sections "HeaderAssets"
498
     * and "FooterAssets" from the template that is being rendered, inserting the
499
     * rendered content into either page header or footer, as appropriate. Both
500
     * sections are optional and can be used one or both in combination.
501
     *
502
     * You can add assets with this method without worrying about duplicates, if
503
     * for example you do this in a plugin that gets used multiple time on a page.
504
     *
505
     * @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request
506
     *
507
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
508
     */
509
    protected function renderAssetsForRequest($request)
510
    {
511
        if (!$this->view instanceof TemplateView) {
512
            // Only TemplateView (from Fluid engine, so this includes all TYPO3 Views based
513
            // on TYPO3's AbstractTemplateView) supports renderSection(). The method is not
514
            // declared on ViewInterface - so we must assert a specific class. We silently skip
515
            // asset processing if the View doesn't match, so we don't risk breaking custom Views.
516
            return;
517
        }
518
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
519
        $variables = ['request' => $request, 'arguments' => $this->arguments];
520
        $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
521
        $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
522
        if (!empty(trim($headerAssets))) {
523
            $pageRenderer->addHeaderData($headerAssets);
524
        }
525
        if (!empty(trim($footerAssets))) {
526
            $pageRenderer->addFooterData($footerAssets);
527
        }
528
    }
529
530
    /**
531
     * Resolves and checks the current action method name
532
     *
533
     * @return string Method name of the current action
534
     * @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).
535
     *
536
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
537
     */
538
    protected function resolveActionMethodName()
539
    {
540
        $actionMethodName = $this->request->getControllerActionName() . 'Action';
541
        if (!method_exists($this, $actionMethodName)) {
542
            throw new NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
543
        }
544
        return $actionMethodName;
545
    }
546
547
    /**
548
     * Calls the specified action method and passes the arguments.
549
     *
550
     * If the action returns a string, it is appended to the content in the
551
     * response object. If the action doesn't return anything and a valid
552
     * view exists, the view is rendered automatically.
553
     *
554
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
555
     */
556
    protected function callActionMethod(RequestInterface $request): ResponseInterface
557
    {
558
        // incoming request is not needed yet but can be passed into the action in the future like in symfony
559
        // todo: support this via method-reflection
560
561
        $preparedArguments = [];
562
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
563
        foreach ($this->arguments as $argument) {
564
            $preparedArguments[] = $argument->getValue();
565
        }
566
        $validationResult = $this->arguments->validate();
567
        if (!$validationResult->hasErrors()) {
568
            $this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments));
569
            $actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
570
        } else {
571
            $actionResult = $this->{$this->errorMethodName}();
572
        }
573
574
        if ($actionResult instanceof ResponseInterface) {
575
            return $actionResult;
576
        }
577
578
        trigger_error(
579
            sprintf(
580
                'Controller action %s does not return an instance of %s which is deprecated.',
581
                __CLASS__ . '::' . $this->{$this->actionMethodName},
582
                ResponseInterface::class
583
            ),
584
            E_USER_DEPRECATED
585
        );
586
587
        $response = new \TYPO3\CMS\Core\Http\Response();
588
        $body = new Stream('php://temp', 'rw');
589
        if ($actionResult === null && $this->view instanceof ViewInterface) {
590
            if ($this->view instanceof JsonView) {
591
                // this is just a temporary solution until Extbase uses PSR-7 responses and users are forced to return a
592
                // response object in their controller actions.
593
594
                if (!empty($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
595
                    /** @var TypoScriptFrontendController $typoScriptFrontendController */
596
                    $typoScriptFrontendController = $GLOBALS['TSFE'];
597
                    if (empty($typoScriptFrontendController->config['config']['disableCharsetHeader'])) {
598
                        // If the charset header is *not* disabled in configuration,
599
                        // TypoScriptFrontendController will send the header later with the Content-Type which we set here.
600
                        $typoScriptFrontendController->setContentType('application/json');
601
                    } else {
602
                        // Although the charset header is disabled in configuration, we *must* send a Content-Type header here.
603
                        // Content-Type headers optionally carry charset information at the same time.
604
                        // Since we have the information about the charset, there is no reason to not include the charset information although disabled in TypoScript.
605
                        $response = $response->withHeader('Content-Type', 'application/json; charset=' . trim($typoScriptFrontendController->metaCharset));
606
                    }
607
                } else {
608
                    $response = $response->withHeader('Content-Type', 'application/json');
609
                }
610
            }
611
612
            $body->write($this->view->render());
613
        } elseif (is_string($actionResult) && $actionResult !== '') {
614
            $body->write($actionResult);
615
        } elseif (is_object($actionResult) && method_exists($actionResult, '__toString')) {
616
            $body->write((string)$actionResult);
617
        }
618
619
        $body->rewind();
620
        return $response->withBody($body);
621
    }
622
623
    /**
624
     * Prepares a view for the current action.
625
     * By default, this method tries to locate a view with a name matching the current action.
626
     *
627
     * @return ViewInterface
628
     *
629
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
630
     */
631
    protected function resolveView()
632
    {
633
        if ($this->viewResolver instanceof GenericViewResolver) {
634
            /*
635
             * This setter is not part of the ViewResolverInterface as it's only necessary to set
636
             * the default view class from this point when using the generic view resolver which
637
             * must respect the possibly overridden property defaultViewObjectName.
638
             */
639
            $this->viewResolver->setDefaultViewClass($this->defaultViewObjectName);
640
        }
641
642
        $view = $this->viewResolver->resolve(
643
            $this->request->getControllerObjectName(),
644
            $this->request->getControllerActionName(),
645
            $this->request->getFormat()
646
        );
647
648
        if ($view instanceof ViewInterface) {
0 ignored issues
show
introduced by
$view is always a sub-type of TYPO3\CMS\Extbase\Mvc\View\ViewInterface.
Loading history...
649
            $this->setViewConfiguration($view);
650
            if ($view->canRender($this->controllerContext) === false) {
651
                $view = null;
652
            }
653
        }
654
        if (!isset($view)) {
655
            $view = $this->objectManager->get(NotFoundView::class);
656
            $view->assign('errorMessage', 'No template was found. View could not be resolved for action "'
657
                . $this->request->getControllerActionName() . '" in class "' . $this->request->getControllerObjectName() . '"');
658
        }
659
        $view->setControllerContext($this->controllerContext);
660
        if (method_exists($view, 'injectSettings')) {
661
            $view->injectSettings($this->settings);
662
        }
663
        $view->initializeView();
664
        // In TYPO3.Flow, solved through Object Lifecycle methods, we need to call it explicitly
665
        $view->assign('settings', $this->settings);
666
        // same with settings injection.
667
        return $view;
668
    }
669
670
    /**
671
     * @param ViewInterface $view
672
     *
673
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
674
     */
675
    protected function setViewConfiguration(ViewInterface $view)
676
    {
677
        // Template Path Override
678
        $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(
679
            ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
680
        );
681
682
        // set TemplateRootPaths
683
        $viewFunctionName = 'setTemplateRootPaths';
684
        if (method_exists($view, $viewFunctionName)) {
685
            $setting = 'templateRootPaths';
686
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
687
            // no need to bother if there is nothing to set
688
            if ($parameter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parameter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
689
                $view->$viewFunctionName($parameter);
690
            }
691
        }
692
693
        // set LayoutRootPaths
694
        $viewFunctionName = 'setLayoutRootPaths';
695
        if (method_exists($view, $viewFunctionName)) {
696
            $setting = 'layoutRootPaths';
697
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
698
            // no need to bother if there is nothing to set
699
            if ($parameter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parameter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
700
                $view->$viewFunctionName($parameter);
701
            }
702
        }
703
704
        // set PartialRootPaths
705
        $viewFunctionName = 'setPartialRootPaths';
706
        if (method_exists($view, $viewFunctionName)) {
707
            $setting = 'partialRootPaths';
708
            $parameter = $this->getViewProperty($extbaseFrameworkConfiguration, $setting);
709
            // no need to bother if there is nothing to set
710
            if ($parameter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parameter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
711
                $view->$viewFunctionName($parameter);
712
            }
713
        }
714
    }
715
716
    /**
717
     * Handles the path resolving for *rootPath(s)
718
     *
719
     * numerical arrays get ordered by key ascending
720
     *
721
     * @param array $extbaseFrameworkConfiguration
722
     * @param string $setting parameter name from TypoScript
723
     *
724
     * @return array
725
     *
726
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
727
     */
728
    protected function getViewProperty($extbaseFrameworkConfiguration, $setting)
729
    {
730
        $values = [];
731
        if (
732
            !empty($extbaseFrameworkConfiguration['view'][$setting])
733
            && is_array($extbaseFrameworkConfiguration['view'][$setting])
734
        ) {
735
            $values = $extbaseFrameworkConfiguration['view'][$setting];
736
        }
737
738
        return $values;
739
    }
740
741
    /**
742
     * A special action which is called if the originally intended action could
743
     * not be called, for example if the arguments were not valid.
744
     *
745
     * The default implementation sets a flash message, request errors and forwards back
746
     * to the originating action. This is suitable for most actions dealing with form input.
747
     *
748
     * We clear the page cache by default on an error as well, as we need to make sure the
749
     * data is re-evaluated when the user changes something.
750
     *
751
     * @return ResponseInterface
752
     */
753
    protected function errorAction()
754
    {
755
        $this->clearCacheOnError();
756
        $this->addErrorFlashMessage();
757
        if (($response = $this->forwardToReferringRequest()) !== null) {
758
            return $response;
759
        }
760
761
        return $this->htmlResponse($this->getFlattenedValidationErrorMessage());
762
    }
763
764
    /**
765
     * Clear cache of current page on error. Needed because we want a re-evaluation of the data.
766
     * Better would be just do delete the cache for the error action, but that is not possible right now.
767
     *
768
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
769
     */
770
    protected function clearCacheOnError()
771
    {
772
        $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
773
        if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
774
            if (isset($GLOBALS['TSFE'])) {
775
                $pageUid = $GLOBALS['TSFE']->id;
776
                $this->cacheService->clearPageCache([$pageUid]);
777
            }
778
        }
779
    }
780
781
    /**
782
     * If an error occurred during this request, this adds a flash message describing the error to the flash
783
     * message container.
784
     *
785
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
786
     */
787
    protected function addErrorFlashMessage()
788
    {
789
        $errorFlashMessage = $this->getErrorFlashMessage();
790
        if ($errorFlashMessage !== false) {
0 ignored issues
show
introduced by
The condition $errorFlashMessage !== false is always true.
Loading history...
791
            $this->addFlashMessage($errorFlashMessage, '', FlashMessage::ERROR);
792
        }
793
    }
794
795
    /**
796
     * A template method for displaying custom error flash messages, or to
797
     * display no flash message at all on errors. Override this to customize
798
     * the flash message in your action controller.
799
     *
800
     * @return string The flash message or FALSE if no flash message should be set
801
     *
802
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
803
     */
804
    protected function getErrorFlashMessage()
805
    {
806
        return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
807
    }
808
809
    /**
810
     * If information on the request before the current request was sent, this method forwards back
811
     * to the originating request. This effectively ends processing of the current request, so do not
812
     * call this method before you have finished the necessary business logic!
813
     *
814
     * @return ResponseInterface|null
815
     *
816
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
817
     */
818
    protected function forwardToReferringRequest(): ?ResponseInterface
819
    {
820
        $referringRequest = null;
821
        $referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null;
822
        if (is_string($referringRequestArguments['@request'] ?? null)) {
823
            $referrerArray = json_decode(
824
                $this->hashService->validateAndStripHmac($referringRequestArguments['@request']),
825
                true
826
            );
827
            $arguments = [];
828
            if (is_string($referringRequestArguments['arguments'] ?? null)) {
829
                $arguments = unserialize(
830
                    base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments']))
831
                );
832
            }
833
            // todo: Remove ReferringRequest. It's only used here in this context to trigger the logic of
834
            //       \TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest::setArgument() and its parent method which should then
835
            //       be extracted from the request class.
836
            $referringRequest = new ReferringRequest();
837
            $referringRequest->setArguments(array_replace_recursive($arguments, $referrerArray));
838
        }
839
840
        if ($referringRequest !== null) {
841
            return (new ForwardResponse((string)$referringRequest->getControllerActionName()))
842
                ->withControllerName((string)$referringRequest->getControllerName())
843
                ->withExtensionName((string)$referringRequest->getControllerExtensionName())
844
                ->withArguments($referringRequest->getArguments())
845
                ->withArgumentsValidationResult($this->arguments->validate())
846
            ;
847
        }
848
849
        return null;
850
    }
851
852
    /**
853
     * Returns a string with a basic error message about validation failure.
854
     * We may add all validation error messages to a log file in the future,
855
     * but for security reasons (@see #54074) we do not return these here.
856
     *
857
     * @return string
858
     *
859
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
860
     */
861
    protected function getFlattenedValidationErrorMessage()
862
    {
863
        $outputMessage = 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
864
        return $outputMessage;
865
    }
866
867
    /**
868
     * @return ControllerContext
869
     */
870
    public function getControllerContext()
871
    {
872
        return $this->controllerContext;
873
    }
874
875
    /**
876
     * Creates a Message object and adds it to the FlashMessageQueue.
877
     *
878
     * @param string $messageBody The message
879
     * @param string $messageTitle Optional message title
880
     * @param int $severity Optional severity, must be one of \TYPO3\CMS\Core\Messaging\FlashMessage constants
881
     * @param bool $storeInSession Optional, defines whether the message should be stored in the session (default) or not
882
     * @throws \InvalidArgumentException if the message body is no string
883
     * @see \TYPO3\CMS\Core\Messaging\FlashMessage
884
     */
885
    public function addFlashMessage($messageBody, $messageTitle = '', $severity = AbstractMessage::OK, $storeInSession = true)
886
    {
887
        if (!is_string($messageBody)) {
0 ignored issues
show
introduced by
The condition is_string($messageBody) is always true.
Loading history...
888
            throw new \InvalidArgumentException('The message body must be of type string, "' . gettype($messageBody) . '" given.', 1243258395);
889
        }
890
        /* @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */
891
        $flashMessage = GeneralUtility::makeInstance(
892
            FlashMessage::class,
893
            (string)$messageBody,
894
            (string)$messageTitle,
895
            $severity,
896
            $storeInSession
897
        );
898
899
        $this->getFlashMessageQueue()->enqueue($flashMessage);
900
    }
901
902
    /**
903
     * todo: As soon as the incoming request contains the compiled plugin namespace, extbase will offer a trait to
904
     *       create a flash message identifier from the current request. Users then should inject the flash message
905
     *       service themselves if needed.
906
     *
907
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
908
     */
909
    protected function getFlashMessageQueue(string $identifier = null): FlashMessageQueue
910
    {
911
        if ($identifier === null) {
912
            $pluginNamespace = $this->internalExtensionService->getPluginNamespace(
913
                $this->request->getControllerExtensionName(),
914
                $this->request->getPluginName()
915
            );
916
            $identifier = 'extbase.flashmessages.' . $pluginNamespace;
917
        }
918
919
        return $this->internalFlashMessageService->getMessageQueueByIdentifier($identifier);
920
    }
921
922
    /**
923
     * Initialize the controller context
924
     *
925
     * @return \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext ControllerContext to be passed to the view
926
     *
927
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
928
     */
929
    protected function buildControllerContext()
930
    {
931
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext */
932
        $controllerContext = $this->objectManager->get(ControllerContext::class);
933
        $controllerContext->setRequest($this->request);
934
        if ($this->arguments !== null) {
935
            $controllerContext->setArguments($this->arguments);
936
        }
937
        $controllerContext->setUriBuilder($this->uriBuilder);
938
939
        return $controllerContext;
940
    }
941
942
    /**
943
     * Forwards the request to another action and / or controller.
944
     *
945
     * Request is directly transferred to the other action / controller
946
     * without the need for a new request.
947
     *
948
     * @param string $actionName Name of the action to forward to
949
     * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
950
     * @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
951
     * @param array|null $arguments Arguments to pass to the target action
952
     * @throws StopActionException
953
     * @see redirect()
954
     * @deprecated since TYPO3 11.0, will be removed in 12.0
955
     */
956
    public function forward($actionName, $controllerName = null, $extensionName = null, array $arguments = null)
957
    {
958
        trigger_error(
959
            sprintf('Method %s is deprecated. To forward to another action, return a %s instead.', __METHOD__, ForwardResponse::class),
960
            E_USER_DEPRECATED
961
        );
962
963
        $this->request->setDispatched(false);
964
        $this->request->setControllerActionName($actionName);
965
966
        if ($controllerName !== null) {
967
            $this->request->setControllerName($controllerName);
968
        }
969
970
        if ($extensionName !== null) {
971
            $this->request->setControllerExtensionName($extensionName);
972
        }
973
974
        if ($arguments !== null) {
975
            $this->request->setArguments($arguments);
976
        }
977
        throw new StopActionException('forward', 1476045801);
978
    }
979
980
    /**
981
     * Redirects the request to another action and / or controller.
982
     *
983
     * Redirect will be sent to the client which then performs another request to the new URI.
984
     *
985
     * NOTE: This method only supports web requests and will thrown an exception
986
     * if used with other request types.
987
     *
988
     * @param string $actionName Name of the action to forward to
989
     * @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
990
     * @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
991
     * @param array|null $arguments Arguments to pass to the target action
992
     * @param int|null $pageUid Target page uid. If NULL, the current page uid is used
993
     * @param int $delay (optional) The delay in seconds. Default is no delay.
994
     * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
995
     * @throws StopActionException
996
     */
997
    protected function redirect($actionName, $controllerName = null, $extensionName = null, array $arguments = null, $pageUid = null, $delay = 0, $statusCode = 303)
998
    {
999
        if ($controllerName === null) {
1000
            $controllerName = $this->request->getControllerName();
1001
        }
1002
        $this->uriBuilder->reset()->setCreateAbsoluteUri(true);
1003
        if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
1004
            $this->uriBuilder->setTargetPageUid((int)$pageUid);
1005
        }
1006
        if (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
1007
            $this->uriBuilder->setAbsoluteUriScheme('https');
1008
        }
1009
        $uri = $this->uriBuilder->uriFor($actionName, $arguments, $controllerName, $extensionName);
1010
        $this->redirectToUri($uri, $delay, $statusCode);
1011
    }
1012
1013
    /**
1014
     * Redirects the web request to another uri.
1015
     *
1016
     * NOTE: This method only supports web requests and will thrown an exception if used with other request types.
1017
     *
1018
     * @param mixed $uri A string representation of a URI
1019
     * @param int $delay (optional) The delay in seconds. Default is no delay.
1020
     * @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
1021
     * @throws StopActionException
1022
     */
1023
    protected function redirectToUri($uri, $delay = 0, $statusCode = 303)
1024
    {
1025
        $this->objectManager->get(CacheService::class)->clearCachesOfRegisteredPageIds();
1026
1027
        $uri = $this->addBaseUriIfNecessary($uri);
1028
        $escapedUri = htmlentities($uri, ENT_QUOTES, 'utf-8');
1029
1030
        $response = new HtmlResponse(
1031
            '<html><head><meta http-equiv="refresh" content="' . (int)$delay . ';url=' . $escapedUri . '"/></head></html>',
1032
            $statusCode,
1033
            [
1034
                'Location' => (string)$uri
1035
            ]
1036
        );
1037
1038
        // Avoid caching the plugin when we issue a redirect response
1039
        // This means that even when an action is configured as cachable
1040
        // we avoid the plugin to be cached, but keep the page cache untouched
1041
        $contentObject = $this->configurationManager->getContentObject();
1042
        if ($contentObject->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER) {
0 ignored issues
show
introduced by
The condition $contentObject->getUserO...nderer::OBJECTTYPE_USER is always false.
Loading history...
1043
            $contentObject->convertToUserIntObject();
1044
        }
1045
1046
        throw new StopActionException('redirectToUri', 1476045828, null, $response);
1047
    }
1048
1049
    /**
1050
     * Adds the base uri if not already in place.
1051
     *
1052
     * @param string $uri The URI
1053
     * @return string
1054
     *
1055
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
1056
     */
1057
    protected function addBaseUriIfNecessary($uri)
1058
    {
1059
        return GeneralUtility::locationHeaderUrl((string)$uri);
1060
    }
1061
1062
    /**
1063
     * Sends the specified HTTP status immediately.
1064
     *
1065
     * NOTE: This method only supports web requests and will thrown an exception if used with other request types.
1066
     *
1067
     * @param int $statusCode The HTTP status code
1068
     * @param string $statusMessage A custom HTTP status message
1069
     * @param string $content Body content which further explains the status
1070
     * @throws StopActionException
1071
     */
1072
    public function throwStatus($statusCode, $statusMessage = null, $content = null)
1073
    {
1074
        if ($content === null) {
1075
            $content = $statusCode . ' ' . $statusMessage;
1076
        }
1077
1078
        $response = new \TYPO3\CMS\Core\Http\Response($content);
1079
1080
        throw new StopActionException('throwStatus', 1476045871, null, $response);
1081
    }
1082
1083
    /**
1084
     * Maps arguments delivered by the request object to the local controller arguments.
1085
     *
1086
     * @throws Exception\RequiredArgumentMissingException
1087
     *
1088
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
1089
     */
1090
    protected function mapRequestArgumentsToControllerArguments()
1091
    {
1092
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
1093
        foreach ($this->arguments as $argument) {
1094
            $argumentName = $argument->getName();
1095
            if ($this->request->hasArgument($argumentName)) {
1096
                $this->setArgumentValue($argument, $this->request->getArgument($argumentName));
1097
            } elseif ($argument->isRequired()) {
1098
                throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
1099
            }
1100
        }
1101
    }
1102
1103
    /**
1104
     * @param Argument $argument
1105
     * @param mixed $rawValue
1106
     */
1107
    private function setArgumentValue(Argument $argument, $rawValue): void
1108
    {
1109
        if ($rawValue === null) {
1110
            $argument->setValue(null);
1111
            return;
1112
        }
1113
        $dataType = $argument->getDataType();
1114
        if (is_object($rawValue) && $rawValue instanceof $dataType) {
1115
            $argument->setValue($rawValue);
1116
            return;
1117
        }
1118
        $this->propertyMapper->resetMessages();
1119
        try {
1120
            $argument->setValue(
1121
                $this->propertyMapper->convert(
1122
                    $rawValue,
1123
                    $dataType,
1124
                    $argument->getPropertyMappingConfiguration()
1125
                )
1126
            );
1127
        } catch (TargetNotFoundException $e) {
1128
            // for optional arguments no exception is thrown.
1129
            if ($argument->isRequired()) {
1130
                throw $e;
1131
            }
1132
        }
1133
        $argument->getValidationResults()->merge($this->propertyMapper->getMessages());
1134
    }
1135
1136
    /**
1137
     * Returns a response object with either the given html string or the current rendered view as content.
1138
     *
1139
     * @param string|null $html
1140
     * @return ResponseInterface
1141
     */
1142
    protected function htmlResponse(string $html = null): ResponseInterface
1143
    {
1144
        return $this->responseFactory->createHtmlResponse(
1145
            $html ?? $this->view->render()
1146
        );
1147
    }
1148
}
1149