Passed
Push — master ( a73d8f...d183e8 )
by
unknown
14:04
created

ActionController::forward()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1030
        $response = $this->responseFactory->createResponse((int)$statusCode, /** @scrutinizer ignore-type */ $statusMessage);
Loading history...
1031
        $response->getBody()->write($content);
1032
        throw new PropagateResponseException($response, 1476045871);
1033
    }
1034
1035
    /**
1036
     * Maps arguments delivered by the request object to the local controller arguments.
1037
     *
1038
     * @throws Exception\RequiredArgumentMissingException
1039
     *
1040
     * @internal only to be used within Extbase, not part of TYPO3 Core API.
1041
     */
1042
    protected function mapRequestArgumentsToControllerArguments()
1043
    {
1044
        /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */
1045
        foreach ($this->arguments as $argument) {
1046
            $argumentName = $argument->getName();
1047
            if ($this->request->hasArgument($argumentName)) {
1048
                $this->setArgumentValue($argument, $this->request->getArgument($argumentName));
1049
            } elseif ($argument->isRequired()) {
1050
                throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
1051
            }
1052
        }
1053
    }
1054
1055
    /**
1056
     * @param Argument $argument
1057
     * @param mixed $rawValue
1058
     */
1059
    private function setArgumentValue(Argument $argument, $rawValue): void
1060
    {
1061
        if ($rawValue === null) {
1062
            $argument->setValue(null);
1063
            return;
1064
        }
1065
        $dataType = $argument->getDataType();
1066
        if (is_object($rawValue) && $rawValue instanceof $dataType) {
1067
            $argument->setValue($rawValue);
1068
            return;
1069
        }
1070
        $this->propertyMapper->resetMessages();
1071
        try {
1072
            $argument->setValue(
1073
                $this->propertyMapper->convert(
1074
                    $rawValue,
1075
                    $dataType,
1076
                    $argument->getPropertyMappingConfiguration()
1077
                )
1078
            );
1079
        } catch (TargetNotFoundException $e) {
1080
            // for optional arguments no exception is thrown.
1081
            if ($argument->isRequired()) {
1082
                throw $e;
1083
            }
1084
        }
1085
        $argument->getValidationResults()->merge($this->propertyMapper->getMessages());
1086
    }
1087
1088
    /**
1089
     * Returns a response object with either the given html string or the current rendered view as content.
1090
     *
1091
     * @param string|null $html
1092
     * @return ResponseInterface
1093
     */
1094
    protected function htmlResponse(string $html = null): ResponseInterface
1095
    {
1096
        $response = $this->responseFactory->createResponse()
1097
            ->withHeader('Content-Type', 'text/html; charset=utf-8');
1098
        $response->getBody()->write($html ?? $this->view->render());
1099
        return $response;
1100
    }
1101
}
1102