TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:28 +02:00
commit 3a4ca58ca1
90 changed files with 9646 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Impexp\Export;
/**
* Command for exporting T3D/XML data files
*/
#[AsCommand('impexp:export', 'Exports a T3D / XML file with content of a page tree')]
class ExportCommand extends Command
{
public function __construct(protected readonly Export $export)
{
parent::__construct();
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this
->addArgument(
'filename',
InputArgument::OPTIONAL,
'The filename to export to (without file extension).'
)
->addOption(
'type',
null,
InputOption::VALUE_OPTIONAL,
'The file type (xml, t3d, t3d_compressed).',
$this->export->getExportFileType()
)
->addOption(
'pid',
null,
InputOption::VALUE_OPTIONAL,
'The root page of the exported page tree.',
$this->export->getPid()
)
->addOption(
'levels',
null,
InputOption::VALUE_OPTIONAL,
sprintf(
'The depth of the exported page tree. '
. '"%d": "Records on this page", '
. '"0": "This page", '
. '"1": "1 level down", '
. '.. '
. '"%d": "Infinite levels".',
Export::LEVELS_RECORDS_ON_THIS_PAGE,
Export::LEVELS_INFINITE
),
$this->export->getLevels()
)
->addOption(
'table',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Include all records of this table. Examples: "_ALL", "tt_content", "sys_file_reference", etc.'
)
->addOption(
'record',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Include this specific record. Pattern is "{table}:{record}". Examples: "tt_content:12", etc.'
)
->addOption(
'list',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Include the records of this table and this page. Pattern is "{table}:{pid}". Examples: "be_users:0", etc.'
)
->addOption(
'include-related',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Include record relations to this table, including the related record. Examples: "_ALL", "sys_category", etc.'
)
->addOption(
'include-static',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Include record relations to this table, excluding the related record. Examples: "_ALL", "be_users", etc.'
)
->addOption(
'exclude',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Exclude this specific record. Pattern is "{table}:{record}". Examples: "fe_users:3", etc.'
)
->addOption(
'exclude-disabled-records',
null,
InputOption::VALUE_NONE,
'Exclude records which are handled as disabled by their TCA configuration, e.g. by fields "disabled", "starttime" or "endtime".'
)
->addOption(
'title',
null,
InputOption::VALUE_OPTIONAL,
'The meta title of the export.'
)
->addOption(
'description',
null,
InputOption::VALUE_OPTIONAL,
'The meta description of the export.'
)
->addOption(
'notes',
null,
InputOption::VALUE_OPTIONAL,
'The meta notes of the export.'
)
->addOption(
'dependency',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'This TYPO3 extension is required for the exported records. Examples: "news", "powermail", etc.'
)
->addOption(
'save-files-outside-export-file',
null,
InputOption::VALUE_NONE,
'Save files into separate folder instead of including them into the common export file. Folder name pattern is "{filename}.files".'
)
->addOption(
'include-site-configurations',
null,
InputOption::VALUE_NONE,
'Include site configurations for exported root pages.'
)
;
}
/**
* Executes the command for exporting a t3d/xml file from the TYPO3 system
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Ensure the _cli_ user is authenticated
Bootstrap::initializeBackendAuthentication();
$io = new SymfonyStyle($input, $output);
try {
$this->export->setExportFileName(PathUtility::basename((string)$input->getArgument('filename')));
$this->export->setExportFileType((string)$input->getOption('type'));
$this->export->setPid((int)$input->getOption('pid'));
$this->export->setLevels((int)$input->getOption('levels'));
$this->export->setTables($input->getOption('table'));
$this->export->setRecord($input->getOption('record'));
$this->export->setList($input->getOption('list'));
$this->export->setRelOnlyTables($input->getOption('include-related'));
$this->export->setRelStaticTables($input->getOption('include-static'));
$this->export->setExcludeMap(array_fill_keys($input->getOption('exclude'), 1));
$this->export->setExcludeDisabledRecords($input->getOption('exclude-disabled-records'));
$this->export->setTitle((string)$input->getOption('title'));
$this->export->setDescription((string)$input->getOption('description'));
$this->export->setNotes((string)$input->getOption('notes'));
$this->export->setExtensionDependencies($input->getOption('dependency'));
$this->export->setSaveFilesOutsideExportFile($input->getOption('save-files-outside-export-file'));
$this->export->setIncludeSiteConfigurations($input->getOption('include-site-configurations'));
$this->export->process();
$saveFile = $this->export->saveToFile();
$io->success('Exporting to ' . $saveFile->getPublicUrl() . ' succeeded.');
return Command::SUCCESS;
} catch (\Exception $e) {
$saveFolder = $this->export->getOrCreateDefaultImportExportFolder();
$io->error('Exporting to ' . $saveFolder->getPublicUrl() . ' failed.');
if ($io->isVerbose()) {
$io->writeln($e->getMessage());
}
return Command::FAILURE;
}
}
}
+158
View File
@@ -0,0 +1,158 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Impexp\Import;
/**
* Command for importing T3D/XML data files
*/
#[AsCommand('impexp:import', 'Imports a T3D / XML file with content into a page tree')]
class ImportCommand extends Command
{
public function __construct(protected readonly Import $import)
{
parent::__construct();
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this
->addArgument(
'file',
InputArgument::REQUIRED,
'The file path to import from (.t3d or .xml).'
)
->addArgument(
'pid',
InputArgument::OPTIONAL,
'The page to import to.',
0
)
->addOption(
'update-records',
null,
InputOption::VALUE_NONE,
'If set, existing records with the same UID will be updated instead of inserted.'
)
->addOption(
'ignore-pid',
null,
InputOption::VALUE_NONE,
'If set, page IDs of updated records are not corrected (only works in conjunction with --update-records).'
)
->addOption(
'force-uid',
null,
InputOption::VALUE_NONE,
'If set, UIDs from file will be forced.'
)
->addOption(
'import-mode',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
sprintf(
'Set the import mode of this specific record. ' . PHP_EOL
. 'Pattern is "{table}:{record}={mode}". ' . PHP_EOL
. 'Available modes for new records are "%1$s" and "%3$s" '
. 'and for existing records "%2$s", "%4$s", "%5$s" and "%3$s".' . PHP_EOL
. 'Examples are "pages:987=%1$s", "tt_content:1=%2$s", etc.',
Import::IMPORT_MODE_FORCE_UID,
Import::IMPORT_MODE_AS_NEW,
Import::IMPORT_MODE_EXCLUDE,
Import::IMPORT_MODE_IGNORE_PID,
Import::IMPORT_MODE_RESPECT_PID
)
)
->addOption(
'enable-log',
null,
InputOption::VALUE_NONE,
'If set, all database actions are logged.'
)
;
}
/**
* Executes the command for importing a t3d/xml file into the TYPO3 system
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Ensure the _cli_ user is authenticated
Bootstrap::initializeBackendAuthentication();
$io = new SymfonyStyle($input, $output);
try {
$this->import->setPid((int)$input->getArgument('pid'));
$this->import->setUpdate($input->getOption('update-records'));
$this->import->setGlobalIgnorePid($input->getOption('ignore-pid'));
$this->import->setForceAllUids($input->getOption('force-uid'));
$this->import->setEnableLogging($input->getOption('enable-log'));
$this->import->setImportMode($this->parseAssociativeArray($input, 'import-mode', '='));
$this->import->loadFile((string)$input->getArgument('file'));
$this->import->checkImportPrerequisites();
$this->import->importData();
$io->success('Importing ' . $input->getArgument('file') . ' to page ' . $input->getArgument('pid') . ' succeeded.');
return Command::SUCCESS;
} catch (\Exception $e) {
// Since impexp triggers core and DataHandler with potential hooks, and exception could come from "everywhere".
$io->error('Importing ' . $input->getArgument('file') . ' to page ' . $input->getArgument('pid') . ' failed.');
if ($io->isVerbose()) {
$io->writeln($e->getMessage());
$io->writeln($this->import->getErrorLog());
}
return Command::FAILURE;
}
}
/**
* Parse a basic commandline option array into an associative array by splitting each entry into a key part and
* a value part using a specific separator.
*/
protected function parseAssociativeArray(InputInterface &$input, string $optionName, string $separator): array
{
$array = [];
foreach ($input->getOption($optionName) as &$value) {
$parts = GeneralUtility::trimExplode($separator, $value, true, 2);
if (count($parts) === 2) {
$array[$parts[0]] = $parts[1];
} else {
throw new \InvalidArgumentException(
sprintf('Command line option "%s" has invalid entry "%s".', $optionName, $value),
1610464090
);
}
}
return $array;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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\ContextMenu;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;
use TYPO3\CMS\Backend\Routing\UriBuilder;
/**
* Context menu item provider adding export and import items
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class ItemProvider extends AbstractProvider
{
/**
* @var array
*/
protected $itemsConfiguration = [
'exportT3d' => [
'type' => 'item',
'label' => 'LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:export',
'iconIdentifier' => 'actions-document-export-t3d',
'callbackAction' => 'exportT3d',
],
'importT3d' => [
'type' => 'item',
'label' => 'LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:import',
'iconIdentifier' => 'actions-document-import-t3d',
'callbackAction' => 'importT3d',
],
];
public function __construct(
private readonly UriBuilder $uriBuilder,
) {
parent::__construct();
}
/**
* Export item is added for all database records except files
*/
public function canHandle(): bool
{
return $this->table !== 'sys_file';
}
/**
* This needs to be lower than priority of the RecordProvider
*/
public function getPriority(): int
{
return 50;
}
/**
* Adds import/export items to the "submenu" if available
*/
public function addItems(array $items): array
{
$this->initDisabledItems();
$localItems = $this->prepareItems($this->itemsConfiguration);
if (isset($items['more']['childItems'])) {
$items['more']['childItems'] = $items['more']['childItems'] + $localItems;
} else {
$items += $localItems;
}
return $items;
}
protected function canRender(string $itemName, string $type): bool
{
if (in_array($itemName, $this->disabledItems, true)) {
return false;
}
$canRender = false;
switch ($itemName) {
case 'exportT3d':
$canRender = $this->backendUser->isExportEnabled();
break;
case 'importT3d':
$canRender = $this->table === 'pages' && $this->backendUser->isImportEnabled();
break;
}
return $canRender;
}
/**
* Registers custom JS module with item onclick behaviour
*/
protected function getAdditionalAttributes(string $itemName): array
{
$attributes = [
'data-callback-module' => '@typo3/impexp/context-menu-actions',
];
// Add action url for items
switch ($itemName) {
case 'exportT3d':
$attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('tx_impexp_export');
break;
case 'importT3d':
$attributes['data-action-url'] = (string)$this->uriBuilder->buildUriFromRoute('tx_impexp_import');
break;
}
return $attributes;
}
}
+386
View File
@@ -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'];
}
}
+291
View File
@@ -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'];
}
}
@@ -0,0 +1,183 @@
<?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\Domain\Repository;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Impexp\Exception\InsufficientUserPermissionsException;
use TYPO3\CMS\Impexp\Exception\MalformedPresetException;
use TYPO3\CMS\Impexp\Exception\PresetNotFoundException;
/**
* Export preset repository manages export presets.
*
* @internal This class is not considered part of the public TYPO3 API.
*/
final readonly class PresetRepository
{
private const string PRESET_TABLE = 'tx_impexp_presets';
public function __construct(
private ConnectionPool $connectionPool,
private Context $context,
) {}
public function getPresets(int $pageId): array
{
$backendUser = $this->getBackendUser();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::PRESET_TABLE);
$queryBuilder->select('*')
->from(self::PRESET_TABLE)
->where(
$queryBuilder->expr()->or(
$queryBuilder->expr()->gt('public', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('user_uid', $queryBuilder->createNamedParameter($backendUser->user['uid'], Connection::PARAM_INT))
)
);
if ($pageId) {
$queryBuilder->andWhere(
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq('item_uid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('item_uid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
)
);
}
$presets = $queryBuilder->executeQuery();
// @todo: View should handle default option and data details parsing, not repository.
$options = [''];
while ($presetCfg = $presets->fetchAssociative()) {
$options[$presetCfg['uid']] = $presetCfg['title'] . ' [' . $presetCfg['uid'] . ']'
. ($presetCfg['public'] ? ' [Public]' : '')
. ((int)$presetCfg['user_uid'] === (int)$backendUser->user['uid'] ? ' [Own]' : '');
}
return $options;
}
public function createPreset(array $data): void
{
$timestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
$this->connectionPool->getConnectionForTable(self::PRESET_TABLE)->insert(
self::PRESET_TABLE,
[
'user_uid' => $this->getBackendUser()->user['uid'],
'public' => $data['preset']['public'],
'title' => $data['preset']['title'],
'item_uid' => (int)$data['pagetree']['id'],
'preset_data' => serialize($data),
'tstamp' => $timestamp,
'crdate' => $timestamp,
],
['preset_data' => Connection::PARAM_LOB]
);
}
/**
* @throws InsufficientUserPermissionsException
* @throws PresetNotFoundException
*/
public function updatePreset(int $uid, array $data): void
{
$backendUser = $this->getBackendUser();
$preset = $this->getPreset($uid);
if (!($backendUser->isAdmin() || (int)$preset['user_uid'] === (int)$backendUser->user['uid'])) {
throw new InsufficientUserPermissionsException(
'ERROR: You were not the owner of the preset so you could not delete it.',
1604584766
);
}
$timestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
$this->connectionPool->getConnectionForTable(self::PRESET_TABLE)->update(
self::PRESET_TABLE,
[
'public' => $data['preset']['public'],
'title' => $data['preset']['title'],
'item_uid' => $data['pagetree']['id'],
'preset_data' => serialize($data),
'tstamp' => $timestamp,
],
['uid' => $uid],
['preset_data' => Connection::PARAM_LOB]
);
}
/**
* @throws MalformedPresetException
*/
public function loadPreset(int $uid): array
{
$preset = $this->getPreset($uid);
// @todo: Move this to a json array instead.
$presetData = unserialize($preset['preset_data'], ['allowed_classes' => false]);
if (!is_array($presetData)) {
throw new MalformedPresetException(
'ERROR: No configuration data found in preset record!',
1604608922
);
}
return $presetData;
}
/**
* @throws InsufficientUserPermissionsException
* @throws PresetNotFoundException
*/
public function deletePreset(int $uid): void
{
$backendUser = $this->getBackendUser();
$preset = $this->getPreset($uid);
if (!($backendUser->isAdmin() || (int)$preset['user_uid'] === (int)$backendUser->user['uid'])) {
throw new InsufficientUserPermissionsException(
'ERROR: You were not the owner of the preset so you could not delete it.',
1604564346
);
}
$this->connectionPool->getConnectionForTable(self::PRESET_TABLE)->delete(
self::PRESET_TABLE,
['uid' => $uid]
);
}
/**
* Get raw preset from database. Does not unserialize preset_data as opposed to public loadPreset().
*
* @throws PresetNotFoundException
*/
private function getPreset(int $uid): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::PRESET_TABLE);
$preset = $queryBuilder->select('*')
->from(self::PRESET_TABLE)
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)))
->executeQuery()
->fetchAssociative();
if (!is_array($preset)) {
throw new PresetNotFoundException(
'ERROR: No valid preset #' . $uid . ' found.',
1604608843
);
}
return $preset;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+44
View File
@@ -0,0 +1,44 @@
<?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\Event;
use TYPO3\CMS\Impexp\Import;
/**
* This event is triggered when an import file is about to be imported
*/
final readonly class BeforeImportEvent
{
public function __construct(
private Import $import,
private string $file
) {}
public function getImport(): Import
{
return $this->import;
}
/**
* The file being about to be imported
*/
public function getFile(): string
{
return $this->file;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?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;
/**
* An exception.
* More specific exceptions extend this.
*
* @internal This class is not considered part of the public TYPO3 API. It is allowed to catch exceptions of this type.
*/
class Exception extends \Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* Import failed
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class ImportFailedException extends Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* Backend user is not allowed to perform import / export action
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class InsufficientUserPermissionsException extends Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* File could not be loaded
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class LoadingFileFailedException extends Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* Export preset is corrupted and cannot be used
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class MalformedPresetException extends Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* Prerequisites are not met
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class PrerequisitesNotMetException extends Exception {}
@@ -0,0 +1,27 @@
<?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\Exception;
use TYPO3\CMS\Impexp\Exception;
/**
* Export preset could not be found
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class PresetNotFoundException extends Exception {}
+1337
View File
File diff suppressed because it is too large Load Diff
+1677
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
<?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\Initialization;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Package\Initialization\CheckForImportRequirements;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Impexp\Utility\ImportExportUtility;
/**
* Listener to import a T3D or XML file after package activation
*/
final readonly class ImportContentOnPackageInitialization
{
public function __construct(
private Registry $registry,
private ImportExportUtility $importExportUtility,
private LoggerInterface $logger,
) {}
#[AsEventListener(after: CheckForImportRequirements::class)]
public function __invoke(PackageInitializationEvent $event): void
{
$packagePath = $event->getPackage()->getPackagePath();
$registryKeyPrefix = $event->getExtensionKey();
$registryKeysToCheck = [
$registryKeyPrefix . ':Initialisation/data.t3d',
$registryKeyPrefix . ':Initialisation/dataImported',
];
foreach ($registryKeysToCheck as $registryKeyToCheck) {
if ($this->registry->get('extensionDataImport', $registryKeyToCheck)) {
// Data was imported before -> early return
return;
}
}
$importFileToUse = null;
$possibleImportFiles = [
$packagePath . 'Initialisation/data.t3d',
$packagePath . 'Initialisation/data.xml',
];
foreach ($possibleImportFiles as $possibleImportFile) {
if (!file_exists($possibleImportFile)) {
continue;
}
$importFileToUse = $possibleImportFile;
}
if ($importFileToUse === null) {
return;
}
try {
// If the package ships its own site configurations in Initialisation/Site/, defer to
// ImportSiteConfigurationsOnPackageInitialization which handles the remapping itself.
if (is_dir($packagePath . 'Initialisation/Site')) {
$this->importExportUtility->disableSiteConfigurationImport();
}
$importResult = $this->importExportUtility->importT3DFile($importFileToUse, 0);
$this->registry->set('extensionDataImport', $registryKeyPrefix . ':Initialisation/dataImported', 1);
$event->addStorageEntry(__CLASS__, [
'importResult' => $importResult,
'importFileToUse' => $importFileToUse,
'import' => $this->importExportUtility->getImport(),
]);
} catch (\ErrorException $e) {
$this->logger->warning($e->getMessage(), ['exception' => $e]);
}
}
}
@@ -0,0 +1,115 @@
<?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\Initialization;
use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\Finder;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
use TYPO3\CMS\Core\Configuration\SiteWriter;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Package\Event\PackageInitializationEvent;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Impexp\Import;
/**
* Listener to import site configurations after package initialization
*/
final readonly class ImportSiteConfigurationsOnPackageInitialization
{
public function __construct(
private Registry $registry,
private SiteConfiguration $siteConfiguration,
private SiteWriter $siteWriter,
private LoggerInterface $logger,
) {}
#[AsEventListener(after: ImportContentOnPackageInitialization::class)]
public function __invoke(PackageInitializationEvent $event): void
{
if (!$event->hasStorageEntry(ImportContentOnPackageInitialization::class)
|| !($import = $event->getStorageEntry(ImportContentOnPackageInitialization::class)->getResult()['import'] ?? null) instanceof Import
) {
return;
}
$extensionKey = $event->getExtensionKey();
$importAbsFolder = $event->getPackage()->getPackagePath() . 'Initialisation/Site';
if (!is_dir($importAbsFolder)) {
return;
}
$destinationFolder = Environment::getConfigPath() . '/sites';
GeneralUtility::mkdir($destinationFolder);
$existingSites = $this->siteConfiguration->resolveAllExistingSites(false);
// @todo: Get rid of symfony finder here: We should use low level tools
// here to locate such files.
$finder = GeneralUtility::makeInstance(Finder::class);
$finder->directories()->ignoreUnreadableDirs()->in($importAbsFolder);
if ($finder->hasResults()) {
foreach ($finder as $siteConfigDirectory) {
$siteIdentifier = $siteConfigDirectory->getBasename();
if (isset($existingSites[$siteIdentifier])) {
$this->logger->warning('Skipped importing site configuration from {key} due to existing site identifier {site}', [
'key' => $extensionKey,
'site' => $siteIdentifier,
]);
continue;
}
$targetDir = $destinationFolder . '/' . $siteIdentifier;
if (!$this->registry->get('siteConfigImport', $siteIdentifier) && !is_dir($targetDir)) {
GeneralUtility::mkdir($targetDir);
GeneralUtility::copyDirectory($siteConfigDirectory->getPathname(), $targetDir);
$this->registry->set('siteConfigImport', $siteIdentifier, 1);
}
}
}
$newSites = array_diff_key($this->siteConfiguration->resolveAllExistingSites(false), $existingSites);
$importedPages = $import->getImportMapId()['pages'] ?? [];
$newSiteIdentifierList = [];
foreach ($newSites as $newSite) {
$exportedPageId = $newSite->getRootPageId();
$siteIdentifier = $newSite->getIdentifier();
$newSiteIdentifierList[] = $siteIdentifier;
$importedPageId = $importedPages[$exportedPageId] ?? null;
if ($importedPageId === null) {
$this->logger->warning('Imported site configuration with identifier {site} could not be mapped to imported page id', [
'site' => $siteIdentifier,
]);
continue;
}
$configuration = $this->siteConfiguration->load($siteIdentifier);
$configuration['rootPageId'] = $importedPageId;
try {
$this->siteWriter->write($siteIdentifier, $configuration);
} catch (SiteConfigurationWriteException $e) {
$this->logger->warning(
sprintf(
'Imported site configuration with identifier %s could not be written: %s',
$newSite->getIdentifier(),
$e->getMessage()
)
);
continue;
}
}
$event->addStorageEntry(__CLASS__, $newSiteIdentifierList);
}
}
+99
View File
@@ -0,0 +1,99 @@
<?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\Utility;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Impexp\Event\BeforeImportEvent;
use TYPO3\CMS\Impexp\Import;
/**
* Utility for import / export
* Can be used for API access for simple importing of files.
*
* @internal This class is not considered part of the public TYPO3 API.
*/
#[Autoconfigure(public: true)]
class ImportExportUtility
{
protected ?Import $import = null;
protected EventDispatcherInterface $eventDispatcher;
protected bool $importSiteConfigurations = true;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function getImport(): ?Import
{
return $this->import;
}
public function disableSiteConfigurationImport(): void
{
$this->importSiteConfigurations = false;
}
/**
* Import a T3D file directly
*
* @param string $file The full absolute path to the file
* @param int $pid The pid under which the t3d file should be imported
* @throws \ErrorException
* @return int ID of first created page
*/
public function importT3DFile(string $file, int $pid): int
{
$this->import = GeneralUtility::makeInstance(Import::class);
$this->import->setPid($pid);
if (!$this->importSiteConfigurations) {
$this->import->disableSiteConfigurationImport();
}
$this->eventDispatcher->dispatch(new BeforeImportEvent($this->import, $file));
try {
$this->import->loadFile($file);
$this->import->importData();
} catch (\Exception $e) {
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->warning(
$e->getMessage() . PHP_EOL . implode(PHP_EOL, $this->import->getErrorLog())
);
}
// Get id of first created page:
$importResponse = 0;
$importMapId = $this->import->getImportMapId();
if (isset($importMapId['pages'])) {
$newPages = $importMapId['pages'];
$importResponse = (int)reset($newPages);
}
// Check for errors during the import process:
if ($this->import->hasErrors()) {
if (!$importResponse) {
throw new \ErrorException('No page records imported', 1377625537);
}
}
return $importResponse;
}
}
+216
View File
@@ -0,0 +1,216 @@
<?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\View;
use TYPO3\CMS\Backend\Tree\View\AbstractTreeView;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extension of the page tree class. Used to get the tree of pages to export.
*
* @internal This class is not considered part of the public TYPO3 API.
*/
class ExportPageTreeView extends AbstractTreeView
{
/**
* If set, then ALL items will be expanded, regardless of stored settings.
*/
protected bool $expandAll = false;
/**
* Points to the current mountpoint key
* @var int
*/
public $bank = 0;
/**
* Init function
* REMEMBER to feed a $clause which will filter out non-readable pages!
*
* @param string $clause Part of where query which will filter out non-readable pages.
* @param string $orderByFields Record ORDER BY field
*/
public function init($clause = '', $orderByFields = '')
{
$tag = \Local\Multilanguage\Service\DefaultLanguageTagService::getTag();
parent::init(" AND deleted=0 AND (language_tag='" . $tag . "' OR language_tag='') " . $clause, $orderByFields);
}
/**
* Creates title attribute content for pages.
* Uses API function in \TYPO3\CMS\Backend\Utility\BackendUtility which will retrieve lots of useful information for pages.
*
* @param array $row The table row.
* @return string
*/
protected function getTitleAttrib($row)
{
return BackendUtility::titleAttribForPages($row, '1=1 ', false);
}
/**
* Wrapping Plus/Minus icon, unused in Export Page Tree
*/
protected function PMicon($row, $a, $c, $nextCount, $isOpen)
{
return '';
}
/**
* Construction of the tree structure with predefined depth.
*
* @param int $pid Page ID
* @param int $levels Page tree levels
*/
public function buildTreeByLevels(int $pid, int $levels): void
{
$this->expandAll = true;
$checkSub = $levels > 0;
$this->buildTree($pid, $levels, $checkSub);
}
/**
* Creation of a tree structure with predefined depth to prepare the export.
*
* @param int $pid Page ID
* @param int $levels Page tree levels
* @param bool $checkSub Should root page be checked for sub pages?
*/
protected function buildTree(int $pid, int $levels, bool $checkSub): void
{
$this->reset();
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
// Root page
if ($pid > 0) {
$rootRecord = BackendUtility::getRecordWSOL('pages', $pid);
$rootHtml = $iconFactory->getIconForRecord('pages', $rootRecord, IconSize::SMALL)->render();
} else {
$rootRecord = [
'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
'uid' => 0,
];
$rootHtml = $iconFactory->getIcon('apps-pagetree-root', IconSize::SMALL)->render();
}
$this->tree[] = [
'HTML' => $rootHtml,
'row' => $rootRecord,
'hasSub' => $checkSub,
'bank' => $this->bank,
];
// Subtree
if ($checkSub) {
$this->getTree($pid, $levels);
}
$idH = [];
$idH[$pid]['uid'] = $pid;
if (!empty($this->buffer_idH)) {
$idH[$pid]['subrow'] = $this->buffer_idH;
}
$this->buffer_idH = $idH;
// Check if root page has subtree
if (empty($this->buffer_idH)) {
$this->tree[0]['hasSub'] = false;
}
}
/**
* Compiles the HTML code for displaying the structure found inside the ->tree array
*
* @param array|string $treeArr "tree-array" - if blank string, the internal ->tree array is used.
* @return string The HTML code for the tree
*/
public function printTree($treeArr = '')
{
if (!is_array($treeArr)) {
$treeArr = $this->tree;
}
$out = '';
$closeDepth = [];
foreach ($treeArr as $treeItem) {
if ($treeItem['isFirst'] ?? false) {
$out .= '<ul class="treelist">';
}
$idAttr = htmlspecialchars('pages' . $treeItem['row']['uid']);
$out .= '
<li id="' . $idAttr . '">
<span class="treelist-group">
<span class="treelist-icon">' . $treeItem['HTML'] . '</span>
<span class="treelist-title">' . $this->getTitleStr($treeItem['row']) . '</span>
</span>';
if (!($treeItem['hasSub'] ?? false)) {
$out .= '</li>';
}
// We have to remember if this is the last one
// on level X so the last child on level X+1 closes the <ul>-tag
if ($treeItem['isLast'] ?? false) {
$closeDepth[$treeItem['invertedDepth']] = 1;
}
// If this is the last one and does not have subitems, we need to close
// the tree as long as the upper levels have last items too
if (($treeItem['isLast'] ?? false) && !($treeItem['hasSub'] ?? false)) {
for ($i = $treeItem['invertedDepth']; ($closeDepth[$i] ?? 0) == 1; $i++) {
$closeDepth[$i] = 0;
$out .= '</ul></li>';
}
}
}
return '<ul class="treelist treelist-root treelist-root-clean">' . $out . '</ul>';
}
/**
* Returns TRUE/FALSE if the next level for $id should be expanded - based on the ->expandAll flag.
* Extending parent function
*
* @param int $id Record id/key
* @return bool
* @internal
* @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::expandNext()
*/
public function expandNext($id)
{
return $this->expandAll;
}
/**
* Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
* Do NOT htmlspecialchar the string from this function - has already been done.
*
* @param array $row The input row array (where the key "title" is used for the title)
* @return string The title.
*/
protected function getTitleStr(array $row): string
{
$recordTitle = $row['title'] ?? '';
if (trim($recordTitle) === '') {
return '<em>[' . htmlspecialchars($this->getLanguageService()->sL('core.core:labels.no_title')) . ']</em>';
}
return htmlspecialchars(BackendUtility::cropToTitleLength($recordTitle));
}
}