<?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\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Authentication\PasswordReset;
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\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewInterface;

/**
 * Controller responsible for rendering and processing backend user password reset requests.
 *
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
 */
#[AsController]
class ResetPasswordController
{
    use PageRendererBackendSetupTrait;

    protected string $loginProvider = '';
    protected ViewInterface $view;

    public function __construct(
        protected readonly Context $context,
        protected readonly Locales $locales,
        protected readonly Features $features,
        protected readonly UriBuilder $uriBuilder,
        protected readonly PageRenderer $pageRenderer,
        protected readonly PasswordReset $passwordReset,
        protected readonly Typo3Information $typo3Information,
        protected readonly AuthenticationStyleInformation $authenticationStyleInformation,
        protected readonly ExtensionConfiguration $extensionConfiguration,
        protected readonly BackendViewFactory $backendViewFactory,
    ) {}

    /**
     * Show a form to enter an email address to request a password reset email.
     */
    public function forgetPasswordFormAction(ServerRequestInterface $request): ResponseInterface
    {
        if (!$this->passwordReset->isEnabled()) {
            return $this->redirectToLoginForm($request);
        }
        $this->initialize($request);
        $this->initializeForgetPasswordView($request);
        $this->pageRenderer->setBodyContent('<body>' . $this->view->render('Login/ForgetPasswordForm'));
        return $this->pageRenderer->renderResponse($request);
    }

    /**
     * Validate the email address.
     *
     * Restricted to POST method in Configuration/Backend/Routes.php
     */
    public function initiatePasswordResetAction(ServerRequestInterface $request): ResponseInterface
    {
        if (!$this->passwordReset->isEnabled()) {
            return $this->redirectToLoginForm($request);
        }
        $this->initialize($request);
        $this->initializeForgetPasswordView($request);
        $emailAddress = $request->getParsedBody()['email'] ?? '';
        if (!is_string($emailAddress)) {
            $emailAddress = '';
        }
        $this->view->assign('email', $emailAddress);
        if (!GeneralUtility::validEmail($emailAddress)) {
            $this->view->assign('invalidEmail', true);
        } else {
            $this->passwordReset->initiateReset($request, $this->context, $emailAddress);
            $this->view->assign('resetInitiated', true);
        }
        $this->pageRenderer->setBodyContent('<body>' . $this->view->render('Login/ForgetPasswordForm'));
        // Prevent time based information disclosure by waiting a random time
        // before sending a response. This prevents that the response time
        // can be an indicator if the used email exists or not. Wait a random
        // time between 200 milliseconds and 3 seconds.
        usleep(random_int(200000, 3000000));
        return $this->pageRenderer->renderResponse($request);
    }

    /**
     * Validates the link and show a form to enter the new password.
     */
    public function passwordResetAction(ServerRequestInterface $request): ResponseInterface
    {
        if (!$this->passwordReset->isEnabled()) {
            return $this->redirectToLoginForm($request);
        }
        $this->initialize($request);
        $this->initializeResetPasswordView($request);
        if (!$this->passwordReset->isValidResetTokenFromRequest($request)) {
            $this->view->assign('invalidToken', true);
        }
        $this->pageRenderer->setBodyContent('<body>' . $this->view->render('Login/ResetPasswordForm'));
        return $this->pageRenderer->renderResponse($request);
    }

    /**
     * Updates the password in the database.
     *
     * Restricted to POST method in Configuration/Backend/Routes.php
     */
    public function passwordResetFinishAction(ServerRequestInterface $request): ResponseInterface
    {
        if (!$this->passwordReset->isEnabled()) {
            return $this->redirectToLoginForm($request);
        }
        // Token is invalid
        if (!$this->passwordReset->isValidResetTokenFromRequest($request)) {
            return $this->passwordResetAction($request);
        }
        $this->initialize($request);
        $this->initializeResetPasswordView($request);
        if ($this->passwordReset->resetPassword($request, $this->context)) {
            $this->view->assign('resetExecuted', true);
        } else {
            $this->view->assign('error', true);
        }
        $this->pageRenderer->setBodyContent('<body>' . $this->view->render('Login/ResetPasswordForm'));
        return $this->pageRenderer->renderResponse($request);
    }

    private function redirectToLoginForm(ServerRequestInterface $request): ResponseInterface
    {
        return new RedirectResponse(
            $this->uriBuilder->buildUriWithRedirect('login', [], RouteRedirect::createFromRequest($request)),
            303
        );
    }

