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

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Impexp\Import;

/**
 * Import module controller
 *
 * @internal This class is not considered part of the public TYPO3 API.
 */
#[AsController]
readonly class ImportController
{
    protected const NO_UPLOAD = 0;
    protected const UPLOAD_DONE = 1;
    protected const UPLOAD_FAILED = 2;

    /**
     * File extensions accepted by the import upload. Uploading any other file type is rejected
     * before the file is written to storage.
     */
    protected const ALLOWED_UPLOAD_EXTENSIONS = ['t3d', 'xml'];
    protected const ALLOWED_UPLOAD_EXTENSION_LIST = '.t3d,.xml';

    public function __construct(
        protected IconFactory $iconFactory,
        protected ModuleTemplateFactory $moduleTemplateFactory,
        protected ExtendedFileUtility $fileProcessor,
        protected ResourceFactory $resourceFactory,
        protected ComponentFactory $componentFactory,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        if (!$this->getBackendUser()->isImportEnabled()) {
            throw new \RuntimeException(
                'Import module is disabled for non admin users and user TSconfig options.impexp.enableImportForNonAdminUser is not enabled.',
                1464435459
            );
        }

        $backendUser = $this->getBackendUser();
        $languageService = $this->getLanguageService();
        $queryParams = $request->getQueryParams();
        $parsedBody = $request->getParsedBody();

        $id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
        $permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
        $pageInfo = BackendUtility::readPageAccess($id, $permsClause) ?: [];
        if ($pageInfo === []) {
            throw new \RuntimeException("You don't have access to this page.", 1604308205);
        }

        $inputData = $request->getParsedBody()['tx_impexp'] ?? $request->getQueryParams()['tx_impexp'] ?? [];
        if ($inputData['new_import'] ?? false) {
            unset($inputData['import_mode']);
        }

        $view = $this->moduleTemplateFactory->create($request);

        $import = GeneralUtility::makeInstance(Import::class);
        $import->setPid($id);
        // Resolve the upload destination server-side. The import upload target is pinned to this
        // folder and must never be taken from the (client-controlled) request body.
        $importFolder = $import->getOrCreateDefaultImportExportFolder();

        $uploadStatus = self::NO_UPLOAD;
        $uploadedFileName = '';
        if ($request->getMethod() === 'POST' && empty($parsedBody)) {
            // This happens if the post request was larger than allowed on the server.
            $view->addFlashMessage(
                $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:importdata_upload_nodata'),
                $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:importdata_upload_error'),
                ContextualFeedbackSeverity::ERROR
            );
        }
        if ($request->getMethod() === 'POST' && isset($parsedBody['_upload'])) {
            $uploadStatus = self::UPLOAD_FAILED;
            $file = $this->handleFileUpload($request, $importFolder, $view);
            if ($file !== null) {
                $inputData['file'] = $file->getCombinedIdentifier();
                $uploadStatus = self::UPLOAD_DONE;
                $uploadedFileName = $file->getName();
            }
        }

        $this->configureImportFromFormDataAndImportIfRequested($view, $import, $inputData);

        if (!$this->getBackendUser()->isAdmin()
            && $import->getSiteConfigurations() !== []
        ) {
            $view->addFlashMessage(
                $languageService->translate('importdata_siteConfigurationsAdminOnly', 'impexp.messages'),
                $languageService->translate('importdata_siteConfigurations', 'impexp.messages'),
                ContextualFeedbackSeverity::WARNING
            );
        }

        $view->assignMultiple([
            'importFolder' => $importFolder?->getCombinedIdentifier() ?? '',
            'import' => $import,
            'errors' => $import->getErrorLog(),
            'preview' => $import->renderPreview(),
            'id' => $id,
            'fileSelectOptions' => $this->getSelectableFileList($import),
            'inData' => $inputData,
            'isAdmin' => $this->getBackendUser()->isAdmin(),
            'uploadedFile' => $uploadedFileName,
            'uploadStatus' => $uploadStatus,
            'allowedUploadExtensionList' => self::ALLOWED_UPLOAD_EXTENSION_LIST,
        ]);
        $view->setModuleName('');
        $view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);
        if ((int)($pageInfo['uid'] ?? 0) > 0) {
            $view->addButtonToButtonBar($this->componentFactory->createViewButton(PreviewUriBuilder::create($pageInfo)
                ->withRootLine(BackendUtility::BEgetRootLine($pageInfo['uid']))
                ->buildDispatcherDataAttributes() ?? []));
        }
        return $view->renderResponse('Import');
    }

    protected function handleFileUpload(ServerRequestInterface $request, ?Folder $importFolder, ModuleTemplate $view): ?File
    {
        if ($importFolder === null) {
            return null;
        }
        // Reject any file that is not an import file before it is written to storage. The import
        // upload must never be used to place arbitrary file types in a (potentially public) storage.
        $uploadedFile = $request->getUploadedFiles()['upload_1'] ?? null;
        if (!$uploadedFile instanceof UploadedFileInterface) {
            return null;
        }
        $uploadExtension = strtolower(pathinfo((string)$uploadedFile->getClientFilename(), PATHINFO_EXTENSION));
        if (!in_array($uploadExtension, self::ALLOWED_UPLOAD_EXTENSIONS, true)) {
            $view->addFlashMessage(
                $this->getLanguageService()->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:importdata_upload_invalidExtension'),
                $this->getLanguageService()->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:importdata_upload_error'),
                ContextualFeedbackSeverity::ERROR
            );
            return null;
        }
        $parsedBody = $request->getParsedBody() ?? [];
        $conflictMode = empty($parsedBody['overwriteExistingFiles']) ? DuplicationBehavior::CANCEL : DuplicationBehavior::REPLACE;
        // The upload target is pinned to the import/export folder resolved server-side and must
        // not be taken from the (client-controlled) request body, otherwise an uploaded file
        // could be redirected to an arbitrary, potentially publicly accessible, storage location.
        $fileCommands = [
            'upload' => [
                1 => [
                    'target' => $importFolder->getCombinedIdentifier(),
                    'data' => '1',
                ],
            ],
        ];
        $this->fileProcessor->setActionPermissions();
        $this->fileProcessor->setExistingFilesConflictMode($conflictMode);
        $this->fileProcessor->start($fileCommands, $request->getUploadedFiles());
        $result = $this->fileProcessor->processData();
        // If upload went well, set the new file as the import file.
        return $result['upload'][0][0] ?? null;
    }

    /**
     * @throws \BadFunctionCallException
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
    protected function configureImportFromFormDataAndImportIfRequested(ModuleTemplate $view, Import $import, array $inputData): void
    {
        $import->setUpdate((bool)($inputData['do_update'] ?? false));
        $import->setImportMode((array)($inputData['import_mode'] ?? null));
        $import->setEnableLogging((bool)($inputData['enableLogging'] ?? false));
        $import->setGlobalIgnorePid((bool)($inputData['global_ignore_pid'] ?? false));
        $import->setForceAllUids((bool)($inputData['force_all_UIDS'] ?? false));
        $import->setShowDiff(!(bool)($inputData['notShowDiff'] ?? false));
        $import->setSoftrefInputValues((array)($inputData['softrefInputValues'] ?? null));
        if (!empty($inputData['file'])) {
            if (PathUtility::isExtensionPath($inputData['file'])) {
                $filePath = $inputData['file'];
            } else {
                $filePath = $this->getFilePathWithinFileMountBoundaries((string)$inputData['file']);
            }
            try {
                $import->loadFile($filePath);
                $import->checkImportPrerequisites();
                if ($inputData['import_file'] ?? false) {
                    $import->importData();
                    BackendUtility::setUpdateSignal('updatePageTree');
                }
            } catch (\Exception $e) {
                $view->addFlashMessage($e->getMessage(), '', ContextualFeedbackSeverity::ERROR);
            }
        }
    }

    protected function getFilePathWithinFileMountBoundaries(string $filePath): string
    {
        try {
            $file = $this->resourceFactory->getFileObjectFromCombinedIdentifier($filePath);
            return $file->getForLocalProcessing(false);
        } catch (\Exception $exception) {
            return '';
        }
    }

    protected function getSelectableFileList(Import $import): array
    {
        $exportFiles = [];

        // Fileadmin
        $folder = $import->getOrCreateDefaultImportExportFolder();
        if ($folder !== null) {
            $filter = GeneralUtility::makeInstance(FileExtensionFilter::class);
            $filter->setAllowedFileExtensions(['t3d', 'xml']);
            $folder->getStorage()->addFileAndFolderNameFilter([$filter, 'filterFileList']);
            $exportFiles = $folder->getFiles();
        }
        $selectableFiles = [''];
        foreach ($exportFiles as $file) {
            $selectableFiles[$file->getCombinedIdentifier()] = $file->getPublicUrl();
        }

        // Extension Distribution
        if ($this->getBackendUser()->isAdmin()) {
            $possibleImportFiles = [
                'Initialisation/data.t3d',
                'Initialisation/data.xml',
            ];
            $activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
            foreach ($activePackages as $package) {
                foreach ($possibleImportFiles as $possibleImportFile) {
                    if (!file_exists($package->getPackagePath() . $possibleImportFile)) {
                        continue;
                    }
                    $selectableFiles['EXT:' . $package->getPackageKey() . '/' . $possibleImportFile] = 'EXT:' . $package->getPackageKey() . '/' . $possibleImportFile;
                }
            }
        }

        return $selectableFiles;
    }

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

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