<?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\Install\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Install\Controller\InstallerController;
use TYPO3\CMS\Install\Service\EnableFileService;
use TYPO3\CMS\Install\Service\LateBootService;
use TYPO3\CMS\Install\Service\SessionService;

/**
 * Middleware to walk through the web installation process of TYPO3
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
 */
readonly class Installer implements MiddlewareInterface
{
    public function __construct(
        private LateBootService $lateBootService,
        private FormProtectionFactory $formProtectionFactory,
        private SessionService $sessionService
    ) {}

    /**
     * Handles an Install Tool request when nothing is there
     *
     * @throws \RuntimeException
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!$this->canHandleRequest()) {
            return $handler->handle($request);
        }
        // This is required for icon API, that still has no way to pass
        // a request/ normalizedParams to the icon URL generation
        $GLOBALS['TYPO3_REQUEST'] = $request;

        $container = $this->lateBootService->getContainer();
        $backup = $this->lateBootService->makeCurrent($container);

        // Lazy load InstallerController, to instantiate the class and the dependencies only if we handle an install request.
        $controller = $container->get(InstallerController::class);
        $actionName = $request->getParsedBody()['install']['action'] ?? $request->getQueryParams()['install']['action'] ?? 'init';
        $action = $actionName . 'Action';

        if ($actionName === 'init' || $actionName === 'mainLayout') {
            $response = $controller->$action($request);
        } elseif ($actionName === 'checkInstallerAvailable') {
            $response = new JsonResponse([
                'success' => $this->isInstallerAvailable(),
            ]);
        } elseif ($actionName === 'showInstallerNotAvailable') {
            $response = $controller->showInstallerNotAvailableAction($request);
        } elseif ($actionName === 'checkEnvironmentAndFolders'
            || $actionName === 'showEnvironmentAndFolders'
            || $actionName === 'executeEnvironmentAndFolders'
        ) {
            $this->throwIfInstallerIsNotAvailable();
            $response = $controller->$action($request);
        } else {
            $this->throwIfInstallerIsNotAvailable();
            // With main folder layout available, sessions can be handled
            $this->sessionService->installSessionHandler($request);
            $this->sessionService->startSession();
            if ($this->sessionService->isExpired($request)) {
                $this->sessionService->refreshSession();
            }
            $postValues = $request->getParsedBody()['install'] ?? [];
            $sessionTokenOk = false;
            if (empty($postValues)) {
                // No post data is there, no token check necessary
                $sessionTokenOk = true;
            }
            if (isset($postValues['token'])) {
                // A token must be given as soon as there is POST data
                $formProtection = $this->formProtectionFactory->createFromRequest($request);
                if ($actionName === '') {
                    throw new \RuntimeException('No POST action given for token check', 1505647681);
                }
                $sessionTokenOk = $formProtection->validateToken($postValues['token'], 'installTool', $actionName);
            }
            if (!$sessionTokenOk) {
                $this->sessionService->resetSession();
                $this->sessionService->startSession();
                throw new \RuntimeException('Invalid session token', 1505647737);
            }

            if (!method_exists($controller, $action)) {
                // Sanitize action method, preventing injecting whatever method name
                throw new \RuntimeException(
                    'Unknown action method ' . $action . ' in controller InstallerController',
                    1505687700
                );
            }

            $response = $controller->$action($request);

            if ($actionName === 'executeDefaultConfiguration') {
                // Executing last step cleans session
                $this->sessionService->destroySession($request);
            }
        }

        $this->lateBootService->makeCurrent(null, $backup);

        return $response;
    }

    /**
     * First installation is in progress, if system/settings.php does not exist,
     * or if FIRST_INSTALL file exists.
     */
    protected function canHandleRequest(): bool
    {
        $localConfigurationFileLocation = (new ConfigurationManager())->getSystemConfigurationFileLocation();
        return !@is_file($localConfigurationFileLocation) || EnableFileService::isFirstInstallAllowed();
    }

    /**
     * @throws \RuntimeException If installer is not available due to missing FIRST_INSTALL
     */
    protected function throwIfInstallerIsNotAvailable()
    {
        if (!$this->isInstallerAvailable()) {
            throw new \RuntimeException(
                'Installer not available',
                1505637427
            );
        }
    }

    /**
     * @return bool TRUE if FIRST_INSTALL file exists
     */
    protected function isInstallerAvailable(): bool
    {
        if (EnableFileService::isFirstInstallAllowed()) {
            return true;
        }
        return false;
    }
}
