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