292 lines
12 KiB
PHP
292 lines
12 KiB
PHP
<?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'];
|
|
}
|
|
}
|