    protected function initializeForgetPasswordView(ServerRequestInterface $request): void
    {
        $parameters = array_filter(['loginProvider' => $this->loginProvider]);
        $this->view->assignMultiple([
            'formUrl' => $this->uriBuilder->buildUriWithRedirect('password_forget_initiate_reset', $parameters, RouteRedirect::createFromRequest($request)),
            'returnUrl' => $this->uriBuilder->buildUriWithRedirect('login', $parameters, RouteRedirect::createFromRequest($request)),
        ]);
    }

    protected function initializeResetPasswordView(ServerRequestInterface $request): void
    {
        $token = $request->getQueryParams()['t'] ?? '';
        $identity = $request->getQueryParams()['i'] ?? '';
        $expirationDate = $request->getQueryParams()['e'] ?? '';
        $parameters = array_filter(['loginProvider' => $this->loginProvider]);
        $formUrl = $this->uriBuilder->buildUriWithRedirect(
            'password_reset_finish',
            array_filter(array_merge($parameters, [
                't' => $token,
                'i' => $identity,
                'e' => $expirationDate,
            ])),
            RouteRedirect::createFromRequest($request)
        );
        $this->view->assignMultiple([
            'token' => $token,
            'identity' => $identity,
            'expirationDate' => $expirationDate,
            'formUrl' => $formUrl,
            'restartUrl' => $this->uriBuilder->buildUriWithRedirect('password_forget', $parameters, RouteRedirect::createFromRequest($request)),
            'passwordRequirements' => $this->getPasswordRequirements(),
        ]);
    }

    protected function initialize(ServerRequestInterface $request): void
    {
        $languageService = $this->getLanguageService();

        // Only allow to execute this if not logged in as a user right now
        if ($this->context->getAspect('backend.user')->isLoggedIn()) {
            throw new PropagateResponseException(
                new RedirectResponse($this->uriBuilder->buildUriFromRoute('login'), 303),
                1618342858
            );
        }

        // Fetch login provider from the request
        $this->loginProvider = $request->getQueryParams()['loginProvider'] ?? '';

        // Try to get the preferred browser language
        $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'] ?? '';
        $preferredBrowserLanguage = $this->locales->getPreferredClientLanguage($httpAcceptLanguage);

        // If we found a $preferredBrowserLanguage, which is not the default language
        // initialize $this->getLanguageService() again with $preferredBrowserLanguage.
        // Additionally, set the language to the backend user object, so labels in fluid views are translated
        if ($preferredBrowserLanguage !== 'default') {
            $languageService->init($preferredBrowserLanguage);
            $this->getBackendUserAuthentication()->user['lang'] = $preferredBrowserLanguage;
        }

        $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $languageService);
        $this->pageRenderer->setTitle('TYPO3 CMS Login: ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? ''));
        $this->pageRenderer->loadJavaScriptModule('bootstrap');
        $this->pageRenderer->loadJavaScriptModule('@typo3/backend/login.js');

        $this->view = $this->backendViewFactory->create($request);
        $this->view->assignMultiple([
            'enablePasswordReset' => $this->passwordReset->isEnabled(),
            'referrerCheckEnabled' => $this->features->isFeatureEnabled('security.backend.enforceReferrer'),
            'loginUrl' => (string)$request->getUri(),
        ]);

        $this->provideCustomLoginStyling($request);
    }

    protected function provideCustomLoginStyling(ServerRequestInterface $request): void
    {
        if (($backgroundImageStyles = $this->authenticationStyleInformation->getBackgroundImageStyles($request)) !== '') {
            $this->pageRenderer->addCssInlineBlock('loginBackgroundImage', $backgroundImageStyles, null, false, true);
        }
        if (($footerNote = $this->authenticationStyleInformation->getFooterNote()) !== '') {
            $this->view->assign('loginFootnote', $footerNote);
        }
        if (($highlightColorStyles = $this->authenticationStyleInformation->getHighlightColorStyles()) !== '') {
            $this->pageRenderer->addCssInlineBlock('loginHighlightColor', $highlightColorStyles, null, false, true);
        }
        $this->view->assignMultiple([
            'copyright' => $this->typo3Information->getCopyrightNotice(),
        ]);
    }

    protected function getPasswordRequirements(): array
    {
        $passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] ?? 'default';
        $passwordPolicyValidator = GeneralUtility::makeInstance(
            PasswordPolicyValidator::class,
            PasswordPolicyAction::UPDATE_USER_PASSWORD,
            is_string($passwordPolicy) ? $passwordPolicy : ''
        );
        return $passwordPolicyValidator->getRequirements();
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }

    protected function getBackendUserAuthentication(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }
}
