[TASK] Use Environment API instead of PATH_site in impexp
[Packages/TYPO3.CMS.git] / typo3 / sysext / impexp / Classes / Import.php
1 <?php
2 namespace TYPO3\CMS\Impexp;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
19 use TYPO3\CMS\Core\Core\Environment;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\DataHandling\DataHandler;
22 use TYPO3\CMS\Core\Exception;
23 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
24 use TYPO3\CMS\Core\Resource\File;
25 use TYPO3\CMS\Core\Resource\FileInterface;
26 use TYPO3\CMS\Core\Resource\ResourceFactory;
27 use TYPO3\CMS\Core\Resource\ResourceStorage;
28 use TYPO3\CMS\Core\Resource\StorageRepository;
29 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32 use TYPO3\CMS\Core\Utility\PathUtility;
33 use TYPO3\CMS\Core\Utility\StringUtility;
34
35 /**
36 * T3D file Import library (TYPO3 Record Document)
37 */
38 class Import extends ImportExport
39 {
40 /**
41 * Used to register the forged UID values for imported records that we want
42 * to create with the same UIDs as in the import file. Admin-only feature.
43 *
44 * @var array
45 */
46 public $suggestedInsertUids = [];
47
48 /**
49 * Disable logging when importing
50 *
51 * @var bool
52 */
53 public $enableLogging = false;
54
55 /**
56 * Keys are [tablename]:[new NEWxxx ids (or when updating it is uids)]
57 * while values are arrays with table/uid of the original record it is based on.
58 * With the array keys the new ids can be looked up inside DataHandler
59 *
60 * @var array
61 */
62 public $import_newId = [];
63
64 /**
65 * Page id map for page tree (import)
66 *
67 * @var array
68 */
69 public $import_newId_pids = [];
70
71 /**
72 * Internal data accumulation for writing records during import
73 *
74 * @var array
75 */
76 public $import_data = [];
77
78 /**
79 * Array of current registered storage objects
80 *
81 * @var ResourceStorage[]
82 */
83 protected $storageObjects = [];
84
85 /**
86 * @var string|null
87 */
88 protected $filesPathForImport = null;
89
90 /**
91 * @var array
92 */
93 protected $unlinkFiles = [];
94
95 /**
96 * @var array
97 */
98 protected $alternativeFileName = [];
99
100 /**
101 * @var array
102 */
103 protected $alternativeFilePath = [];
104
105 /**
106 * @var array
107 */
108 protected $filePathMap = [];
109
110 /**************************
111 * Initialize
112 *************************/
113
114 /**
115 * Init the object
116 */
117 public function init()
118 {
119 parent::init();
120 $this->mode = 'import';
121 }
122
123 /***********************
124 * Import
125 ***********************/
126
127 /**
128 * Initialize all settings for the import
129 */
130 protected function initializeImport()
131 {
132 // Set this flag to indicate that an import is being/has been done.
133 $this->doesImport = 1;
134 // Initialize:
135 // These vars MUST last for the whole section not being cleared. They are used by the method setRelations() which are called at the end of the import session.
136 $this->import_mapId = [];
137 $this->import_newId = [];
138 $this->import_newId_pids = [];
139 // Temporary files stack initialized:
140 $this->unlinkFiles = [];
141 $this->alternativeFileName = [];
142 $this->alternativeFilePath = [];
143
144 $this->initializeStorageObjects();
145 }
146
147 /**
148 * Initialize the all present storage objects
149 */
150 protected function initializeStorageObjects()
151 {
152 /** @var $storageRepository StorageRepository */
153 $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
154 $this->storageObjects = $storageRepository->findAll();
155 }
156
157 /**
158 * Imports the internal data array to $pid.
159 *
160 * @param int $pid Page ID in which to import the content
161 */
162 public function importData($pid)
163 {
164 $this->initializeImport();
165
166 // Write sys_file_storages first
167 $this->writeSysFileStorageRecords();
168 // Write sys_file records and write the binary file data
169 $this->writeSysFileRecords();
170 // Write records, first pages, then the rest
171 // Fields with "hard" relations to database, files and flexform fields are kept empty during this run
172 $this->writeRecords_pages($pid);
173 $this->writeRecords_records($pid);
174 // Finally all the file and DB record references must be fixed. This is done after all records have supposedly been written to database:
175 // $this->import_mapId will indicate two things: 1) that a record WAS written to db and 2) that it has got a new id-number.
176 $this->setRelations();
177 // And when all DB relations are in place, we can fix file and DB relations in flexform fields (since data structures often depends on relations to a DS record):
178 $this->setFlexFormRelations();
179 // Unlink temporary files:
180 $this->unlinkTempFiles();
181 // Finally, traverse all records and process softreferences with substitution attributes.
182 $this->processSoftReferences();
183 }
184
185 /**
186 * Imports the sys_file_storage records from internal data array.
187 */
188 protected function writeSysFileStorageRecords()
189 {
190 if (!isset($this->dat['header']['records']['sys_file_storage'])) {
191 return;
192 }
193 $sysFileStorageUidsToBeResetToDefaultStorage = [];
194 foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) {
195 $storageRecord = $this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data'];
196 // continue with Local, writable and online storage only
197 if ($storageRecord['driver'] === 'Local' && $storageRecord['is_writable'] && $storageRecord['is_online']) {
198 foreach ($this->storageObjects as $localStorage) {
199 if ($this->isEquivalentObjectStorage($localStorage, $storageRecord)) {
200 $this->import_mapId['sys_file_storage'][$sysFileStorageUid] = $localStorage->getUid();
201 break;
202 }
203 }
204
205 if (!isset($this->import_mapId['sys_file_storage'][$sysFileStorageUid])) {
206 // Local, writable and online storage. Is allowed to be used to later write files in.
207 // Does currently not exist so add the record.
208 $this->addSingle('sys_file_storage', $sysFileStorageUid, 0);
209 }
210 } else {
211 // Storage with non Local drivers could be imported but must not be used to saves files in, because you
212 // could not be sure, that this is supported. The default storage will be used in this case.
213 // It could happen that non writable and non online storage will be created as dupes because you could not
214 // check the detailed configuration options at this point
215 $this->addSingle('sys_file_storage', $sysFileStorageUid, 0);
216 $sysFileStorageUidsToBeResetToDefaultStorage[] = $sysFileStorageUid;
217 }
218 }
219
220 // Importing the added ones
221 $tce = $this->getNewTCE();
222 // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
223 $tce->reverseOrder = 1;
224 $tce->isImporting = true;
225 $tce->start($this->import_data, []);
226 $tce->process_datamap();
227 $this->addToMapId($tce->substNEWwithIDs);
228
229 $defaultStorageUid = null;
230 // get default storage
231 $defaultStorage = ResourceFactory::getInstance()->getDefaultStorage();
232 if ($defaultStorage !== null) {
233 $defaultStorageUid = $defaultStorage->getUid();
234 }
235 foreach ($sysFileStorageUidsToBeResetToDefaultStorage as $sysFileStorageUidToBeResetToDefaultStorage) {
236 $this->import_mapId['sys_file_storage'][$sysFileStorageUidToBeResetToDefaultStorage] = $defaultStorageUid;
237 }
238
239 // unset the sys_file_storage records to prevent an import in writeRecords_records
240 unset($this->dat['header']['records']['sys_file_storage']);
241 }
242
243 /**
244 * Determines whether the passed storage object and record (sys_file_storage) can be
245 * seen as equivalent during import.
246 *
247 * @param ResourceStorage $storageObject The storage object which should get compared
248 * @param array $storageRecord The storage record which should get compared
249 * @return bool Returns TRUE when both object storages can be seen as equivalent
250 */
251 protected function isEquivalentObjectStorage(ResourceStorage $storageObject, array $storageRecord)
252 {
253 // compare the properties: driver, writable and online
254 if ($storageObject->getDriverType() === $storageRecord['driver']
255 && (bool)$storageObject->isWritable() === (bool)$storageRecord['is_writable']
256 && (bool)$storageObject->isOnline() === (bool)$storageRecord['is_online']
257 ) {
258 $storageRecordConfiguration = ResourceFactory::getInstance()->convertFlexFormDataToConfigurationArray($storageRecord['configuration']);
259 $storageObjectConfiguration = $storageObject->getConfiguration();
260 // compare the properties: pathType and basePath
261 if ($storageRecordConfiguration['pathType'] === $storageObjectConfiguration['pathType']
262 && $storageRecordConfiguration['basePath'] === $storageObjectConfiguration['basePath']
263 ) {
264 return true;
265 }
266 }
267 return false;
268 }
269
270 /**
271 * Checks any prerequisites necessary to get fulfilled before import
272 *
273 * @return array Messages explaining issues which need to get resolved before import
274 */
275 public function checkImportPrerequisites()
276 {
277 $messages = [];
278
279 // Check #1: Extension dependencies
280 $extKeysToInstall = [];
281 foreach ($this->dat['header']['extensionDependencies'] as $extKey) {
282 if (!empty($extKey) && !ExtensionManagementUtility::isLoaded($extKey)) {
283 $extKeysToInstall[] = $extKey;
284 }
285 }
286 if (!empty($extKeysToInstall)) {
287 $messages['missingExtensions'] = 'Before you can install this T3D file you need to install the extensions "'
288 . implode('", "', $extKeysToInstall) . '".';
289 }
290
291 // Check #2: If the path for every local storage object exists.
292 // Else files can't get moved into a newly imported storage.
293 if (!empty($this->dat['header']['records']['sys_file_storage'])) {
294 foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) {
295 $storageRecord = $this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data'];
296 // continue with Local, writable and online storage only
297 if ($storageRecord['driver'] === 'Local'
298 && $storageRecord['is_writable']
299 && $storageRecord['is_online']
300 ) {
301 foreach ($this->storageObjects as $localStorage) {
302 if ($this->isEquivalentObjectStorage($localStorage, $storageRecord)) {
303 // There is already an existing storage
304 break;
305 }
306
307 // The storage from the import does not have an equivalent storage
308 // in the current instance (same driver, same path, etc.). Before
309 // the storage record can get inserted later on take care the path
310 // it points to really exists and is accessible.
311 $storageRecordUid = $storageRecord['uid'];
312 // Unset the storage record UID when trying to create the storage object
313 // as the record does not already exist in DB. The constructor of the
314 // storage object will check whether the target folder exists and set the
315 // isOnline flag depending on the outcome.
316 $storageRecord['uid'] = 0;
317 $resourceStorage = ResourceFactory::getInstance()->createStorageObject($storageRecord);
318 if (!$resourceStorage->isOnline()) {
319 $configuration = $resourceStorage->getConfiguration();
320 $messages['resourceStorageFolderMissing_' . $storageRecordUid] =
321 'The resource storage "'
322 . $resourceStorage->getName()
323 . $configuration['basePath']
324 . '" does not exist. Please create the directory prior to starting the import!';
325 }
326 }
327 }
328 }
329 }
330
331 return $messages;
332 }
333
334 /**
335 * Imports the sys_file records and the binary files data from internal data array.
336 */
337 protected function writeSysFileRecords()
338 {
339 if (!isset($this->dat['header']['records']['sys_file'])) {
340 return;
341 }
342 $this->addGeneralErrorsByTable('sys_file');
343
344 // fetch fresh storage records from database
345 $storageRecords = $this->fetchStorageRecords();
346
347 $defaultStorage = ResourceFactory::getInstance()->getDefaultStorage();
348
349 $sanitizedFolderMappings = [];
350
351 foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) {
352 $fileRecord = $this->dat['records']['sys_file:' . $sysFileUid]['data'];
353
354 $temporaryFile = null;
355 // check if there is the right file already in the local folder
356 if ($this->filesPathForImport !== null) {
357 if (is_file($this->filesPathForImport . '/' . $fileRecord['sha1']) && sha1_file($this->filesPathForImport . '/' . $fileRecord['sha1']) === $fileRecord['sha1']) {
358 $temporaryFile = $this->filesPathForImport . '/' . $fileRecord['sha1'];
359 }
360 }
361
362 // save file to disk
363 if ($temporaryFile === null) {
364 $fileId = md5($fileRecord['storage'] . ':' . $fileRecord['identifier_hash']);
365 $temporaryFile = $this->writeTemporaryFileFromData($fileId);
366 if ($temporaryFile === null) {
367 // error on writing the file. Error message was already added
368 continue;
369 }
370 }
371
372 $originalStorageUid = $fileRecord['storage'];
373 $useStorageFromStorageRecords = false;
374
375 // replace storage id, if an alternative one was registered
376 if (isset($this->import_mapId['sys_file_storage'][$fileRecord['storage']])) {
377 $fileRecord['storage'] = $this->import_mapId['sys_file_storage'][$fileRecord['storage']];
378 $useStorageFromStorageRecords = true;
379 }
380
381 if (empty($fileRecord['storage']) && !$this->isFallbackStorage($fileRecord['storage'])) {
382 // no storage for the file is defined, mostly because of a missing default storage.
383 $this->error('Error: No storage for the file "' . $fileRecord['identifier'] . '" with storage uid "' . $originalStorageUid . '"');
384 continue;
385 }
386
387 // using a storage from the local storage is only allowed, if the uid is present in the
388 // mapping. Only in this case we could be sure, that it's a local, online and writable storage.
389 if ($useStorageFromStorageRecords && isset($storageRecords[$fileRecord['storage']])) {
390 /** @var $storage \TYPO3\CMS\Core\Resource\ResourceStorage */
391 $storage = ResourceFactory::getInstance()->getStorageObject($fileRecord['storage'], $storageRecords[$fileRecord['storage']]);
392 } elseif ($this->isFallbackStorage($fileRecord['storage'])) {
393 $storage = ResourceFactory::getInstance()->getStorageObject(0);
394 } elseif ($defaultStorage !== null) {
395 $storage = $defaultStorage;
396 } else {
397 $this->error('Error: No storage available for the file "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
398 continue;
399 }
400
401 $newFile = null;
402
403 // check, if there is an identical file
404 try {
405 if ($storage->hasFile($fileRecord['identifier'])) {
406 $file = $storage->getFile($fileRecord['identifier']);
407 if ($file->getSha1() === $fileRecord['sha1']) {
408 $newFile = $file;
409 }
410 }
411 } catch (Exception $e) {
412 }
413
414 if ($newFile === null) {
415 $folderName = PathUtility::dirname(ltrim($fileRecord['identifier'], '/'));
416 if (in_array($folderName, $sanitizedFolderMappings)) {
417 $folderName = $sanitizedFolderMappings[$folderName];
418 }
419 if (!$storage->hasFolder($folderName)) {
420 try {
421 $importFolder = $storage->createFolder($folderName);
422 if ($importFolder->getIdentifier() !== $folderName && !in_array($folderName, $sanitizedFolderMappings)) {
423 $sanitizedFolderMappings[$folderName] = $importFolder->getIdentifier();
424 }
425 } catch (Exception $e) {
426 $this->error('Error: Folder "' . $folderName . '" could not be created for file "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
427 continue;
428 }
429 } else {
430 $importFolder = $storage->getFolder($folderName);
431 }
432
433 try {
434 /** @var $newFile File */
435 $newFile = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']);
436 } catch (Exception $e) {
437 $this->error('Error: File could not be added to the storage: "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
438 continue;
439 }
440
441 if ($newFile->getSha1() !== $fileRecord['sha1']) {
442 $this->error('Error: The hash of the written file is not identical to the import data! File could be corrupted! File: "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
443 }
444 }
445
446 // save the new uid in the import id map
447 $this->import_mapId['sys_file'][$fileRecord['uid']] = $newFile->getUid();
448 $this->fixUidLocalInSysFileReferenceRecords($fileRecord['uid'], $newFile->getUid());
449 }
450
451 // unset the sys_file records to prevent an import in writeRecords_records
452 unset($this->dat['header']['records']['sys_file']);
453 // remove all sys_file_reference records that point to file records which are unknown
454 // in the system to prevent exceptions
455 $this->removeSysFileReferenceRecordsFromImportDataWithRelationToMissingFile();
456 }
457
458 /**
459 * Removes all sys_file_reference records from the import data array that are pointing to sys_file records which
460 * are missing not in the import data to prevent exceptions on checking the related file started by the Datahandler.
461 */
462 protected function removeSysFileReferenceRecordsFromImportDataWithRelationToMissingFile()
463 {
464 if (!isset($this->dat['header']['records']['sys_file_reference'])) {
465 return;
466 }
467
468 foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
469 $fileReferenceRecord = $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data'];
470 if (!in_array($fileReferenceRecord['uid_local'], $this->import_mapId['sys_file'])) {
471 unset($this->dat['header']['records']['sys_file_reference'][$sysFileReferenceUid]);
472 unset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]);
473 $this->error(
474 'Error: sys_file_reference record ' . (int)$sysFileReferenceUid
475 . ' with relation to sys_file record ' . (int)$fileReferenceRecord['uid_local']
476 . ', which is not part of the import data, was not imported.'
477 );
478 }
479 }
480 }
481
482 /**
483 * Checks if the $storageId is the id of the fallback storage
484 *
485 * @param int|string $storageId
486 * @return bool
487 */
488 protected function isFallbackStorage($storageId)
489 {
490 return $storageId === 0 || $storageId === '0';
491 }
492
493 /**
494 * Normally the importer works like the following:
495 * Step 1: import the records with cleared field values of relation fields (see addSingle())
496 * Step 2: update the records with the right relation ids (see setRelations())
497 *
498 * In step 2 the saving fields of type "relation to sys_file_reference" checks the related sys_file_reference
499 * record (created in step 1) with the FileExtensionFilter for matching file extensions of the related file.
500 * To make this work correct, the uid_local of sys_file_reference records has to be not empty AND has to
501 * relate to the correct (imported) sys_file record uid!!!
502 *
503 * This is fixed here.
504 *
505 * @param int $oldFileUid
506 * @param int $newFileUid
507 */
508 protected function fixUidLocalInSysFileReferenceRecords($oldFileUid, $newFileUid)
509 {
510 if (!isset($this->dat['header']['records']['sys_file_reference'])) {
511 return;
512 }
513
514 foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
515 if (!isset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'])
516 && $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] == $oldFileUid
517 ) {
518 $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'] = true;
519 $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] = $newFileUid;
520 }
521 }
522 }
523
524 /**
525 * Fetched fresh storage records from database because the new imported
526 * ones are not in cached data of the StorageRepository
527 *
528 * @return bool|array
529 */
530 protected function fetchStorageRecords()
531 {
532 $result = GeneralUtility::makeInstance(ConnectionPool::class)
533 ->getQueryBuilderForTable('sys_file_storage')
534 ->select('*')
535 ->from('sys_file_storage')
536 ->orderBy('uid')
537 ->execute();
538 $rows = [];
539 while ($row = $result->fetch()) {
540 $rows[$row['uid']] = $row;
541 }
542 return $rows;
543 }
544
545 /**
546 * Writes the file from import array to temp dir and returns the filename of it.
547 *
548 * @param string $fileId
549 * @param string $dataKey
550 * @return string Absolute filename of the temporary filename of the file
551 */
552 protected function writeTemporaryFileFromData($fileId, $dataKey = 'files_fal')
553 {
554 $temporaryFilePath = null;
555 if (is_array($this->dat[$dataKey][$fileId])) {
556 $temporaryFilePathInternal = GeneralUtility::tempnam('import_temp_');
557 GeneralUtility::writeFile($temporaryFilePathInternal, $this->dat[$dataKey][$fileId]['content']);
558 clearstatcache();
559 if (@is_file($temporaryFilePathInternal)) {
560 $this->unlinkFiles[] = $temporaryFilePathInternal;
561 $temporaryFilePath = $temporaryFilePathInternal;
562 } else {
563 $this->error('Error: temporary file ' . $temporaryFilePathInternal . ' was not written as it should have been!');
564 }
565 } else {
566 $this->error('Error: No file found for ID ' . $fileId);
567 }
568 return $temporaryFilePath;
569 }
570
571 /**
572 * Writing pagetree/pages to database:
573 *
574 * @param int $pid PID in which to import. 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
575 * @see writeRecords_records()
576 */
577 public function writeRecords_pages($pid)
578 {
579 // First, write page structure if any:
580 if (is_array($this->dat['header']['records']['pages'])) {
581 $this->addGeneralErrorsByTable('pages');
582 // $pageRecords is a copy of the pages array in the imported file. Records here are unset one by one when the addSingle function is called.
583 $pageRecords = $this->dat['header']['records']['pages'];
584 $this->import_data = [];
585 // First add page tree if any
586 if (is_array($this->dat['header']['pagetree'])) {
587 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
588 foreach ($pagesFromTree as $uid) {
589 $thisRec = $this->dat['header']['records']['pages'][$uid];
590 // PID: Set the main $pid, unless a NEW-id is found
591 $setPid = $this->import_newId_pids[$thisRec['pid']] ?? $pid;
592 $this->addSingle('pages', $uid, $setPid);
593 unset($pageRecords[$uid]);
594 }
595 }
596 // Then add all remaining pages not in tree on root level:
597 if (!empty($pageRecords)) {
598 $remainingPageUids = array_keys($pageRecords);
599 foreach ($remainingPageUids as $pUid) {
600 $this->addSingle('pages', $pUid, $pid);
601 }
602 }
603 // Now write to database:
604 $tce = $this->getNewTCE();
605 $tce->isImporting = true;
606 $this->callHook('before_writeRecordsPages', [
607 'tce' => &$tce,
608 'data' => &$this->import_data
609 ]);
610 $tce->suggestedInsertUids = $this->suggestedInsertUids;
611 $tce->start($this->import_data, []);
612 $tce->process_datamap();
613 $this->callHook('after_writeRecordsPages', [
614 'tce' => &$tce
615 ]);
616 // post-processing: Registering new ids (end all DataHandler sessions with this)
617 $this->addToMapId($tce->substNEWwithIDs);
618 // In case of an update, order pages from the page tree correctly:
619 if ($this->update && is_array($this->dat['header']['pagetree'])) {
620 $this->writeRecords_pages_order();
621 }
622 }
623 }
624
625 /**
626 * Organize all updated pages in page tree so they are related like in the import file
627 * Only used for updates and when $this->dat['header']['pagetree'] is an array.
628 *
629 * @access private
630 * @see writeRecords_pages(), writeRecords_records_order()
631 */
632 public function writeRecords_pages_order()
633 {
634 $cmd_data = [];
635 // Get uid-pid relations and traverse them in order to map to possible new IDs
636 $pidsFromTree = $this->flatInversePageTree_pid($this->dat['header']['pagetree']);
637 foreach ($pidsFromTree as $origPid => $newPid) {
638 if ($newPid >= 0 && $this->dontIgnorePid('pages', $origPid)) {
639 // If the page had a new id (because it was created) use that instead!
640 if (substr($this->import_newId_pids[$origPid], 0, 3) === 'NEW') {
641 if ($this->import_mapId['pages'][$origPid]) {
642 $mappedPid = $this->import_mapId['pages'][$origPid];
643 $cmd_data['pages'][$mappedPid]['move'] = $newPid;
644 }
645 } else {
646 $cmd_data['pages'][$origPid]['move'] = $newPid;
647 }
648 }
649 }
650 // Execute the move commands if any:
651 if (!empty($cmd_data)) {
652 $tce = $this->getNewTCE();
653 $this->callHook('before_writeRecordsPagesOrder', [
654 'tce' => &$tce,
655 'data' => &$cmd_data
656 ]);
657 $tce->start([], $cmd_data);
658 $tce->process_cmdmap();
659 $this->callHook('after_writeRecordsPagesOrder', [
660 'tce' => &$tce
661 ]);
662 }
663 }
664
665 /**
666 * Recursively flattening the idH array, setting PIDs as values
667 *
668 * @param array $idH Page uid hierarchy
669 * @param array $a Accumulation array of pages (internal, don't set from outside)
670 * @param int $pid PID value (internal)
671 * @return array Array with uid-pid pairs for all pages in the page tree.
672 * @see ImportExport::flatInversePageTree()
673 */
674 public function flatInversePageTree_pid($idH, $a = [], $pid = -1)
675 {
676 if (is_array($idH)) {
677 $idH = array_reverse($idH);
678 foreach ($idH as $v) {
679 $a[$v['uid']] = $pid;
680 if (is_array($v['subrow'])) {
681 $a = $this->flatInversePageTree_pid($v['subrow'], $a, $v['uid']);
682 }
683 }
684 }
685 return $a;
686 }
687
688 /**
689 * Write all database records except pages (writtein in writeRecords_pages())
690 *
691 * @param int $pid Page id in which to import
692 * @see writeRecords_pages()
693 */
694 public function writeRecords_records($pid)
695 {
696 // Write the rest of the records
697 $this->import_data = [];
698 if (is_array($this->dat['header']['records'])) {
699 foreach ($this->dat['header']['records'] as $table => $recs) {
700 $this->addGeneralErrorsByTable($table);
701 if ($table !== 'pages') {
702 foreach ($recs as $uid => $thisRec) {
703 // PID: Set the main $pid, unless a NEW-id is found
704 $setPid = isset($this->import_mapId['pages'][$thisRec['pid']])
705 ? (int)$this->import_mapId['pages'][$thisRec['pid']]
706 : (int)$pid;
707 if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel'])) {
708 $rootLevelSetting = (int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'];
709 if ($rootLevelSetting === 1) {
710 $setPid = 0;
711 } elseif ($rootLevelSetting === 0 && $setPid === 0) {
712 $this->error('Error: Record type ' . $table . ' is not allowed on pid 0');
713 continue;
714 }
715 }
716 // Add record:
717 $this->addSingle($table, $uid, $setPid);
718 }
719 }
720 }
721 } else {
722 $this->error('Error: No records defined in internal data array.');
723 }
724 // Now write to database:
725 $tce = $this->getNewTCE();
726 $this->callHook('before_writeRecordsRecords', [
727 'tce' => &$tce,
728 'data' => &$this->import_data
729 ]);
730 $tce->suggestedInsertUids = $this->suggestedInsertUids;
731 // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
732 $tce->reverseOrder = 1;
733 $tce->isImporting = true;
734 $tce->start($this->import_data, []);
735 $tce->process_datamap();
736 $this->callHook('after_writeRecordsRecords', [
737 'tce' => &$tce
738 ]);
739 // post-processing: Removing files and registering new ids (end all DataHandler sessions with this)
740 $this->addToMapId($tce->substNEWwithIDs);
741 // In case of an update, order pages from the page tree correctly:
742 if ($this->update) {
743 $this->writeRecords_records_order($pid);
744 }
745 }
746
747 /**
748 * Organize all updated record to their new positions.
749 * Only used for updates
750 *
751 * @param int $mainPid Main PID into which we import.
752 * @access private
753 * @see writeRecords_records(), writeRecords_pages_order()
754 */
755 public function writeRecords_records_order($mainPid)
756 {
757 $cmd_data = [];
758 if (is_array($this->dat['header']['pagetree'])) {
759 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
760 } else {
761 $pagesFromTree = [];
762 }
763 if (is_array($this->dat['header']['pid_lookup'])) {
764 foreach ($this->dat['header']['pid_lookup'] as $pid => $recList) {
765 $newPid = $this->import_mapId['pages'][$pid] ?? $mainPid;
766 if (MathUtility::canBeInterpretedAsInteger($newPid)) {
767 foreach ($recList as $tableName => $uidList) {
768 // If $mainPid===$newPid then we are on root level and we can consider to move pages as well!
769 // (they will not be in the page tree!)
770 if (($tableName !== 'pages' || !$pagesFromTree[$pid]) && is_array($uidList)) {
771 $uidList = array_reverse(array_keys($uidList));
772 foreach ($uidList as $uid) {
773 if ($this->dontIgnorePid($tableName, $uid)) {
774 $cmd_data[$tableName][$uid]['move'] = $newPid;
775 }
776 }
777 }
778 }
779 }
780 }
781 }
782 // Execute the move commands if any:
783 if (!empty($cmd_data)) {
784 $tce = $this->getNewTCE();
785 $this->callHook('before_writeRecordsRecordsOrder', [
786 'tce' => &$tce,
787 'data' => &$cmd_data
788 ]);
789 $tce->start([], $cmd_data);
790 $tce->process_cmdmap();
791 $this->callHook('after_writeRecordsRecordsOrder', [
792 'tce' => &$tce
793 ]);
794 }
795 }
796
797 /**
798 * Adds a single record to the $importData array. Also copies files to tempfolder.
799 * However all File/DB-references and flexform field contents are set to blank for now!
800 * That is done with setRelations() later
801 *
802 * @param string $table Table name (from import memory)
803 * @param int $uid Record UID (from import memory)
804 * @param int $pid Page id
805 * @see writeRecords()
806 */
807 public function addSingle($table, $uid, $pid)
808 {
809 if ($this->import_mode[$table . ':' . $uid] === 'exclude') {
810 return;
811 }
812 $record = $this->dat['records'][$table . ':' . $uid]['data'];
813 if (is_array($record)) {
814 if ($this->update && $this->doesRecordExist($table, $uid) && $this->import_mode[$table . ':' . $uid] !== 'as_new') {
815 $ID = $uid;
816 } elseif ($table === 'sys_file_metadata' && $record['sys_language_uid'] == '0' && $this->import_mapId['sys_file'][$record['file']]) {
817 // on adding sys_file records the belonging sys_file_metadata record was also created
818 // if there is one the record need to be overwritten instead of creating a new one.
819 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
820 ->getQueryBuilderForTable('sys_file_metadata');
821 $recordInDatabase = $queryBuilder->select('uid')
822 ->from('sys_file_metadata')
823 ->where(
824 $queryBuilder->expr()->eq(
825 'file',
826 $queryBuilder->createNamedParameter(
827 $this->import_mapId['sys_file'][$record['file']],
828 \PDO::PARAM_INT
829 )
830 ),
831 $queryBuilder->expr()->eq(
832 'sys_language_uid',
833 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
834 ),
835 $queryBuilder->expr()->eq(
836 'pid',
837 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
838 )
839 )
840 ->execute()
841 ->fetch();
842 // if no record could be found, $this->import_mapId['sys_file'][$record['file']] is pointing
843 // to a file, that was already there, thus a new metadata record should be created
844 if (is_array($recordInDatabase)) {
845 $this->import_mapId['sys_file_metadata'][$record['uid']] = $recordInDatabase['uid'];
846 $ID = $recordInDatabase['uid'];
847 } else {
848 $ID = StringUtility::getUniqueId('NEW');
849 }
850 } else {
851 $ID = StringUtility::getUniqueId('NEW');
852 }
853 $this->import_newId[$table . ':' . $ID] = ['table' => $table, 'uid' => $uid];
854 if ($table === 'pages') {
855 $this->import_newId_pids[$uid] = $ID;
856 }
857 // Set main record data:
858 $this->import_data[$table][$ID] = $record;
859 $this->import_data[$table][$ID]['tx_impexp_origuid'] = $this->import_data[$table][$ID]['uid'];
860 // Reset permission data:
861 if ($table === 'pages') {
862 // Have to reset the user/group IDs so pages are owned by importing user. Otherwise strange things may happen for non-admins!
863 unset($this->import_data[$table][$ID]['perms_userid']);
864 unset($this->import_data[$table][$ID]['perms_groupid']);
865 }
866 // PID and UID:
867 unset($this->import_data[$table][$ID]['uid']);
868 // Updates:
869 if (MathUtility::canBeInterpretedAsInteger($ID)) {
870 unset($this->import_data[$table][$ID]['pid']);
871 } else {
872 // Inserts:
873 $this->import_data[$table][$ID]['pid'] = $pid;
874 if (($this->import_mode[$table . ':' . $uid] === 'force_uid' && $this->update || $this->force_all_UIDS) && $this->getBackendUser()->isAdmin()) {
875 $this->import_data[$table][$ID]['uid'] = $uid;
876 $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE';
877 }
878 }
879 // Setting db/file blank:
880 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
881 switch ((string)$config['type']) {
882 case 'db':
883
884 case 'file':
885 // Fixed later in ->setRelations() [because we need to know ALL newly created IDs before we can map relations!]
886 // In the meantime we set NO values for relations.
887 //
888 // BUT for field uid_local of table sys_file_reference the relation MUST not be cleared here,
889 // because the value is already the uid of the right imported sys_file record.
890 // @see fixUidLocalInSysFileReferenceRecords()
891 // If it's empty or a uid to another record the FileExtensionFilter will throw an exception or
892 // delete the reference record if the file extension of the related record doesn't match.
893 if ($table !== 'sys_file_reference' && $field !== 'uid_local') {
894 $this->import_data[$table][$ID][$field] = '';
895 }
896 break;
897 case 'flex':
898 // Fixed later in setFlexFormRelations()
899 // In the meantime we set NO value for flexforms - this is mainly because file references
900 // inside will not be processed properly; In fact references will point to no file
901 // or existing files (in which case there will be double-references which is a big problem of course!)
902 $this->import_data[$table][$ID][$field] = '';
903 break;
904 }
905 }
906 } elseif ($table . ':' . $uid != 'pages:0') {
907 // On root level we don't want this error message.
908 $this->error('Error: no record was found in data array!');
909 }
910 }
911
912 /**
913 * Registers the substNEWids in memory.
914 *
915 * @param array $substNEWwithIDs From DataHandler to be merged into internal mapping variable in this object
916 * @see writeRecords()
917 */
918 public function addToMapId($substNEWwithIDs)
919 {
920 foreach ($this->import_data as $table => $recs) {
921 foreach ($recs as $id => $value) {
922 $old_uid = $this->import_newId[$table . ':' . $id]['uid'];
923 if (isset($substNEWwithIDs[$id])) {
924 $this->import_mapId[$table][$old_uid] = $substNEWwithIDs[$id];
925 } elseif ($this->update) {
926 // Map same ID to same ID....
927 $this->import_mapId[$table][$old_uid] = $id;
928 } else {
929 // if $this->import_mapId contains already the right mapping, skip the error msg.
930 // See special handling of sys_file_metadata in addSingle() => nothing to do
931 if (!($table === 'sys_file_metadata' && isset($this->import_mapId[$table][$old_uid]) && $this->import_mapId[$table][$old_uid] == $id)) {
932 $this->error('Possible error: ' . $table . ':' . $old_uid . ' had no new id assigned to it. This indicates that the record was not added to database during import. Please check changelog!');
933 }
934 }
935 }
936 }
937 }
938
939 /**
940 * Returns a new $TCE object
941 *
942 * @return DataHandler $TCE object
943 */
944 public function getNewTCE()
945 {
946 $tce = GeneralUtility::makeInstance(DataHandler::class);
947 $tce->dontProcessTransformations = 1;
948 $tce->enableLogging = $this->enableLogging;
949 $tce->alternativeFileName = $this->alternativeFileName;
950 $tce->alternativeFilePath = $this->alternativeFilePath;
951 return $tce;
952 }
953
954 /**
955 * Cleaning up all the temporary files stored in typo3temp/ folder
956 */
957 public function unlinkTempFiles()
958 {
959 foreach ($this->unlinkFiles as $fileName) {
960 if (GeneralUtility::isFirstPartOfStr($fileName, Environment::getPublicPath() . '/typo3temp/')) {
961 GeneralUtility::unlink_tempfile($fileName);
962 clearstatcache();
963 if (is_file($fileName)) {
964 $this->error('Error: ' . $fileName . ' was NOT unlinked as it should have been!');
965 }
966 } else {
967 $this->error('Error: ' . $fileName . ' was not in temp-path. Not removed!');
968 }
969 }
970 $this->unlinkFiles = [];
971 }
972
973 /***************************
974 * Import / Relations setting
975 ***************************/
976
977 /**
978 * At the end of the import process all file and DB relations should be set properly (that is relations
979 * to imported records are all re-created so imported records are correctly related again)
980 * Relations in flexform fields are processed in setFlexFormRelations() after this function
981 *
982 * @see setFlexFormRelations()
983 */
984 public function setRelations()
985 {
986 $updateData = [];
987 // import_newId contains a register of all records that was in the import memorys "records" key
988 foreach ($this->import_newId as $nId => $dat) {
989 $table = $dat['table'];
990 $uid = $dat['uid'];
991 // original UID - NOT the new one!
992 // If the record has been written and received a new id, then proceed:
993 if (is_array($this->import_mapId[$table]) && isset($this->import_mapId[$table][$uid])) {
994 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
995 if (is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
996 // Traverse relation fields of each record
997 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
998 // uid_local of sys_file_reference needs no update because the correct reference uid was already written
999 // @see ImportExport::fixUidLocalInSysFileReferenceRecords()
1000 if ($table === 'sys_file_reference' && $field === 'uid_local') {
1001 continue;
1002 }
1003 switch ((string)$config['type']) {
1004 case 'db':
1005 if (is_array($config['itemArray']) && !empty($config['itemArray'])) {
1006 $itemConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1007 $valArray = $this->setRelations_db($config['itemArray'], $itemConfig);
1008 $updateData[$table][$thisNewUid][$field] = implode(',', $valArray);
1009 }
1010 break;
1011 case 'file':
1012 if (is_array($config['newValueFiles']) && !empty($config['newValueFiles'])) {
1013 $valArr = [];
1014 foreach ($config['newValueFiles'] as $fI) {
1015 $valArr[] = $this->import_addFileNameToBeCopied($fI);
1016 }
1017 $updateData[$table][$thisNewUid][$field] = implode(',', $valArr);
1018 }
1019 break;
1020 }
1021 }
1022 } else {
1023 $this->error('Error: no record was found in data array!');
1024 }
1025 } else {
1026 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')');
1027 }
1028 }
1029 if (!empty($updateData)) {
1030 $tce = $this->getNewTCE();
1031 $tce->isImporting = true;
1032 $this->callHook('before_setRelation', [
1033 'tce' => &$tce,
1034 'data' => &$updateData
1035 ]);
1036 $tce->start($updateData, []);
1037 $tce->process_datamap();
1038 $this->callHook('after_setRelations', [
1039 'tce' => &$tce
1040 ]);
1041 }
1042 }
1043
1044 /**
1045 * Maps relations for database
1046 *
1047 * @param array $itemArray Array of item sets (table/uid) from a dbAnalysis object
1048 * @param array $itemConfig Array of TCA config of the field the relation to be set on
1049 * @return array Array with values [table]_[uid] or [uid] for field of type group / internal_type file_reference. These values have the regular DataHandler-input group/select type which means they will automatically be processed into a uid-list or MM relations.
1050 */
1051 public function setRelations_db($itemArray, $itemConfig)
1052 {
1053 $valArray = [];
1054 foreach ($itemArray as $relDat) {
1055 if (is_array($this->import_mapId[$relDat['table']]) && isset($this->import_mapId[$relDat['table']][$relDat['id']])) {
1056 // Since non FAL file relation type group internal_type file_reference are handled as reference to
1057 // sys_file records Datahandler requires the value as uid of the the related sys_file record only
1058 if ($itemConfig['type'] === 'group' && $itemConfig['internal_type'] === 'file_reference') {
1059 $value = $this->import_mapId[$relDat['table']][$relDat['id']];
1060 } elseif ($itemConfig['type'] === 'input' && isset($itemConfig['wizards']['link'])) {
1061 // If an input field has a relation to a sys_file record this need to be converted back to
1062 // the public path. But use getPublicUrl here, because could normally only be a local file path.
1063 $fileUid = $this->import_mapId[$relDat['table']][$relDat['id']];
1064 // Fallback value
1065 $value = 'file:' . $fileUid;
1066 try {
1067 $file = ResourceFactory::getInstance()->retrieveFileOrFolderObject($fileUid);
1068 } catch (\Exception $e) {
1069 $file = null;
1070 }
1071 if ($file instanceof FileInterface) {
1072 $value = $file->getPublicUrl();
1073 }
1074 } else {
1075 $value = $relDat['table'] . '_' . $this->import_mapId[$relDat['table']][$relDat['id']];
1076 }
1077 $valArray[] = $value;
1078 } elseif ($this->isTableStatic($relDat['table']) || $this->isExcluded($relDat['table'], $relDat['id']) || $relDat['id'] < 0) {
1079 // Checking for less than zero because some select types could contain negative values,
1080 // eg. fe_groups (-1, -2) and sys_language (-1 = ALL languages). This must be handled on both export and import.
1081 $valArray[] = $relDat['table'] . '_' . $relDat['id'];
1082 } else {
1083 $this->error('Lost relation: ' . $relDat['table'] . ':' . $relDat['id']);
1084 }
1085 }
1086 return $valArray;
1087 }
1088
1089 /**
1090 * Writes the file from import array to temp dir and returns the filename of it.
1091 *
1092 * @param array $fI File information with three keys: "filename" = filename without path, "ID_absFile" = absolute filepath to the file (including the filename), "ID" = md5 hash of "ID_absFile
1093 * @return string|null Absolute filename of the temporary filename of the file. In ->alternativeFileName the original name is set.
1094 */
1095 public function import_addFileNameToBeCopied($fI)
1096 {
1097 if (is_array($this->dat['files'][$fI['ID']])) {
1098 $tmpFile = null;
1099 // check if there is the right file already in the local folder
1100 if ($this->filesPathForImport !== null) {
1101 if (is_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) &&
1102 md5_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) === $this->dat['files'][$fI['ID']]['content_md5']) {
1103 $tmpFile = $this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5'];
1104 }
1105 }
1106 if ($tmpFile === null) {
1107 $tmpFile = GeneralUtility::tempnam('import_temp_');
1108 GeneralUtility::writeFile($tmpFile, $this->dat['files'][$fI['ID']]['content']);
1109 }
1110 clearstatcache();
1111 if (@is_file($tmpFile)) {
1112 $this->unlinkFiles[] = $tmpFile;
1113 $this->alternativeFileName[$tmpFile] = $fI['filename'];
1114 $this->alternativeFilePath[$tmpFile] = $this->dat['files'][$fI['ID']]['relFileRef'];
1115 return $tmpFile;
1116 }
1117 $this->error('Error: temporary file ' . $tmpFile . ' was not written as it should have been!');
1118 } else {
1119 $this->error('Error: No file found for ID ' . $fI['ID']);
1120 }
1121 return null;
1122 }
1123
1124 /**
1125 * After all DB relations has been set in the end of the import (see setRelations()) then it is time to correct all relations inside of FlexForm fields.
1126 * The reason for doing this after is that the setting of relations may affect (quite often!) which data structure is used for the flexforms field!
1127 *
1128 * @see setRelations()
1129 */
1130 public function setFlexFormRelations()
1131 {
1132 $updateData = [];
1133 // import_newId contains a register of all records that was in the import memorys "records" key
1134 foreach ($this->import_newId as $nId => $dat) {
1135 $table = $dat['table'];
1136 $uid = $dat['uid'];
1137 // original UID - NOT the new one!
1138 // If the record has been written and received a new id, then proceed:
1139 if (!isset($this->import_mapId[$table][$uid])) {
1140 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')');
1141 continue;
1142 }
1143
1144 if (!is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
1145 $this->error('Error: no record was found in data array!');
1146 continue;
1147 }
1148 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1149 // Traverse relation fields of each record
1150 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1151 switch ((string)$config['type']) {
1152 case 'flex':
1153 // Get XML content and set as default value (string, non-processed):
1154 $updateData[$table][$thisNewUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field];
1155 // If there has been registered relations inside the flex form field, run processing on the content:
1156 if (!empty($config['flexFormRels']['db']) || !empty($config['flexFormRels']['file'])) {
1157 $origRecordRow = BackendUtility::getRecord($table, $thisNewUid, '*');
1158 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
1159 $fieldTca = $GLOBALS['TCA'][$table]['columns'][$field];
1160 if (is_array($origRecordRow) && is_array($fieldTca['config']) && $fieldTca['config']['type'] === 'flex') {
1161 // Get current data structure and value array:
1162 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1163 $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1164 $fieldTca,
1165 $table,
1166 $field,
1167 $origRecordRow
1168 );
1169 $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1170 $currentValueArray = GeneralUtility::xml2array($updateData[$table][$thisNewUid][$field]);
1171 // Do recursive processing of the XML data:
1172 $iteratorObj = GeneralUtility::makeInstance(DataHandler::class);
1173 $iteratorObj->callBackObj = $this;
1174 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData(
1175 $currentValueArray['data'],
1176 [],
1177 [],
1178 $dataStructureArray,
1179 [$table, $thisNewUid, $field, $config],
1180 'remapListedDBRecords_flexFormCallBack'
1181 );
1182 // The return value is set as an array which means it will be processed by DataHandler for file and DB references!
1183 if (is_array($currentValueArray['data'])) {
1184 $updateData[$table][$thisNewUid][$field] = $currentValueArray;
1185 }
1186 }
1187 }
1188 break;
1189 }
1190 }
1191 }
1192 if (!empty($updateData)) {
1193 $tce = $this->getNewTCE();
1194 $tce->isImporting = true;
1195 $this->callHook('before_setFlexFormRelations', [
1196 'tce' => &$tce,
1197 'data' => &$updateData
1198 ]);
1199 $tce->start($updateData, []);
1200 $tce->process_datamap();
1201 $this->callHook('after_setFlexFormRelations', [
1202 'tce' => &$tce
1203 ]);
1204 }
1205 }
1206
1207 /**
1208 * Callback function for traversing the FlexForm structure in relation to remapping database relations
1209 *
1210 * @param array $pParams Set of parameters in numeric array: table, uid, field
1211 * @param array $dsConf TCA config for field (from Data Structure of course)
1212 * @param string $dataValue Field value (from FlexForm XML)
1213 * @param string $dataValue_ext1 Not used
1214 * @param string $dataValue_ext2 Not used
1215 * @param string $path Path of where the data structure of the element is found
1216 * @return array Array where the "value" key carries the value.
1217 * @see setFlexFormRelations()
1218 */
1219 public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
1220 {
1221 // Extract parameters:
1222 list(, , , $config) = $pParams;
1223 // In case the $path is used as index without a trailing slash we will remove that
1224 if (!is_array($config['flexFormRels']['db'][$path]) && is_array($config['flexFormRels']['db'][rtrim($path, '/')])) {
1225 $path = rtrim($path, '/');
1226 }
1227 if (is_array($config['flexFormRels']['db'][$path])) {
1228 $valArray = $this->setRelations_db($config['flexFormRels']['db'][$path], $dsConf);
1229 $dataValue = implode(',', $valArray);
1230 }
1231 if (is_array($config['flexFormRels']['file'][$path])) {
1232 $valArr = [];
1233 foreach ($config['flexFormRels']['file'][$path] as $fI) {
1234 $valArr[] = $this->import_addFileNameToBeCopied($fI);
1235 }
1236 $dataValue = implode(',', $valArr);
1237 }
1238 return ['value' => $dataValue];
1239 }
1240
1241 /**************************
1242 * Import / Soft References
1243 *************************/
1244
1245 /**
1246 * Processing of soft references
1247 */
1248 public function processSoftReferences()
1249 {
1250 // Initialize:
1251 $inData = [];
1252 // Traverse records:
1253 if (is_array($this->dat['header']['records'])) {
1254 foreach ($this->dat['header']['records'] as $table => $recs) {
1255 foreach ($recs as $uid => $thisRec) {
1256 // If there are soft references defined, traverse those:
1257 if (isset($GLOBALS['TCA'][$table]) && is_array($thisRec['softrefs'])) {
1258 // First traversal is to collect softref configuration and split them up based on fields.
1259 // This could probably also have been done with the "records" key instead of the header.
1260 $fieldsIndex = [];
1261 foreach ($thisRec['softrefs'] as $softrefDef) {
1262 // If a substitution token is set:
1263 if ($softrefDef['field'] && is_array($softrefDef['subst']) && $softrefDef['subst']['tokenID']) {
1264 $fieldsIndex[$softrefDef['field']][$softrefDef['subst']['tokenID']] = $softrefDef;
1265 }
1266 }
1267 // The new id:
1268 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1269 // Now, if there are any fields that require substitution to be done, lets go for that:
1270 foreach ($fieldsIndex as $field => $softRefCfgs) {
1271 if (is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
1272 if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex') {
1273 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
1274 $origRecordRow = BackendUtility::getRecord($table, $thisNewUid, '*');
1275 if (is_array($origRecordRow)) {
1276 // Get current data structure and value array:
1277 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1278 $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1279 $GLOBALS['TCA'][$table]['columns'][$field],
1280 $table,
1281 $field,
1282 $origRecordRow
1283 );
1284 $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1285 $currentValueArray = GeneralUtility::xml2array($origRecordRow[$field]);
1286 // Do recursive processing of the XML data:
1287 /** @var $iteratorObj DataHandler */
1288 $iteratorObj = GeneralUtility::makeInstance(DataHandler::class);
1289 $iteratorObj->callBackObj = $this;
1290 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $softRefCfgs], 'processSoftReferences_flexFormCallBack');
1291 // The return value is set as an array which means it will be processed by DataHandler for file and DB references!
1292 if (is_array($currentValueArray['data'])) {
1293 $inData[$table][$thisNewUid][$field] = $currentValueArray;
1294 }
1295 }
1296 } else {
1297 // Get tokenizedContent string and proceed only if that is not blank:
1298 $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'];
1299 if (strlen($tokenizedContent) && is_array($softRefCfgs)) {
1300 $inData[$table][$thisNewUid][$field] = $this->processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid);
1301 }
1302 }
1303 }
1304 }
1305 }
1306 }
1307 }
1308 }
1309 // Now write to database:
1310 $tce = $this->getNewTCE();
1311 $tce->isImporting = true;
1312 $this->callHook('before_processSoftReferences', [
1313 'tce' => $tce,
1314 'data' => &$inData
1315 ]);
1316 $tce->enableLogging = true;
1317 $tce->start($inData, []);
1318 $tce->process_datamap();
1319 $this->callHook('after_processSoftReferences', [
1320 'tce' => $tce
1321 ]);
1322 }
1323
1324 /**
1325 * Callback function for traversing the FlexForm structure in relation to remapping softreference relations
1326 *
1327 * @param array $pParams Set of parameters in numeric array: table, uid, field
1328 * @param array $dsConf TCA config for field (from Data Structure of course)
1329 * @param string $dataValue Field value (from FlexForm XML)
1330 * @param string $dataValue_ext1 Not used
1331 * @param string $dataValue_ext2 Not used
1332 * @param string $path Path of where the data structure where the element is found
1333 * @return array Array where the "value" key carries the value.
1334 * @see setFlexFormRelations()
1335 */
1336 public function processSoftReferences_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
1337 {
1338 // Extract parameters:
1339 list($table, $origUid, $field, $softRefCfgs) = $pParams;
1340 if (is_array($softRefCfgs)) {
1341 // First, find all soft reference configurations for this structure path (they are listed flat in the header):
1342 $thisSoftRefCfgList = [];
1343 foreach ($softRefCfgs as $sK => $sV) {
1344 if ($sV['structurePath'] === $path) {
1345 $thisSoftRefCfgList[$sK] = $sV;
1346 }
1347 }
1348 // If any was found, do processing:
1349 if (!empty($thisSoftRefCfgList)) {
1350 // Get tokenizedContent string and proceed only if that is not blank:
1351 $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$path]['tokenizedContent'];
1352 if (strlen($tokenizedContent)) {
1353 $dataValue = $this->processSoftReferences_substTokens($tokenizedContent, $thisSoftRefCfgList, $table, $origUid);
1354 }
1355 }
1356 }
1357 // Return
1358 return ['value' => $dataValue];
1359 }
1360
1361 /**
1362 * Substition of softreference tokens
1363 *
1364 * @param string $tokenizedContent Content of field with soft reference tokens in.
1365 * @param array $softRefCfgs Soft reference configurations
1366 * @param string $table Table for which the processing occurs
1367 * @param string $uid UID of record from table
1368 * @return string The input content with tokens substituted according to entries in softRefCfgs
1369 */
1370 public function processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid)
1371 {
1372 // traverse each softref type for this field:
1373 foreach ($softRefCfgs as $cfg) {
1374 // Get token ID:
1375 $tokenID = $cfg['subst']['tokenID'];
1376 // Default is current token value:
1377 $insertValue = $cfg['subst']['tokenValue'];
1378 // Based on mode:
1379 switch ((string)$this->softrefCfg[$tokenID]['mode']) {
1380 case 'exclude':
1381 // Exclude is a simple passthrough of the value
1382 break;
1383 case 'editable':
1384 // Editable always picks up the value from this input array:
1385 $insertValue = $this->softrefInputValues[$tokenID];
1386 break;
1387 default:
1388 // Mapping IDs/creating files: Based on type, look up new value:
1389 switch ((string)$cfg['subst']['type']) {
1390 case 'file':
1391 // Create / Overwrite file:
1392 $insertValue = $this->processSoftReferences_saveFile($cfg['subst']['relFileName'], $cfg, $table, $uid);
1393 break;
1394 case 'db':
1395 default:
1396 // Trying to map database element if found in the mapID array:
1397 list($tempTable, $tempUid) = explode(':', $cfg['subst']['recordRef']);
1398 if (isset($this->import_mapId[$tempTable][$tempUid])) {
1399 $insertValue = BackendUtility::wsMapId($tempTable, $this->import_mapId[$tempTable][$tempUid]);
1400 // Look if reference is to a page and the original token value was NOT an integer - then we assume is was an alias and try to look up the new one!
1401 if ($tempTable === 'pages' && !MathUtility::canBeInterpretedAsInteger($cfg['subst']['tokenValue'])) {
1402 $recWithUniqueValue = BackendUtility::getRecord($tempTable, $insertValue, 'alias');
1403 if ($recWithUniqueValue['alias']) {
1404 $insertValue = $recWithUniqueValue['alias'];
1405 }
1406 } elseif (strpos($cfg['subst']['tokenValue'], ':') !== false) {
1407 list($tokenKey) = explode(':', $cfg['subst']['tokenValue']);
1408 $insertValue = $tokenKey . ':' . $insertValue;
1409 }
1410 }
1411 }
1412 }
1413 // Finally, swap the soft reference token in tokenized content with the insert value:
1414 $tokenizedContent = str_replace('{softref:' . $tokenID . '}', $insertValue, $tokenizedContent);
1415 }
1416 return $tokenizedContent;
1417 }
1418
1419 /**
1420 * Process a soft reference file
1421 *
1422 * @param string $relFileName Old Relative filename
1423 * @param array $cfg soft reference configuration array
1424 * @param string $table Table for which the processing occurs
1425 * @param string $uid UID of record from table
1426 * @return string New relative filename (value to insert instead of the softref token)
1427 */
1428 public function processSoftReferences_saveFile($relFileName, $cfg, $table, $uid)
1429 {
1430 if ($fileHeaderInfo = $this->dat['header']['files'][$cfg['file_ID']]) {
1431 // Initialize; Get directory prefix for file and find possible RTE filename
1432 $dirPrefix = PathUtility::dirname($relFileName) . '/';
1433 $rteOrigName = $this->getRTEoriginalFilename(PathUtility::basename($relFileName));
1434 // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
1435 if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/')) {
1436 // RTE:
1437 // First, find unique RTE file name:
1438 if (@is_dir(Environment::getPublicPath() . '/' . $dirPrefix)) {
1439 // From the "original" RTE filename, produce a new "original" destination filename which is unused.
1440 // Even if updated, the image should be unique. Currently the problem with this is that it leaves a lot of unused RTE images...
1441 $fileProcObj = $this->getFileProcObj();
1442 $origDestName = $fileProcObj->getUniqueName($rteOrigName, Environment::getPublicPath() . '/' . $dirPrefix);
1443 // Create copy file name:
1444 $pI = pathinfo($relFileName);
1445 $copyDestName = PathUtility::dirname($origDestName) . '/RTEmagicC_' . substr(PathUtility::basename($origDestName), 10) . '.' . $pI['extension'];
1446 if (
1447 !@is_file($copyDestName) && !@is_file($origDestName)
1448 && $origDestName === GeneralUtility::getFileAbsFileName($origDestName)
1449 && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)
1450 ) {
1451 if ($this->dat['header']['files'][$fileHeaderInfo['RTE_ORIG_ID']]) {
1452 // Write the copy and original RTE file to the respective filenames:
1453 $this->writeFileVerify($copyDestName, $cfg['file_ID'], true);
1454 $this->writeFileVerify($origDestName, $fileHeaderInfo['RTE_ORIG_ID'], true);
1455 // Return the relative path of the copy file name:
1456 return PathUtility::stripPathSitePrefix($copyDestName);
1457 }
1458 $this->error('ERROR: Could not find original file ID');
1459 } else {
1460 $this->error('ERROR: The destination filenames "' . $copyDestName . '" and "' . $origDestName . '" either existed or have non-valid names');
1461 }
1462 } else {
1463 $this->error('ERROR: "' . Environment::getPublicPath() . '/' . $dirPrefix . '" was not a directory, so could not process file "' . $relFileName . '"');
1464 }
1465 } elseif (GeneralUtility::isFirstPartOfStr($dirPrefix, $this->fileadminFolderName . '/')) {
1466 // File in fileadmin/ folder:
1467 // Create file (and possible resources)
1468 $newFileName = $this->processSoftReferences_saveFile_createRelFile($dirPrefix, PathUtility::basename($relFileName), $cfg['file_ID'], $table, $uid);
1469 if (strlen($newFileName)) {
1470 $relFileName = $newFileName;
1471 } else {
1472 $this->error('ERROR: No new file created for "' . $relFileName . '"');
1473 }
1474 } else {
1475 $this->error('ERROR: Sorry, cannot operate on non-RTE files which are outside the fileadmin folder.');
1476 }
1477 } else {
1478 $this->error('ERROR: Could not find file ID in header.');
1479 }
1480 // Return (new) filename relative to public web path
1481 return $relFileName;
1482 }
1483
1484 /**
1485 * Create file in directory and return the new (unique) filename
1486 *
1487 * @param string $origDirPrefix Directory prefix, relative, with trailing slash
1488 * @param string $fileName Filename (without path)
1489 * @param string $fileID File ID from import memory
1490 * @param string $table Table for which the processing occurs
1491 * @param string $uid UID of record from table
1492 * @return string|null New relative filename, if any
1493 */
1494 public function processSoftReferences_saveFile_createRelFile($origDirPrefix, $fileName, $fileID, $table, $uid)
1495 {
1496 // If the fileID map contains an entry for this fileID then just return the relative filename of that entry;
1497 // we don't want to write another unique filename for this one!
1498 if (isset($this->fileIDMap[$fileID])) {
1499 return PathUtility::stripPathSitePrefix($this->fileIDMap[$fileID]);
1500 }
1501 // Verify FileMount access to dir-prefix. Returns the best alternative relative path if any
1502 $dirPrefix = $this->verifyFolderAccess($origDirPrefix);
1503 if ($dirPrefix && (!$this->update || $origDirPrefix === $dirPrefix) && $this->checkOrCreateDir($dirPrefix)) {
1504 $fileHeaderInfo = $this->dat['header']['files'][$fileID];
1505 $updMode = $this->update && $this->import_mapId[$table][$uid] === $uid && $this->import_mode[$table . ':' . $uid] !== 'as_new';
1506 // Create new name for file:
1507 // Must have same ID in map array (just for security, is not really needed) and NOT be set "as_new".
1508
1509 // Write main file:
1510 if ($updMode) {
1511 $newName = Environment::getPublicPath() . '/' . $dirPrefix . $fileName;
1512 } else {
1513 // Create unique filename:
1514 $fileProcObj = $this->getFileProcObj();
1515 $newName = $fileProcObj->getUniqueName($fileName, Environment::getPublicPath() . '/' . $dirPrefix);
1516 }
1517 if ($this->writeFileVerify($newName, $fileID)) {
1518 // If the resource was an HTML/CSS file with resources attached, we will write those as well!
1519 if (is_array($fileHeaderInfo['EXT_RES_ID'])) {
1520 $tokenizedContent = $this->dat['files'][$fileID]['tokenizedContent'];
1521 $tokenSubstituted = false;
1522 $fileProcObj = $this->getFileProcObj();
1523 if ($updMode) {
1524 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1525 if ($this->dat['files'][$res_fileID]['filename']) {
1526 // Resolve original filename:
1527 $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
1528 $absResourceFileName = GeneralUtility::resolveBackPath(Environment::getPublicPath() . '/' . $origDirPrefix . $relResourceFileName);
1529 $absResourceFileName = GeneralUtility::getFileAbsFileName($absResourceFileName);
1530 if ($absResourceFileName && GeneralUtility::isFirstPartOfStr($absResourceFileName, Environment::getPublicPath() . '/' . $this->fileadminFolderName . '/')) {
1531 $destDir = PathUtility::stripPathSitePrefix(PathUtility::dirname($absResourceFileName) . '/');
1532 if ($this->verifyFolderAccess($destDir, true) && $this->checkOrCreateDir($destDir)) {
1533 $this->writeFileVerify($absResourceFileName, $res_fileID);
1534 } else {
1535 $this->error('ERROR: Could not create file in directory "' . $destDir . '"');
1536 }
1537 } else {
1538 $this->error('ERROR: Could not resolve path for "' . $relResourceFileName . '"');
1539 }
1540 $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1541 $tokenSubstituted = true;
1542 }
1543 }
1544 } else {
1545 // Create the resouces directory name (filename without extension, suffixed "_FILES")
1546 $resourceDir = PathUtility::dirname($newName) . '/' . preg_replace('/\\.[^.]*$/', '', PathUtility::basename($newName)) . '_FILES';
1547 if (GeneralUtility::mkdir($resourceDir)) {
1548 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1549 if ($this->dat['files'][$res_fileID]['filename']) {
1550 $absResourceFileName = $fileProcObj->getUniqueName($this->dat['files'][$res_fileID]['filename'], $resourceDir);
1551 $relResourceFileName = substr($absResourceFileName, strlen(PathUtility::dirname($resourceDir)) + 1);
1552 $this->writeFileVerify($absResourceFileName, $res_fileID);
1553 $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1554 $tokenSubstituted = true;
1555 }
1556 }
1557 }
1558 }
1559 // If substitutions has been made, write the content to the file again:
1560 if ($tokenSubstituted) {
1561 GeneralUtility::writeFile($newName, $tokenizedContent);
1562 }
1563 }
1564 return PathUtility::stripPathSitePrefix($newName);
1565 }
1566 }
1567 return null;
1568 }
1569
1570 /**
1571 * Writes a file from the import memory having $fileID to file name $fileName which must be an absolute path inside public web path
1572 *
1573 * @param string $fileName Absolute filename inside public web path to write to
1574 * @param string $fileID File ID from import memory
1575 * @param bool $bypassMountCheck Bypasses the checking against filemounts - only for RTE files!
1576 * @return bool Returns TRUE if it went well. Notice that the content of the file is read again, and md5 from import memory is validated.
1577 */
1578 public function writeFileVerify($fileName, $fileID, $bypassMountCheck = false)
1579 {
1580 $fileProcObj = $this->getFileProcObj();
1581 if (!$fileProcObj->actionPerms['addFile']) {
1582 $this->error('ERROR: You did not have sufficient permissions to write the file "' . $fileName . '"');
1583 return false;
1584 }
1585 // Just for security, check again. Should actually not be necessary.
1586 if (!$bypassMountCheck) {
1587 try {
1588 ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier(PathUtility::dirname($fileName));
1589 } catch (InsufficientFolderAccessPermissionsException $e) {
1590 $this->error('ERROR: Filename "' . $fileName . '" was not allowed in destination path!');
1591 return false;
1592 }
1593 }
1594 $fI = GeneralUtility::split_fileref($fileName);
1595 if (!$fileProcObj->checkIfAllowed($fI['fileext'], $fI['path'], $fI['file']) && (!$this->allowPHPScripts || !$this->getBackendUser()->isAdmin())) {
1596 $this->error('ERROR: Filename "' . $fileName . '" failed against extension check or deny-pattern!');
1597 return false;
1598 }
1599 if (!GeneralUtility::getFileAbsFileName($fileName)) {
1600 $this->error('ERROR: Filename "' . $fileName . '" was not a valid relative file path!');
1601 return false;
1602 }
1603 if (!$this->dat['files'][$fileID]) {
1604 $this->error('ERROR: File ID "' . $fileID . '" could not be found');
1605 return false;
1606 }
1607 GeneralUtility::writeFile($fileName, $this->dat['files'][$fileID]['content']);
1608 $this->fileIDMap[$fileID] = $fileName;
1609 if (md5(file_get_contents($fileName)) == $this->dat['files'][$fileID]['content_md5']) {
1610 return true;
1611 }
1612 $this->error('ERROR: File content "' . $fileName . '" was corrupted');
1613 return false;
1614 }
1615
1616 /**
1617 * Returns TRUE if directory exists and if it doesn't it will create directory and return TRUE if that succeeded.
1618 *
1619 * @param string $dirPrefix Directory to create. Having a trailing slash. Must be in fileadmin/. Relative to public web path
1620 * @return bool TRUE, if directory exists (was created)
1621 */
1622 public function checkOrCreateDir($dirPrefix)
1623 {
1624 // Split dir path and remove first directory (which should be "fileadmin")
1625 $filePathParts = explode('/', $dirPrefix);
1626 $firstDir = array_shift($filePathParts);
1627 if ($firstDir === $this->fileadminFolderName && GeneralUtility::getFileAbsFileName($dirPrefix)) {
1628 $pathAcc = '';
1629 foreach ($filePathParts as $dirname) {
1630 $pathAcc .= '/' . $dirname;
1631 if (strlen($dirname)) {
1632 if (!@is_dir(Environment::getPublicPath() . '/' . $this->fileadminFolderName . $pathAcc)) {
1633 if (!GeneralUtility::mkdir(Environment::getPublicPath() . '/' . $this->fileadminFolderName . $pathAcc)) {
1634 $this->error('ERROR: Directory could not be created....B');
1635 return false;
1636 }
1637 }
1638 } elseif ($dirPrefix === $this->fileadminFolderName . $pathAcc) {
1639 return true;
1640 } else {
1641 $this->error('ERROR: Directory could not be created....A');
1642 }
1643 }
1644 }
1645 return false;
1646 }
1647
1648 /**************************
1649 * File Input
1650 *************************/
1651
1652 /**
1653 * Loads the header section/all of the $filename into memory
1654 *
1655 * @param string $filename Filename, absolute
1656 * @param bool $all If set, all information is loaded (header, records and files). Otherwise the default is to read only the header information
1657 * @return bool TRUE if the operation went well
1658 */
1659 public function loadFile($filename, $all = false)
1660 {
1661 if (!@is_file($filename)) {
1662 $this->error('Filename not found: ' . $filename);
1663 return false;
1664 }
1665 $fI = pathinfo($filename);
1666 if (@is_dir($filename . '.files')) {
1667 if (GeneralUtility::isAllowedAbsPath($filename . '.files')) {
1668 // copy the folder lowlevel to typo3temp, because the files would be deleted after import
1669 $temporaryFolderName = $this->getTemporaryFolderName();
1670 GeneralUtility::copyDirectory($filename . '.files', $temporaryFolderName);
1671 $this->filesPathForImport = $temporaryFolderName;
1672 } else {
1673 $this->error('External import files for the given import source is currently not supported.');
1674 }
1675 }
1676 if (strtolower($fI['extension']) === 'xml') {
1677 // XML:
1678 $xmlContent = file_get_contents($filename);
1679 if (strlen($xmlContent)) {
1680 $this->dat = GeneralUtility::xml2array($xmlContent, '', true);
1681 if (is_array($this->dat)) {
1682 if ($this->dat['_DOCUMENT_TAG'] === 'T3RecordDocument' && is_array($this->dat['header']) && is_array($this->dat['records'])) {
1683 $this->loadInit();
1684 return true;
1685 }
1686 $this->error('XML file did not contain proper XML for TYPO3 Import');
1687 } else {
1688 $this->error('XML could not be parsed: ' . $this->dat);
1689 }
1690 } else {
1691 $this->error('Error opening file: ' . $filename);
1692 }
1693 } else {
1694 // T3D
1695 if ($fd = fopen($filename, 'rb')) {
1696 $this->dat['header'] = $this->getNextFilePart($fd, 1, 'header');
1697 if ($all) {
1698 $this->dat['records'] = $this->getNextFilePart($fd, 1, 'records');
1699 $this->dat['files'] = $this->getNextFilePart($fd, 1, 'files');
1700 $this->dat['files_fal'] = $this->getNextFilePart($fd, 1, 'files_fal');
1701 }
1702 $this->loadInit();
1703 return true;
1704 }
1705 $this->error('Error opening file: ' . $filename);
1706
1707 fclose($fd);
1708 }
1709 return false;
1710 }
1711
1712 /**
1713 * Returns the next content part form the fileresource (t3d), $fd
1714 *
1715 * @param resource $fd File pointer
1716 * @param bool $unserialize If set, the returned content is unserialized into an array, otherwise you get the raw string
1717 * @param string $name For error messages this indicates the section of the problem.
1718 * @return string|null Data string or NULL in case of an error
1719 * @access private
1720 * @see loadFile()
1721 */
1722 public function getNextFilePart($fd, $unserialize = false, $name = '')
1723 {
1724 $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
1725 // Getting header data
1726 $initStr = fread($fd, $initStrLen);
1727 if (empty($initStr)) {
1728 $this->error('File does not contain data for "' . $name . '"');
1729 return null;
1730 }
1731 $initStrDat = explode(':', $initStr);
1732 if (strstr($initStrDat[0], 'Warning')) {
1733 $this->error('File read error: Warning message in file. (' . $initStr . fgets($fd) . ')');
1734 return null;
1735 }
1736 if ((string)$initStrDat[3] !== '') {
1737 $this->error('File read error: InitString had a wrong length. (' . $name . ')');
1738 return null;
1739 }
1740 $datString = fread($fd, (int)$initStrDat[2]);
1741 fread($fd, 1);
1742 if (md5($datString) === $initStrDat[0]) {
1743 if ($initStrDat[1]) {
1744 if ($this->compress) {
1745 $datString = gzuncompress($datString);
1746 } else {
1747 $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.');
1748 return null;
1749 }
1750 }
1751 return $unserialize ? unserialize($datString, ['allowed_classes' => false]) : $datString;
1752 }
1753 $this->error('MD5 check failed (' . $name . ')');
1754
1755 return null;
1756 }
1757
1758 /**
1759 * Loads T3D file content into the $this->dat array
1760 * (This function can be used to test the output strings from ->compileMemoryToFileContent())
1761 *
1762 * @param string $filecontent File content
1763 */
1764 public function loadContent($filecontent)
1765 {
1766 $pointer = 0;
1767 $this->dat['header'] = $this->getNextContentPart($filecontent, $pointer, 1, 'header');
1768 $this->dat['records'] = $this->getNextContentPart($filecontent, $pointer, 1, 'records');
1769 $this->dat['files'] = $this->getNextContentPart($filecontent, $pointer, 1, 'files');
1770 $this->loadInit();
1771 }
1772
1773 /**
1774 * Returns the next content part from the $filecontent
1775 *
1776 * @param string $filecontent File content string
1777 * @param int $pointer File pointer (where to read from)
1778 * @param bool $unserialize If set, the returned content is unserialized into an array, otherwise you get the raw string
1779 * @param string $name For error messages this indicates the section of the problem.
1780 * @return string|null Data string
1781 */
1782 public function getNextContentPart($filecontent, &$pointer, $unserialize = false, $name = '')
1783 {
1784 $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
1785 // getting header data
1786 $initStr = substr($filecontent, $pointer, $initStrLen);
1787 $pointer += $initStrLen;
1788 $initStrDat = explode(':', $initStr);
1789 if ((string)$initStrDat[3] !== '') {
1790 $this->error('Content read error: InitString had a wrong length. (' . $name . ')');
1791 return null;
1792 }
1793 $datString = substr($filecontent, $pointer, (int)$initStrDat[2]);
1794 $pointer += (int)$initStrDat[2] + 1;
1795 if (md5($datString) === $initStrDat[0]) {
1796 if ($initStrDat[1]) {
1797 if ($this->compress) {
1798 $datString = gzuncompress($datString);
1799 return $unserialize ? unserialize($datString, ['allowed_classes' => false]) : $datString;
1800 }
1801 $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.');
1802 }
1803 } else {
1804 $this->error('MD5 check failed (' . $name . ')');
1805 }
1806 return null;
1807 }
1808
1809 /**
1810 * Setting up the object based on the recently loaded ->dat array
1811 */
1812 public function loadInit()
1813 {
1814 $this->relStaticTables = (array)$this->dat['header']['relStaticTables'];
1815 $this->excludeMap = (array)$this->dat['header']['excludeMap'];
1816 $this->softrefCfg = (array)$this->dat['header']['softrefCfg'];
1817 }
1818 }