Passed
Push — master ( 36133f...970d9b )
by
unknown
13:42
created

ActionController::injectResponseFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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