TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
<?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\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
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\Exception as CoreException;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Impexp\Domain\Repository\PresetRepository;
|
||||
use TYPO3\CMS\Impexp\Exception\InsufficientUserPermissionsException;
|
||||
use TYPO3\CMS\Impexp\Exception\MalformedPresetException;
|
||||
use TYPO3\CMS\Impexp\Exception\PresetNotFoundException;
|
||||
use TYPO3\CMS\Impexp\Export;
|
||||
|
||||
/**
|
||||
* Export module controller
|
||||
*
|
||||
* @internal This class is not considered part of the public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
class ExportController
|
||||
{
|
||||
protected array $defaultInputData = [
|
||||
'excludeDisabled' => 1,
|
||||
'preset' => [],
|
||||
'external_static' => [
|
||||
'tables' => [],
|
||||
],
|
||||
'external_ref' => [
|
||||
'tables' => [],
|
||||
],
|
||||
'pagetree' => [
|
||||
'tables' => [],
|
||||
],
|
||||
'extension_dep' => [],
|
||||
'meta' => [
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'notes' => '',
|
||||
],
|
||||
'record' => [],
|
||||
'list' => [],
|
||||
'includeSiteConfigurations' => 0,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected readonly ResponseFactoryInterface $responseFactory,
|
||||
protected readonly PresetRepository $presetRepository,
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
) {}
|
||||
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->getBackendUser()->isExportEnabled() === false) {
|
||||
throw new \RuntimeException(
|
||||
'Export module is disabled for non admin users and '
|
||||
. 'user TSconfig options.impexp.enableExportForNonAdminUser is not enabled.',
|
||||
1636901978
|
||||
);
|
||||
}
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
$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.", 1604308206);
|
||||
}
|
||||
|
||||
// @todo: Only small parts of tx_impexp can be hand over as GET, e.g. ['list'] and 'id', drop GET of everything else.
|
||||
// Also, there's a clash with id: it can be ['list']'table:id', it can be 'id', it can be tx_impexp['id']. This
|
||||
// should be de-messed somehow.
|
||||
$inputDataFromGetPost = $parsedBody['tx_impexp'] ?? $queryParams['tx_impexp'] ?? [];
|
||||
$inputData = $this->defaultInputData;
|
||||
ArrayUtility::mergeRecursiveWithOverrule($inputData, $inputDataFromGetPost);
|
||||
if ($inputData['resetExclude'] ?? false) {
|
||||
$inputData['exclude'] = [];
|
||||
}
|
||||
$inputData['preset']['public'] = (int)($inputData['preset']['public'] ?? 0);
|
||||
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
|
||||
$presetAction = $parsedBody['preset'] ?? [];
|
||||
$inputData = $this->processPresets($view, $presetAction, $inputData);
|
||||
|
||||
$export = $this->configureExportFromFormData($inputData);
|
||||
$export->process();
|
||||
|
||||
if ($inputData['download_export'] ?? false) {
|
||||
return $this->getDownload($export);
|
||||
}
|
||||
$saveFolder = $export->getOrCreateDefaultImportExportFolder();
|
||||
if (($inputData['save_export'] ?? false) && $saveFolder !== null) {
|
||||
$this->saveExportToFile($view, $export, $saveFolder);
|
||||
}
|
||||
$inputData['filename'] = $export->getExportFileName();
|
||||
|
||||
$view->assignMultiple([
|
||||
'id' => $id,
|
||||
'errors' => $export->getErrorLog(),
|
||||
'preview' => $export->renderPreview(),
|
||||
'tableSelectOptions' => $this->getTableSelectOptions(['pages']),
|
||||
'treeHTML' => $export->getTreeHTML(),
|
||||
'levelSelectOptions' => $this->getPageLevelSelectOptions($inputData),
|
||||
'records' => $this->getRecordSelectOptions($inputData),
|
||||
'tableList' => $this->getSelectableTableList($inputData),
|
||||
'externalReferenceTableSelectOptions' => $this->getTableSelectOptions(),
|
||||
'externalStaticTableSelectOptions' => $this->getTableSelectOptions(),
|
||||
'presetSelectOptions' => $this->presetRepository->getPresets($id),
|
||||
'fileName' => '',
|
||||
'filetypeSelectOptions' => $this->getFileSelectOptions($export),
|
||||
'saveFolder' => $saveFolder?->getPublicUrl() ?? '',
|
||||
'hasSaveFolder' => true,
|
||||
'extensions' => $this->getExtensionList(),
|
||||
'inData' => $inputData,
|
||||
'isAdmin' => $backendUser->isAdmin(),
|
||||
]);
|
||||
$view->setModuleName('');
|
||||
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);
|
||||
return $view->renderResponse('Export');
|
||||
}
|
||||
|
||||
protected function processPresets(ModuleTemplate $view, array $presetAction, array $inputData): array
|
||||
{
|
||||
if (empty($presetAction)) {
|
||||
return $inputData;
|
||||
}
|
||||
$presetUid = (int)$presetAction['select'];
|
||||
try {
|
||||
if (isset($presetAction['save'])) {
|
||||
if ($presetUid > 0) {
|
||||
// Update existing
|
||||
$this->presetRepository->updatePreset($presetUid, $inputData);
|
||||
$view->addFlashMessage('Preset #' . $presetUid . ' saved!', 'Presets', ContextualFeedbackSeverity::INFO);
|
||||
} else {
|
||||
// Insert new
|
||||
$this->presetRepository->createPreset($inputData);
|
||||
$view->addFlashMessage('New preset "' . $inputData['preset']['title'] . '" is created', 'Presets', ContextualFeedbackSeverity::INFO);
|
||||
}
|
||||
}
|
||||
if (isset($presetAction['delete'])) {
|
||||
if ($presetUid > 0) {
|
||||
$this->presetRepository->deletePreset($presetUid);
|
||||
$view->addFlashMessage('Preset #' . $presetUid . ' deleted!', 'Presets', ContextualFeedbackSeverity::INFO);
|
||||
} else {
|
||||
$view->addFlashMessage('ERROR: No preset selected for deletion.', 'Presets', ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
if (isset($presetAction['load']) || isset($presetAction['merge'])) {
|
||||
if ($presetUid > 0) {
|
||||
$presetData = $this->presetRepository->loadPreset($presetUid);
|
||||
if (isset($presetAction['merge'])) {
|
||||
// Merge records
|
||||
if (is_array($presetData['record'] ?? null)) {
|
||||
$inputData['record'] = array_merge((array)$inputData['record'], $presetData['record']);
|
||||
}
|
||||
// Merge lists
|
||||
if (is_array($presetData['list'] ?? null)) {
|
||||
$inputData['list'] = array_merge((array)$inputData['list'], $presetData['list']);
|
||||
}
|
||||
$view->addFlashMessage('Preset #' . $presetUid . ' merged!', 'Presets', ContextualFeedbackSeverity::INFO);
|
||||
} else {
|
||||
$inputData = $presetData;
|
||||
$view->addFlashMessage('Preset #' . $presetUid . ' loaded!', 'Presets', ContextualFeedbackSeverity::INFO);
|
||||
}
|
||||
} else {
|
||||
$view->addFlashMessage('ERROR: No preset selected for loading.', 'Presets', ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
} catch (PresetNotFoundException|InsufficientUserPermissionsException|MalformedPresetException $e) {
|
||||
$view->addFlashMessage($e->getMessage(), 'Presets', ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
return $inputData;
|
||||
}
|
||||
|
||||
protected function configureExportFromFormData(array $inputData): Export
|
||||
{
|
||||
$export = GeneralUtility::makeInstance(Export::class);
|
||||
$export->setExcludeMap((array)($inputData['exclude'] ?? []));
|
||||
$export->setSoftrefCfg((array)($inputData['softrefCfg'] ?? []));
|
||||
$export->setExtensionDependencies((($inputData['extension_dep'] ?? '') === '') ? [] : (array)$inputData['extension_dep']);
|
||||
$export->setShowStaticRelations((bool)($inputData['showStaticRelations'] ?? false));
|
||||
$export->setExcludeDisabledRecords((bool)($inputData['excludeDisabled'] ?? false));
|
||||
if (!empty($inputData['filetype'])) {
|
||||
$export->setExportFileType((string)$inputData['filetype']);
|
||||
}
|
||||
$export->setExportFileName((string)($inputData['filename'] ?? ''));
|
||||
$export->setRelStaticTables((($inputData['external_static']['tables'] ?? '') === '') ? [] : (array)$inputData['external_static']['tables']);
|
||||
$export->setRelOnlyTables((($inputData['external_ref']['tables'] ?? '') === '') ? [] : (array)$inputData['external_ref']['tables']);
|
||||
if (isset($inputData['save_export'], $inputData['saveFilesOutsideExportFile']) && $inputData['saveFilesOutsideExportFile'] === '1') {
|
||||
$export->setSaveFilesOutsideExportFile(true);
|
||||
}
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
$export->setIncludeSiteConfigurations((bool)($inputData['includeSiteConfigurations'] ?? false));
|
||||
}
|
||||
$export->setTitle((string)($inputData['meta']['title'] ?? ''));
|
||||
$export->setDescription((string)($inputData['meta']['description'] ?? ''));
|
||||
$export->setNotes((string)($inputData['meta']['notes'] ?? ''));
|
||||
$export->setRecord((($inputData['record'] ?? '') === '') ? [] : (array)$inputData['record']);
|
||||
$export->setList((($inputData['list'] ?? '') === '') ? [] : (array)$inputData['list']);
|
||||
if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['id'] ?? null)) {
|
||||
$export->setPid((int)$inputData['pagetree']['id']);
|
||||
}
|
||||
if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['levels'] ?? null)) {
|
||||
$export->setLevels((int)$inputData['pagetree']['levels']);
|
||||
}
|
||||
$export->setTables((($inputData['pagetree']['tables'] ?? '') === '') ? [] : (array)$inputData['pagetree']['tables']);
|
||||
return $export;
|
||||
}
|
||||
|
||||
protected function getDownload(Export $export): ResponseInterface
|
||||
{
|
||||
$fileName = $export->getOrGenerateExportFileNameWithFileExtension();
|
||||
$fileContent = $export->render();
|
||||
$response = $this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'application/octet-stream')
|
||||
->withHeader('Content-Length', (string)strlen($fileContent))
|
||||
->withHeader('Content-Disposition', 'attachment; filename=' . PathUtility::basename($fileName));
|
||||
$response->getBody()->write($export->render());
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function saveExportToFile(ModuleTemplate $view, Export $export, Folder $saveFolder): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
try {
|
||||
$saveFile = $export->saveToFile();
|
||||
$saveFileSize = $saveFile->getProperty('size');
|
||||
$view->addFlashMessage(
|
||||
sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_savedInSBytes'), $saveFile->getPublicUrl(), GeneralUtility::formatSize($saveFileSize)),
|
||||
$languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_savedFile')
|
||||
);
|
||||
} catch (CoreException $e) {
|
||||
$view->addFlashMessage(
|
||||
sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_badPathS'), $saveFolder->getPublicUrl()),
|
||||
$languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_problemsSavingFile'),
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getPageLevelSelectOptions(array $inputData): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$options = [];
|
||||
if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['id'] ?? '')) {
|
||||
$options = [
|
||||
Export::LEVELS_RECORDS_ON_THIS_PAGE => $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makeconfig_tablesOnThisPage'),
|
||||
0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
|
||||
1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
|
||||
2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
|
||||
3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
|
||||
4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
|
||||
Export::LEVELS_INFINITE => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'),
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
protected function getRecordSelectOptions(array $inputData): array
|
||||
{
|
||||
$records = [];
|
||||
foreach ($inputData['record'] ?? [] as $tableNameColonUid) {
|
||||
[$tableName, $recordUid] = explode(':', $tableNameColonUid);
|
||||
if ($record = BackendUtility::getRecordWSOL((string)$tableName, (int)$recordUid)) {
|
||||
$records[] = [
|
||||
'icon' => $this->iconFactory->getIconForRecord($tableName, $record, IconSize::SMALL)->render(),
|
||||
'title' => BackendUtility::getRecordTitle($tableName, $record, true),
|
||||
'tableName' => $tableName,
|
||||
'recordUid' => $recordUid,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $records;
|
||||
}
|
||||
|
||||
protected function getSelectableTableList(array $inputData): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$languageService = $this->getLanguageService();
|
||||
$tableList = [];
|
||||
foreach ($inputData['list'] ?? [] as $reference) {
|
||||
$referenceParts = explode(':', $reference);
|
||||
$tableName = $referenceParts[0];
|
||||
if ($backendUser->check('tables_select', $tableName)) {
|
||||
// If the page is actually the root, handle it differently.
|
||||
// NOTE: we don't compare integers, because the number comes from the split string above
|
||||
if ($referenceParts[1] === '0') {
|
||||
$iconAndTitle = $this->iconFactory->getIcon('apps-pagetree-root', IconSize::SMALL)->render() . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
|
||||
} else {
|
||||
$record = BackendUtility::getRecordWSOL('pages', (int)$referenceParts[1]);
|
||||
$iconAndTitle = $this->iconFactory->getIconForRecord('pages', $record, IconSize::SMALL)->render()
|
||||
. BackendUtility::getRecordTitle('pages', $record, true);
|
||||
}
|
||||
$tableList[] = [
|
||||
'iconAndTitle' => sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makeconfig_tableListEntry'), $tableName, $iconAndTitle),
|
||||
'reference' => $reference,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $tableList;
|
||||
}
|
||||
|
||||
protected function getExtensionList(): array
|
||||
{
|
||||
$loadedExtensions = ExtensionManagementUtility::getLoadedExtensionListArray();
|
||||
return array_combine($loadedExtensions, $loadedExtensions);
|
||||
}
|
||||
|
||||
protected function getFileSelectOptions(Export $export): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$fileTypeOptions = [];
|
||||
foreach ($export->getSupportedFileTypes() as $supportedFileType) {
|
||||
$fileTypeOptions[$supportedFileType] = $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makesavefo_' . $supportedFileType);
|
||||
}
|
||||
return $fileTypeOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all exportable tables - basically all TCA tables. Blacklist some if wanted.
|
||||
* Returned array keys are table names, values are "translations".
|
||||
*/
|
||||
protected function getTableSelectOptions(array $excludeList = []): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUser();
|
||||
$options = [
|
||||
'_ALL' => $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:ALL_tables'),
|
||||
];
|
||||
foreach ($this->tcaSchemaFactory->all()->getNames() as $table) {
|
||||
if (!in_array($table, $excludeList, true) && $backendUser->check('tables_select', $table)) {
|
||||
$options[$table] = $table;
|
||||
}
|
||||
}
|
||||
natsort($options);
|
||||
return $options;
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user