TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,967 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionAuthorizationDeniedEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
|
||||
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionRateLimitResponseEvent;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentNameException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMapper;
|
||||
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
|
||||
use TYPO3\CMS\Extbase\Security\HashScope;
|
||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||
use TYPO3\CMS\Extbase\Service\FileHandlingService;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
|
||||
|
||||
/**
|
||||
* A multi action controller. This is by far the most common base class for Controllers.
|
||||
*/
|
||||
abstract class ActionController implements ControllerInterface
|
||||
{
|
||||
protected ResponseFactoryInterface $responseFactory;
|
||||
protected StreamFactoryInterface $streamFactory;
|
||||
protected HashService $hashService;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ReflectionService $reflectionService;
|
||||
|
||||
/**
|
||||
* The current view, as resolved by resolveView()
|
||||
*/
|
||||
protected ViewInterface $view;
|
||||
|
||||
/**
|
||||
* The default view class to use. Keep this 'null' for default fluid
|
||||
* view, or set to 'JsonView::class' or some inheriting class.
|
||||
*
|
||||
* @var class-string|null
|
||||
*/
|
||||
protected ?string $defaultViewObjectName = null;
|
||||
|
||||
/**
|
||||
* Name of the action method
|
||||
* @var non-empty-string
|
||||
* @internal
|
||||
*/
|
||||
protected string $actionMethodName = 'indexAction';
|
||||
|
||||
/**
|
||||
* Name of the special error action method which is called in case of errors
|
||||
*/
|
||||
protected string $errorMethodName = 'errorAction';
|
||||
|
||||
protected MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService;
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
protected FileHandlingService $fileHandlingService;
|
||||
protected RequestInterface $request;
|
||||
protected UriBuilder $uriBuilder;
|
||||
protected RateLimitRegistry $rateLimitRegistry;
|
||||
protected AuthorizeRegistry $authorizeRegistry;
|
||||
|
||||
/**
|
||||
* Contains the settings of the current extension
|
||||
*/
|
||||
protected array $settings;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ValidatorResolver $validatorResolver;
|
||||
|
||||
private ViewFactoryInterface $viewFactory;
|
||||
|
||||
protected Arguments $arguments;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected ConfigurationManagerInterface $configurationManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private PropertyMapper $propertyMapper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private FlashMessageService $internalFlashMessageService;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private ExtensionService $internalExtensionService;
|
||||
|
||||
final public function injectResponseFactory(ResponseFactoryInterface $responseFactory): void
|
||||
{
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
final public function injectStreamFactory(StreamFactoryInterface $streamFactory): void
|
||||
{
|
||||
$this->streamFactory = $streamFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
|
||||
{
|
||||
$this->configurationManager = $configurationManager;
|
||||
$this->settings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
|
||||
$this->arguments = GeneralUtility::makeInstance(Arguments::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectValidatorResolver(ValidatorResolver $validatorResolver): void
|
||||
{
|
||||
$this->validatorResolver = $validatorResolver;
|
||||
}
|
||||
|
||||
final public function injectViewFactory(ViewFactoryInterface $viewFactory): void
|
||||
{
|
||||
$this->viewFactory = $viewFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectReflectionService(ReflectionService $reflectionService): void
|
||||
{
|
||||
$this->reflectionService = $reflectionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectHashService(HashService $hashService): void
|
||||
{
|
||||
$this->hashService = $hashService;
|
||||
}
|
||||
|
||||
public function injectMvcPropertyMappingConfigurationService(MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService): void
|
||||
{
|
||||
$this->mvcPropertyMappingConfigurationService = $mvcPropertyMappingConfigurationService;
|
||||
}
|
||||
|
||||
public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function injectFileHandlingService(FileHandlingService $fileHandlingService): void
|
||||
{
|
||||
$this->fileHandlingService = $fileHandlingService;
|
||||
}
|
||||
|
||||
public function injectRateLimitRegistry(RateLimitRegistry $rateLimitRegistry): void
|
||||
{
|
||||
$this->rateLimitRegistry = $rateLimitRegistry;
|
||||
}
|
||||
|
||||
public function injectAuthorizeRegistry(AuthorizeRegistry $authorizeRegistry): void
|
||||
{
|
||||
$this->authorizeRegistry = $authorizeRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function injectPropertyMapper(PropertyMapper $propertyMapper): void
|
||||
{
|
||||
$this->propertyMapper = $propertyMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final public function injectInternalFlashMessageService(FlashMessageService $flashMessageService): void
|
||||
{
|
||||
$this->internalFlashMessageService = $flashMessageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final public function injectInternalExtensionService(ExtensionService $extensionService): void
|
||||
{
|
||||
$this->internalExtensionService = $extensionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller before invoking an action method.
|
||||
*
|
||||
* Override this method to solve tasks which all actions have in
|
||||
* common.
|
||||
*/
|
||||
protected function initializeAction(): void {}
|
||||
|
||||
/**
|
||||
* Implementation of the arguments initialization in the action controller:
|
||||
* Automatically registers arguments of the current action
|
||||
*
|
||||
* Don't override this method - use initializeAction() instead.
|
||||
*
|
||||
* @throws InvalidArgumentTypeException
|
||||
* @see initializeArguments()
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function initializeActionMethodArguments(): void
|
||||
{
|
||||
$methodParameters = $this->reflectionService
|
||||
->getClassSchema(static::class)
|
||||
->getMethod($this->actionMethodName)->getParameters();
|
||||
|
||||
foreach ($methodParameters as $parameterName => $parameter) {
|
||||
$dataType = null;
|
||||
if ($parameter->getType() !== null) {
|
||||
$dataType = $parameter->getType();
|
||||
} elseif ($parameter->isArray()) {
|
||||
$dataType = 'array';
|
||||
}
|
||||
if ($dataType === null) {
|
||||
throw new InvalidArgumentTypeException('The argument type for parameter $' . $parameterName . ' of method ' . static::class . '->' . $this->actionMethodName . '() could not be detected.', 1253175643);
|
||||
}
|
||||
$defaultValue = $parameter->hasDefaultValue() ? $parameter->getDefaultValue() : null;
|
||||
$this->arguments->addNewArgument($parameterName, $dataType, !$parameter->isOptional(), $defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the needed validators to the Arguments:
|
||||
*
|
||||
* - Validators checking the data type from the param annotation
|
||||
* - Custom validators specified with #[Validate] attributes.
|
||||
* - Model-based validators (#[Validate] attributes in the model)
|
||||
* - Custom model validator classes
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function initializeActionMethodValidators(): void
|
||||
{
|
||||
if ($this->arguments->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$classSchemaMethod = $this->reflectionService->getClassSchema(static::class)->getMethod($this->actionMethodName);
|
||||
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$classSchemaMethodParameter = $classSchemaMethod->getParameter($argument->getName());
|
||||
// At this point validation is skipped if there is an #[IgnoreValidation] attribute.
|
||||
// @todo: IgnoreValidation attributes could be evaluated in the ClassSchema and result in
|
||||
// no validators being applied to the method parameter.
|
||||
if ($classSchemaMethodParameter->ignoreValidation()) {
|
||||
continue;
|
||||
}
|
||||
/** @var ConjunctionValidator $validator */
|
||||
$validator = $this->validatorResolver->createValidator(ConjunctionValidator::class);
|
||||
foreach ($classSchemaMethodParameter->getValidators() as $validatorDefinition) {
|
||||
if (isset($validatorDefinition['constraint'])) {
|
||||
$validatorInstance = $validatorDefinition['constraint'];
|
||||
} else {
|
||||
$validatorInstance = $this->validatorResolver->createValidator(
|
||||
$validatorDefinition['className'],
|
||||
$validatorDefinition['options'],
|
||||
$this->request,
|
||||
);
|
||||
}
|
||||
if ($validatorInstance !== null) {
|
||||
$validator->addValidator($validatorInstance);
|
||||
}
|
||||
}
|
||||
$baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction(
|
||||
$argument->getDataType(),
|
||||
$this->request
|
||||
);
|
||||
if ($baseValidatorConjunction->count() > 0) {
|
||||
$validator->addValidator($baseValidatorConjunction);
|
||||
}
|
||||
$argument->setValidator($validator);
|
||||
}
|
||||
}
|
||||
|
||||
protected function initializeStateFromExtbaseRequestParameters(): void
|
||||
{
|
||||
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
||||
if (!$extbaseRequestParameters instanceof ExtbaseRequestParameters) {
|
||||
return;
|
||||
}
|
||||
$flashMessageQueue = $this->getFlashMessageQueue();
|
||||
foreach ($extbaseRequestParameters->getOriginalFlashMessages() as $flashMessage) {
|
||||
$flashMessage->setStoreInSession(false);
|
||||
$flashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an incoming request and returns a response object
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function processRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var Request $request */
|
||||
$this->request = $request;
|
||||
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$this->uriBuilder->setRequest($request);
|
||||
$this->actionMethodName = $this->resolveActionMethodName();
|
||||
$this->initializeActionMethodArguments();
|
||||
$this->initializeActionMethodValidators();
|
||||
$this->initializeStateFromExtbaseRequestParameters();
|
||||
$this->mvcPropertyMappingConfigurationService->initializePropertyMappingConfigurationFromRequest($request, $this->arguments);
|
||||
$this->fileHandlingService->initializeFileUploadConfigurationsFromRequest($request, $this->arguments);
|
||||
$this->initializeAction();
|
||||
$actionInitializationMethodName = 'initialize' . ucfirst($this->actionMethodName);
|
||||
/** @var callable|null $callable */
|
||||
$callable = [$this, $actionInitializationMethodName];
|
||||
if (is_callable($callable)) {
|
||||
$callable();
|
||||
}
|
||||
$this->mapRequestArgumentsToControllerArguments();
|
||||
$this->view = $this->resolveView();
|
||||
if (method_exists($this, 'initializeView')) {
|
||||
// @todo: We may want to get rid of this and declare actions should actively create own
|
||||
// views using ViewFactoryInterface instead. See comment on resolveView() below.
|
||||
// Currently, this method is pretty much only helpful in 'xclass' scenarios,
|
||||
// since actions can already do whatever happens here within their action body.
|
||||
$this->initializeView($this->view);
|
||||
}
|
||||
$response = $this->callActionMethod($request);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and checks the current action method name
|
||||
*
|
||||
* @throws NoSuchActionException if the action specified in the request object does not exist (and if there's no default action either).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function resolveActionMethodName(): string
|
||||
{
|
||||
$actionMethodName = $this->request->getControllerActionName() . 'Action';
|
||||
if (!method_exists($this, $actionMethodName)) {
|
||||
throw new NoSuchActionException('An action "' . $actionMethodName . '" does not exist in controller "' . static::class . '".', 1186669086);
|
||||
}
|
||||
return $actionMethodName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified action method and passes the arguments.
|
||||
*
|
||||
* If the action returns a string, it is appended to the content in the
|
||||
* response object. If the action doesn't return anything and a valid
|
||||
* view exists, the view is rendered automatically.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function callActionMethod(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
// incoming request is not needed yet but can be passed into the action in the future like in symfony
|
||||
// todo: support this via method-reflection
|
||||
|
||||
$this->fileHandlingService->initializeFileUploadDeletionConfigurationsFromRequest($request, $this->arguments);
|
||||
$validationResult = $this->arguments->validate();
|
||||
if (!$validationResult->hasErrors()) {
|
||||
$preparedArguments = [];
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$this->fileHandlingService->applyDeletionsToArgument($argument);
|
||||
$this->fileHandlingService->mapUploadedFilesToArgument($argument);
|
||||
$preparedArguments[] = $argument->getValue();
|
||||
}
|
||||
|
||||
if (($authorizeResponse = $this->performAuthorizationChecks($request, $preparedArguments)) !== null) {
|
||||
return $authorizeResponse;
|
||||
}
|
||||
|
||||
if (($rateLimitResponse = $this->handleRateLimit($request)) !== null) {
|
||||
return $rateLimitResponse;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(new BeforeActionCallEvent(static::class, $this->actionMethodName, $preparedArguments, $this->request));
|
||||
$actionResult = $this->{$this->actionMethodName}(...$preparedArguments);
|
||||
} else {
|
||||
$actionResult = $this->{$this->errorMethodName}();
|
||||
}
|
||||
|
||||
if ($actionResult instanceof ResponseInterface) {
|
||||
return $actionResult;
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
'Controller action %s did not return an instance of %s.',
|
||||
static::class . '::' . $this->actionMethodName,
|
||||
ResponseInterface::class
|
||||
),
|
||||
1638554283
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a view for the current action.
|
||||
*
|
||||
* @internal
|
||||
* @todo We may want to decide in extbase to go away from the automatic view preparation via
|
||||
* processRequest() and this method for actions. We could very well postulate actions
|
||||
* should take care of creating "their" view on their own using a ViewFactoryInterface
|
||||
* implementation, similar to what is done with request creation already (which needs
|
||||
* further work, too), and have a helper in this class to easily create a standard view.
|
||||
* This would dissolve the ugly $this->defaultViewObjectName property, which is more
|
||||
* a burden than helpful since controllers then need to have an initializeFooAction()
|
||||
* just to set this property when different actions want different views. Also, it does
|
||||
* not allow actions to have no view prepared at all, for instance when they just want to
|
||||
* create a json response by json_encode()'ing stuff. We should look at this in v14, which
|
||||
* renders property defaultViewObjectName even more useless.
|
||||
*/
|
||||
protected function resolveView(): ViewInterface
|
||||
{
|
||||
if ($this->defaultViewObjectName !== null && is_a($this->defaultViewObjectName, JsonView::class, true)) {
|
||||
// @todo: JsonView is a very extbase specific thing. It comes with setVariablesToRender() and
|
||||
// setConfiguration(). We don't let it run through a factory here, since consumers need
|
||||
// to deal with these specialities anyways. Often, one would rather want to either have
|
||||
// an own view prepared in a controller (or action), or have a custom factory that deals
|
||||
// with stuff and returns a ViewInterface, or directly json_encode() data in an action.
|
||||
// This is related to the comment above, too.
|
||||
$view = new JsonView();
|
||||
$view->assign('settings', $this->settings);
|
||||
return $view;
|
||||
}
|
||||
$configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
|
||||
$extensionKey = $this->request->getControllerExtensionKey();
|
||||
$templateRootPaths = $this->addDefaultPathToPaths($configuration['view']['templateRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Templates/');
|
||||
$layoutRootPaths = $this->addDefaultPathToPaths($configuration['view']['layoutRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Layouts/');
|
||||
$partialRootPaths = $this->addDefaultPathToPaths($configuration['view']['partialRootPaths'] ?? [], 'EXT:' . $extensionKey . '/Resources/Private/Partials/');
|
||||
if ($this->defaultViewObjectName === null) {
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templateRootPaths: $templateRootPaths,
|
||||
partialRootPaths: $partialRootPaths,
|
||||
layoutRootPaths: $layoutRootPaths,
|
||||
request: $this->request,
|
||||
format: $this->request->getFormat(),
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
if ($view instanceof FluidViewAdapter) {
|
||||
// This specific magic is tailored to Fluid. Ignore if we're not dealing with a fluid view here.
|
||||
$renderingContext = $view->getRenderingContext();
|
||||
$renderingContext->setControllerName($this->request->getControllerName());
|
||||
$renderingContext->setControllerAction($this->request->getControllerActionName());
|
||||
}
|
||||
$view->assign('settings', $this->settings);
|
||||
return $view;
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'The only allowed values for $this->defaultViewObjectName are null or extbase JsonView::class.'
|
||||
. ' Please create an own view in your action if that is not sufficient, or inject a different'
|
||||
. ' ViewFactoryInterface',
|
||||
1729780151
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds extbase's default template path to the configured list of
|
||||
* template paths. The default path is usually used as a fallback if
|
||||
* no paths are specified or if the template cannot be found in any
|
||||
* of the configured paths. However, if the default path is already
|
||||
* present in the configured paths, the specified position takes
|
||||
* precedence. This allows the default path to be "moved" within
|
||||
* the list of paths via configuration.
|
||||
*
|
||||
* @return string[]
|
||||
* @internal
|
||||
*/
|
||||
protected function addDefaultPathToPaths(mixed $paths, string $defaultPath): array
|
||||
{
|
||||
if (!is_array($paths) || empty($paths)) {
|
||||
$paths = [$defaultPath];
|
||||
} else {
|
||||
$paths = ArrayUtility::sortArrayWithIntegerKeys($paths);
|
||||
if (!in_array($defaultPath, $paths)) {
|
||||
$paths = array_merge([$defaultPath], $paths);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* A special action which is called if the originally intended action could
|
||||
* not be called, for example if the arguments were not valid.
|
||||
*
|
||||
* The default implementation sets a flash message, request errors and forwards back
|
||||
* to the originating action. This is suitable for most actions dealing with form input.
|
||||
*/
|
||||
protected function errorAction(): ResponseInterface
|
||||
{
|
||||
if (($response = $this->forwardToReferringRequest()) !== null) {
|
||||
if ($response instanceof ForwardResponse) {
|
||||
// Add flash messages to queue
|
||||
$this->addErrorFlashMessage();
|
||||
// Extract all pending flash messages out of th queue and ensure they
|
||||
// are passed along the response but without invoking the session.
|
||||
$flashMessages = $this->getFlashMessageQueue()->getAllMessagesAndFlush();
|
||||
$response = $response->withFlashMessages(...$flashMessages);
|
||||
}
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
$response = $this->htmlResponse($this->getFlattenedValidationErrorMessage());
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error occurred during this request, this adds a flash message describing the error to the flash
|
||||
* message container.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function addErrorFlashMessage(): void
|
||||
{
|
||||
$errorFlashMessage = $this->getErrorFlashMessage();
|
||||
if (is_string($errorFlashMessage)) {
|
||||
$this->addFlashMessage($errorFlashMessage, '', ContextualFeedbackSeverity::ERROR, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A template method for displaying custom error flash messages, or to
|
||||
* display no flash message at all on errors. Override this to customize
|
||||
* the flash message in your action controller.
|
||||
*
|
||||
* Returns either the flash message or "false" if no flash message should be set
|
||||
*/
|
||||
protected function getErrorFlashMessage(): bool|string
|
||||
{
|
||||
return 'An error occurred while trying to call ' . static::class . '->' . $this->actionMethodName . '()';
|
||||
}
|
||||
|
||||
/**
|
||||
* If information on the request before the current request was sent, this method forwards back
|
||||
* to the originating request. This effectively ends processing of the current request, so do not
|
||||
* call this method before you have finished the necessary business logic!
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function forwardToReferringRequest(): ?ResponseInterface
|
||||
{
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $this->request->getAttribute('extbase');
|
||||
$referringRequestArguments = $extbaseRequestParameters->getInternalArgument('__referrer') ?? null;
|
||||
if (is_string($referringRequestArguments['@request'] ?? null)) {
|
||||
$referrerArray = json_decode(
|
||||
$this->hashService->validateAndStripHmac($referringRequestArguments['@request'], HashScope::ReferringRequest->prefix(), HashAlgo::SHA3_256),
|
||||
true
|
||||
);
|
||||
$arguments = [];
|
||||
if (is_string($referringRequestArguments['arguments'] ?? null)) {
|
||||
/* @phpstan-ignore unserialize.allowedClasses.insecure (Integrity check already happens via HMAC validation) */
|
||||
$arguments = unserialize(
|
||||
base64_decode($this->hashService->validateAndStripHmac(
|
||||
$referringRequestArguments['arguments'],
|
||||
HashScope::ReferringArguments->prefix(),
|
||||
HashAlgo::SHA3_256
|
||||
)),
|
||||
['allowed_classes' => true]
|
||||
);
|
||||
}
|
||||
$replacedArguments = array_replace_recursive($arguments, $referrerArray);
|
||||
$nonExtbaseBaseArguments = [];
|
||||
foreach ($replacedArguments as $argumentName => $argumentValue) {
|
||||
if (!is_string($argumentName) || $argumentName === '') {
|
||||
throw new InvalidArgumentNameException('Invalid argument name.', 1623940985);
|
||||
}
|
||||
if (str_starts_with($argumentName, '__')
|
||||
|| in_array($argumentName, ['@extension', '@subpackage', '@controller', '@action', '@format'], true)
|
||||
) {
|
||||
// Don't handle internalArguments here, not needed for forwardResponse()
|
||||
continue;
|
||||
}
|
||||
$nonExtbaseBaseArguments[$argumentName] = $argumentValue;
|
||||
}
|
||||
return (new ForwardResponse((string)($replacedArguments['@action'] ?? 'index')))
|
||||
->withControllerName((string)($replacedArguments['@controller'] ?? 'Standard'))
|
||||
->withExtensionName((string)($replacedArguments['@extension'] ?? ''))
|
||||
->withArguments($nonExtbaseBaseArguments)
|
||||
->withArgumentsValidationResult($this->arguments->validate());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with a basic error message about validation failure.
|
||||
* We may add all validation error messages to a log file in the future,
|
||||
* but for security reasons (@see #54074) we do not return these here.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function getFlattenedValidationErrorMessage(): string
|
||||
{
|
||||
return 'Validation failed while trying to call ' . static::class . '->' . $this->actionMethodName . '().' . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Message object and adds it to the FlashMessageQueue.
|
||||
*
|
||||
* @throws \InvalidArgumentException if the message body is no string
|
||||
* @see FlashMessage
|
||||
*/
|
||||
public function addFlashMessage(
|
||||
string $messageBody,
|
||||
string $messageTitle = '',
|
||||
ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK,
|
||||
bool $storeInSession = true
|
||||
): void {
|
||||
$flashMessage = new FlashMessage(
|
||||
$messageBody,
|
||||
$messageTitle,
|
||||
$severity,
|
||||
$storeInSession
|
||||
);
|
||||
|
||||
$this->getFlashMessageQueue()->enqueue($flashMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* todo: As soon as the incoming request contains the compiled plugin namespace, extbase will offer a trait to
|
||||
* create a flash message identifier from the current request. Users then should inject the flash message
|
||||
* service themselves if needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function getFlashMessageQueue(?string $identifier = null): FlashMessageQueue
|
||||
{
|
||||
if ($identifier === null) {
|
||||
$pluginNamespace = $this->internalExtensionService->getPluginNamespace(
|
||||
$this->request->getControllerExtensionName(),
|
||||
$this->request->getPluginName()
|
||||
);
|
||||
$identifier = 'extbase.flashmessages.' . $pluginNamespace;
|
||||
}
|
||||
|
||||
return $this->internalFlashMessageService->getMessageQueueByIdentifier($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the request to another action and / or controller.
|
||||
*
|
||||
* Redirect will be sent to the client which then performs another request to the new URI.
|
||||
*
|
||||
* @param string|null $actionName Name of the action to forward to
|
||||
* @param string|null $controllerName Unqualified object name of the controller to forward to. If not specified, the current controller is used.
|
||||
* @param string|null $extensionName Name of the extension containing the controller to forward to. If not specified, the current extension is assumed.
|
||||
* @param array|null $arguments Arguments to pass to the target action
|
||||
* @param int|null $pageUid Target page uid. If NULL, the current page uid is used
|
||||
* @param null $_ (optional) Unused
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
|
||||
*/
|
||||
protected function redirect(
|
||||
?string $actionName,
|
||||
?string $controllerName = null,
|
||||
?string $extensionName = null,
|
||||
?array $arguments = null,
|
||||
?int $pageUid = null,
|
||||
$_ = null,
|
||||
int $statusCode = 303
|
||||
): ResponseInterface {
|
||||
if ($controllerName === null) {
|
||||
$controllerName = $this->request->getControllerName();
|
||||
}
|
||||
$this->uriBuilder->reset()->setCreateAbsoluteUri(true);
|
||||
if (MathUtility::canBeInterpretedAsInteger($pageUid)) {
|
||||
$this->uriBuilder->setTargetPageUid((int)$pageUid);
|
||||
}
|
||||
if ($this->request->getAttribute('normalizedParams')->isHttps()) {
|
||||
$this->uriBuilder->setAbsoluteUriScheme('https');
|
||||
}
|
||||
$uri = $this->uriBuilder->uriFor($actionName, $arguments, $controllerName, $extensionName);
|
||||
return $this->redirectToUri($uri, null, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the web request to another uri.
|
||||
*
|
||||
* @param string|UriInterface $uri A string representation of a URI
|
||||
* @param null $_ (optional) Unused
|
||||
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other"
|
||||
*/
|
||||
protected function redirectToUri(string|UriInterface $uri, $_ = null, int $statusCode = 303): ResponseInterface
|
||||
{
|
||||
$uri = $this->addBaseUriIfNecessary((string)$uri);
|
||||
return new RedirectResponse($uri, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the base uri if not already in place.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function addBaseUriIfNecessary(string $uri): string
|
||||
{
|
||||
return GeneralUtility::locationHeaderUrl($uri, $this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the specified HTTP status immediately and only stops to run back through the middleware stack.
|
||||
* Note: If any other plugin or content or hook is used within a frontend request, this is skipped by design.
|
||||
*
|
||||
* @param int $statusCode The HTTP status code
|
||||
* @param string $statusMessage A custom HTTP status message
|
||||
* @param string|null $content Body content which further explains the status
|
||||
* @throws PropagateResponseException
|
||||
*/
|
||||
public function throwStatus(int $statusCode, string $statusMessage = '', ?string $content = null): never
|
||||
{
|
||||
if ($content === null) {
|
||||
$content = $statusCode . ' ' . $statusMessage;
|
||||
}
|
||||
$response = $this->responseFactory
|
||||
->createResponse($statusCode, $statusMessage)
|
||||
->withBody($this->streamFactory->createStream((string)$content));
|
||||
throw new PropagateResponseException($response, 1476045871);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method processes exceptions that occur due to missing or not found targets or arguments during argument
|
||||
* mapping. Based on configuration settings, either a "page not found" response is triggered or the original
|
||||
* exception is propagated.
|
||||
*
|
||||
* Extension authors can override this function to implement additional/custom argument mapping exception handling
|
||||
*/
|
||||
protected function handleArgumentMappingExceptions(\Exception $exception): void
|
||||
{
|
||||
$configuration = $this->configurationManager->getConfiguration(
|
||||
ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK
|
||||
);
|
||||
|
||||
$handleTargetNotFoundException = $exception instanceof TargetNotFoundException
|
||||
&& (bool)($configuration['mvc']['showPageNotFoundIfTargetNotFoundException'] ?? false);
|
||||
$handleRequiredArgumentMissingException = $exception instanceof RequiredArgumentMissingException
|
||||
&& (bool)($configuration['mvc']['showPageNotFoundIfRequiredArgumentIsMissingException'] ?? false);
|
||||
|
||||
if ($handleTargetNotFoundException || $handleRequiredArgumentMissingException) {
|
||||
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
|
||||
$this->request,
|
||||
$exception->getMessage()
|
||||
);
|
||||
throw new PropagateResponseException($response, 1720242346);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps arguments delivered by the request object to the local controller arguments.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function mapRequestArgumentsToControllerArguments(): void
|
||||
{
|
||||
try {
|
||||
/** @var Argument $argument */
|
||||
foreach ($this->arguments as $argument) {
|
||||
$argumentName = $argument->getName();
|
||||
if ($this->request->hasArgument($argumentName)) {
|
||||
$this->setArgumentValue($argument, $this->request->getArgument($argumentName));
|
||||
} elseif ($argument->isRequired()) {
|
||||
throw new RequiredArgumentMissingException('Required argument "' . $argumentName . '" is not set for ' . $this->request->getControllerObjectName() . '->' . $this->request->getControllerActionName() . '.', 1298012500);
|
||||
}
|
||||
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
$uploadedFiles = $this->request->getUploadedFiles()[$argumentName] ?? [];
|
||||
$argument->setUploadedFiles($uploadedFiles);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->handleArgumentMappingExceptions($exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function setArgumentValue(Argument $argument, mixed $rawValue): void
|
||||
{
|
||||
if ($rawValue === null) {
|
||||
$argument->setValue(null);
|
||||
return;
|
||||
}
|
||||
$dataType = $argument->getDataType();
|
||||
if ($rawValue instanceof $dataType) {
|
||||
$argument->setValue($rawValue);
|
||||
return;
|
||||
}
|
||||
$this->propertyMapper->resetMessages();
|
||||
try {
|
||||
$argument->setValue(
|
||||
$this->propertyMapper->convert(
|
||||
$rawValue,
|
||||
$dataType,
|
||||
$argument->getPropertyMappingConfiguration()
|
||||
)
|
||||
);
|
||||
} catch (TargetNotFoundException $e) {
|
||||
// for optional arguments no exception is thrown.
|
||||
if ($argument->isRequired()) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$argument->getValidationResults()->merge($this->propertyMapper->getMessages());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a response object with either the given html string or the current rendered view as content.
|
||||
*/
|
||||
protected function htmlResponse(?string $html = null): ResponseInterface
|
||||
{
|
||||
return $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->withBody($this->streamFactory->createStream(($html ?? $this->view->render())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a response object with either the given json string or the current rendered
|
||||
* view as content. Mainly to be used for actions / controllers using the JsonView.
|
||||
*/
|
||||
protected function jsonResponse(?string $json = null): ResponseInterface
|
||||
{
|
||||
return $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
->withBody($this->streamFactory->createStream(($json ?? $this->view->render())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rate-limiting for the given action request. Checks if the current request exceeds
|
||||
* a possible defined rate limit for the action method and generates an appropriate response
|
||||
* if the limit is reached.
|
||||
*
|
||||
* @internal
|
||||
* @return ResponseInterface|null The rate-limited response if the limit is exceeded, or null if no rate-limiting applies.
|
||||
*/
|
||||
protected function handleRateLimit(RequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
$rateLimiter = $this->rateLimitRegistry->createLimiter(static::class, $this->actionMethodName, $this->request);
|
||||
if ($rateLimiter === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rateLimit = $this->rateLimitRegistry->getRateLimit(static::class, $this->actionMethodName);
|
||||
$limit = $rateLimiter->consume();
|
||||
if ($limit->isAccepted()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customMessage = null;
|
||||
if ($rateLimit->message !== '') {
|
||||
$customMessage = LocalizationUtility::translate($rateLimit->message, $this->request->getControllerExtensionName());
|
||||
}
|
||||
$message = $customMessage ?? LocalizationUtility::translate('ratelimit.action.defaultmessage', 'extbase');
|
||||
|
||||
$response = $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->withStatus(429)
|
||||
->withBody($this->streamFactory->createStream($message));
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeActionRateLimitResponseEvent($request, static::class, $this->actionMethodName, $rateLimit, $response)
|
||||
);
|
||||
|
||||
return $event->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs authorization checks for actions with the #[Authorize] attribute. If access is denied, a HTTP 403
|
||||
* response is propagated. This behavior can be customized by implementing a event listener for the
|
||||
* {@see BeforeActionAuthorizationDeniedEvent}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function performAuthorizationChecks(RequestInterface $request, array $preparedArguments): ?ResponseInterface
|
||||
{
|
||||
$result = $this->authorizeRegistry->checkAuthorization($this, $this->actionMethodName, $preparedArguments);
|
||||
|
||||
if ($result === null || $result->isAllowed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$message = match ($result->failureReason) {
|
||||
AuthorizationFailureReason::NOT_LOGGED_IN => 'Access denied: Login required',
|
||||
AuthorizationFailureReason::MISSING_GROUP => 'Access denied: Insufficient permissions',
|
||||
AuthorizationFailureReason::CALLBACK_DENIED, null => 'Access denied',
|
||||
};
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeActionAuthorizationDeniedEvent(
|
||||
$request,
|
||||
static::class,
|
||||
$this->actionMethodName,
|
||||
$result->failedAttribute,
|
||||
$result->failureReason,
|
||||
)
|
||||
);
|
||||
|
||||
if (!$event->getResponse()) {
|
||||
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
|
||||
$this->request,
|
||||
$message,
|
||||
[
|
||||
'code' => PageAccessFailureReasons::ACCESS_DENIED_GENERAL,
|
||||
]
|
||||
);
|
||||
throw new PropagateResponseException($response, 1761287264);
|
||||
}
|
||||
|
||||
return $event->getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use TYPO3\CMS\Core\Http\UploadedFile;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* A controller argument
|
||||
*/
|
||||
class Argument
|
||||
{
|
||||
protected MvcPropertyMappingConfiguration $propertyMappingConfiguration;
|
||||
protected FileHandlingServiceConfiguration $fileHandlingServiceConfiguration;
|
||||
protected string $name = '';
|
||||
protected string $shortName = '';
|
||||
protected string $dataType = '';
|
||||
protected bool $isRequired = false;
|
||||
protected mixed $value = null;
|
||||
private bool $hasBeenValidated = false;
|
||||
|
||||
/**
|
||||
* Uploaded files for the argument
|
||||
* @var array<string, UploadedFileInterface|list<UploadedFileInterface>>
|
||||
*/
|
||||
protected array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* Default value. Used if argument is optional.
|
||||
*/
|
||||
protected mixed $defaultValue = null;
|
||||
|
||||
/**
|
||||
* A custom validator, used supplementary to the base validation
|
||||
*/
|
||||
protected ?ValidatorInterface $validator = null;
|
||||
|
||||
/**
|
||||
* The validation results. This can be asked if the argument has errors.
|
||||
*/
|
||||
protected Result $validationResults;
|
||||
|
||||
/**
|
||||
* Constructs this controller argument
|
||||
*
|
||||
* @throws \InvalidArgumentException if $name is empty string
|
||||
*/
|
||||
public function __construct(string $name, string $dataType)
|
||||
{
|
||||
if ($name === '') {
|
||||
throw new \InvalidArgumentException('$name must be a non-empty string.', 1232551853);
|
||||
}
|
||||
$this->name = $name;
|
||||
$this->dataType = TypeHandlingUtility::normalizeType($dataType);
|
||||
|
||||
$this->validationResults = new Result();
|
||||
$this->propertyMappingConfiguration = GeneralUtility::makeInstance(MvcPropertyMappingConfiguration::class);
|
||||
$this->fileHandlingServiceConfiguration = GeneralUtility::makeInstance(FileHandlingServiceConfiguration::class);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if $shortName is not a character
|
||||
*/
|
||||
public function setShortName(string $shortName): Argument
|
||||
{
|
||||
if (strlen($shortName) !== 1) {
|
||||
throw new \InvalidArgumentException('$shortName must be a single character or NULL', 1195824959);
|
||||
}
|
||||
$this->shortName = $shortName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getShortName(): string
|
||||
{
|
||||
return $this->shortName;
|
||||
}
|
||||
|
||||
public function getDataType(): string
|
||||
{
|
||||
return $this->dataType;
|
||||
}
|
||||
|
||||
public function setRequired(bool $required): Argument
|
||||
{
|
||||
$this->isRequired = $required;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->isRequired;
|
||||
}
|
||||
|
||||
public function setDefaultValue(mixed $defaultValue): Argument
|
||||
{
|
||||
$this->defaultValue = $defaultValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefaultValue(): mixed
|
||||
{
|
||||
return $this->defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom validator which is used supplementary to the base validation
|
||||
*/
|
||||
public function setValidator(ValidatorInterface $validator): Argument
|
||||
{
|
||||
$this->validator = $validator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidator(): ?ValidatorInterface
|
||||
{
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
public function setValue(mixed $rawValue): Argument
|
||||
{
|
||||
$this->value = $rawValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): mixed
|
||||
{
|
||||
if ($this->value === null) {
|
||||
return $this->defaultValue;
|
||||
}
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Property Mapping Configuration used for this argument; can be used by the initialize*action to modify the Property Mapping.
|
||||
*/
|
||||
public function getPropertyMappingConfiguration(): MvcPropertyMappingConfiguration
|
||||
{
|
||||
return $this->propertyMappingConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FileHandlingServiceConfiguration used for this argument; can be used by the
|
||||
* initialize*action to modify the file upload configuration for properties.
|
||||
*/
|
||||
public function getFileHandlingServiceConfiguration(): FileHandlingServiceConfiguration
|
||||
{
|
||||
return $this->fileHandlingServiceConfiguration;
|
||||
}
|
||||
|
||||
public function getUploadedFiles(): array
|
||||
{
|
||||
return $this->uploadedFiles;
|
||||
}
|
||||
|
||||
public function setUploadedFiles(array $uploadedFiles): void
|
||||
{
|
||||
$this->uploadedFiles = $uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool TRUE if the argument is valid, FALSE otherwise
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return !$this->validate()->hasErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this argument's value
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string)$this->value;
|
||||
}
|
||||
|
||||
public function validate(): Result
|
||||
{
|
||||
if ($this->hasBeenValidated) {
|
||||
return $this->validationResults;
|
||||
}
|
||||
|
||||
if ($this->validator !== null) {
|
||||
$validationMessages = $this->validator->validate($this->value);
|
||||
$this->validationResults->merge($validationMessages);
|
||||
}
|
||||
|
||||
if ($this->fileHandlingServiceConfiguration->hasfileUploadConfigurations()) {
|
||||
$fileOperationValidationResults = $this->fileHandlingServiceConfiguration->validateFileOperations($this);
|
||||
$this->validationResults->merge($fileOperationValidationResults);
|
||||
}
|
||||
|
||||
$this->hasBeenValidated = true;
|
||||
return $this->validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of possible UploadedFile objects for the given property
|
||||
* @return list<UploadedFileInterface>
|
||||
*/
|
||||
public function getUploadedFilesForProperty(string $propertyName): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
$uploadedFiles = ArrayUtility::getValueByPath($this->uploadedFiles, $propertyName, '.');
|
||||
if ($uploadedFiles instanceof UploadedFile) {
|
||||
$result = [$uploadedFiles];
|
||||
} elseif (is_iterable($uploadedFiles)) {
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$result[] = $uploadedFile;
|
||||
}
|
||||
}
|
||||
} catch (MissingArrayPathException) {
|
||||
// Do nothing, empty array will be returned
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getValidationResults(): Result
|
||||
{
|
||||
return $this->validationResults;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
|
||||
|
||||
/**
|
||||
* A composite of controller arguments
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class Arguments extends \ArrayObject
|
||||
{
|
||||
protected array $argumentNames = [];
|
||||
protected array $argumentShortNames = [];
|
||||
|
||||
/**
|
||||
* Constructor. If this one is removed, reflection breaks.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or replaces the argument specified by $value. The argument's name is taken from the
|
||||
* argument object itself, therefore the $offset does not have any meaning in this context.
|
||||
*
|
||||
* @param mixed $offset Offset - not used here
|
||||
* @throws \InvalidArgumentException if the argument is not a valid Controller Argument object
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if (!$value instanceof Argument) {
|
||||
throw new \InvalidArgumentException('Controller arguments must be valid TYPO3\\CMS\\Extbase\\Mvc\\Controller\\Argument objects.', 1187953786);
|
||||
}
|
||||
$argumentName = $value->getName();
|
||||
parent::offsetSet($argumentName, $value);
|
||||
$this->argumentNames[$argumentName] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an argument, aliased to offsetSet()
|
||||
*
|
||||
* @throws \InvalidArgumentException if the argument is not a valid Controller Argument object
|
||||
*/
|
||||
public function append(mixed $value): void
|
||||
{
|
||||
if (!$value instanceof Argument) {
|
||||
throw new \InvalidArgumentException('Controller arguments must be valid TYPO3\\CMS\\Extbase\\Mvc\\Controller\\Argument objects.', 1187953787);
|
||||
}
|
||||
$this->offsetSet(null, $value);
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
parent::offsetUnset($translatedOffset);
|
||||
unset($this->argumentNames[$translatedOffset]);
|
||||
if ($offset != $translatedOffset) {
|
||||
unset($this->argumentShortNames[$offset]);
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
return parent::offsetExists($translatedOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value at the specified index
|
||||
*
|
||||
* @throws NoSuchArgumentException if the argument does not exist
|
||||
*/
|
||||
public function offsetGet(mixed $offset): Argument
|
||||
{
|
||||
$translatedOffset = $this->translateToLongArgumentName($offset);
|
||||
if ($translatedOffset === '') {
|
||||
throw new NoSuchArgumentException('The argument "' . $offset . '" does not exist.', 1216909923);
|
||||
}
|
||||
return parent::offsetGet($translatedOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates, adds and returns a new controller argument to this composite object.
|
||||
* If an argument with the same name exists already, it will be replaced by the
|
||||
* new argument object.
|
||||
*/
|
||||
public function addNewArgument(string $name, string $dataType = 'Text', bool $isRequired = false, mixed $defaultValue = null): Argument
|
||||
{
|
||||
$argument = GeneralUtility::makeInstance(Argument::class, $name, $dataType);
|
||||
$argument->setRequired($isRequired);
|
||||
$argument->setDefaultValue($defaultValue);
|
||||
$this->addArgument($argument);
|
||||
return $argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified controller argument to this composite object.
|
||||
* If an argument with the same name exists already, it will be replaced by the
|
||||
* new argument object.
|
||||
*
|
||||
* Note that the argument will be cloned, not referenced.
|
||||
*/
|
||||
public function addArgument(Argument $argument): void
|
||||
{
|
||||
$this->offsetSet(null, $argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an argument specified by name
|
||||
*
|
||||
* @throws NoSuchArgumentException
|
||||
*/
|
||||
public function getArgument(string $argumentName): Argument
|
||||
{
|
||||
if (!$this->offsetExists($argumentName)) {
|
||||
throw new NoSuchArgumentException('An argument "' . $argumentName . '" does not exist.', 1195815178);
|
||||
}
|
||||
return $this->offsetGet($argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an argument with the specified name exists
|
||||
*
|
||||
* @see offsetExists()
|
||||
*/
|
||||
public function hasArgument(string $argumentName): bool
|
||||
{
|
||||
return $this->offsetExists($argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of all arguments contained in this object
|
||||
*/
|
||||
public function getArgumentNames(): array
|
||||
{
|
||||
return array_keys($this->argumentNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the short names of all arguments contained in this object that have one.
|
||||
*/
|
||||
public function getArgumentShortNames(): array
|
||||
{
|
||||
$argumentShortNames = [];
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
$argumentShortNames[$argument->getShortName()] = true;
|
||||
}
|
||||
return array_keys($argumentShortNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic setter method for the argument values. Each argument
|
||||
* value can be set by just calling the setArgumentName() method.
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function __call(string $methodName, array $arguments): void
|
||||
{
|
||||
if (!str_starts_with($methodName, 'set')) {
|
||||
throw new \LogicException('Unknown method "' . $methodName . '".', 1210858451);
|
||||
}
|
||||
$firstLowerCaseArgumentName = $this->translateToLongArgumentName(strtolower($methodName[3]) . substr($methodName, 4));
|
||||
$firstUpperCaseArgumentName = $this->translateToLongArgumentName(ucfirst(substr($methodName, 3)));
|
||||
if (in_array($firstLowerCaseArgumentName, $this->getArgumentNames())) {
|
||||
$argument = parent::offsetGet($firstLowerCaseArgumentName);
|
||||
$argument->setValue($arguments[0]);
|
||||
} elseif (in_array($firstUpperCaseArgumentName, $this->getArgumentNames())) {
|
||||
$argument = parent::offsetGet($firstUpperCaseArgumentName);
|
||||
$argument->setValue($arguments[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a short argument name to its corresponding long name. If the
|
||||
* specified argument name is a real argument name already, it will be returned again.
|
||||
*
|
||||
* If an argument with the specified name or short name does not exist, an empty
|
||||
* string is returned.
|
||||
*/
|
||||
protected function translateToLongArgumentName(string $argumentName): string
|
||||
{
|
||||
if (in_array($argumentName, $this->getArgumentNames())) {
|
||||
return $argumentName;
|
||||
}
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
if ($argumentName === $argument->getShortName()) {
|
||||
return $argument->getName();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all arguments and resets this object
|
||||
*/
|
||||
public function removeAll(): void
|
||||
{
|
||||
foreach ($this->argumentNames as $argumentName => $booleanValue) {
|
||||
parent::offsetUnset($argumentName);
|
||||
}
|
||||
$this->argumentNames = [];
|
||||
}
|
||||
|
||||
public function validate(): Result
|
||||
{
|
||||
$results = new Result();
|
||||
/** @var Argument $argument */
|
||||
foreach ($this as $argument) {
|
||||
$argumentValidationResults = $argument->validate();
|
||||
$results->forProperty($argument->getName())->merge($argumentValidationResults);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Extbase\Attribute\Authorize;
|
||||
use TYPO3\CMS\Extbase\Authorization\AuthorizationResult;
|
||||
use TYPO3\CMS\Extbase\Service\ActionAuthorizationService;
|
||||
|
||||
/**
|
||||
* Registry for authorization configurations of extbase controller actions,
|
||||
* populated at compile time via {@see \TYPO3\CMS\Extbase\DependencyInjection\AuthorizePass}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AuthorizeRegistry
|
||||
{
|
||||
/** @var array<string, array<string, list<Authorize>>> */
|
||||
private array $authorizations = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ActionAuthorizationService $authorizationService,
|
||||
) {}
|
||||
|
||||
public function add(string $controllerClass, string $actionMethod, string|array|null $callback, bool $requireLogin, array $requireGroups): void
|
||||
{
|
||||
$this->authorizations[$controllerClass][$actionMethod][] = new Authorize($callback, $requireLogin, $requireGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Authorize>
|
||||
*/
|
||||
public function getAuthorizeAttributes(string $controllerClass, string $actionMethod): array
|
||||
{
|
||||
return $this->authorizations[$controllerClass][$actionMethod] ?? [];
|
||||
}
|
||||
|
||||
public function checkAuthorization(ActionController $controller, string $actionMethod, array $preparedArguments): ?AuthorizationResult
|
||||
{
|
||||
$authorizeAttributes = $this->getAuthorizeAttributes($controller::class, $actionMethod);
|
||||
if ($authorizeAttributes === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->authorizationService->checkAuthorization($controller, $authorizeAttributes, $preparedArguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
|
||||
/**
|
||||
* Interface for controllers
|
||||
*/
|
||||
interface ControllerInterface
|
||||
{
|
||||
/**
|
||||
* Processes a general request. The result can be returned by altering the given response.
|
||||
*
|
||||
* @param \TYPO3\CMS\Extbase\Mvc\RequestInterface $request The request object
|
||||
*/
|
||||
public function processRequest(RequestInterface $request): ResponseInterface;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception;
|
||||
|
||||
/**
|
||||
* An "Invalid Argument Name" exception
|
||||
*/
|
||||
class RequiredArgumentMissingException extends Exception {}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Error\Error;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileExtensionMimeTypeConsistencyValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileNameValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileHandlingServiceConfiguration
|
||||
{
|
||||
/**
|
||||
* @var ObjectStorage<FileUploadConfiguration>
|
||||
*/
|
||||
protected ObjectStorage $fileUploadConfigurations;
|
||||
|
||||
/**
|
||||
* @var ObjectStorage<FileUploadDeletionConfiguration>
|
||||
*/
|
||||
protected ObjectStorage $fileUploadDeletionConfigurations;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->fileUploadConfigurations = new ObjectStorage();
|
||||
$this->fileUploadDeletionConfigurations = new ObjectStorage();
|
||||
}
|
||||
|
||||
public function addFileUploadConfiguration(FileUploadConfiguration $configuration): void
|
||||
{
|
||||
$this->fileUploadConfigurations->attach($configuration);
|
||||
}
|
||||
|
||||
public function getFileUploadConfigurations(): ObjectStorage
|
||||
{
|
||||
return $this->fileUploadConfigurations;
|
||||
}
|
||||
|
||||
public function hasfileUploadConfigurations(): bool
|
||||
{
|
||||
return $this->fileUploadConfigurations->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileUploadConfiguration for the given propertyName
|
||||
*/
|
||||
public function getFileUploadConfigurationForProperty(string $propertyName): ?FileUploadConfiguration
|
||||
{
|
||||
foreach ($this->fileUploadConfigurations as $configuration) {
|
||||
if ($configuration->getPropertyName() === $propertyName) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a file deletion for the given property and file reference uid
|
||||
*/
|
||||
public function registerFileDeletion(string $property, int $fileReferenceUid): void
|
||||
{
|
||||
$fileUploadDeletionConfiguration = $this->getFileUploadDeletionConfigurationForProperty($property);
|
||||
if (!$fileUploadDeletionConfiguration) {
|
||||
$fileUploadDeletionConfiguration = GeneralUtility::makeInstance(FileUploadDeletionConfiguration::class, $property);
|
||||
$this->fileUploadDeletionConfigurations->attach($fileUploadDeletionConfiguration);
|
||||
}
|
||||
$fileUploadDeletionConfiguration->addFileReferenceUid($fileReferenceUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all file deletion configurations
|
||||
*/
|
||||
public function getFileUploadDeletionConfigurations(): ObjectStorage
|
||||
{
|
||||
return $this->fileUploadDeletionConfigurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileUploadDeletionConfiguration for the given propertyName
|
||||
*/
|
||||
public function getFileUploadDeletionConfigurationForProperty(string $propertyName): ?FileUploadDeletionConfiguration
|
||||
{
|
||||
foreach ($this->fileUploadDeletionConfigurations as $configuration) {
|
||||
if ($configuration->getPropertyName() === $propertyName) {
|
||||
return $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of configured file deletions for the given property
|
||||
*/
|
||||
private function getFileUploadDeletionCountForProperty(string $propertyName): int
|
||||
{
|
||||
$fileUploadDeletionConfiguration = $this->getFileUploadDeletionConfigurationForProperty($propertyName);
|
||||
if ($fileUploadDeletionConfiguration) {
|
||||
return count($fileUploadDeletionConfiguration->getFileReferenceUids());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file operations for the given argument by checking file upload and file deletion configurations and
|
||||
* returning the validation result.
|
||||
*/
|
||||
public function validateFileOperations(Argument $argument): Result
|
||||
{
|
||||
$validationResults = new Result();
|
||||
$value = $argument->getValue();
|
||||
|
||||
foreach ($this->fileUploadConfigurations as $configuration) {
|
||||
$uploadedFilesForProperty = $argument->getUploadedFilesForProperty(
|
||||
$configuration->getPropertyName()
|
||||
);
|
||||
$fileDeletionCount = $this->getFileUploadDeletionCountForProperty($configuration->getPropertyName());
|
||||
$currentPropertyValue = null;
|
||||
if ($value) {
|
||||
$currentPropertyValue = ObjectAccess::getPropertyPath($value, $configuration->getPropertyName());
|
||||
}
|
||||
$validationResult = $this->getValidationResultsForProperty(
|
||||
$configuration,
|
||||
$configuration->getPropertyName(),
|
||||
$currentPropertyValue,
|
||||
$uploadedFilesForProperty,
|
||||
$fileDeletionCount
|
||||
);
|
||||
$validationResults->merge($validationResult);
|
||||
}
|
||||
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file uploads and file deletions for the given propertyPath and currentPropertyValue and returns
|
||||
* the validation result.
|
||||
*/
|
||||
private function getValidationResultsForProperty(
|
||||
FileUploadConfiguration $configuration,
|
||||
string $propertyPath,
|
||||
mixed $currentPropertyValue,
|
||||
array $uploadedFiles,
|
||||
int $fileDeletionCount
|
||||
): Result {
|
||||
$validationResults = new Result();
|
||||
|
||||
if ($currentPropertyValue instanceof FileReference) {
|
||||
$currentAmount = 1;
|
||||
} elseif ($currentPropertyValue instanceof ObjectStorage) {
|
||||
$currentAmount = $currentPropertyValue->count();
|
||||
} else {
|
||||
$currentAmount = 0;
|
||||
}
|
||||
|
||||
// Validate, that minimum files requirement is valid after file deletion(s)
|
||||
if ($fileDeletionCount > 0
|
||||
&& ($currentPropertyValue instanceof FileReference || $currentPropertyValue instanceof ObjectStorage)
|
||||
) {
|
||||
$newAmount = $currentAmount - $fileDeletionCount + count($uploadedFiles);
|
||||
if ($newAmount < $configuration->getMinFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.minfiles.delete.notvalid',
|
||||
'extbase',
|
||||
),
|
||||
1714557062
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
}
|
||||
|
||||
// If the given $currentPropertyValue (which is the target property for file upload) is either a FileReference
|
||||
// or a non empty ObjectStorage and no uploaded files are available, the rest of the validation can be skipped.
|
||||
if ($uploadedFiles === []
|
||||
&& ($currentPropertyValue instanceof FileReference
|
||||
|| ($currentPropertyValue instanceof ObjectStorage && $currentPropertyValue->count() > 0))
|
||||
) {
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
if (count($uploadedFiles) < $configuration->getMinFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.minfiles.notvalid',
|
||||
'extbase',
|
||||
[$configuration->getMinFiles()]
|
||||
),
|
||||
1708596527
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
|
||||
if ((count($uploadedFiles) + $currentAmount - $fileDeletionCount) > $configuration->getMaxFiles()) {
|
||||
$minFilesError = new Error(
|
||||
$this->translateErrorMessage(
|
||||
'filehandlingserviceconfiguration.maxfiles.notvalid',
|
||||
'extbase',
|
||||
[$configuration->getMaxFiles()]
|
||||
),
|
||||
1708596528
|
||||
);
|
||||
$validationResults->forProperty($propertyPath)
|
||||
->addError($minFilesError);
|
||||
}
|
||||
|
||||
$validators = $this->enforceDefaultValidators(
|
||||
...$configuration->getValidators()
|
||||
);
|
||||
foreach ($validators as $validator) {
|
||||
foreach ($uploadedFiles as $uploadedFile) {
|
||||
$validatorResult = $validator->validate($uploadedFile);
|
||||
if ($validatorResult->hasErrors()) {
|
||||
$validationResults->forProperty($propertyPath)->merge($validatorResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $validationResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ValidatorInterface>
|
||||
*/
|
||||
private function enforceDefaultValidators(ValidatorInterface ...$validators): array
|
||||
{
|
||||
$enforceValidators = [
|
||||
FileNameValidator::class,
|
||||
FileExtensionMimeTypeConsistencyValidator::class,
|
||||
];
|
||||
$existingValidators = array_map(get_class(...), $validators);
|
||||
$missingValidators = array_diff($enforceValidators, $existingValidators);
|
||||
foreach ($missingValidators as $missingValidator) {
|
||||
$validators[] = GeneralUtility::makeInstance($missingValidator);
|
||||
}
|
||||
return $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to translate error messages
|
||||
*/
|
||||
private function translateErrorMessage(string $translateKey, string $extensionName, array $arguments = []): string
|
||||
{
|
||||
return LocalizationUtility::translate(
|
||||
$translateKey,
|
||||
$extensionName,
|
||||
$arguments
|
||||
) ?? '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\FileSizeValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator;
|
||||
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileUploadConfiguration
|
||||
{
|
||||
protected string $uploadFolder = '';
|
||||
protected int $minFiles = 0;
|
||||
protected int $maxFiles = PHP_INT_MAX;
|
||||
protected bool $addRandomSuffix = true;
|
||||
protected bool $createUploadFolderIfNotExist = true;
|
||||
protected DuplicationBehavior $duplicationBehavior = DuplicationBehavior::RENAME;
|
||||
|
||||
/**
|
||||
* @var array<ValidatorInterface>
|
||||
*/
|
||||
protected array $validators = [];
|
||||
|
||||
public function __construct(protected readonly string $propertyName) {}
|
||||
|
||||
/**
|
||||
* Initializes the object with the given configuration array. Typically used with configuration from
|
||||
* #[FileUpload] attribute.
|
||||
*/
|
||||
public function initializeWithConfiguration(array $configuration): self
|
||||
{
|
||||
if (!isset($configuration['validation']) || $configuration['validation'] === []) {
|
||||
throw new \RuntimeException('Extbase file upload must at least define one validation rule.', 1711947120);
|
||||
}
|
||||
|
||||
$this->initializeUploadValidation($configuration['validation']);
|
||||
|
||||
if (isset($configuration['uploadFolder']) && $configuration['uploadFolder'] !== '') {
|
||||
$this->uploadFolder = $configuration['uploadFolder'];
|
||||
}
|
||||
|
||||
if (isset($configuration['addRandomSuffix'])) {
|
||||
$this->addRandomSuffix = (bool)$configuration['addRandomSuffix'];
|
||||
}
|
||||
|
||||
if (isset($configuration['duplicationBehavior'])) {
|
||||
$this->duplicationBehavior = $configuration['duplicationBehavior'];
|
||||
}
|
||||
|
||||
if (isset($configuration['createUploadFolderIfNotExist'])) {
|
||||
$this->createUploadFolderIfNotExist = $configuration['createUploadFolderIfNotExist'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addValidator(ValidatorInterface $validator): self
|
||||
{
|
||||
$this->validators[] = $validator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidators(): array
|
||||
{
|
||||
return $this->validators;
|
||||
}
|
||||
|
||||
public function resetValidators(): self
|
||||
{
|
||||
$this->validators = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
public function setRequired(): self
|
||||
{
|
||||
$this->minFiles = 1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMinFiles(): int
|
||||
{
|
||||
return $this->minFiles;
|
||||
}
|
||||
|
||||
public function setMinFiles(int $minFiles): self
|
||||
{
|
||||
$this->minFiles = $minFiles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMaxFiles(): int
|
||||
{
|
||||
return $this->maxFiles;
|
||||
}
|
||||
|
||||
public function setMaxFiles(int $maxFiles): self
|
||||
{
|
||||
$this->maxFiles = $maxFiles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUploadFolder(): string
|
||||
{
|
||||
return $this->uploadFolder;
|
||||
}
|
||||
|
||||
public function setUploadFolder(string $uploadFolder): self
|
||||
{
|
||||
$this->uploadFolder = $uploadFolder;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAddRandomSuffix(): bool
|
||||
{
|
||||
return $this->addRandomSuffix;
|
||||
}
|
||||
|
||||
public function setAddRandomSuffix(bool $addRandomSuffix): self
|
||||
{
|
||||
$this->addRandomSuffix = $addRandomSuffix;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isCreateUploadFolderIfNotExist(): bool
|
||||
{
|
||||
return $this->createUploadFolderIfNotExist;
|
||||
}
|
||||
|
||||
public function setCreateUploadFolderIfNotExist(bool $createUploadFolderIfNotExist): self
|
||||
{
|
||||
$this->createUploadFolderIfNotExist = $createUploadFolderIfNotExist;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDuplicationBehavior(): DuplicationBehavior
|
||||
{
|
||||
return $this->duplicationBehavior;
|
||||
}
|
||||
|
||||
public function setDuplicationBehavior(DuplicationBehavior $duplicationBehavior): void
|
||||
{
|
||||
$this->duplicationBehavior = $duplicationBehavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current configuration is considered valid for the given target type and throws
|
||||
* an exception, if the configuration is invalid.
|
||||
*/
|
||||
public function ensureValidConfiguration(string $targetType): void
|
||||
{
|
||||
if ($targetType !== FileReference::class) {
|
||||
throw new \RuntimeException('The FileUploadConfiguration can only be used for properties of type FileReference.', 1721623184);
|
||||
}
|
||||
|
||||
if (str_contains($this->getPropertyName(), '.')) {
|
||||
throw new \RuntimeException('The property name for the FileUploadConfiguration must not contain any dot.', 1724585391);
|
||||
}
|
||||
|
||||
if ($this->getUploadFolder() === '') {
|
||||
throw new \RuntimeException('An upload folder must be defined for the FileUploadConfiguration.', 1711799735);
|
||||
}
|
||||
|
||||
if (!$this->isCombinedStoragePathIdentifier($this->getUploadFolder())) {
|
||||
throw new \RuntimeException('The upload folder must be a combined identifier - e.g. 1:/user_upload/', 1711801071);
|
||||
}
|
||||
|
||||
if ($this->getMaxFiles() < $this->getMinFiles()) {
|
||||
throw new \RuntimeException('Maximum number of files cannot be less than minimum number of files.', 1711799765);
|
||||
}
|
||||
}
|
||||
|
||||
private function isCombinedStoragePathIdentifier(string $identifier): bool
|
||||
{
|
||||
return str_contains($identifier, ':')
|
||||
&& !str_starts_with($identifier, ':')
|
||||
&& !str_ends_with($identifier, ':')
|
||||
&& MathUtility::canBeInterpretedAsInteger(substr($identifier, 0, strpos($identifier, ':')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes validators based on the given array of validation configuration
|
||||
*/
|
||||
private function initializeUploadValidation(array $validationConfiguration): void
|
||||
{
|
||||
if ($validationConfiguration['required'] ?? false) {
|
||||
$this->minFiles = 1;
|
||||
}
|
||||
|
||||
if ((int)($validationConfiguration['minFiles'] ?? PHP_INT_MAX) < PHP_INT_MAX) {
|
||||
$this->minFiles = (int)($validationConfiguration['minFiles']);
|
||||
}
|
||||
|
||||
if ((int)($validationConfiguration['maxFiles'] ?? PHP_INT_MAX) < PHP_INT_MAX) {
|
||||
$this->maxFiles = (int)($validationConfiguration['maxFiles']);
|
||||
}
|
||||
|
||||
// Migrate allowedMimeTypes to mimeType configuration, if mimeType configuration is not defined
|
||||
if (($validationConfiguration['allowedMimeTypes'] ?? false)
|
||||
&& is_array($validationConfiguration['allowedMimeTypes'])
|
||||
&& !isset($validationConfiguration['mimeType'])
|
||||
) {
|
||||
$validationConfiguration['mimeType'] = ['allowedMimeTypes' => $validationConfiguration['allowedMimeTypes']];
|
||||
unset($validationConfiguration['allowedMimeTypes']);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['mimeType'] ?? false)
|
||||
&& is_array($validationConfiguration['mimeType'])
|
||||
) {
|
||||
$mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class);
|
||||
$mimeTypeValidator->setOptions($validationConfiguration['mimeType']);
|
||||
$this->addValidator($mimeTypeValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['fileExtension'] ?? false)
|
||||
&& is_array($validationConfiguration['fileExtension'])
|
||||
) {
|
||||
$fileExtensionValidator = GeneralUtility::makeInstance(FileExtensionValidator::class);
|
||||
$fileExtensionValidator->setOptions($validationConfiguration['fileExtension']);
|
||||
$this->addValidator($fileExtensionValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['fileSize'] ?? false)
|
||||
&& is_array($validationConfiguration['fileSize'])
|
||||
) {
|
||||
$fileSizeValidator = GeneralUtility::makeInstance(FileSizeValidator::class);
|
||||
$fileSizeValidator->setOptions($validationConfiguration['fileSize']);
|
||||
$this->addValidator($fileSizeValidator);
|
||||
}
|
||||
|
||||
if (($validationConfiguration['imageDimensions'] ?? false)
|
||||
&& is_array($validationConfiguration['imageDimensions'])
|
||||
) {
|
||||
$imageDimensionsValidator = GeneralUtility::makeInstance(ImageDimensionsValidator::class);
|
||||
$imageDimensionsValidator->setOptions($validationConfiguration['imageDimensions']);
|
||||
$this->addValidator($imageDimensionsValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
/**
|
||||
* @internal Only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class FileUploadDeletionConfiguration
|
||||
{
|
||||
public function __construct(protected readonly string $propertyName, protected array $fileReferenceUids = []) {}
|
||||
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
return $this->propertyName;
|
||||
}
|
||||
|
||||
public function addFileReferenceUid(int $fileReferenceUid): void
|
||||
{
|
||||
$this->fileReferenceUids[] = $fileReferenceUid;
|
||||
}
|
||||
|
||||
public function getFileReferenceUids(): array
|
||||
{
|
||||
return $this->fileReferenceUids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration;
|
||||
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
|
||||
|
||||
/**
|
||||
* The default property mapping configuration is available
|
||||
* inside the Argument-object.
|
||||
*/
|
||||
class MvcPropertyMappingConfiguration extends PropertyMappingConfiguration
|
||||
{
|
||||
/**
|
||||
* Allow creation of a certain sub property
|
||||
*
|
||||
* @param string $propertyPath
|
||||
*/
|
||||
public function allowCreationForSubProperty($propertyPath)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow modification for a given property path
|
||||
*
|
||||
* @param string $propertyPath
|
||||
*/
|
||||
public function allowModificationForSubProperty($propertyPath)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target type for a certain property. Especially useful
|
||||
* if there is an object which has a nested object which is abstract,
|
||||
* and you want to instantiate a concrete object instead.
|
||||
*
|
||||
* @param string $propertyPath
|
||||
* @param string $targetType
|
||||
*/
|
||||
public function setTargetTypeForSubProperty($propertyPath, $targetType)
|
||||
{
|
||||
$this->forProperty($propertyPath)->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_TARGET_TYPE, $targetType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Error\Http\BadRequestException;
|
||||
use TYPO3\CMS\Core\Exception\Crypto\InvalidHashStringException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Mvc\Request;
|
||||
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
|
||||
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
|
||||
use TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException;
|
||||
use TYPO3\CMS\Extbase\Security\HashScope;
|
||||
|
||||
/**
|
||||
* This is a service which can generate a request hash and check whether the currently given arguments
|
||||
* fit to the request hash.
|
||||
*
|
||||
* It is used when forms are generated and submitted:
|
||||
* After a form has been generated, the method "generateTrustedPropertiesToken" is called with the names of all form fields.
|
||||
* It cleans up the array of form fields and creates another representation of it, which is then json encoded and a hmac
|
||||
* is appended. This is called the request hash.
|
||||
*
|
||||
* The json encoded form field list and the appended hmac will be submitted with the form (as attribute __trustedProperties).
|
||||
*
|
||||
* On the validation side, the validation happens in two steps:
|
||||
* 1) Check if the request hash is consistent (the hmac value fits to the json encoded field list string)
|
||||
* 2) Check that _all_ GET/POST parameters submitted occur inside the form field list of the request hash.
|
||||
*
|
||||
* Note: It is crucially important that a private key is computed into the hash value! This is done inside the HashService.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class MvcPropertyMappingConfigurationService
|
||||
{
|
||||
protected HashService $hashService;
|
||||
|
||||
public function injectHashService(HashService $hashService): void
|
||||
{
|
||||
$this->hashService = $hashService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a request hash for a list of form fields
|
||||
*/
|
||||
public function generateTrustedPropertiesToken(array $formFieldNames, string $fieldNamePrefix = ''): string
|
||||
{
|
||||
$formFieldArray = [];
|
||||
foreach ($formFieldNames as $formField) {
|
||||
$formFieldParts = explode('[', $formField);
|
||||
$currentPosition = &$formFieldArray;
|
||||
$formFieldPartsCount = count($formFieldParts);
|
||||
for ($i = 0; $i < $formFieldPartsCount; $i++) {
|
||||
$formFieldPart = $formFieldParts[$i];
|
||||
$formFieldPart = rtrim($formFieldPart, ']');
|
||||
if (!is_array($currentPosition)) {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is declared as array, but it collides with a previous form field of the same name which declared the field as string. This is an inconsistency you need to fix inside your Fluid form. (String overridden by Array)', 1255072196);
|
||||
}
|
||||
if ($i === $formFieldPartsCount - 1) {
|
||||
if (isset($currentPosition[$formFieldPart]) && is_array($currentPosition[$formFieldPart])) {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is declared as string, but it collides with a previous form field of the same name which declared the field as array. This is an inconsistency you need to fix inside your Fluid form. (Array overridden by String)', 1255072587);
|
||||
}
|
||||
// Last iteration - add a string
|
||||
if ($formFieldPart === '') {
|
||||
$currentPosition[] = 1;
|
||||
} else {
|
||||
$currentPosition[$formFieldPart] = 1;
|
||||
}
|
||||
} else {
|
||||
if ($formFieldPart === '') {
|
||||
throw new InvalidArgumentForHashGenerationException('The form field "' . $formField . '" is invalid. Reason: "[]" used not as last argument, but somewhere in the middle (like foo[][bar]).', 1255072832);
|
||||
}
|
||||
if (!isset($currentPosition[$formFieldPart])) {
|
||||
$currentPosition[$formFieldPart] = [];
|
||||
}
|
||||
$currentPosition = &$currentPosition[$formFieldPart];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($fieldNamePrefix !== '') {
|
||||
$formFieldArray = ($formFieldArray[$fieldNamePrefix] ?? []);
|
||||
}
|
||||
return $this->encodeAndHashFormFieldArray($formFieldArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode and hash the form field array
|
||||
*/
|
||||
protected function encodeAndHashFormFieldArray(array $formFieldArray): string
|
||||
{
|
||||
$encodedFormFieldArray = json_encode($formFieldArray);
|
||||
return $this->hashService->appendHmac($encodedFormFieldArray, HashScope::TrustedProperties->prefix(), HashAlgo::SHA3_256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the property mapping configuration in $controllerArguments if
|
||||
* the trusted properties are set inside the request.
|
||||
*
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function initializePropertyMappingConfigurationFromRequest(Request $request, Arguments $controllerArguments): void
|
||||
{
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = $request->getAttribute('extbase');
|
||||
$trustedPropertiesToken = $extbaseRequestParameters->getInternalArgument('__trustedProperties');
|
||||
if (!is_string($trustedPropertiesToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$encodedTrustedProperties = $this->hashService->validateAndStripHmac($trustedPropertiesToken, HashScope::TrustedProperties->prefix(), HashAlgo::SHA3_256);
|
||||
} catch (InvalidHashStringException $e) {
|
||||
throw new BadRequestException('The HMAC of the form could not be validated.', 1581862822);
|
||||
}
|
||||
$trustedProperties = json_decode($encodedTrustedProperties, true);
|
||||
if (!is_array($trustedProperties)) {
|
||||
if (str_starts_with($encodedTrustedProperties, 'a:')) {
|
||||
throw new BadRequestException('Trusted properties used outdated serialization format instead json.', 1699604555);
|
||||
}
|
||||
throw new BadRequestException('The HMAC of the form could not be utilized.', 1691267306);
|
||||
}
|
||||
|
||||
foreach ($trustedProperties as $propertyName => $propertyConfiguration) {
|
||||
$propertyName = (string)$propertyName;
|
||||
if (!$controllerArguments->hasArgument($propertyName) || !is_array($propertyConfiguration)) {
|
||||
continue;
|
||||
}
|
||||
$propertyMappingConfiguration = $controllerArguments->getArgument($propertyName)->getPropertyMappingConfiguration();
|
||||
$this->modifyPropertyMappingConfiguration($propertyConfiguration, $propertyMappingConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the passed $propertyMappingConfiguration according to the $propertyConfiguration which
|
||||
* has been generated by Fluid. In detail, if the $propertyConfiguration contains
|
||||
* an __identity field, we allow modification of objects; else we allow creation.
|
||||
*
|
||||
* All other properties are specified as allowed properties.
|
||||
*/
|
||||
protected function modifyPropertyMappingConfiguration(
|
||||
array $propertyConfiguration,
|
||||
PropertyMappingConfigurationInterface $propertyMappingConfiguration
|
||||
): void {
|
||||
if (isset($propertyConfiguration['__identity'])) {
|
||||
$propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true);
|
||||
unset($propertyConfiguration['__identity']);
|
||||
} else {
|
||||
$propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
|
||||
}
|
||||
|
||||
foreach ($propertyConfiguration as $innerKey => $innerValue) {
|
||||
if (is_array($innerValue)) {
|
||||
$this->modifyPropertyMappingConfiguration(
|
||||
$innerValue,
|
||||
$propertyMappingConfiguration->forProperty((string)$innerKey)
|
||||
);
|
||||
}
|
||||
$propertyMappingConfiguration->allowProperties($innerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Mvc\Controller;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\RateLimiter\LimiterInterface;
|
||||
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;
|
||||
use TYPO3\CMS\Extbase\Attribute\RateLimit;
|
||||
|
||||
/**
|
||||
* Registry for rate limit configurations of extbase controller actions,
|
||||
* populated at compile time via {@see \TYPO3\CMS\Extbase\DependencyInjection\RateLimitPass}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RateLimitRegistry
|
||||
{
|
||||
/** @var array<string, array<string, RateLimit>> */
|
||||
private array $rateLimits = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly RateLimiterFactoryInterface $rateLimiterFactory,
|
||||
) {}
|
||||
|
||||
public function add(string $controllerClass, string $actionMethod, int $limit, string $interval, string $policy, string $message): void
|
||||
{
|
||||
$this->rateLimits[$controllerClass][$actionMethod] = new RateLimit($limit, $interval, $policy, $message);
|
||||
}
|
||||
|
||||
public function getRateLimit(string $controllerClass, string $actionMethod): ?RateLimit
|
||||
{
|
||||
return $this->rateLimits[$controllerClass][$actionMethod] ?? null;
|
||||
}
|
||||
|
||||
public function createLimiter(string $controllerClass, string $actionMethod, ServerRequestInterface $request): ?LimiterInterface
|
||||
{
|
||||
$rateLimit = $this->getRateLimit($controllerClass, $actionMethod);
|
||||
if ($rateLimit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$identifier = strtolower(str_replace('\\', '-', $controllerClass) . '-' . $actionMethod);
|
||||
return $this->rateLimiterFactory->createRequestBasedLimiter($request, $rateLimit->getConfiguration($identifier));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user