0byt3m1n1
Path:
/
data
/
applications
/
aps
/
typo3
/
12.4.7
/
standard
/
htdocs
/
typo3
/
sysext
/
impexp
/
Classes
/
[
Home
]
File: ImportExport.php
<?php declare(strict_types=1); /* * This file is part of the TYPO3 CMS project. * * It is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, either version 2 * of the License, or any later version. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. * * The TYPO3 project - inspiring people to share! */ namespace TYPO3\CMS\Impexp; use TYPO3\CMS\Backend\Routing\PreviewUriBuilder; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException; use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Resource\Security\FileNameValidator; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\DiffUtility; use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; /** * T3D file Import / Export library (TYPO3 Record Document) * * @internal This class is not considered part of the public TYPO3 API. */ abstract class ImportExport { /** * Whether "import" or "export" mode of object. * * @var string */ protected $mode = ''; /** * A WHERE clause for selection records from the pages table based on read-permissions of the current backend user. * * @var string */ protected $permsClause; /** * Root page of import or export page tree * * @var int */ protected $pid = -1; /** * Root page record of import or of export page tree */ protected ?array $pidRecord = null; /** * If set, static relations (not exported) will be shown in preview as well * * @var bool */ protected $showStaticRelations = false; /** * Updates all records that has same UID instead of creating new! * * @var bool */ protected $update = false; /** * Set by importData() when an import is started. * * @var bool */ protected $doesImport = false; /** * Setting the import mode for specific import records. * Available options are: force_uid, as_new, exclude, ignore_pid, respect_pid * * @var array */ protected $importMode = []; /** * If set, PID correct is ignored globally * * @var bool */ protected $globalIgnorePid = false; /** * If set, all UID values are forced! (update or import) * * @var bool */ protected $forceAllUids = false; /** * If set, a diff-view column is added to the preview. * * @var bool */ protected $showDiff = false; /** * Array of values to substitute in editable soft references. * * @var array */ protected $softrefInputValues = []; /** * Mapping between the fileID from import memory and the final filenames they are written to. * * @var array */ protected $fileIdMap = []; /** * Add tables names here which should not be exported with the file. * (Where relations should be mapped to same UIDs in target system). * * @var array */ protected $relStaticTables = []; /** * Exclude map. Keys are table:uid pairs and if set, records are not added to the export. * * @var array */ protected $excludeMap = []; /** * Soft reference token ID modes. * * @var array */ protected $softrefCfg = []; /** * Listing extension dependencies. * * @var array */ protected $extensionDependencies = []; /** * After records are written this array is filled with [table][original_uid] = [new_uid] * * @var array */ protected $importMapId = []; /** * Error log. * * @var array */ protected $errorLog = []; /** * Cache for record paths * * @var array */ protected $cacheGetRecordPath = []; /** * Internal import/export memory * * @var array */ protected $dat = []; /** * File processing object * * @var ExtendedFileUtility */ protected $fileProcObj; /** * @var DiffUtility */ protected $diffUtility; /** * @var array */ protected $remainHeader = []; /** * @var LanguageService */ protected $lang; /** * @var IconFactory */ protected $iconFactory; /** * Name of the "fileadmin" folder where files for export/import should be located * * @var string */ protected $fileadminFolderName = ''; protected ?string $temporaryFolderName = null; protected ?Folder $defaultImportExportFolder = null; /** * Flag to control whether all disabled records and their children are excluded (true) or included (false). Defaults * to the old behaviour of including everything. * * @var bool */ protected $excludeDisabledRecords = false; /** * Array of currently registered storage objects * * @var ResourceStorage[] */ protected $storages = []; /** * Array of currently registered storage objects available for importing files to * * @var ResourceStorage[] */ protected $storagesAvailableForImport = []; /** * Currently registered default storage object */ protected ?ResourceStorage $defaultStorage = null; /** * @var StorageRepository */ protected $storageRepository; /** * The constructor */ public function __construct() { $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); $this->lang = $this->getLanguageService(); $this->permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW); $this->fetchStorages(); } /** * Fetch all available file storages and index by storage UID * * Note: It also creates a default storage record if the database table sys_file_storage is empty, * e.g. during tests. */ protected function fetchStorages(): void { $this->storages = []; $this->storagesAvailableForImport = []; $this->defaultStorage = null; $this->getStorageRepository()->flush(); $storages = $this->getStorageRepository()->findAll(); // @todo: Why by reference here? Test ImagesWithStoragesTest::importMultipleImagesWithMultipleStorages fails otherwise foreach ($storages as &$storage) { $this->storages[$storage->getUid()] = $storage; if ($storage->isOnline() && $storage->isWritable() && $storage->getDriverType() === 'Local') { $this->storagesAvailableForImport[$storage->getUid()] = $storage; } if ($this->defaultStorage === null && $storage->isDefault()) { $this->defaultStorage = $storage; } } } /******************************************************** * Visual rendering of import/export memory, $this->dat ********************************************************/ /** * Displays a preview of the import or export. * * @return array The preview data */ public function renderPreview(): array { // @todo: Why is this done? unset($this->dat['files']); $previewData = [ 'update' => $this->update, 'showDiff' => $this->showDiff, 'insidePageTree' => [], 'outsidePageTree' => [], ]; if (!isset($this->dat['header']['pagetree']) && !isset($this->dat['header']['records'])) { return $previewData; } // Traverse header: $this->remainHeader = $this->dat['header']; // Preview of the page tree to be exported if (is_array($this->dat['header']['pagetree'] ?? null)) { $this->traversePageTree($this->dat['header']['pagetree'], $previewData['insidePageTree']); foreach ($previewData['insidePageTree'] as &$line) { $line['controls'] = $this->renderControls($line); $line['message'] = (($line['msg'] ?? '') && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($line['msg']) . '</span>' : ''); } } // Preview the remaining records that were not included in the page tree if (is_array($this->remainHeader['records'] ?? null)) { if (is_array($this->remainHeader['records']['pages'] ?? null)) { $this->traversePageRecords($this->remainHeader['records']['pages'], $previewData['outsidePageTree']); } $this->traverseAllRecords($this->remainHeader['records'], $previewData['outsidePageTree']); foreach ($previewData['outsidePageTree'] as &$line) { $line['controls'] = $this->renderControls($line); $line['message'] = (($line['msg'] ?? '') && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($line['msg']) . '</span>' : ''); } } return $previewData; } /** * Go through page tree for display * * @param array<int, array> $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree]) * @param array $lines Output lines array * @param int $indent Indentation level */ protected function traversePageTree(array $pageTree, array &$lines, int $indent = 0): void { foreach ($pageTree as $pageUid => $page) { if ($this->excludeDisabledRecords === true && $this->isRecordDisabled('pages', $pageUid)) { $this->excludePageAndRecords($pageUid, $page); continue; } // Add page $this->addRecord('pages', $pageUid, $lines, $indent); // Add records if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) { foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) { $table = (string)$table; if ($table !== 'pages') { foreach (array_keys($records) as $uid) { $this->addRecord($table, (int)$uid, $lines, $indent + 1); } } } unset($this->remainHeader['pid_lookup'][$pageUid]); } // Add subtree if (is_array($page['subrow'] ?? null)) { $this->traversePageTree($page['subrow'], $lines, $indent + 1); } } } /** * Test whether a record is disabled (e.g. hidden) * * @param string $table Name of the records' database table * @param int $uid Database uid of the record * @return bool true if the record is disabled, false otherwise */ protected function isRecordDisabled(string $table, int $uid): bool { return (bool)($this->dat['records'][$table . ':' . $uid]['data'][ $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] ?? '' ] ?? false); } /** * Exclude a page, its sub pages (recursively) and records placed in them from this import/export * * @param int $pageUid Uid of the page to exclude * @param array $page Page array with uid/subrow (from ->dat[header][pagetree]) */ protected function excludePageAndRecords(int $pageUid, array $page): void { // Exclude page unset($this->remainHeader['records']['pages'][$pageUid]); // Exclude records if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) { foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) { if ($table !== 'pages') { foreach (array_keys($records) as $uid) { unset($this->remainHeader['records'][$table][$uid]); } } } unset($this->remainHeader['pid_lookup'][$pageUid]); } // Exclude subtree if (is_array($page['subrow'] ?? null)) { foreach ($page['subrow'] as $subPageUid => $subPage) { $this->excludePageAndRecords($subPageUid, $subPage); } } } /** * Go through remaining pages (not in tree) * * @param array<int, array> $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree]) * @param array $lines Output lines array */ protected function traversePageRecords(array $pageTree, array &$lines): void { foreach ($pageTree as $pageUid => $_) { // Add page $this->addRecord('pages', (int)$pageUid, $lines, 0, true); // Add records if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) { foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) { if ($table !== 'pages') { foreach (array_keys($records) as $uid) { $this->addRecord((string)$table, (int)$uid, $lines, 2); } } } unset($this->remainHeader['pid_lookup'][$pageUid]); } } } /** * Go through ALL records (if the pages are displayed first, those will not be among these!) * * @param array $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree]) * @param array $lines Output lines array */ protected function traverseAllRecords(array $pageTree, array &$lines): void { foreach ($pageTree as $table => $records) { $this->addGeneralErrorsByTable($table); if ($table !== 'pages') { foreach (array_keys($records) as $uid) { $this->addRecord((string)$table, (int)$uid, $lines, 0, true); } } } } /** * Log general error message for a given table * * @param string $table database table name */ protected function addGeneralErrorsByTable(string $table): void { if ($this->update && $table === 'sys_file') { $this->addError('Updating sys_file records is not supported! They will be imported as new records!'); } if ($this->forceAllUids && $table === 'sys_file') { $this->addError('Forcing uids of sys_file records is not supported! They will be imported as new records!'); } } /** * Add a record, its relations and soft references, to the preview * * @param string $table Table name * @param int $uid Record uid * @param array $lines Output lines array * @param int $indent Indentation level * @param bool $checkImportInPidRecord If you want import validation, you can set this so it checks if the import can take place on the specified page. */ protected function addRecord(string $table, int $uid, array &$lines, int $indent, bool $checkImportInPidRecord = false): void { $record = $this->dat['header']['records'][$table][$uid] ?? null; unset($this->remainHeader['records'][$table][$uid]); if (!is_array($record) && !($table === 'pages' && $uid === 0)) { $this->addError('MISSING RECORD: ' . $table . ':' . $uid); } // Create record information for preview $line = []; $line['ref'] = $table . ':' . $uid; $line['type'] = 'record'; $line['msg'] = ''; if ($table === '_SOFTREF_') { // Record is a soft reference $line['preCode'] = $this->renderIndent($indent); $line['title'] = '<em>' . htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_softReferencesFiles')) . '</em>'; } elseif (!isset($GLOBALS['TCA'][$table])) { // Record is of unknown table $line['preCode'] = $this->renderIndent($indent); $line['title'] = '<em>' . htmlspecialchars((string)$record['title']) . '</em>'; $line['msg'] = 'UNKNOWN TABLE "' . $line['ref'] . '"'; } else { $pidRecord = $this->getPidRecord(); $line['preCode'] = '' . $this->renderIndent($indent) . $this->iconFactory ->getIconForRecord( $table, (array)($this->dat['records'][$table . ':' . $uid]['data'] ?? []), Icon::SIZE_SMALL ) ->setTitle($line['ref']) ->render(); $line['title'] = htmlspecialchars($record['title'] ?? ''); // Link to page view if ($table === 'pages') { $viewID = $this->mode === 'export' ? $uid : ($this->doesImport ? ($this->importMapId['pages'][$uid] ?? 0) : 0); if ($viewID) { $attributes = PreviewUriBuilder::create($viewID)->serializeDispatcherAttributes(); if ($attributes) { $line['title'] = sprintf('<a href="#" %s>%s</a>', $attributes, $line['title']); } } } $line['active'] = !$this->isRecordDisabled($table, $uid) ? 'active' : 'hidden'; if ($this->mode === 'import' && $pidRecord !== null) { if ($checkImportInPidRecord) { if (!$this->getBackendUser()->doesUserHaveAccess($pidRecord, ($table === 'pages' ? 8 : 16))) { $line['msg'] .= '"' . $line['ref'] . '" cannot be INSERTED on this page! '; } if ($this->pid > 0 && !$this->checkDokType($table, $pidRecord['doktype']) && !$GLOBALS['TCA'][$table]['ctrl']['rootLevel']) { $line['msg'] .= '"' . $table . '" cannot be INSERTED on this page type (change page type to "Folder".) '; } } if (!$this->getBackendUser()->check('tables_modify', $table)) { $line['msg'] .= 'You are not allowed to CREATE "' . $table . '" tables! '; } if ($GLOBALS['TCA'][$table]['ctrl']['readOnly'] ?? false) { $line['msg'] .= 'TABLE "' . $table . '" is READ ONLY! '; } if (($GLOBALS['TCA'][$table]['ctrl']['adminOnly'] ?? false) && !$this->getBackendUser()->isAdmin()) { $line['msg'] .= 'TABLE "' . $table . '" is ADMIN ONLY! '; } if ($GLOBALS['TCA'][$table]['ctrl']['is_static'] ?? false) { $line['msg'] .= 'TABLE "' . $table . '" is a STATIC TABLE! '; } if ((int)($GLOBALS['TCA'][$table]['ctrl']['rootLevel'] ?? 0) === 1) { $line['msg'] .= 'TABLE "' . $table . '" will be inserted on ROOT LEVEL! '; } $databaseRecord = null; if ($this->update) { $databaseRecord = $this->getRecordFromDatabase($table, $uid, $this->showDiff ? '*' : 'uid,pid'); if ($databaseRecord === null) { $line['updatePath'] = '<strong>NEW!</strong>'; } else { $line['updatePath'] = htmlspecialchars($this->getRecordPath((int)($databaseRecord['pid'] ?? 0))); } if ($table === 'sys_file') { $line['updateMode'] = ''; } else { $line['updateMode'] = $this->renderImportModeSelector( $table, $uid, $databaseRecord !== null ); } } // Diff view if ($this->showDiff) { $diffInverse = $this->update ? true : false; // For imports, get new id: if (isset($this->importMapId[$table][$uid]) && $newUid = $this->importMapId[$table][$uid]) { $diffInverse = false; $databaseRecord = $this->getRecordFromDatabase($table, $newUid, '*'); BackendUtility::workspaceOL($table, $databaseRecord); } /** @var array|null $importRecord */ $importRecord = $this->dat['records'][$table . ':' . $uid]['data'] ?? null; if ($databaseRecord === null) { $line['showDiffContent'] = ''; } elseif (is_array($databaseRecord) && is_array($importRecord)) { $line['showDiffContent'] = $this->compareRecords($databaseRecord, $importRecord, $table, $diffInverse); } else { $line['showDiffContent'] = 'ERROR: One of the inputs were not an array!'; } } } } $lines[] = $line; // File relations if (is_array($record['filerefs'] ?? null)) { $this->addFiles($record['filerefs'], $lines, $indent); } // Database relations if (is_array($record['rels'] ?? null)) { $this->addRelations($record['rels'], $lines, $indent); } // Soft references if (is_array($record['softrefs'] ?? null)) { $this->addSoftRefs($record['softrefs'], $lines, $indent); } } /** * Add database relations of a record to the preview * * @param array $relations Array of relations * @param array $lines Output lines array * @param int $indent Indentation level * @param array $recursionCheck Recursion check stack * * @see addRecord() */ protected function addRelations(array $relations, array &$lines, int $indent, array $recursionCheck = []): void { foreach ($relations as $relation) { $table = $relation['table']; $uid = $relation['id']; $line = []; $line['ref'] = $table . ':' . $uid; $line['type'] = 'rel'; $line['msg'] = ''; if (in_array($line['ref'], $recursionCheck, true)) { continue; } $iconName = 'status-status-checked'; $staticFixed = false; $record = null; if ($uid > 0) { $record = $this->dat['header']['records'][$table][$uid] ?? null; if (!is_array($record)) { if ($this->isTableStatic($table) || $this->isRecordExcluded($table, (int)$uid) || ($relation['tokenID'] ?? '') && !$this->isSoftRefIncluded($relation['tokenID'] ?? '')) { $line['title'] = htmlspecialchars('STATIC: ' . $line['ref']); $staticFixed = true; } else { $databaseRecord = $this->getRecordFromDatabase($table, (int)$uid); $recordPath = $this->getRecordPath($databaseRecord === null ? 0 : ($table === 'pages' ? (int)$databaseRecord['uid'] : (int)$databaseRecord['pid'])); $line['title'] = sprintf( '<span title="%s">%s</span>', htmlspecialchars($recordPath), htmlspecialchars($line['ref']) ); $line['msg'] = 'LOST RELATION' . ($databaseRecord === null ? ' (Record not found!)' : ' (Path: ' . $recordPath . ')'); $iconName = 'status-dialog-warning'; } } else { $recordPath = $this->getRecordPath($table === 'pages' ? (int)$record['uid'] : (int)$record['pid']); $line['title'] = sprintf( '<span title="%s">%s</span>', htmlspecialchars($recordPath), htmlspecialchars((string)$record['title']) ); } } else { // Negative values in relation fields. These are typically fields of e.g. fe_users. // They are static values. They CAN theoretically be negative pointers to uids in other tables, // but this is so rarely used that it is not supported. $line['title'] = htmlspecialchars('FIXED: ' . $line['ref']); $staticFixed = true; } $line['preCode'] = '' . $this->renderIndent($indent + 1) . $this->iconFactory ->getIcon($iconName, Icon::SIZE_SMALL) ->setTitle($line['ref']) ->render(); if (!$staticFixed || $this->showStaticRelations) { $lines[] = $line; if (is_array($record) && is_array($record['rels'] ?? null)) { $this->addRelations($record['rels'], $lines, $indent + 1, array_merge($recursionCheck, [$line['ref']])); } } } } /** * Add file relations of a record to the preview. * * Public access for testing purpose only. * * @param array $relations Array of file IDs * @param array $lines Output lines array * @param int $indent Indentation level * @param string $tokenID Token ID if this is a soft reference (in which case it only makes sense with a single element in the $relations array!) * * @see addRecord() */ public function addFiles(array $relations, array &$lines, int $indent, string $tokenID = ''): void { foreach ($relations as $ID) { $line = []; $line['msg'] = ''; $fileInfo = $this->dat['header']['files'][$ID]; if (!is_array($fileInfo)) { if ($tokenID !== '' || $this->isSoftRefIncluded($tokenID)) { $line['msg'] = 'MISSING FILE: ' . $ID; $this->addError('MISSING FILE: ' . $ID); } else { return; } } $line['ref'] = 'FILE'; $line['type'] = 'file'; $line['preCode'] = '' . $this->renderIndent($indent + 1) . $this->iconFactory ->getIcon('status-reference-hard', Icon::SIZE_SMALL) ->setTitle($line['ref']) ->render(); $line['title'] = htmlspecialchars($fileInfo['filename']); $line['showDiffContent'] = PathUtility::stripPathSitePrefix((string)($this->fileIdMap[$ID] ?? '')); // If import mode and there is a non-RTE soft reference, check the destination directory. if ($this->mode === 'import' && $tokenID !== '' && !($fileInfo['RTE_ORIG_ID'] ?? false)) { // Check folder existence if (isset($fileInfo['parentRelFileName'])) { $line['msg'] = 'Seems like this file is already referenced from within an HTML/CSS file. That takes precedence. '; } else { $origDirPrefix = PathUtility::dirname($fileInfo['relFileName']) . '/'; $dirPrefix = $this->resolveStoragePath($origDirPrefix); if ($dirPrefix === null) { $line['msg'] = 'ERROR: There are no available file mounts to write file in!'; } elseif ($origDirPrefix !== $dirPrefix) { $line['msg'] = 'File will be attempted written to "' . $dirPrefix . '". '; } } // Check file existence if (file_exists(Environment::getPublicPath() . '/' . $fileInfo['relFileName'])) { if ($this->update) { $line['updatePath'] = 'File exists.'; } else { $line['msg'] .= 'File already exists! '; } } // Check file extension $fileProcObj = $this->getFileProcObj(); if ($fileProcObj->actionPerms['addFile']) { $pathInfo = GeneralUtility::split_fileref(Environment::getPublicPath() . '/' . $fileInfo['relFileName']); if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($pathInfo['file'])) { $line['msg'] .= 'File extension was not allowed!'; } } else { $line['msg'] = 'Your user profile does not allow you to create files on the server!'; } } $lines[] = $line; unset($this->remainHeader['files'][$ID]); // RTE originals if ($fileInfo['RTE_ORIG_ID'] ?? false) { $ID = $fileInfo['RTE_ORIG_ID']; $line = []; $fileInfo = $this->dat['header']['files'][$ID]; if (!is_array($fileInfo)) { $line['msg'] = 'MISSING RTE original FILE: ' . $ID; $this->addError('MISSING RTE original FILE: ' . $ID); } $line['ref'] = 'FILE'; $line['type'] = 'file'; $line['preCode'] = '' . $this->renderIndent($indent + 1) . $this->iconFactory ->getIcon('status-reference-hard', Icon::SIZE_SMALL) ->setTitle($line['ref']) ->render(); $line['title'] = htmlspecialchars($fileInfo['filename']) . ' <em>(Original)</em>'; $line['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIdMap[$ID]); $lines[] = $line; unset($this->remainHeader['files'][$ID]); } // External resources if (is_array($fileInfo['EXT_RES_ID'] ?? null)) { foreach ($fileInfo['EXT_RES_ID'] as $extID) { $line = []; $fileInfo = $this->dat['header']['files'][$extID]; if (!is_array($fileInfo)) { $line['msg'] = 'MISSING External Resource FILE: ' . $extID; $this->addError('MISSING External Resource FILE: ' . $extID); } else { $line['updatePath'] = $fileInfo['parentRelFileName']; } $line['ref'] = 'FILE'; $line['type'] = 'file'; $line['preCode'] = '' . $this->renderIndent($indent + 1) . $this->iconFactory ->getIcon('actions-insert-reference', Icon::SIZE_SMALL) ->setTitle($line['ref']) ->render(); $line['title'] = htmlspecialchars($fileInfo['filename']) . ' <em>(Resource)</em>'; $line['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIdMap[$extID]); $lines[] = $line; unset($this->remainHeader['files'][$extID]); } } } } /** * Add soft references of a record to the preview * * @param array $softrefs Soft references * @param array $lines Output lines array * @param int $indent Indentation level * * @see addRecord() */ protected function addSoftRefs(array $softrefs, array &$lines, int $indent): void { foreach ($softrefs as $softref) { $line = []; $line['ref'] = 'SOFTREF'; $line['type'] = 'softref'; $line['msg'] = ''; $line['preCode'] = '' . $this->renderIndent($indent) . $this->iconFactory ->getIcon('status-reference-soft', Icon::SIZE_SMALL) ->setTitle($line['ref']) ->render(); $line['title'] = sprintf( '<em>%s, "%s"</em>: <span title="%s">%s</span>', $softref['field'], $softref['spKey'], htmlspecialchars($softref['matchString'] ?? ''), htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['matchString'] ?? '', 60)) ); if ($softref['subst']['type'] ?? false) { if ($softref['subst']['title'] ?? false) { $line['title'] .= sprintf( '<br>%s <strong>%s</strong> %s', $this->renderIndent($indent + 1), htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_title')), htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['subst']['title'], 60)) ); } if ($softref['subst']['description'] ?? false) { $line['title'] .= sprintf( '<br>%s <strong>%s</strong> %s', $this->renderIndent($indent + 1), htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_descr')), htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['subst']['description'], 60)) ); } if ($softref['subst']['type'] === 'db') { $line['title'] .= sprintf( '<br>%s <strong>%s</strong> %s', $this->renderIndent($indent + 1), htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_softrefsel_record')), $softref['subst']['recordRef'] ); } elseif ($softref['subst']['type'] === 'file') { $line['title'] .= sprintf( '<br>%s <strong>%s</strong> %s', $this->renderIndent($indent + 1), htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_filename')), $softref['subst']['relFileName'] ); } elseif ($softref['subst']['type'] === 'string') { $line['title'] .= sprintf( '<br>%s <strong>%s</strong> %s', $this->renderIndent($indent + 1), htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_value')), $softref['subst']['tokenValue'] ); } } $line['_softRefInfo'] = $softref; $mode = $this->softrefCfg[$softref['subst']['tokenID'] ?? null]['mode'] ?? ''; if (isset($softref['error']) && $mode !== Import::SOFTREF_IMPORT_MODE_EDITABLE && $mode !== Import::SOFTREF_IMPORT_MODE_EXCLUDE) { $line['msg'] .= $softref['error']; } $lines[] = $line; // Add database relations if (($softref['subst']['type'] ?? '') === 'db') { [$referencedTable, $referencedUid] = explode(':', $softref['subst']['recordRef']); $relations = [['table' => $referencedTable, 'id' => $referencedUid, 'tokenID' => $softref['subst']['tokenID']]]; $this->addRelations($relations, $lines, $indent + 1); } // Add files relations if (($softref['subst']['type'] ?? '') === 'file') { $relations = [$softref['file_ID']]; $this->addFiles($relations, $lines, $indent + 1, $softref['subst']['tokenID']); } } } protected function renderIndent(int $indent): string { return $indent > 0 ? '<span class="indent indent-inline-block" style="--indent-level: ' . $indent . '"></span>' : ''; } /** * Verifies that a table is allowed on a certain doktype of a page. * * @param string $table Table name to check * @param int $dokType Page doktype * @return bool TRUE if OK */ protected function checkDokType(string $table, int $dokType): bool { return GeneralUtility::makeInstance(PageDoktypeRegistry::class)->isRecordTypeAllowedForDoktype($table, $dokType); } /** * Render input controls for import or export * * @param array $line Output line array * @return string HTML */ protected function renderControls(array $line): string { if ($this->mode === 'export') { if ($line['type'] === 'record') { return $this->renderRecordExcludeCheckbox($line['ref']); } if ($line['type'] === 'softref') { return $this->renderSoftRefExportSelector($line['_softRefInfo']); } } elseif ($this->mode === 'import') { if ($line['type'] === 'softref') { return $this->renderSoftRefImportTextField($line['_softRefInfo']); } } return ''; } /** * Render check box for exclusion of a record from export. * * @param string $recordRef The record ID of the form [table]:[id]. * @return string HTML */ protected function renderRecordExcludeCheckbox(string $recordRef): string { return '' . '<div class="form-check mb-0">' . '<input class="form-check-input t3js-exclude-checkbox" type="checkbox" name="tx_impexp[exclude][' . $recordRef . ']" id="checkExclude' . $recordRef . '" value="1" />' . '<label class="form-check-label" for="checkExclude' . $recordRef . '">' . htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_exclude')) . '</label>' . '</div>'; } /** * Render text field when importing a soft reference. * * @param array $softref Soft reference * @return string HTML */ protected function renderSoftRefImportTextField(array $softref): string { if (isset($softref['subst']['tokenID'])) { $tokenID = $softref['subst']['tokenID']; $cfg = $this->softrefCfg[$tokenID] ?? []; if (($cfg['mode'] ?? '') === Import::SOFTREF_IMPORT_MODE_EDITABLE) { $html = ''; if ($cfg['title'] ?? false) { $html .= '<strong>' . htmlspecialchars((string)$cfg['title']) . '</strong><br>'; } $html .= htmlspecialchars((string)$cfg['description']) . '<br>'; $html .= sprintf( '<input type="text" name="tx_impexp[softrefInputValues][%s]" value="%s" />', $tokenID, htmlspecialchars($this->softrefInputValues[$tokenID] ?? $cfg['defValue']) ); return $html; } } return ''; } /** * Render select box with export options for soft references. * An export box is shown only if a substitution scheme is found for the soft reference. * * @param array $softref Soft reference * @return string HTML */ protected function renderSoftRefExportSelector(array $softref): string { $fileInfo = isset($softref['file_ID']) ? $this->dat['header']['files'][$softref['file_ID']] : []; // Substitution scheme has to be around and RTE images MUST be exported. if (isset($softref['subst']['tokenID']) && !isset($fileInfo['RTE_ORIG_ID'])) { $options = []; $options[''] = ''; $options[Import::SOFTREF_IMPORT_MODE_EDITABLE] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_softrefsel_editable'); $options[Import::SOFTREF_IMPORT_MODE_EXCLUDE] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_softrefsel_exclude'); $value = $this->softrefCfg[$softref['subst']['tokenID']]['mode'] ?? ''; $selectHtml = $this->renderSelectBox( 'tx_impexp[softrefCfg][' . $softref['subst']['tokenID'] . '][mode]', $value, $options ); $textFieldHtml = ''; if ($value === Import::SOFTREF_IMPORT_MODE_EDITABLE) { if ($softref['subst']['title'] ?? false) { $textFieldHtml .= sprintf( ' <input type="hidden" name="tx_impexp[softrefCfg][%1$s][title]" value="%2$s" /> <strong>%2$s</strong><br>', $softref['subst']['tokenID'], htmlspecialchars($softref['subst']['title']) ); } if (!($softref['subst']['description'] ?? false)) { $textFieldHtml .= sprintf( ' %s<br> <input type="text" name="tx_impexp[softrefCfg][%s][description]" value="%s" />', htmlspecialchars($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_printerror_description')), $softref['subst']['tokenID'], htmlspecialchars($this->softrefCfg[$softref['subst']['tokenID']]['description'] ?? '') ); } else { $textFieldHtml .= sprintf( ' <input type="hidden" name="tx_impexp[softrefCfg][%1$s][description]" value="%2$s" />%2$s', $softref['subst']['tokenID'], htmlspecialchars($softref['subst']['description']) ); } $textFieldHtml .= sprintf( ' <input type="hidden" name="tx_impexp[softrefCfg][%s][defValue]" value="%s" />', $softref['subst']['tokenID'], htmlspecialchars($softref['subst']['tokenValue']) ); } return $selectHtml . $textFieldHtml; } return ''; } /** * Render select box with import options for the record. * * @param string $table Table name * @param int $uid Record UID * @param bool $doesRecordExist Is there already a record with this UID in the database? * @return string HTML */ protected function renderImportModeSelector(string $table, int $uid, bool $doesRecordExist): string { $options = []; if (!$doesRecordExist) { $options[] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_insert'); if ($this->getBackendUser()->isAdmin()) { $options[Import::IMPORT_MODE_FORCE_UID] = sprintf($this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_forceUidSAdmin'), $uid); } } else { $options[] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_update'); $options[Import::IMPORT_MODE_AS_NEW] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_importAsNew'); if (!$this->globalIgnorePid) { $options[Import::IMPORT_MODE_IGNORE_PID] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_ignorePid'); } else { $options[Import::IMPORT_MODE_RESPECT_PID] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_respectPid'); } } $options[Import::IMPORT_MODE_EXCLUDE] = $this->lang->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:impexpcore_singlereco_exclude'); return $this->renderSelectBox( 'tx_impexp[import_mode][' . $table . ':' . $uid . ']', (string)($this->importMode[$table . ':' . $uid] ?? ''), $options ); } /** * Renders a select box from option values. * * @param string $name Form element name * @param string $value Current value * @param array $options Options to display (key/value pairs) * @return string HTML */ protected function renderSelectBox(string $name, string $value, array $options): string { $optionsHtml = ''; $isValueInOptions = false; foreach ($options as $k => $v) { if ((string)$k === $value) { $isValueInOptions = true; $selectedHtml = ' selected="selected"'; } else { $selectedHtml = ''; } $optionsHtml .= sprintf( '<option value="%s"%s>%s</option>', htmlspecialchars((string)$k), $selectedHtml, htmlspecialchars((string)$v) ); } // Append and select the current value as an option of the form "[value]" // if it is not available in the options. if (!$isValueInOptions && $value !== '') { $optionsHtml .= sprintf( '<option value="%s" selected="selected">%s</option>', htmlspecialchars($value), htmlspecialchars('[\'' . $value . '\']') ); } return '<select class="form-select form-select-sm" name="' . $name . '" style="width: 100px">' . $optionsHtml . '</select>'; } public function getFileadminFolderName(): string { if (empty($this->fileadminFolderName)) { if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'])) { $this->fileadminFolderName = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/'); } else { $this->fileadminFolderName = 'fileadmin'; } } return $this->fileadminFolderName; } public function getOrCreateTemporaryFolderName(): string { if (empty($this->temporaryFolderName)) { $this->temporaryFolderName = $this->createTemporaryFolderName(); } return $this->temporaryFolderName; } protected function createTemporaryFolderName(): string { $temporaryPath = Environment::getVarPath() . '/transient'; do { $temporaryFolderName = sprintf( '%s/impexp_%s_files_%d', $temporaryPath, $this->mode, random_int(1, PHP_INT_MAX) ); } while (is_dir($temporaryFolderName)); GeneralUtility::mkdir_deep($temporaryFolderName); return $temporaryFolderName; } public function removeTemporaryFolderName(): void { if (!empty($this->temporaryFolderName)) { GeneralUtility::rmdir($this->temporaryFolderName, true); $this->temporaryFolderName = null; } } /** * Returns a \TYPO3\CMS\Core\Resource\Folder object for saving export files * to the server and is also used for uploading import files. */ public function getOrCreateDefaultImportExportFolder(): ?Folder { if (empty($this->defaultImportExportFolder)) { $this->createDefaultImportExportFolder(); } return $this->defaultImportExportFolder; } /** * Creates a \TYPO3\CMS\Core\Resource\Folder object for saving export files * to the server and is also used for uploading import files. */ protected function createDefaultImportExportFolder(): void { $defaultTemporaryFolder = $this->getBackendUser()->getDefaultUploadTemporaryFolder(); $defaultImportExportFolder = null; $importExportFolderName = 'importexport'; if ($defaultTemporaryFolder !== null) { if ($defaultTemporaryFolder->hasFolder($importExportFolderName) === false) { $defaultImportExportFolder = $defaultTemporaryFolder->createFolder($importExportFolderName); } else { $defaultImportExportFolder = $defaultTemporaryFolder->getSubfolder($importExportFolderName); } } $this->defaultImportExportFolder = $defaultImportExportFolder; } public function removeDefaultImportExportFolder(): void { if (!empty($this->defaultImportExportFolder)) { $this->defaultImportExportFolder->delete(true); $this->defaultImportExportFolder = null; } } /** * Checks if the input path relative to the public web path can be found in the file mounts of the backend user. * If not, it checks all file mounts of the user for the relative path and returns it if found. * * @param string $dirPrefix Path relative to public web path. * @param bool $checkAlternatives If set to false, do not look for an alternative path. * @return string If a path is available, it will be returned, otherwise NULL. * @throws \Exception */ protected function resolveStoragePath(string $dirPrefix, bool $checkAlternatives = true): ?string { try { GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($dirPrefix); return $dirPrefix; } catch (InsufficientFolderAccessPermissionsException $e) { if ($checkAlternatives) { $storagesByUser = $this->getBackendUser()->getFileStorages(); foreach ($storagesByUser as $storage) { try { $folder = $storage->getFolder(rtrim($dirPrefix, '/')); return $folder->getPublicUrl(); } catch (InsufficientFolderAccessPermissionsException $e) { } } } } return null; } /** * Recursively flattening the $pageTree array to a one-dimensional array with uid-pid pairs. * * @param array $pageTree Page tree array * @param array $list List with uid-pid pairs * @param int $pid PID value (internal, don't set from outside) */ protected function flatInversePageTree(array $pageTree, array &$list, int $pid = -1): void { // @todo: return $list instead of by-reference?! $pageTreeInverse = array_reverse($pageTree); foreach ($pageTreeInverse as $page) { $list[$page['uid']] = $pid; if (is_array($page['subrow'] ?? null)) { $this->flatInversePageTree($page['subrow'], $list, (int)$page['uid']); } } } /** * Returns TRUE if the input table name is to be regarded as a static relation (that is, not exported etc). * * @param string $table Table name * @return bool TRUE, if table is marked static */ protected function isTableStatic(string $table): bool { if (is_array($GLOBALS['TCA'][$table] ?? null)) { return ($GLOBALS['TCA'][$table]['ctrl']['is_static'] ?? false) || in_array($table, $this->relStaticTables, true) || in_array('_ALL', $this->relStaticTables, true); } return false; } /** * Returns TRUE if the element should be excluded from import and export. * * @param string $table Table name * @param int $uid Record UID * @return bool TRUE, if the record should be excluded */ protected function isRecordExcluded(string $table, int $uid): bool { return (bool)($this->excludeMap[$table . ':' . $uid] ?? false); } /** * Returns TRUE if the soft reference should be included in export. * * @param string $tokenID Token ID for soft reference * @return bool TRUE, if soft reference should be included */ protected function isSoftRefIncluded(string $tokenID): bool { $mode = $this->softrefCfg[$tokenID]['mode'] ?? ''; return $tokenID && $mode !== Import::SOFTREF_IMPORT_MODE_EXCLUDE && $mode !== Import::SOFTREF_IMPORT_MODE_EDITABLE; } /** * Returns given fields of record if it exists. * * @param string $table Table name * @param int $uid UID of record * @param string $fields Field list to select. Default is "uid,pid" * @return array|null Result of \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord() which means the record if found, otherwise NULL */ protected function getRecordFromDatabase(string $table, int $uid, string $fields = 'uid,pid'): ?array { return BackendUtility::getRecord($table, $uid, $fields); } /** * Returns the page title path of a PID value. Results are cached internally * * @param int $pid Record PID to check * @return string The path for the input PID */ protected function getRecordPath(int $pid): string { if (!isset($this->cacheGetRecordPath[$pid])) { $this->cacheGetRecordPath[$pid] = (string)BackendUtility::getRecordPath($pid, $this->permsClause, 20); } return $this->cacheGetRecordPath[$pid]; } /** * Compares two records, the current database record and the one from the import memory. * Will return HTML code to show any differences between them! * * @param array $databaseRecord Database record, all fields (old values) * @param array $importRecord Import memory record for the same table/uid, all fields (new values) * @param string $table The table name of the record * @param bool $inverse Inverse the diff view (switch red/green, needed for pre-update difference view) * @return string HTML */ protected function compareRecords(array $databaseRecord, array $importRecord, string $table, bool $inverse = false): string { $diffHtml = ''; // Updated fields foreach ($databaseRecord as $fieldName => $_) { if (is_array($GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null) && $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] !== 'passthrough' ) { if (isset($importRecord[$fieldName])) { if (trim((string)$databaseRecord[$fieldName]) !== trim((string)$importRecord[$fieldName])) { $diffFieldHtml = $this->getDiffUtility()->makeDiffDisplay( (string)BackendUtility::getProcessedValue( $table, $fieldName, !$inverse ? $importRecord[$fieldName] : $databaseRecord[$fieldName], 0, true, true ), (string)BackendUtility::getProcessedValue( $table, $fieldName, !$inverse ? $databaseRecord[$fieldName] : $importRecord[$fieldName], 0, true, true ) ); $diffHtml .= sprintf( '<tr><td>%s (%s)</td><td>%s</td></tr>' . PHP_EOL, htmlspecialchars($this->lang->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])), htmlspecialchars((string)$fieldName), $diffFieldHtml ); } unset($importRecord[$fieldName]); } } } // New fields foreach ($importRecord as $fieldName => $_) { if (is_array($GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null) && $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] !== 'passthrough' ) { $diffFieldHtml = '<strong>Field missing</strong> in database'; $diffHtml .= sprintf( '<tr><td>%s (%s)</td><td>%s</td></tr>' . PHP_EOL, htmlspecialchars($this->lang->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])), htmlspecialchars((string)$fieldName), $diffFieldHtml ); } } if ($diffHtml !== '') { $diffHtml = '<table class="table table-striped table-hover">' . PHP_EOL . $diffHtml . '</table>'; } else { $diffHtml = 'Match'; } return sprintf( '<strong class="text-nowrap">[%s]:</strong>' . PHP_EOL . '%s', htmlspecialchars($table . ':' . $importRecord['uid'] . ' => ' . $databaseRecord['uid']), $diffHtml ); } /** * Returns string comparing object, initialized only once. * * @return DiffUtility String comparing object */ protected function getDiffUtility(): DiffUtility { if ($this->diffUtility === null) { $this->diffUtility = GeneralUtility::makeInstance(DiffUtility::class); } return $this->diffUtility; } /** * Returns file processing object, initialized only once. * * @return ExtendedFileUtility File processor object */ protected function getFileProcObj(): ExtendedFileUtility { if ($this->fileProcObj === null) { $this->fileProcObj = GeneralUtility::makeInstance(ExtendedFileUtility::class); $this->fileProcObj->setActionPermissions(); } return $this->fileProcObj; } /** * Returns storage repository object, initialized only once. * * @return StorageRepository Storage repository object */ protected function getStorageRepository(): StorageRepository { if ($this->storageRepository === null) { $this->storageRepository = GeneralUtility::makeInstance(StorageRepository::class); } return $this->storageRepository; } /***************************** * Error handling *****************************/ /** * Sets error message in the internal error log * * @param string $message Error message */ protected function addError(string $message): void { $this->errorLog[] = $message; } public function hasErrors(): bool { return empty($this->errorLog) === false; } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } public function getPid(): int { return $this->pid; } public function setPid(int $pid): void { $this->pid = $pid; $this->pidRecord = null; } /** * Return record of root page of import or of export page tree * - or null if access denied to that page. * * If the page is the root of the page tree, * add some basic but missing information. */ protected function getPidRecord(): ?array { if ($this->pidRecord === null && $this->pid >= 0) { $pidRecord = BackendUtility::readPageAccess($this->pid, $this->permsClause); if (is_array($pidRecord)) { if ($this->pid === 0) { $pidRecord += ['title' => '[root-level]', 'uid' => 0, 'pid' => 0]; } $this->pidRecord = $pidRecord; } } return $this->pidRecord; } /** * Set flag to control whether disabled records and their children are excluded (true) or included (false). Defaults * to the old behaviour of including everything. * * @param bool $excludeDisabledRecords Set to true if if all disabled records should be excluded, false otherwise */ public function setExcludeDisabledRecords(bool $excludeDisabledRecords): void { $this->excludeDisabledRecords = $excludeDisabledRecords; } public function isExcludeDisabledRecords(): bool { return $this->excludeDisabledRecords; } public function getExcludeMap(): array { return $this->excludeMap; } public function setExcludeMap(array $excludeMap): void { $this->excludeMap = $excludeMap; } public function getSoftrefCfg(): array { return $this->softrefCfg; } public function setSoftrefCfg(array $softrefCfg): void { $this->softrefCfg = $softrefCfg; } public function getExtensionDependencies(): array { return $this->extensionDependencies; } public function setExtensionDependencies(array $extensionDependencies): void { $this->extensionDependencies = $extensionDependencies; } public function isShowStaticRelations(): bool { return $this->showStaticRelations; } public function setShowStaticRelations(bool $showStaticRelations): void { $this->showStaticRelations = $showStaticRelations; } public function getRelStaticTables(): array { return $this->relStaticTables; } public function setRelStaticTables(array $relStaticTables): void { $this->relStaticTables = $relStaticTables; } public function getErrorLog(): array { return $this->errorLog; } public function setErrorLog(array $errorLog): void { $this->errorLog = $errorLog; } public function isUpdate(): bool { return $this->update; } public function setUpdate(bool $update): void { $this->update = $update; } public function getImportMode(): array { return $this->importMode; } public function setImportMode(array $importMode): void { $this->importMode = $importMode; } public function isGlobalIgnorePid(): bool { return $this->globalIgnorePid; } public function setGlobalIgnorePid(bool $globalIgnorePid): void { $this->globalIgnorePid = $globalIgnorePid; } public function isForceAllUids(): bool { return $this->forceAllUids; } public function setForceAllUids(bool $forceAllUids): void { $this->forceAllUids = $forceAllUids; } public function isShowDiff(): bool { return $this->showDiff; } public function setShowDiff(bool $showDiff): void { $this->showDiff = $showDiff; } public function getSoftrefInputValues(): array { return $this->softrefInputValues; } public function setSoftrefInputValues(array $softrefInputValues): void { $this->softrefInputValues = $softrefInputValues; } public function getMode(): string { return $this->mode; } public function setMode(string $mode): void { $this->mode = $mode; } public function getImportMapId(): array { return $this->importMapId; } public function setImportMapId(array $importMapId): void { $this->importMapId = $importMapId; } public function getDat(): array { return $this->dat; } }