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