<?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\Page\PageRenderer;
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);
        $this->renderAssetsForRequest($request);
        return $response;
    }

    /**
     * Method which initializes assets that should be attached to the response
     * for the given $request, which contains parameters that an override can
     * use to determine which assets to add via PageRenderer.
     *
     * This default implementation will attempt to render the sections "HeaderAssets"
     * and "FooterAssets" from the template that is being rendered, inserting the
     * rendered content into either page header or footer, as appropriate. Both
     * sections are optional and can be used one or both in combination.
     *
     * You can add assets with this method without worrying about duplicates, if
     * for example you do this in a plugin that gets used multiple time on a page.
     *
     * @internal
     */
    protected function renderAssetsForRequest(RequestInterface $request): void
    {
        if (!($this->view instanceof FluidViewAdapter)) {
            return;
        }
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
        $variables = ['request' => $request, 'arguments' => $this->arguments];
        $headerAssets = $this->view->renderSection('HeaderAssets', $variables, true);
        $footerAssets = $this->view->renderSection('FooterAssets', $variables, true);
        if (!empty(trim($headerAssets))) {
            trigger_error(
                'HeaderAssets section has been deprecated in TYPO3 v14.0 and will be removed in v15.0. Use f:page.headerData viewHelper instead.',
                E_USER_DEPRECATED
            );
            $pageRenderer->addHeaderData($headerAssets);
        }
        if (!empty(trim($footerAssets))) {
            trigger_error(
                'FooterAssets section has been deprecated in TYPO3 v14.0 and will be removed in v15.0. Use f:page.footerData viewHelper instead.',
                E_USER_DEPRECATED
            );
            $pageRenderer->addFooterData($footerAssets);
        }
    }

    /**
     * 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)) {
                $arguments = unserialize(
                    base64_decode($this->hashService->validateAndStripHmac(
                        $referringRequestArguments['arguments'],
                        HashScope::ReferringArguments->prefix(),
                        HashAlgo::SHA3_256
                    ))
                );
            }
            $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 {
        /* @var FlashMessage $flashMessage */
        $flashMessage = GeneralUtility::makeInstance(
            FlashMessage::class,
            $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();
    }
}
