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(); } }