fetchStorages(); } /** * Disable automatic site configuration import. Call this when the invoking * code (e.g. a distribution installer) handles site configuration itself. */ public function disableSiteConfigurationImport(): void { $this->importSiteConfigurations = false; } /** * 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->storageRepository->flush(); $storages = $this->storageRepository->findAll(); 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; } } } /** * Loads the TYPO3 import file $fileName into memory. * * @param string $fileName File path, has to be within the TYPO3's base folder * @throws LoadingFileFailedException */ public function loadFile(string $fileName): void { $filePath = GeneralUtility::getFileAbsFileName($fileName); if (empty($filePath)) { $this->addError('File path is not valid: ' . $fileName); } elseif (!@is_file($filePath)) { $this->addError('File not found: ' . $filePath); } if ($this->hasErrors()) { throw new LoadingFileFailedException( sprintf('Loading of the import file "%s" failed.', $fileName), 1484484619 ); } $pathInfo = pathinfo($filePath); $fileExtension = strtolower($pathInfo['extension']); if (!in_array($fileExtension, $this->supportedFileExtensions, true)) { $this->addError( sprintf( 'File extension "%s" is not valid. Supported file extensions are %s.', $fileExtension, implode(', ', array_map(static function ($supportedFileExtension) { return '"' . $supportedFileExtension . '"'; }, $this->supportedFileExtensions)) ) ); } if ($this->hasErrors() === false) { if (@is_dir($filePath . '.files')) { if (GeneralUtility::isAllowedAbsPath($filePath . '.files')) { // copy the folder lowlevel to typo3temp, because the files would be deleted after import GeneralUtility::copyDirectory($filePath . '.files', $this->getOrCreateTemporaryFolderName()); } else { $this->addError('External import files for the given import source is currently not supported.'); } $this->isFilesSavedOutsideImportFile = true; } else { $this->isFilesSavedOutsideImportFile = false; } if ($fileExtension === 'xml') { $xmlContent = (string)file_get_contents($filePath); if (strlen($xmlContent)) { try { $dat = (new Typo3XmlParser())->decode( $xmlContent, new Typo3XmlSerializerOptions([ Typo3XmlSerializerOptions::RETURN_ROOT_NODE_NAME => true, Typo3XmlSerializerOptions::LOAD_OPTIONS => \LIBXML_NONET | \LIBXML_NOBLANKS | \LIBXML_PARSEHUGE, // @todo check if needed for imports/throw deprecation for invalid xml Typo3XmlSerializerOptions::ALLOW_UNDEFINED_NAMESPACES, ]) ); $this->dat = is_array($dat) ? $dat : [$dat]; if ($this->dat['_DOCUMENT_TAG'] === 'T3RecordDocument' && is_array($this->dat['header'] ?? null) && is_array($this->dat['records'] ?? null)) { $this->loadInit(); } else { $this->addError('XML file did not contain proper XML for TYPO3 Import'); } } catch (\Throwable $e) { $this->addError('XML could not be parsed: ' . $e->getMessage()); } } else { $this->addError('Error opening file: ' . $filePath); } } elseif ($fileExtension === 't3d') { if ($fd = fopen($filePath, 'rb')) { $this->dat['header'] = $this->getNextFilePart($fd, 'header'); $this->dat['records'] = $this->getNextFilePart($fd, 'records'); $this->dat['files'] = $this->getNextFilePart($fd, 'files'); $this->dat['files_fal'] = $this->getNextFilePart($fd, 'files_fal'); $this->loadInit(); fclose($fd); } else { $this->addError('Error opening file: ' . $filePath); } } } if ($this->hasErrors()) { throw new LoadingFileFailedException( sprintf('Loading of the import file "%s" failed.', $fileName), 1484484620 ); } } /** * Extracts the next content part of the T3D file * * @param resource $fd Import file pointer * @param string $name For error messages this indicates the section of the problem. * @return array|null Data array or NULL in case of an error */ protected function getNextFilePart($fd, string $name): ?array { $headerLength = 32 + 1 + 1 + 1 + 10 + 1; $headerString = fread($fd, $headerLength); if (empty($headerString)) { $this->addError('File does not contain data for "' . $name . '"'); return null; } $header = explode(':', $headerString); if (str_contains($header[0], 'Warning')) { $this->addError('File read error: Warning message in file. (' . $headerString . fgets($fd) . ')'); return null; } if ((string)$header[3] !== '') { $this->addError('File read error: InitString had a wrong length. (' . $name . ')'); return null; } $dataString = (string)fread($fd, (int)$header[2]); $isDataCompressed = $header[1] === '1'; fread($fd, 1); if (!hash_equals($header[0], md5($dataString))) { $this->addError('MD5 check failed (' . $name . ')'); return null; } if ($isDataCompressed) { if (!function_exists('gzuncompress')) { $this->addError('Content read error: This file requires decompression, ' . 'but this server does not offer gzcompress()/gzuncompress() functions.'); return null; } $dataString = (string)gzuncompress($dataString); } return unserialize($dataString, ['allowed_classes' => false]) ?: null; } /** * Setting up the object based on the recently loaded ->dat array */ protected function loadInit(): void { $this->relStaticTables = (array)($this->dat['header']['relStaticTables'] ?? []); $this->excludeMap = (array)($this->dat['header']['excludeMap'] ?? []); $this->softrefCfg = (array)($this->dat['header']['softrefCfg'] ?? []); } public function getMetaData(): array { return $this->dat['header']['meta'] ?? []; } public function getSiteConfigurations(): array { $siteConfigurations = $this->dat['header']['site_configurations'] ?? []; foreach ($siteConfigurations as $identifier => $config) { $rootPageId = (int)($config['rootPageId'] ?? 0); $siteConfigurations[$identifier]['_rootPageTitle'] = $this->dat['header']['records']['pages'][$rootPageId]['title'] ?? ''; } return $siteConfigurations; } /** * Checks all requirements that must be met before import. * * @throws PrerequisitesNotMetException */ public function checkImportPrerequisites(): void { // Check #1: Extension dependencies $extKeysToInstall = []; foreach ($this->dat['header']['extensionDependencies'] ?? [] as $extKey) { if (!empty($extKey) && !ExtensionManagementUtility::isLoaded($extKey)) { $extKeysToInstall[] = $extKey; } } if ($extKeysToInstall !== []) { $this->addError( sprintf( 'Before you can import this file you need to install the extensions "%s".', implode('", "', $extKeysToInstall) ) ); } // Check #2: Presence of imported storage paths foreach ($this->dat['header']['records']['sys_file_storage'] ?? [] as $sysFileStorageUid => $_) { $storageRecord = &$this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data']; if ($storageRecord['driver'] === 'Local' && ($storageRecord['is_writable'] ?? 1) && ($storageRecord['is_online'] ?? 1) ) { $storageMapUid = -1; foreach ($this->storages as $storage) { if ($this->isEquivalentStorage($storage, $storageRecord)) { $storageMapUid = $storage->getUid(); break; } } // The storage from the import does not have an equivalent storage // in the current instance (same driver, same path, etc.). Before // the storage record can get inserted later on take care the path // it points to really exists and is accessible. if ($storageMapUid === -1) { // Unset the storage record UID when trying to create the storage object // as the record does not already exist in database. The constructor of the // storage object will check whether the target folder exists and set the // isOnline flag depending on the outcome. $storageRecordWithUid0 = $storageRecord; $storageRecordWithUid0['uid'] = 0; $storageObject = $this->storageRepository->createFromRecord($storageRecordWithUid0); if (!$storageObject->isOnline()) { $configuration = $storageObject->getConfiguration(); $this->addError( sprintf( 'The file storage "%s" does not exist. ' . 'Please create the directory prior to starting the import!', $storageObject->getName() . $configuration['basePath'] ) ); } } } } if ($this->hasErrors()) { throw new PrerequisitesNotMetException( 'Prerequisites for file import are not met.', 1484484612 ); } } /** * Imports the memory data into the TYPO3 database. * * @throws ImportFailedException */ public function importData(): void { $this->initializeImport(); // Write sys_file_storages first $this->writeSysFileStorageRecords(); // Write sys_file records and write the binary file data $this->writeSysFileRecords(); // Write records, first pages, then the rest // Fields with "hard" relations to database, files and flexform fields are kept empty during this run $this->writePages(); $this->writeRecords(); // Finally all the file and database record references must be fixed. This is done after all records have supposedly // been written to database. $this->importMapId will indicate two things: // 1) that a record WAS written to db and // 2) that it has got a new id-number. $this->setRelations(); // And when all database relations are in place, we can fix file and database relations in flexform fields // - since data structures often depend on relations to a DS record: $this->setFlexFormRelations(); // Finally, traverse all records and process soft references with substitution attributes. $this->processSoftReferences(); // Write site configurations bundled in the export, with rootPageId remapped to imported UIDs. // Skipped when the caller handles site configuration import itself (e.g. distribution installers) // or when the current user is not an admin (site configurations are admin-only). if ($this->importSiteConfigurations && $this->getBackendUser()->isAdmin()) { $this->processSiteConfigurations(); } // Cleanup $this->removeTemporaryFolderName(); if ($this->hasErrors()) { throw new ImportFailedException('The import has failed.', 1484484613); } } /** * Write site configurations embedded in the import file. * Skips any site whose identifier already exists. * Remaps rootPageId from the export UID to the newly imported UID. */ protected function processSiteConfigurations(): void { $siteConfigurations = $this->dat['header']['site_configurations'] ?? []; if (!is_array($siteConfigurations) || $siteConfigurations === []) { return; } $importedPageIds = $this->importMapId['pages'] ?? []; foreach ($siteConfigurations as $siteIdentifier => $configuration) { if (!is_string($siteIdentifier) || !is_array($configuration)) { continue; } $exportedRootPageId = (int)($configuration['rootPageId'] ?? 0); $importedRootPageId = $importedPageIds[$exportedRootPageId] ?? null; if ($importedRootPageId === null) { continue; } // Skip if a site configuration already exists for this root page. try { $this->siteFinder->getSiteByRootPageId((int)$importedRootPageId); continue; } catch (SiteNotFoundException) { // No site exists yet — proceed with creating one. } // Find a free identifier — never merge into an existing site config. $targetIdentifier = $siteIdentifier; $counter = 0; while (true) { try { $this->siteFinder->getSiteByIdentifier($targetIdentifier); $targetIdentifier = $siteIdentifier . '-' . (++$counter); } catch (SiteNotFoundException) { break; } } $configuration['rootPageId'] = (int)$importedRootPageId; $configuration['base'] = '/' . $targetIdentifier . '/'; // @TODO Add error handling / routes etc where page ids are used and configured try { $this->siteWriter->write($targetIdentifier, $configuration); } catch (SiteConfigurationWriteException) { // Site configuration write failures are non-fatal; the imported data remains intact. } } } /** * Initialize all settings for the import */ protected function initializeImport(): void { $this->doesImport = true; $this->importMapId = []; $this->importNewId = []; $this->importNewIdPids = []; } /** * Imports the sys_file_storage records from memory data. */ protected function writeSysFileStorageRecords(): void { if (!isset($this->dat['header']['records']['sys_file_storage'])) { return; } $importData = []; $storageUidsToBeResetToDefaultStorage = []; foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) { $storageRecord = &$this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data']; if ($storageRecord['driver'] === 'Local' && ($storageRecord['is_writable'] ?? 1) && ($storageRecord['is_online'] ?? 1) ) { foreach ($this->storages as $storage) { if ($this->isEquivalentStorage($storage, $storageRecord)) { $this->importMapId['sys_file_storage'][$sysFileStorageUid] = $storage->getUid(); break; } } if (!isset($this->importMapId['sys_file_storage'][$sysFileStorageUid])) { // Local, writable and online storage. May be used later for writing files. // Does not currently exist, mark the storage for import. $this->addSingle($importData, 'sys_file_storage', $sysFileStorageUid, 0); } } else { // Storage with non-local drivers can be imported, but must not be used to save files as you cannot // be sure that this is supported. In this case the default storage is used. Non-writable and // non-online storage may be created as duplicates because you were unable to check the detailed // configuration options at that time. $this->addSingle($importData, 'sys_file_storage', $sysFileStorageUid, 0); $storageUidsToBeResetToDefaultStorage[] = $sysFileStorageUid; } } // Write new storages to the database $dataHandler = $this->createDataHandler(); // Because all records are submitted in the correct order with positive pid numbers, // we should internally reverse the order of submission. $dataHandler->reverseOrder = true; $dataHandler->isImporting = true; $dataHandler->start($importData, []); $dataHandler->process_datamap(); $this->addToMapId($importData, $dataHandler->substNEWwithIDs); // Refresh internal storage representation after potential storage import $this->fetchStorages(); // Map references of non-local / non-writable / non-online storages to the default storage $defaultStorageUid = $this->defaultStorage?->getUid(); foreach ($storageUidsToBeResetToDefaultStorage as $storageUidToBeResetToDefaultStorage) { $this->importMapId['sys_file_storage'][$storageUidToBeResetToDefaultStorage] = $defaultStorageUid; } // Unset the sys_file_storage records to prevent an import in writeRecords() unset($this->dat['header']['records']['sys_file_storage']); } /** * Determines whether the passed storage object and the storage record (sys_file_storage) can be considered * equivalent during the import. * * @param ResourceStorage $storageObject The storage object which should get compared * @param array $storageRecord The storage record which should get compared * @return bool Returns TRUE if both storage representations can be considered equal */ protected function isEquivalentStorage(ResourceStorage $storageObject, array &$storageRecord): bool { if ($storageObject->getDriverType() === $storageRecord['driver'] && $storageObject->isWritable() === (bool)($storageRecord['is_writable'] ?? 1) && $storageObject->isOnline() === (bool)($storageRecord['is_online'] ?? 1) ) { $storageRecordConfiguration = $this->flexFormTools->convertFlexFormContentToArray($storageRecord['configuration'] ?? ''); $storageObjectConfiguration = $storageObject->getConfiguration(); if ($storageRecordConfiguration['pathType'] === $storageObjectConfiguration['pathType'] && $storageRecordConfiguration['basePath'] === $storageObjectConfiguration['basePath'] ) { return true; } } return false; } /** * Imports the sys_file records and the binary files data from internal data array. */ protected function writeSysFileRecords(): void { if (!isset($this->dat['header']['records']['sys_file'])) { return; } $this->addGeneralErrorsByTable('sys_file'); $temporaryFolder = $this->getOrCreateTemporaryFolderName(); $sanitizedFolderMappings = []; foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) { $fileRecord = &$this->dat['records']['sys_file:' . $sysFileUid]['data']; $temporaryFile = null; $temporaryFilePath = $temporaryFolder . '/' . $fileRecord['sha1']; if ($this->isFilesSavedOutsideImportFile) { if (is_file($temporaryFilePath) && sha1_file($temporaryFilePath) === $fileRecord['sha1']) { $temporaryFile = $temporaryFilePath; } else { $this->addError(sprintf( 'Error: Temporary file %s could not be found or does not match the checksum!', $temporaryFilePath )); continue; } } else { $fileId = md5($fileRecord['storage'] . ':' . $fileRecord['identifier_hash']); if (isset($this->dat['files_fal'][$fileId]['content'])) { $fileInfo = &$this->dat['files_fal'][$fileId]; if (GeneralUtility::writeFile($temporaryFilePath, $fileInfo['content'], true)) { clearstatcache(); $temporaryFile = $temporaryFilePath; } else { $this->addError(sprintf( 'Error: Temporary file %s was not written as it should have been!', $temporaryFilePath )); continue; } } else { $this->addError(sprintf('Error: No file found for ID %s', $fileId)); continue; } } $storageUid = $this->importMapId['sys_file_storage'][$fileRecord['storage']] ?? $fileRecord['storage']; if (isset($this->storagesAvailableForImport[$storageUid])) { $storage = $this->storagesAvailableForImport[$storageUid]; } elseif ($storageUid === 0 || $storageUid === '0') { $storage = $this->storageRepository->findByUid(0); } elseif ($this->defaultStorage !== null) { $storage = $this->defaultStorage; } else { $this->addError(sprintf( 'Error: No storage available for the file "%s" with storage uid "%s"', $fileRecord['identifier'], $fileRecord['storage'] )); continue; } $file = null; try { if ($storage->hasFile($fileRecord['identifier'])) { /** @var File $file */ $file = $storage->getFile($fileRecord['identifier']); if ($file->getSha1() !== $fileRecord['sha1']) { $file = null; } } } catch (Exception $e) { // @todo: Can this exception be thrown anywhere? $file = null; } if ($file === null) { $folderName = PathUtility::dirname(ltrim($fileRecord['identifier'], '/')); if (in_array($folderName, $sanitizedFolderMappings, true)) { $folderName = $sanitizedFolderMappings[$folderName]; } if (!$storage->hasFolder($folderName)) { try { $importFolder = $storage->createFolder($folderName); if ($importFolder->getIdentifier() !== $folderName && !in_array($folderName, $sanitizedFolderMappings, true)) { $sanitizedFolderMappings[$folderName] = $importFolder->getIdentifier(); } } catch (Exception $e) { $this->addError(sprintf( 'Error: Folder "%s" could not be created for file "%s" with storage uid "%s"', $folderName, $fileRecord['identifier'], $fileRecord['storage'] )); continue; } } else { $importFolder = $storage->getFolder($folderName); } $this->callHook('before_addSysFileRecord', [ 'fileRecord' => $fileRecord, 'importFolder' => $importFolder, 'temporaryFile' => $temporaryFile, ]); try { $this->skipResourceConsistencyCheckForCommands($storage, $temporaryFile, $fileRecord['name']); $file = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']); } catch (Exception $e) { $this->addError(sprintf( 'Error: File could not be added to the storage: "%s" with storage uid "%s"', $fileRecord['identifier'], $fileRecord['storage'] )); continue; } if ($file->getSha1() !== $fileRecord['sha1']) { $this->addError(sprintf( 'Error: The hash of the written file is not identical to the import data! ' . 'File could be corrupted! File: "%s" with storage uid "%s"', $fileRecord['identifier'], $fileRecord['storage'] )); } } // save the new uid in the import id map $this->importMapId['sys_file'][$fileRecord['uid']] = $file->getUid(); $this->fixUidLocalInSysFileReferenceRecords((int)$fileRecord['uid'], $file->getUid()); } // unset the sys_file records to prevent an import in writeRecords() unset($this->dat['header']['records']['sys_file']); // remove all sys_file_reference records that point to file records which are unknown // in the system to prevent exceptions $this->removeSysFileReferenceRecordsWithRelationToMissingFile(); } /** * Normally the importer works like the following: * Step 1: import the records with cleared field values of relation fields (see addSingle()) * Step 2: update the records with the right relation ids (see setRelations()) * * In step 2 the saving fields of type "relation to sys_file_reference" checks the related sys_file_reference * record (created in step 1) with the FileExtensionFilter for matching file extensions of the related file. * To make this work correct, the uid_local of sys_file_reference records has to be not empty AND has to * relate to the correct (imported) sys_file record uid! * * This is fixed here. * * @param int $oldFileUid * @param int $newFileUid */ protected function fixUidLocalInSysFileReferenceRecords(int $oldFileUid, int $newFileUid): void { if (!isset($this->dat['header']['records']['sys_file_reference'])) { return; } foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) { if (!isset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped']) && $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] == $oldFileUid ) { $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'] = true; $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] = $newFileUid; } } } /** * Removes all sys_file_reference records from the import data array that are pointing to sys_file records which * are missing in the import data to prevent exceptions on checking the related file started by the DataHandler. */ protected function removeSysFileReferenceRecordsWithRelationToMissingFile(): void { if (!isset($this->dat['header']['records']['sys_file_reference'])) { return; } foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) { $fileReferenceRecord = &$this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']; if (!in_array($fileReferenceRecord['uid_local'], (array)($this->importMapId['sys_file'] ?? []))) { unset($this->dat['header']['records']['sys_file_reference'][$sysFileReferenceUid]); unset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]); $this->addError(sprintf( 'Error: sys_file_reference record "%s" with relation to sys_file record "%s"' . ', which is not part of the import data, was not imported.', $sysFileReferenceUid, $fileReferenceRecord['uid_local'] )); } } } /** * Writing page tree / pages to database: * If the operation is an update operation, the root of the page tree inside will be moved to $this->pid * unless it is the same as the root page from the import. */ protected function writePages(): void { if (!isset($this->dat['header']['records']['pages'])) { return; } $importData = []; // Add page tree $remainingPages = $this->dat['header']['records']['pages']; if (is_array($this->dat['header']['pagetree'] ?? null)) { $pageList = []; $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList); foreach ($pageList as $pageUid => $_) { $pid = $this->dat['header']['records']['pages'][$pageUid]['pid'] ?? null; if ($pid !== null) { $pid = (int)$pid; } $pid = $this->importNewIdPids[$pid ?? ''] ?? $this->pid; $this->addSingle($importData, 'pages', (int)$pageUid, $pid); unset($remainingPages[$pageUid]); } } // Add remaining pages on root level foreach ($remainingPages as $pageUid => $_) { $this->addSingle($importData, 'pages', (int)$pageUid, $this->pid); } // Write pages to the database $dataHandler = $this->createDataHandler(); $dataHandler->isImporting = true; $this->callHook('before_writeRecordsPages', [ 'tce' => $dataHandler, 'data' => &$importData, ]); $dataHandler->suggestedInsertUids = $this->suggestedInsertUids; $dataHandler->start($importData, []); $dataHandler->process_datamap(); $this->callHook('after_writeRecordsPages', [ 'tce' => $dataHandler, ]); $this->addToMapId($importData, $dataHandler->substNEWwithIDs); // Sort pages $this->writePagesOrder(); } /** * Organize all updated pages in page tree so they are related like in the import file. * Only used for updates. */ protected function writePagesOrder(): void { if (!$this->update || !is_array($this->dat['header']['pagetree'] ?? null)) { return; } $importCmd = []; // Get uid-pid relations and traverse them in order to map to possible new IDs $pageList = []; $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList); foreach ($pageList as $pageUid => $pagePid) { if ($pagePid >= 0 && $this->doRespectPid('pages', $pageUid)) { // If the page has been assigned a new ID (because it was created), use that instead! if (!MathUtility::canBeInterpretedAsInteger($this->importNewIdPids[$pageUid])) { if ($this->importMapId['pages'][$pageUid]) { $mappedUid = $this->importMapId['pages'][$pageUid]; $importCmd['pages'][$mappedUid]['move'] = $pagePid; } } else { $importCmd['pages'][$pageUid]['move'] = $pagePid; } } } // Move pages in the database if (!empty($importCmd)) { $dataHandler = $this->createDataHandler(); $this->callHook('before_writeRecordsPagesOrder', [ 'tce' => &$dataHandler, 'data' => &$importCmd, ]); $dataHandler->start([], $importCmd); $dataHandler->process_cmdmap(); $this->callHook('after_writeRecordsPagesOrder', [ 'tce' => &$dataHandler, ]); } } /** * Checks if the position of an updated record is configured to be corrected. * This can be disabled globally and changed individually for elements. * * @param string $table Table name * @param int $uid Record UID * @return bool TRUE if the position of the record should be updated to match the one in the import structure */ protected function doRespectPid(string $table, int $uid): bool { return ($this->importMode[$table . ':' . $uid] ?? '') !== self::IMPORT_MODE_IGNORE_PID && (!$this->globalIgnorePid || ($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_RESPECT_PID); } /** * Write all database records except pages (written in writePages()) */ protected function writeRecords(): void { $importData = []; // Write the rest of the records if (is_array($this->dat['header']['records'] ?? null)) { foreach ($this->dat['header']['records'] as $table => $records) { $this->addGeneralErrorsByTable($table); if (!$this->tcaSchemaFactory->has($table)) { continue; } if ($table !== 'pages') { $schema = $this->tcaSchemaFactory->get($table); $rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel); foreach ($records as $uid => $record) { // PID: Set the main $this->pid, unless a NEW-id is found $pid = isset($this->importMapId['pages'][$record['pid']]) ? (int)$this->importMapId['pages'][$record['pid']] : $this->pid; if ($rootLevelCapability->getRootLevelType() === RootLevelCapability::TYPE_ONLY_ON_ROOTLEVEL) { $pid = 0; } elseif (!$rootLevelCapability->canExistOnRootLevel() && $pid === 0) { $this->addError('Error: Record type ' . $table . ' is not allowed on pid 0'); continue; } // Add record $this->addSingle($importData, $table, $uid, $pid); } } } } else { $this->addError('Error: No records defined in internal data array.'); } // Write records to the database $dataHandler = $this->createDataHandler(); $this->callHook('before_writeRecordsRecords', [ 'tce' => $dataHandler, 'data' => &$importData, ]); $dataHandler->suggestedInsertUids = $this->suggestedInsertUids; // Because all records are submitted in the correct order with positive pid numbers, // we should internally reverse the order of submission. $dataHandler->reverseOrder = true; $dataHandler->isImporting = true; $dataHandler->start($importData, []); $dataHandler->process_datamap(); $this->callHook('after_writeRecordsRecords', [ 'tce' => $dataHandler, ]); $this->addToMapId($importData, $dataHandler->substNEWwithIDs); // Sort records $this->writeRecordsOrder(); } /** * Organize all updated records so they are related like in the import file. * Only used for updates. */ protected function writeRecordsOrder(): void { if (!$this->update) { return; } $importCmd = []; $pageList = []; if (is_array($this->dat['header']['pagetree'] ?? null)) { $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList); } // @todo: drop by-reference and write final $this->dat at the end of method?! if (is_array($this->dat['header']['pid_lookup'] ?? null)) { foreach ($this->dat['header']['pid_lookup'] as $pid => &$recordsByPid) { $mappedPid = $this->importMapId['pages'][$pid] ?? $this->pid; if (MathUtility::canBeInterpretedAsInteger($mappedPid)) { foreach ($recordsByPid as $table => &$records) { // If $mappedPid === $this->pid then we are on root level and we can consider to move pages as well! // (they will not be in the page tree!) if ($table !== 'pages' || !isset($pageList[$pid])) { foreach (array_reverse(array_keys($records)) as $uid) { if ($this->doRespectPid($table, (int)$uid)) { if (isset($this->importMapId[$table][$uid])) { $mappedUid = $this->importMapId[$table][$uid]; $importCmd[$table][$mappedUid]['move'] = $mappedPid; } } } } } } } } // Move records in the database if (!empty($importCmd)) { $dataHandler = $this->createDataHandler(); $this->callHook('before_writeRecordsRecordsOrder', [ 'tce' => $dataHandler, 'data' => &$importCmd, ]); $dataHandler->start([], $importCmd); $dataHandler->process_cmdmap(); $this->callHook('after_writeRecordsRecordsOrder', [ 'tce' => $dataHandler, ]); } } /** * Adds a single record to the $importData array. Also copies files to the temporary folder. * However all file and database references and flexform fields are set to blank for now! * That is processed with setRelations() later. * * @param array $importData Data to be modified or inserted in the database during import * @param string $table Table name * @param int $uid Record UID * @param int|string $pid Page id or NEW-id, e.g. "NEW5fb3c2641281c885267727" */ protected function addSingle(array &$importData, string $table, int $uid, $pid): void { // @todo return modified $importData instead of by-reference. if (($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_EXCLUDE) { return; } $record = $this->dat['records'][$table . ':' . $uid]['data'] ?? null; if (!is_array($record)) { if (!($table === 'pages' && $uid === 0)) { // On root level we don't want this error message. $this->addError('Error: No record was found in data array!'); } return; } // Generate record ID $ID = StringUtility::getUniqueId('NEW'); if ($this->update && $this->getRecordFromDatabase($table, $uid) !== null && ($this->importMode[$table . ':' . $uid] ?? '') !== self::IMPORT_MODE_AS_NEW ) { $ID = $uid; } elseif ($table === 'sys_file_metadata' && $record['language_tag'] === '0' && isset($this->importMapId['sys_file'][$record['file']]) ) { // On adding sys_file records the belonging sys_file_metadata record was also created: // If there is one, the record needs to be overwritten instead of a new one created. $databaseRecord = $this->getSysFileMetaDataFromDatabase( $this->importMapId['sys_file'][$record['file']], 0 ); if (is_array($databaseRecord)) { $this->importMapId['sys_file_metadata'][$record['uid']] = $databaseRecord['uid']; $ID = $databaseRecord['uid']; } } // Mapping of generated record ID to original record UID $this->importNewId[$table . ':' . $ID] = ['table' => $table, 'uid' => $uid]; if ($table === 'pages') { $this->importNewIdPids[$uid] = $ID; } // Record data $importData[$table][$ID] = $record; $importData[$table][$ID]['tx_impexp_origuid'] = $importData[$table][$ID]['uid']; // Record permissions if ($table === 'pages') { // Have to reset the user/group IDs so pages are owned by the importing user. // Otherwise strange things may happen for non-admins! unset($importData[$table][$ID]['perms_userid']); unset($importData[$table][$ID]['perms_groupid']); } // Record UID and PID unset($importData[$table][$ID]['uid']); // - for existing record if (MathUtility::canBeInterpretedAsInteger($ID)) { unset($importData[$table][$ID]['pid']); } // - for new record else { $importData[$table][$ID]['pid'] = $pid; if ((($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_FORCE_UID && $this->update || $this->forceAllUids) && $this->getBackendUser()->isAdmin() ) { $importData[$table][$ID]['uid'] = $uid; $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE'; } } // Record relations $schema = $this->tcaSchemaFactory->get($table); foreach ($this->dat['records'][$table . ':' . $uid]['rels'] ?? [] as $field => &$relation) { switch ($relation['type'] ?? '') { case 'db': case 'file': // Set blank now, fix later in setRelations(), // because we need to know ALL newly created IDs before we can map relations! // In the meantime we set NO values for relations. // // BUT for field uid_local of table sys_file_reference the relation MUST not be cleared here, // because the value is already the uid of the right imported sys_file record. // @see fixUidLocalInSysFileReferenceRecords() // If it's empty or a uid to another record the FileExtensionFilter will throw an exception or // delete the reference record if the file extension of the related record doesn't match. if (!($table === 'sys_file_reference' && $field === 'uid_local') && $schema->hasField($field)) { $importData[$table][$ID][$field] = $this->getReferenceDefaultValue($schema->getField($field)->getConfiguration()); } $translationSourceFieldName = null; if ($schema->isLanguageAware()) { $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $translationSourceFieldName = $languageCapability->getTranslationSourceField()?->getName(); } // Set to "0" for integer fields, or else we will get a db error in DataHandler persistence. if ($translationSourceFieldName && $field === $translationSourceFieldName) { $importData[$table][$ID][$field] = 0; } break; case 'flex': // Set blank now, fix later in setFlexFormRelations(). // In the meantime we set NO values for flexforms - this is mainly because file references // inside will not be processed properly. In fact references will point to no file // or existing files (in which case there will be double-references which is a big problem of // course!). // // BUT for the field "configuration" of the table "sys_file_storage" the relation MUST NOT be // cleared, because the configuration array contains only string values, which are furthermore // important for the further import, e.g. the base path. if (!($table === 'sys_file_storage' && $field === 'configuration')) { $importData[$table][$ID][$field] = $this->getReferenceDefaultValue($schema->getField($field)->getConfiguration()); } break; } } } /** * Get the default value for a reference field. * * @param array $configuration The TCA configuration of the accordant field */ protected function getReferenceDefaultValue(array $configuration): int|float|string { if (!empty($configuration['MM']) || !empty($configuration['foreign_field'])) { return 0; } if (array_key_exists('default', $configuration)) { return $configuration['default']; } return ''; } /** * Selects sys_file_metadata database record. */ protected function getSysFileMetaDataFromDatabase(int $file, int $sysLanguageUid): ?array { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file_metadata'); $databaseRecord = $queryBuilder->select('uid') ->from('sys_file_metadata') ->where( $queryBuilder->expr()->eq( 'file', $queryBuilder->createNamedParameter($file, Connection::PARAM_INT) ), $queryBuilder->expr()->eq( 'sys_language_uid', $queryBuilder->createNamedParameter($sysLanguageUid, Connection::PARAM_INT) ) ) ->executeQuery() ->fetchAssociative(); return is_array($databaseRecord) ? $databaseRecord : null; } /** * Store the mapping between the import file record UIDs and the final record UIDs in the database after import. * * @param array $importData Data to be modified or inserted in the database during import * @param array $substNEWwithIDs A map between the "NEW..." string IDs and the eventual record UID in database */ protected function addToMapId(array $importData, array $substNEWwithIDs): void { foreach ($importData as $table => $records) { foreach ($records as $ID => $_) { $uid = $this->importNewId[$table . ':' . $ID]['uid']; if (isset($substNEWwithIDs[$ID])) { $this->importMapId[$table][$uid] = $substNEWwithIDs[$ID]; } elseif ($this->update) { // Map same ID to same ID.... $this->importMapId[$table][$uid] = $ID; } else { // If $this->importMapId contains already the right mapping, skip the error message. // See special handling of sys_file_metadata in addSingle() => nothing to do. if (!($table === 'sys_file_metadata' && isset($this->importMapId[$table][$uid]) && $this->importMapId[$table][$uid] == $ID) ) { $this->addError( 'Possible error: ' . $table . ':' . $uid . ' had no new id assigned to it. ' . 'This indicates that the record was not added to database during import. ' . 'Please check changelog!' ); } } } } } protected function createDataHandler(): DataHandler { $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->dontProcessTransformations = true; $dataHandler->enableLogging = $this->enableLogging; return $dataHandler; } /** * At the end of the import process all file and database relations should be set properly. * This means that the relations to imported records are all recreated so that the imported * records are correctly related again. * Relations in flexform fields are processed in setFlexFormRelations() after this function. */ protected function setRelations(): void { $updateData = []; foreach ($this->importNewId as $original) { $table = $original['table']; $uid = $original['uid']; if (isset($this->importMapId[$table][$uid])) { if (!$this->tcaSchemaFactory->has($table)) { $this->addError(sprintf('Error: This record does not have a TCA schema! (%s:%s)', $table, $uid)); } else { $schema = $this->tcaSchemaFactory->get($table); $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid]); foreach ($this->dat['records'][$table . ':' . $uid]['rels'] ?? [] as $field => $relation) { // Field "uid_local" of sys_file_reference needs no update because the correct reference uid was already written. // @see ImportExport::fixUidLocalInSysFileReferenceRecords() if (isset($relation['type']) && !($table === 'sys_file_reference' && $field === 'uid_local') && $relation['type'] === 'db') { if (!$schema->hasField($field)) { $this->addError(sprintf('Error: Missing TCA "config" for field "%s:%s"', $table, $field)); } elseif (is_array($relation['itemArray'] ?? null) && !empty($relation['itemArray'])) { $fieldInfo = $schema->getField($field); $actualRelations = $this->remapRelationsOfField($relation['itemArray'], $fieldInfo->getConfiguration(), $field); $updateData[$table][$actualUid][$field] = implode(',', $actualRelations); } } } } } else { $this->addError(sprintf('Error: This record does not appear to have been created! (%s:%s)', $table, $uid)); } } if (!empty($updateData)) { $dataHandler = $this->createDataHandler(); $dataHandler->isImporting = true; $this->callHook('before_setRelation', [ 'tce' => $dataHandler, 'data' => &$updateData, ]); $dataHandler->start($updateData, []); $dataHandler->process_datamap(); $this->callHook('after_setRelations', [ 'tce' => $dataHandler, ]); } } /** * Maps the original record UIDs of the relations to the actual UIDs of the imported records and returns relations * as strings of type [table]_[uid] - or file:[uid] or [public url] for field of type "group" and internal_type * "file_reference". These strings have the regular DataHandler input group/select type format which means * they will automatically be processed into a list of UIDs or MM relations. * * @param array $fieldRelations Relations with original record UIDs * @param array $fieldConfig TCA configuration of the record field the relations belong to * @param string $field The TCA fieldname of the relation operated on * @return array Array of relation strings with actual record UIDs */ protected function remapRelationsOfField(array $fieldRelations, array $fieldConfig, string $field = ''): array { $actualRelations = []; foreach ($fieldRelations as $relation) { if (!$this->tcaSchemaFactory->has($relation['table'])) { $this->addError('Lost relation due to missing TCA schema: ' . $relation['table'] . ':' . $relation['id']); } elseif (isset($this->importMapId[$relation['table']][$relation['id']])) { $schema = $this->tcaSchemaFactory->get($relation['table']); $translationSourceFieldName = null; if ($schema->isLanguageAware()) { $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $translationSourceFieldName = $languageCapability->getTranslationSourceField()?->getName(); } $actualUid = $this->importMapId[$relation['table']][$relation['id']]; if ($fieldConfig['type'] === 'input' && isset($fieldConfig['wizards']['link'])) { // If an input field has a relation to a sys_file record this need to be converted back to // the public path. But use getPublicUrl() here, because could normally only be a local file path. try { $file = $this->resourceFactory->retrieveFileOrFolderObject($actualUid); $actualRelations[] = $file->getPublicUrl(); } catch (\Exception) { $actualRelations[] = 'file:' . $actualUid; } } elseif ($translationSourceFieldName && $field === $translationSourceFieldName) { // "l10n_source" is of type "passthrough" so the "_" syntax won't be replaced. $actualRelations[] = $actualUid; } else { $actualRelations[] = $relation['table'] . '_' . $actualUid; } } elseif ($this->isTableStatic($relation['table']) || $this->isRecordExcluded($relation['table'], (int)$relation['id']) || $relation['id'] < 0) { // Some select types could contain negative values, e.g. fe_groups (-1, -2). // This must be handled on both export and import. $actualRelations[] = $relation['table'] . '_' . $relation['id']; } else { $this->addError('Lost relation: ' . $relation['table'] . ':' . $relation['id']); } } return $actualRelations; } /** * After all database relations have been set in the end of the import (see setRelations()) then it is time to * correct all relations inside FlexForm fields. The reason for doing this after is that the setting of relations * may affect (quite often!) which data structure is used for the FlexForm field! */ protected function setFlexFormRelations(): void { $updateData = []; foreach ($this->importNewId as $original) { $table = $original['table']; $uid = $original['uid']; if (isset($this->importMapId[$table][$uid])) { if (!$this->tcaSchemaFactory->has($table)) { $this->addError(sprintf('Error: This record does not appear to have a TCA schema! (%s:%s)', $table, $uid)); } else { $schema = $this->tcaSchemaFactory->get($table); $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid]); foreach ($this->dat['records'][$table . ':' . $uid]['rels'] ?? [] as $field => $relation) { // Field "configuration" of sys_file_storage needs no update because it has not been removed // and has no relations. // @see Import::addSingle() if (isset($relation['type']) && $relation['type'] == 'flex' && !($table === 'sys_file_storage' && $field === 'configuration')) { // Re-insert temporarily removed original FlexForm data as fallback // @see Import::addSingle() $updateData[$table][$actualUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field]; if (!empty($relation['flexFormRels']['db']) && $schema->hasField($field)) { $fieldInfo = $schema->getField($field); if (!$fieldInfo->isType(TableColumnType::FLEX)) { continue; } $actualRecord = BackendUtility::getRecord($table, $actualUid); if (is_array($actualRecord)) { $schema = $this->tcaSchemaFactory->get($table); $dataStructureIdentifier = $this->flexFormTools->getDataStructureIdentifier( ['config' => $fieldInfo->getConfiguration()], $table, $field, $actualRecord, $schema ); $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema); $flexFormData = (new Typo3XmlParser())->decodeWithReturningExceptionAsString( (string)($this->dat['records'][$table . ':' . $uid]['data'][$field] ?? ''), new Typo3XmlSerializerOptions([ Typo3XmlSerializerOptions::ALLOW_UNDEFINED_NAMESPACES, ]) ); if (is_array($flexFormData['data'] ?? null)) { $flexFormData['data'] = $this->remapFlexFormRelationsInData( $flexFormData['data'], $dataStructure, $relation ); } if (is_array($flexFormData['data'] ?? null)) { $updateData[$table][$actualUid][$field] = $flexFormData; } } } } } } } else { $this->addError(sprintf('Error: This record does not appear to have been created! (%s:%s)', $table, $uid)); } } if (!empty($updateData)) { $dataHandler = $this->createDataHandler(); $dataHandler->isImporting = true; $this->callHook('before_setFlexFormRelations', [ 'tce' => $dataHandler, 'data' => &$updateData, ]); $dataHandler->start($updateData, []); $dataHandler->process_datamap(); $this->callHook('after_setFlexFormRelations', [ 'tce' => $dataHandler, ]); } } private function remapFlexFormRelationsInData(array $data, array $dataStructure, array $relation): array { foreach ($dataStructure['sheets'] as $sheetKey => $sheetData) { foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementTca) { if (($sheetElementTca['type'] ?? '') === 'array') { // Section element. if (!is_array($sheetElementTca['el'] ?? false) || !is_array($data[$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) { continue; } foreach ($data[$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) { if (!is_array($valueSectionContainers ?? false)) { continue; } foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) { if (!is_array($sheetElementTca['el'][$valueContainerType]['el'] ?? false)) { continue; } foreach ($sheetElementTca['el'][$valueContainerType]['el'] as $containerElement => $containerElementTca) { if (!isset($data[$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF'])) { continue; } $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/el/' . $valueSectionContainerKey . '/' . $valueContainerType . '/el/' . $containerElement . '/vDEF/'; $fieldRelations = $relation['flexFormRels']['db'][$structurePath] ?? $relation['flexFormRels']['db'][rtrim($structurePath, '/')] ?? null; if (is_array($fieldRelations)) { $data[$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF'] = implode(',', $this->remapRelationsOfField($fieldRelations, $containerElementTca['config'] ?? [])); } } } } } elseif (isset($data[$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) { // Simple field element. $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/vDEF/'; $fieldRelations = $relation['flexFormRels']['db'][$structurePath] ?? $relation['flexFormRels']['db'][rtrim($structurePath, '/')] ?? null; if (is_array($fieldRelations)) { $data[$sheetKey]['lDEF'][$sheetElementKey]['vDEF'] = implode(',', $this->remapRelationsOfField($fieldRelations, $sheetElementTca['config'] ?? [])); } } } } return $data; } /************************** * Import soft references *************************/ /** * Processing of soft references */ protected function processSoftReferences(): void { $updateData = []; foreach ($this->dat['header']['records'] ?? [] as $table => $records) { if (!$this->tcaSchemaFactory->has($table)) { continue; } $schema = $this->tcaSchemaFactory->get($table); foreach ($records as $uid => $record) { if (is_array($record['softrefs'] ?? null)) { $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid] ?? 0); // First, group soft references by record field ... // (this could probably also have been done with $this->dat['records'] instead of $this->dat['header']) $softrefs = []; foreach ($record['softrefs'] as $softref) { if ($softref['field'] && is_array($softref['subst'] ?? null) && $softref['subst']['tokenID']) { $softrefs[$softref['field']][$softref['subst']['tokenID']] = $softref; } } // ... then process only fields which require substitution. foreach ($softrefs as $field => $softrefsByField) { if ($schema->hasField($field)) { $fieldInfo = $schema->getField($field); if ($fieldInfo->isType(TableColumnType::FLEX)) { $actualRecord = BackendUtility::getRecord($table, $actualUid, '*'); if (is_array($actualRecord)) { $schema = $this->tcaSchemaFactory->get($table); $dataStructureIdentifier = $this->flexFormTools->getDataStructureIdentifier( ['config' => $fieldInfo->getConfiguration()], $table, $field, $actualRecord, $schema ); $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema); $flexFormData = (new Typo3XmlParser())->decodeWithReturningExceptionAsString( (string)($actualRecord[$field] ?? ''), new Typo3XmlSerializerOptions([ Typo3XmlSerializerOptions::ALLOW_UNDEFINED_NAMESPACES, ]) ); if (is_array($flexFormData['data'] ?? null)) { $flexFormData['data'] = $this->processFlexFormSoftRefsInData( $flexFormData['data'], $dataStructure, $table, $uid, $field, $softrefsByField ); } if (is_array($flexFormData['data'] ?? null)) { $updateData[$table][$actualUid][$field] = $flexFormData; } } } else { // Get tokenizedContent string and proceed only if that is not blank: $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'] ?? ''; if ($tokenizedContent !== '') { $updateData[$table][$actualUid][$field] = $this->processSoftReferencesSubstTokens($tokenizedContent, $softrefsByField, $table, (string)$uid); } } } } } } } // Update soft references in the database $dataHandler = $this->createDataHandler(); $dataHandler->isImporting = true; $this->callHook('before_processSoftReferences', [ 'tce' => $dataHandler, 'data' => &$updateData, ]); $dataHandler->enableLogging = true; $dataHandler->start($updateData, []); $dataHandler->process_datamap(); $this->callHook('after_processSoftReferences', [ 'tce' => $dataHandler, ]); } private function processFlexFormSoftRefsInData(array $data, array $dataStructure, string $table, string|int $origUid, string $field, array $softrefs): array { foreach ($dataStructure['sheets'] as $sheetKey => $sheetData) { foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementTca) { if (($sheetElementTca['type'] ?? '') === 'array') { // Section element. if (!is_array($sheetElementTca['el'] ?? false) || !is_array($data[$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) { continue; } foreach ($data[$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) { if (!is_array($valueSectionContainers ?? false)) { continue; } foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) { if (!is_array($sheetElementTca['el'][$valueContainerType]['el'] ?? false)) { continue; } foreach (array_keys($sheetElementTca['el'][$valueContainerType]['el']) as $containerElement) { if (!isset($data[$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF'])) { continue; } $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/el/' . $valueSectionContainerKey . '/' . $valueContainerType . '/el/' . $containerElement . '/vDEF/'; $softrefsByPath = array_filter($softrefs, static fn(array $softref): bool => $softref['structurePath'] === $structurePath); if (!empty($softrefsByPath)) { $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$structurePath]['tokenizedContent'] ?? ''; if ($tokenizedContent !== '') { $data[$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF'] = $this->processSoftReferencesSubstTokens($tokenizedContent, $softrefsByPath, $table, (string)$origUid); } } } } } } elseif (isset($data[$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) { // Simple field element. $structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/vDEF/'; $softrefsByPath = array_filter($softrefs, static fn(array $softref): bool => $softref['structurePath'] === $structurePath); if (!empty($softrefsByPath)) { $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$structurePath]['tokenizedContent'] ?? ''; if ($tokenizedContent !== '') { $data[$sheetKey]['lDEF'][$sheetElementKey]['vDEF'] = $this->processSoftReferencesSubstTokens($tokenizedContent, $softrefsByPath, $table, (string)$origUid); } } } } } return $data; } /** * Substitution of soft reference tokens * * @param string $tokenizedContent Content of field with soft reference tokens in. * @param array $softrefs Soft references * @param string $table Table of record for which the processing occurs * @param string $uid UID of record from table * @return string The input content with tokens substituted according to entries in $softrefs */ protected function processSoftReferencesSubstTokens(string $tokenizedContent, array $softrefs, string $table, string $uid): string { foreach ($softrefs as $softref) { $tokenID = $softref['subst']['tokenID']; $insertValue = $softref['subst']['tokenValue']; switch ((string)($this->softrefCfg[$tokenID]['mode'] ?? '')) { case self::SOFTREF_IMPORT_MODE_EXCLUDE: // This is the same as handling static relations: // Do not create or update the related file or record and do not change the link in any way, // but use the link as it was when exported. break; case self::SOFTREF_IMPORT_MODE_EDITABLE: // This is the same as "exclude" with the option to manually edit the link before importing. $insertValue = $this->softrefInputValues[$tokenID]; break; default: // This is almost the same as handling relations: // - Adjusting the record reference to link to the already imported record - if any. [$tempTable, $tempUid] = explode(':', (string)($softref['subst']['recordRef'] ?? ':')); if (isset($this->importMapId[$tempTable][$tempUid])) { $insertValue = BackendUtility::wsMapId($tempTable, $this->importMapId[$tempTable][$tempUid]); $tokenValue = (string)$softref['subst']['tokenValue']; if (str_contains($tokenValue, ':')) { [$tokenKey] = explode(':', $tokenValue); $insertValue = $tokenKey . ':' . $insertValue; } } } // Finally, replace the soft reference token in tokenized content $tokenizedContent = str_replace('{softref:' . $tokenID . '}', (string)$insertValue, $tokenizedContent); } return $tokenizedContent; } /** * @param string $name Name of the hook * @param array $params Array with params */ protected function callHook(string $name, array $params): void { foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/impexp/class.tx_impexp.php'][$name] ?? [] as $hook) { GeneralUtility::callUserFunction($hook, $params, $this); } } public function setEnableLogging(bool $enableLogging): void { $this->enableLogging = $enableLogging; } }