Passed
Push — master ( 5a8a4d...78ad2e )
by
unknown
29:28 queued 16:33
created

ActionController::callActionMethod()   C

Complexity

Conditions 13
Paths 28

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 24
c 1
b 0
f 0
nc 28
nop 0
dl 0
loc 43
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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