Completed
Push — master ( 38f43c...d78f52 )
by
unknown
14:23
created

ActionController::resolveActionMethodName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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