Passed
Push — master ( 9ded7c...48eb23 )
by
unknown
12:55
created

ActionController::canProcessRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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