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