<?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\Backend\Controller;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface;
use TYPO3\CMS\Backend\Routing\RouteRedirect;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait;
use TYPO3\CMS\Backend\View\AuthenticationStyleInformation;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\SysLog\Action\Login;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;

/**
 * Controller to provide a multi-factor authentication endpoint.
 * This is the backend login related view to authenticate against chosen MFA provider.
 *
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
 */
#[AsController]
class MfaController extends AbstractMfaController
{
    use PageRendererBackendSetupTrait;

    public function __construct(
        protected readonly UriBuilder $uriBuilder,
        protected readonly AuthenticationStyleInformation $authenticationStyleInformation,
        protected readonly PageRenderer $pageRenderer,
        protected readonly ExtensionConfiguration $extensionConfiguration,
        protected readonly LoggerInterface $logger,
        protected readonly BackendViewFactory $backendViewFactory,
        protected readonly EventDispatcherInterface $eventDispatcher,
    ) {}

    /**
     * Main entry point, checking prerequisite, initializing and setting
     * up the view and finally dispatching to the requested action.
     */
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $this->initializeMfaConfiguration();
        $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'auth');

        switch ($action) {
            case 'auth':
            case 'verify':
                $mfaProvider = $this->getMfaProviderFromRequest($request);
                // All actions except "cancel" require a provider to deal with.
                // If non is found at this point, throw an exception since this should never happen.
                if ($mfaProvider === null) {
                    throw new \InvalidArgumentException('No active MFA provider was found!', 1611879242);
                }
                return $this->{$action . 'Action'}($request, $mfaProvider);
            case 'cancel':
                return $this->cancelAction($request);
            default:
                throw new \InvalidArgumentException('Action not allowed', 1611879244);
        }
    }

    /**
     * Set up the authentication view for the provider by using provider specific content.
     */
    protected function authAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
    {
        $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService());
        $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? ''));
        $this->pageRenderer->loadJavaScriptModule('bootstrap');
        $view = $this->backendViewFactory->create($request);
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::AUTH);
        $view->assignMultiple([
            'provider' => $mfaProvider,
            'alternativeProviders' => $this->getAlternativeProviders($mfaProvider),
            'isLocked' => $mfaProvider->isLocked($propertyManager),
            'providerContent' => $providerResponse->getBody(),
            'footerNote' => $this->authenticationStyleInformation->getFooterNote(),
            'formUrl' => $this->uriBuilder->buildUriWithRedirect('auth_mfa', ['action' => 'verify'], RouteRedirect::createFromRequest($request)),
            'redirectRoute' => $request->getQueryParams()['redirect'] ?? '',
            'redirectParams' => $request->getQueryParams()['redirectParams'] ?? '',
            'hasAuthError' => (bool)($request->getQueryParams()['failure'] ?? false),
        ]);
        $this->addCustomAuthenticationFormStyles($request);
        $this->pageRenderer->setBodyContent('<body>' . $view->render('Mfa/Auth'));
        return $this->pageRenderer->renderResponse($request);
    }

    /**
     * Handle verification request, receiving from the auth view
     * by forwarding the request to the appropriate provider.
     */
    protected function verifyAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
    {
        $backendUser = $this->getBackendUser();
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser);

        // Check if the provider can process the request and is not temporarily blocked
        if (!$mfaProvider->canProcess($request) || $mfaProvider->isLocked($propertyManager)) {
            // If this fails, cancel the authentication
            return $this->cancelAction($request);
        }
        // Call the provider to verify the request
        if (!$mfaProvider->verify($request, $propertyManager)) {
            $this->log(
                'Multi-factor authentication failed for user \'###USERNAME###\' with provider \'' . $mfaProvider->getIdentifier() . '\'!',
                [],
                null,
                Login::ATTEMPT,
                SystemLogErrorClassification::SECURITY_NOTICE
            );
            $this->eventDispatcher->dispatch(
                new MfaVerificationFailedEvent($request, $propertyManager, $mfaProvider)
            );
            // If failed, initiate a redirect back to the auth view
            return new RedirectResponse($this->uriBuilder->buildUriWithRedirect(
                'auth_mfa',
                [
                    'identifier' => $mfaProvider->getIdentifier(),
                    'failure' => true,
                ],
                RouteRedirect::createFromRequest($request)
            ));
        }
        $this->log('Multi-factor authentication successful for user ###USERNAME###');
        // If verified, store this information in the session
        // and initiate a redirect back to the login view.
        $backendUser->setAndSaveSessionData('mfa', true);
        $backendUser->handleUserLoggedIn($request);
        return new RedirectResponse(
            $this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request))
        );
    }

    /**
     * Allow the user to cancel the multi-factor authentication by
     * calling logoff on the user object, to destroy the session and
     * other already gathered information and finally initiate a
     * redirect back to the login.
     */
    protected function cancelAction(ServerRequestInterface $request): ResponseInterface
    {
        $this->log('Multi-factor authentication canceled for user ###USERNAME###');
        $this->getBackendUser()->logoff();
        return new RedirectResponse($this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request)));
    }

    /**
     * Fetch alternative (activated and allowed) providers for the user to chose from
     *
     * @return ProviderInterface[]
     */
    protected function getAlternativeProviders(MfaProviderManifestInterface $mfaProvider): array
    {
        return array_filter($this->allowedProviders, function (MfaProviderManifestInterface $provider) use ($mfaProvider): bool {
            return $provider !== $mfaProvider
                && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()));
        });
    }

    /**
     * Log debug information for MFA events
     */
    protected function log(
        string $message,
        array $additionalData = [],
        ?MfaProviderManifestInterface $mfaProvider = null,
        int $action = Login::LOGIN,
        int $error = SystemLogErrorClassification::MESSAGE
    ): void {
        $user = $this->getBackendUser();
        $username = $user->getUserName();
        $context = [
            'user' => [
                'uid' => $user->getUserId(),
                'username' => $username,
            ],
        ];
        if ($mfaProvider !== null) {
            $context['provider'] = $mfaProvider->getIdentifier();
            $context['isProviderLocked'] = $mfaProvider->isLocked(
                MfaProviderPropertyManager::create($mfaProvider, $user)
            );
        }
        $message = str_replace('###USERNAME###', $username, $message);
        $data = array_replace_recursive($context, $additionalData);
        $this->logger->debug($message, $data);
        if ($user->writeStdLog) {
            // Write to sys_log if enabled
            $user->writelog(SystemLogType::LOGIN, $action, $error, null, $message, $data);
        }
    }

    protected function getMfaProviderFromRequest(ServerRequestInterface $request): ?MfaProviderManifestInterface
    {
        $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? '');
        // Check if given identifier is valid
        if ($this->isValidIdentifier($identifier)) {
            $provider = $this->mfaProviderRegistry->getProvider($identifier);
            // Only add provider if it was activated by the current user
            if ($provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()))) {
                return $provider;
            }
        }
        return null;
    }

    protected function addCustomAuthenticationFormStyles(ServerRequestInterface $request): void
    {
        if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') {
            $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true);
        }
        if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') {
            $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true);
        }
    }
}
