TYPO3 CMS  TYPO3_8-7
Import.php
Go to the documentation of this file.
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 
33 
37 class Import extends ImportExport
38 {
45  public $suggestedInsertUids = [];
46 
52  public $enableLogging = false;
53 
61  public $import_newId = [];
62 
68  public $import_newId_pids = [];
69 
75  public $import_data = [];
76 
82  protected $storageObjects = [];
83 
87  protected $filesPathForImport = null;
88 
92  protected $unlinkFiles = [];
93 
97  protected $alternativeFileName = [];
98 
102  protected $alternativeFilePath = [];
103 
107  protected $filePathMap = [];
108 
109  /**************************
110  * Initialize
111  *************************/
112 
116  public function init()
117  {
118  parent::init();
119  $this->mode = 'import';
120  }
121 
122  /***********************
123  * Import
124  ***********************/
125 
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 
149  protected function initializeStorageObjects()
150  {
152  $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
153  $this->storageObjects = $storageRepository->findAll();
154  }
155 
161  public function importData($pid)
162  {
163  $this->initializeImport();
164 
165  // Write sys_file_storages first
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 
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 
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 
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 
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']])) {
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  $this->callHook('before_addSysFileRecord', [
433  'fileRecord' => $fileRecord,
434  'importFolder' => $importFolder,
435  'temporaryFile' => $temporaryFile
436  ]);
437 
438  try {
440  $newFile = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']);
441  } catch (Exception $e) {
442  $this->error('Error: File could not be added to the storage: "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
443  continue;
444  }
445 
446  if ($newFile->getSha1() !== $fileRecord['sha1']) {
447  $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'] . '"');
448  }
449  }
450 
451  // save the new uid in the import id map
452  $this->import_mapId['sys_file'][$fileRecord['uid']] = $newFile->getUid();
453  $this->fixUidLocalInSysFileReferenceRecords($fileRecord['uid'], $newFile->getUid());
454  }
455 
456  // unset the sys_file records to prevent an import in writeRecords_records
457  unset($this->dat['header']['records']['sys_file']);
458  // remove all sys_file_reference records that point to file records which are unknown
459  // in the system to prevent exceptions
461  }
462 
468  {
469  if (!isset($this->dat['header']['records']['sys_file_reference'])) {
470  return;
471  }
472 
473  foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
474  $fileReferenceRecord = $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data'];
475  if (!in_array($fileReferenceRecord['uid_local'], $this->import_mapId['sys_file'])) {
476  unset($this->dat['header']['records']['sys_file_reference'][$sysFileReferenceUid]);
477  unset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]);
478  $this->error(
479  'Error: sys_file_reference record ' . (int)$sysFileReferenceUid
480  . ' with relation to sys_file record ' . (int)$fileReferenceRecord['uid_local']
481  . ', which is not part of the import data, was not imported.'
482  );
483  }
484  }
485  }
486 
493  protected function isFallbackStorage($storageId)
494  {
495  return $storageId === 0 || $storageId === '0';
496  }
497 
513  protected function fixUidLocalInSysFileReferenceRecords($oldFileUid, $newFileUid)
514  {
515  if (!isset($this->dat['header']['records']['sys_file_reference'])) {
516  return;
517  }
518 
519  foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
520  if (!isset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'])
521  && $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] == $oldFileUid
522  ) {
523  $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'] = true;
524  $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] = $newFileUid;
525  }
526  }
527  }
528 
535  protected function fetchStorageRecords()
536  {
537  $result = GeneralUtility::makeInstance(ConnectionPool::class)
538  ->getQueryBuilderForTable('sys_file_storage')
539  ->select('*')
540  ->from('sys_file_storage')
541  ->orderBy('uid')
542  ->execute();
543  $rows = [];
544  while ($row = $result->fetch()) {
545  $rows[$row['uid']] = $row;
546  }
547  return $rows;
548  }
549 
557  protected function writeTemporaryFileFromData($fileId, $dataKey = 'files_fal')
558  {
559  $temporaryFilePath = null;
560  if (is_array($this->dat[$dataKey][$fileId])) {
561  $temporaryFilePathInternal = GeneralUtility::tempnam('import_temp_');
562  GeneralUtility::writeFile($temporaryFilePathInternal, $this->dat[$dataKey][$fileId]['content']);
563  clearstatcache();
564  if (@is_file($temporaryFilePathInternal)) {
565  $this->unlinkFiles[] = $temporaryFilePathInternal;
566  if (filesize($temporaryFilePathInternal) == $this->dat[$dataKey][$fileId]['filesize']) {
567  $temporaryFilePath = $temporaryFilePathInternal;
568  } else {
569  $this->error('Error: temporary file ' . $temporaryFilePathInternal . ' had a size (' . filesize($temporaryFilePathInternal) . ') different from the original (' . $this->dat[$dataKey][$fileId]['filesize'] . ')');
570  }
571  } else {
572  $this->error('Error: temporary file ' . $temporaryFilePathInternal . ' was not written as it should have been!');
573  }
574  } else {
575  $this->error('Error: No file found for ID ' . $fileId);
576  }
577  return $temporaryFilePath;
578  }
579 
586  public function writeRecords_pages($pid)
587  {
588  // First, write page structure if any:
589  if (is_array($this->dat['header']['records']['pages'])) {
590  $this->addGeneralErrorsByTable('pages');
591  // $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.
592  $pageRecords = $this->dat['header']['records']['pages'];
593  $this->import_data = [];
594  // First add page tree if any
595  if (is_array($this->dat['header']['pagetree'])) {
596  $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
597  foreach ($pagesFromTree as $uid) {
598  $thisRec = $this->dat['header']['records']['pages'][$uid];
599  // PID: Set the main $pid, unless a NEW-id is found
600  $setPid = isset($this->import_newId_pids[$thisRec['pid']]) ? $this->import_newId_pids[$thisRec['pid']] : $pid;
601  $this->addSingle('pages', $uid, $setPid);
602  unset($pageRecords[$uid]);
603  }
604  }
605  // Then add all remaining pages not in tree on root level:
606  if (!empty($pageRecords)) {
607  $remainingPageUids = array_keys($pageRecords);
608  foreach ($remainingPageUids as $pUid) {
609  $this->addSingle('pages', $pUid, $pid);
610  }
611  }
612  // Now write to database:
613  $tce = $this->getNewTCE();
614  $tce->isImporting = true;
615  $this->callHook('before_writeRecordsPages', [
616  'tce' => &$tce,
617  'data' => &$this->import_data
618  ]);
619  $tce->suggestedInsertUids = $this->suggestedInsertUids;
620  $tce->start($this->import_data, []);
621  $tce->process_datamap();
622  $this->callHook('after_writeRecordsPages', [
623  'tce' => &$tce
624  ]);
625  // post-processing: Registering new ids (end all DataHandler sessions with this)
626  $this->addToMapId($tce->substNEWwithIDs);
627  // In case of an update, order pages from the page tree correctly:
628  if ($this->update && is_array($this->dat['header']['pagetree'])) {
629  $this->writeRecords_pages_order();
630  }
631  }
632  }
633 
641  public function writeRecords_pages_order()
642  {
643  $cmd_data = [];
644  // Get uid-pid relations and traverse them in order to map to possible new IDs
645  $pidsFromTree = $this->flatInversePageTree_pid($this->dat['header']['pagetree']);
646  foreach ($pidsFromTree as $origPid => $newPid) {
647  if ($newPid >= 0 && $this->dontIgnorePid('pages', $origPid)) {
648  // If the page had a new id (because it was created) use that instead!
649  if (substr($this->import_newId_pids[$origPid], 0, 3) === 'NEW') {
650  if ($this->import_mapId['pages'][$origPid]) {
651  $mappedPid = $this->import_mapId['pages'][$origPid];
652  $cmd_data['pages'][$mappedPid]['move'] = $newPid;
653  }
654  } else {
655  $cmd_data['pages'][$origPid]['move'] = $newPid;
656  }
657  }
658  }
659  // Execute the move commands if any:
660  if (!empty($cmd_data)) {
661  $tce = $this->getNewTCE();
662  $this->callHook('before_writeRecordsPagesOrder', [
663  'tce' => &$tce,
664  'data' => &$cmd_data
665  ]);
666  $tce->start([], $cmd_data);
667  $tce->process_cmdmap();
668  $this->callHook('after_writeRecordsPagesOrder', [
669  'tce' => &$tce
670  ]);
671  }
672  }
673 
683  public function flatInversePageTree_pid($idH, $a = [], $pid = -1)
684  {
685  if (is_array($idH)) {
686  $idH = array_reverse($idH);
687  foreach ($idH as $v) {
688  $a[$v['uid']] = $pid;
689  if (is_array($v['subrow'])) {
690  $a = $this->flatInversePageTree_pid($v['subrow'], $a, $v['uid']);
691  }
692  }
693  }
694  return $a;
695  }
696 
703  public function writeRecords_records($pid)
704  {
705  // Write the rest of the records
706  $this->import_data = [];
707  if (is_array($this->dat['header']['records'])) {
708  foreach ($this->dat['header']['records'] as $table => $recs) {
709  $this->addGeneralErrorsByTable($table);
710  if ($table !== 'pages') {
711  foreach ($recs as $uid => $thisRec) {
712  // PID: Set the main $pid, unless a NEW-id is found
713  $setPid = isset($this->import_mapId['pages'][$thisRec['pid']])
714  ? (int)$this->import_mapId['pages'][$thisRec['pid']]
715  : (int)$pid;
716  if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel'])) {
717  $rootLevelSetting = (int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'];
718  if ($rootLevelSetting === 1) {
719  $setPid = 0;
720  } elseif ($rootLevelSetting === 0 && $setPid === 0) {
721  $this->error('Error: Record type ' . $table . ' is not allowed on pid 0');
722  continue;
723  }
724  }
725  // Add record:
726  $this->addSingle($table, $uid, $setPid);
727  }
728  }
729  }
730  } else {
731  $this->error('Error: No records defined in internal data array.');
732  }
733  // Now write to database:
734  $tce = $this->getNewTCE();
735  $this->callHook('before_writeRecordsRecords', [
736  'tce' => &$tce,
737  'data' => &$this->import_data
738  ]);
739  $tce->suggestedInsertUids = $this->suggestedInsertUids;
740  // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
741  $tce->reverseOrder = 1;
742  $tce->isImporting = true;
743  $tce->start($this->import_data, []);
744  $tce->process_datamap();
745  $this->callHook('after_writeRecordsRecords', [
746  'tce' => &$tce
747  ]);
748  // post-processing: Removing files and registering new ids (end all DataHandler sessions with this)
749  $this->addToMapId($tce->substNEWwithIDs);
750  // In case of an update, order pages from the page tree correctly:
751  if ($this->update) {
752  $this->writeRecords_records_order($pid);
753  }
754  }
755 
764  public function writeRecords_records_order($mainPid)
765  {
766  $cmd_data = [];
767  if (is_array($this->dat['header']['pagetree'])) {
768  $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
769  } else {
770  $pagesFromTree = [];
771  }
772  if (is_array($this->dat['header']['pid_lookup'])) {
773  foreach ($this->dat['header']['pid_lookup'] as $pid => $recList) {
774  $newPid = isset($this->import_mapId['pages'][$pid]) ? $this->import_mapId['pages'][$pid] : $mainPid;
776  foreach ($recList as $tableName => $uidList) {
777  // If $mainPid===$newPid then we are on root level and we can consider to move pages as well!
778  // (they will not be in the page tree!)
779  if (($tableName !== 'pages' || !$pagesFromTree[$pid]) && is_array($uidList)) {
780  $uidList = array_reverse(array_keys($uidList));
781  foreach ($uidList as $uid) {
782  if ($this->dontIgnorePid($tableName, $uid)) {
783  $cmd_data[$tableName][$uid]['move'] = $newPid;
784  }
785  }
786  }
787  }
788  }
789  }
790  }
791  // Execute the move commands if any:
792  if (!empty($cmd_data)) {
793  $tce = $this->getNewTCE();
794  $this->callHook('before_writeRecordsRecordsOrder', [
795  'tce' => &$tce,
796  'data' => &$cmd_data
797  ]);
798  $tce->start([], $cmd_data);
799  $tce->process_cmdmap();
800  $this->callHook('after_writeRecordsRecordsOrder', [
801  'tce' => &$tce
802  ]);
803  }
804  }
805 
816  public function addSingle($table, $uid, $pid)
817  {
818  if ($this->import_mode[$table . ':' . $uid] === 'exclude') {
819  return;
820  }
821  $record = $this->dat['records'][$table . ':' . $uid]['data'];
822  if (is_array($record)) {
823  if ($this->update && $this->doesRecordExist($table, $uid) && $this->import_mode[$table . ':' . $uid] !== 'as_new') {
824  $ID = $uid;
825  } elseif ($table === 'sys_file_metadata' && $record['sys_language_uid'] == '0' && $this->import_mapId['sys_file'][$record['file']]) {
826  // on adding sys_file records the belonging sys_file_metadata record was also created
827  // if there is one the record need to be overwritten instead of creating a new one.
828  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
829  ->getQueryBuilderForTable('sys_file_metadata');
830  $recordInDatabase = $queryBuilder->select('uid')
831  ->from('sys_file_metadata')
832  ->where(
833  $queryBuilder->expr()->eq(
834  'file',
835  $queryBuilder->createNamedParameter(
836  $this->import_mapId['sys_file'][$record['file']],
837  \PDO::PARAM_INT
838  )
839  ),
840  $queryBuilder->expr()->eq(
841  'sys_language_uid',
842  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
843  ),
844  $queryBuilder->expr()->eq(
845  'pid',
846  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
847  )
848  )
849  ->execute()
850  ->fetch();
851  // if no record could be found, $this->import_mapId['sys_file'][$record['file']] is pointing
852  // to a file, that was already there, thus a new metadata record should be created
853  if (is_array($recordInDatabase)) {
854  $this->import_mapId['sys_file_metadata'][$record['uid']] = $recordInDatabase['uid'];
855  $ID = $recordInDatabase['uid'];
856  } else {
857  $ID = StringUtility::getUniqueId('NEW');
858  }
859  } else {
860  $ID = StringUtility::getUniqueId('NEW');
861  }
862  $this->import_newId[$table . ':' . $ID] = ['table' => $table, 'uid' => $uid];
863  if ($table === 'pages') {
864  $this->import_newId_pids[$uid] = $ID;
865  }
866  // Set main record data:
867  $this->import_data[$table][$ID] = $record;
868  $this->import_data[$table][$ID]['tx_impexp_origuid'] = $this->import_data[$table][$ID]['uid'];
869  // Reset permission data:
870  if ($table === 'pages') {
871  // Have to reset the user/group IDs so pages are owned by importing user. Otherwise strange things may happen for non-admins!
872  unset($this->import_data[$table][$ID]['perms_userid']);
873  unset($this->import_data[$table][$ID]['perms_groupid']);
874  }
875  // PID and UID:
876  unset($this->import_data[$table][$ID]['uid']);
877  // Updates:
879  unset($this->import_data[$table][$ID]['pid']);
880  } else {
881  // Inserts:
882  $this->import_data[$table][$ID]['pid'] = $pid;
883  if (($this->import_mode[$table . ':' . $uid] === 'force_uid' && $this->update || $this->force_all_UIDS) && $this->getBackendUser()->isAdmin()) {
884  $this->import_data[$table][$ID]['uid'] = $uid;
885  $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE';
886  }
887  }
888  // Setting db/file blank:
889  foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
890  switch ((string)$config['type']) {
891  case 'db':
892 
893  case 'file':
894  // Fixed later in ->setRelations() [because we need to know ALL newly created IDs before we can map relations!]
895  // In the meantime we set NO values for relations.
896  //
897  // BUT for field uid_local of table sys_file_reference the relation MUST not be cleared here,
898  // because the value is already the uid of the right imported sys_file record.
899  // @see fixUidLocalInSysFileReferenceRecords()
900  // If it's empty or a uid to another record the FileExtensionFilter will throw an exception or
901  // delete the reference record if the file extension of the related record doesn't match.
902  if ($table !== 'sys_file_reference' && $field !== 'uid_local') {
903  $this->import_data[$table][$ID][$field] = '';
904  }
905  break;
906  case 'flex':
907  // Fixed later in setFlexFormRelations()
908  // In the meantime we set NO value for flexforms - this is mainly because file references
909  // inside will not be processed properly; In fact references will point to no file
910  // or existing files (in which case there will be double-references which is a big problem of course!)
911  $this->import_data[$table][$ID][$field] = '';
912  break;
913  }
914  }
915  } elseif ($table . ':' . $uid != 'pages:0') {
916  // On root level we don't want this error message.
917  $this->error('Error: no record was found in data array!');
918  }
919  }
920 
927  public function addToMapId($substNEWwithIDs)
928  {
929  foreach ($this->import_data as $table => $recs) {
930  foreach ($recs as $id => $value) {
931  $old_uid = $this->import_newId[$table . ':' . $id]['uid'];
932  if (isset($substNEWwithIDs[$id])) {
933  $this->import_mapId[$table][$old_uid] = $substNEWwithIDs[$id];
934  } elseif ($this->update) {
935  // Map same ID to same ID....
936  $this->import_mapId[$table][$old_uid] = $id;
937  } else {
938  // if $this->import_mapId contains already the right mapping, skip the error msg.
939  // See special handling of sys_file_metadata in addSingle() => nothing to do
940  if (!($table === 'sys_file_metadata' && isset($this->import_mapId[$table][$old_uid]) && $this->import_mapId[$table][$old_uid] == $id)) {
941  $this->error('Possible error: ' . $table . ':' . $old_uid . ' had no new id assigned to it. This indicates that the record was not added to database during import. Please check changelog!');
942  }
943  }
944  }
945  }
946  }
947 
953  public function getNewTCE()
954  {
955  $tce = GeneralUtility::makeInstance(DataHandler::class);
956  $tce->dontProcessTransformations = 1;
957  $tce->enableLogging = $this->enableLogging;
958  $tce->alternativeFileName = $this->alternativeFileName;
959  $tce->alternativeFilePath = $this->alternativeFilePath;
960  return $tce;
961  }
962 
966  public function unlinkTempFiles()
967  {
968  foreach ($this->unlinkFiles as $fileName) {
969  if (GeneralUtility::isFirstPartOfStr($fileName, PATH_site . 'typo3temp/')) {
971  clearstatcache();
972  if (is_file($fileName)) {
973  $this->error('Error: ' . $fileName . ' was NOT unlinked as it should have been!');
974  }
975  } else {
976  $this->error('Error: ' . $fileName . ' was not in temp-path. Not removed!');
977  }
978  }
979  $this->unlinkFiles = [];
980  }
981 
982  /***************************
983  * Import / Relations setting
984  ***************************/
985 
993  public function setRelations()
994  {
995  $updateData = [];
996  // import_newId contains a register of all records that was in the import memorys "records" key
997  foreach ($this->import_newId as $nId => $dat) {
998  $table = $dat['table'];
999  $uid = $dat['uid'];
1000  // original UID - NOT the new one!
1001  // If the record has been written and received a new id, then proceed:
1002  if (is_array($this->import_mapId[$table]) && isset($this->import_mapId[$table][$uid])) {
1003  $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1004  if (is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
1005  // Traverse relation fields of each record
1006  foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1007  // uid_local of sys_file_reference needs no update because the correct reference uid was already written
1008  // @see ImportExport::fixUidLocalInSysFileReferenceRecords()
1009  if ($table === 'sys_file_reference' && $field === 'uid_local') {
1010  continue;
1011  }
1012  switch ((string)$config['type']) {
1013  case 'db':
1014  if (is_array($config['itemArray']) && !empty($config['itemArray'])) {
1015  $itemConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1016  $valArray = $this->setRelations_db($config['itemArray'], $itemConfig);
1017  $updateData[$table][$thisNewUid][$field] = implode(',', $valArray);
1018  }
1019  break;
1020  case 'file':
1021  if (is_array($config['newValueFiles']) && !empty($config['newValueFiles'])) {
1022  $valArr = [];
1023  foreach ($config['newValueFiles'] as $fI) {
1024  $valArr[] = $this->import_addFileNameToBeCopied($fI);
1025  }
1026  $updateData[$table][$thisNewUid][$field] = implode(',', $valArr);
1027  }
1028  break;
1029  }
1030  }
1031  } else {
1032  $this->error('Error: no record was found in data array!');
1033  }
1034  } else {
1035  $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')');
1036  }
1037  }
1038  if (!empty($updateData)) {
1039  $tce = $this->getNewTCE();
1040  $tce->isImporting = true;
1041  $this->callHook('before_setRelation', [
1042  'tce' => &$tce,
1043  'data' => &$updateData
1044  ]);
1045  $tce->start($updateData, []);
1046  $tce->process_datamap();
1047  $this->callHook('after_setRelations', [
1048  'tce' => &$tce
1049  ]);
1050  }
1051  }
1052 
1060  public function setRelations_db($itemArray, $itemConfig)
1061  {
1062  $valArray = [];
1063  foreach ($itemArray as $relDat) {
1064  if (is_array($this->import_mapId[$relDat['table']]) && isset($this->import_mapId[$relDat['table']][$relDat['id']])) {
1065  // Since non FAL file relation type group internal_type file_reference are handled as reference to
1066  // sys_file records Datahandler requires the value as uid of the the related sys_file record only
1067  if ($itemConfig['type'] === 'group' && $itemConfig['internal_type'] === 'file_reference') {
1068  $value = $this->import_mapId[$relDat['table']][$relDat['id']];
1069  } elseif ($itemConfig['type'] === 'input' && isset($itemConfig['wizards']['link'])) {
1070  // If an input field has a relation to a sys_file record this need to be converted back to
1071  // the public path. But use getPublicUrl here, because could normally only be a local file path.
1072  $fileUid = $this->import_mapId[$relDat['table']][$relDat['id']];
1073  // Fallback value
1074  $value = 'file:' . $fileUid;
1075  try {
1076  $file = ResourceFactory::getInstance()->retrieveFileOrFolderObject($fileUid);
1077  } catch (\Exception $e) {
1078  $file = null;
1079  }
1080  if ($file instanceof FileInterface) {
1081  $value = $file->getPublicUrl();
1082  }
1083  } else {
1084  $value = $relDat['table'] . '_' . $this->import_mapId[$relDat['table']][$relDat['id']];
1085  }
1086  $valArray[] = $value;
1087  } elseif ($this->isTableStatic($relDat['table']) || $this->isExcluded($relDat['table'], $relDat['id']) || $relDat['id'] < 0) {
1088  // Checking for less than zero because some select types could contain negative values,
1089  // eg. fe_groups (-1, -2) and sys_language (-1 = ALL languages). This must be handled on both export and import.
1090  $valArray[] = $relDat['table'] . '_' . $relDat['id'];
1091  } else {
1092  $this->error('Lost relation: ' . $relDat['table'] . ':' . $relDat['id']);
1093  }
1094  }
1095  return $valArray;
1096  }
1097 
1104  public function import_addFileNameToBeCopied($fI)
1105  {
1106  if (is_array($this->dat['files'][$fI['ID']])) {
1107  $tmpFile = null;
1108  // check if there is the right file already in the local folder
1109  if ($this->filesPathForImport !== null) {
1110  if (is_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) &&
1111  md5_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) === $this->dat['files'][$fI['ID']]['content_md5']) {
1112  $tmpFile = $this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5'];
1113  }
1114  }
1115  if ($tmpFile === null) {
1116  $tmpFile = GeneralUtility::tempnam('import_temp_');
1117  GeneralUtility::writeFile($tmpFile, $this->dat['files'][$fI['ID']]['content']);
1118  }
1119  clearstatcache();
1120  if (@is_file($tmpFile)) {
1121  $this->unlinkFiles[] = $tmpFile;
1122  if (filesize($tmpFile) == $this->dat['files'][$fI['ID']]['filesize']) {
1123  $this->alternativeFileName[$tmpFile] = $fI['filename'];
1124  $this->alternativeFilePath[$tmpFile] = $this->dat['files'][$fI['ID']]['relFileRef'];
1125  return $tmpFile;
1126  }
1127  $this->error('Error: temporary file ' . $tmpFile . ' had a size (' . filesize($tmpFile) . ') different from the original (' . $this->dat['files'][$fI['ID']]['filesize'] . ')');
1128  } else {
1129  $this->error('Error: temporary file ' . $tmpFile . ' was not written as it should have been!');
1130  }
1131  } else {
1132  $this->error('Error: No file found for ID ' . $fI['ID']);
1133  }
1134  return null;
1135  }
1136 
1143  public function setFlexFormRelations()
1144  {
1145  $updateData = [];
1146  // import_newId contains a register of all records that was in the import memorys "records" key
1147  foreach ($this->import_newId as $nId => $dat) {
1148  $table = $dat['table'];
1149  $uid = $dat['uid'];
1150  // original UID - NOT the new one!
1151  // If the record has been written and received a new id, then proceed:
1152  if (!isset($this->import_mapId[$table][$uid])) {
1153  $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')');
1154  continue;
1155  }
1156 
1157  if (!is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
1158  $this->error('Error: no record was found in data array!');
1159  continue;
1160  }
1161  $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1162  // Traverse relation fields of each record
1163  foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1164  switch ((string)$config['type']) {
1165  case 'flex':
1166  // Get XML content and set as default value (string, non-processed):
1167  $updateData[$table][$thisNewUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field];
1168  // If there has been registered relations inside the flex form field, run processing on the content:
1169  if (!empty($config['flexFormRels']['db']) || !empty($config['flexFormRels']['file'])) {
1170  $origRecordRow = BackendUtility::getRecord($table, $thisNewUid, '*');
1171  // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
1172  $fieldTca = $GLOBALS['TCA'][$table]['columns'][$field];
1173  if (is_array($origRecordRow) && is_array($fieldTca['config']) && $fieldTca['config']['type'] === 'flex') {
1174  // Get current data structure and value array:
1175  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1176  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1177  $fieldTca,
1178  $table,
1179  $field,
1180  $origRecordRow
1181  );
1182  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1183  $currentValueArray = GeneralUtility::xml2array($updateData[$table][$thisNewUid][$field]);
1184  // Do recursive processing of the XML data:
1185  $iteratorObj = GeneralUtility::makeInstance(DataHandler::class);
1186  $iteratorObj->callBackObj = $this;
1187  $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData(
1188  $currentValueArray['data'],
1189  [],
1190  [],
1191  $dataStructureArray,
1192  [$table, $thisNewUid, $field, $config],
1193  'remapListedDBRecords_flexFormCallBack'
1194  );
1195  // The return value is set as an array which means it will be processed by DataHandler for file and DB references!
1196  if (is_array($currentValueArray['data'])) {
1197  $updateData[$table][$thisNewUid][$field] = $currentValueArray;
1198  }
1199  }
1200  }
1201  break;
1202  }
1203  }
1204  }
1205  if (!empty($updateData)) {
1206  $tce = $this->getNewTCE();
1207  $tce->isImporting = true;
1208  $this->callHook('before_setFlexFormRelations', [
1209  'tce' => &$tce,
1210  'data' => &$updateData
1211  ]);
1212  $tce->start($updateData, []);
1213  $tce->process_datamap();
1214  $this->callHook('after_setFlexFormRelations', [
1215  'tce' => &$tce
1216  ]);
1217  }
1218  }
1219 
1232  public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
1233  {
1234  // Extract parameters:
1235  list(, , , $config) = $pParams;
1236  // In case the $path is used as index without a trailing slash we will remove that
1237  if (!is_array($config['flexFormRels']['db'][$path]) && is_array($config['flexFormRels']['db'][rtrim($path, '/')])) {
1238  $path = rtrim($path, '/');
1239  }
1240  if (is_array($config['flexFormRels']['db'][$path])) {
1241  $valArray = $this->setRelations_db($config['flexFormRels']['db'][$path], $dsConf);
1242  $dataValue = implode(',', $valArray);
1243  }
1244  if (is_array($config['flexFormRels']['file'][$path])) {
1245  $valArr = [];
1246  foreach ($config['flexFormRels']['file'][$path] as $fI) {
1247  $valArr[] = $this->import_addFileNameToBeCopied($fI);
1248  }
1249  $dataValue = implode(',', $valArr);
1250  }
1251  return ['value' => $dataValue];
1252  }
1253 
1254  /**************************
1255  * Import / Soft References
1256  *************************/
1257 
1261  public function processSoftReferences()
1262  {
1263  // Initialize:
1264  $inData = [];
1265  // Traverse records:
1266  if (is_array($this->dat['header']['records'])) {
1267  foreach ($this->dat['header']['records'] as $table => $recs) {
1268  foreach ($recs as $uid => $thisRec) {
1269  // If there are soft references defined, traverse those:
1270  if (isset($GLOBALS['TCA'][$table]) && is_array($thisRec['softrefs'])) {
1271  // First traversal is to collect softref configuration and split them up based on fields.
1272  // This could probably also have been done with the "records" key instead of the header.
1273  $fieldsIndex = [];
1274  foreach ($thisRec['softrefs'] as $softrefDef) {
1275  // If a substitution token is set:
1276  if ($softrefDef['field'] && is_array($softrefDef['subst']) && $softrefDef['subst']['tokenID']) {
1277  $fieldsIndex[$softrefDef['field']][$softrefDef['subst']['tokenID']] = $softrefDef;
1278  }
1279  }
1280  // The new id:
1281  $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1282  // Now, if there are any fields that require substitution to be done, lets go for that:
1283  foreach ($fieldsIndex as $field => $softRefCfgs) {
1284  if (is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
1285  if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['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  $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1291  $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1292  $GLOBALS['TCA'][$table]['columns'][$field],
1293  $table,
1294  $field,
1295  $origRecordRow
1296  );
1297  $dataStructureArray = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1298  $currentValueArray = GeneralUtility::xml2array($origRecordRow[$field]);
1299  // Do recursive processing of the XML data:
1301  $iteratorObj = GeneralUtility::makeInstance(DataHandler::class);
1302  $iteratorObj->callBackObj = $this;
1303  $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData($currentValueArray['data'], [], [], $dataStructureArray, [$table, $uid, $field, $softRefCfgs], 'processSoftReferences_flexFormCallBack');
1304  // The return value is set as an array which means it will be processed by DataHandler for file and DB references!
1305  if (is_array($currentValueArray['data'])) {
1306  $inData[$table][$thisNewUid][$field] = $currentValueArray;
1307  }
1308  }
1309  } else {
1310  // Get tokenizedContent string and proceed only if that is not blank:
1311  $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'];
1312  if (strlen($tokenizedContent) && is_array($softRefCfgs)) {
1313  $inData[$table][$thisNewUid][$field] = $this->processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid);
1314  }
1315  }
1316  }
1317  }
1318  }
1319  }
1320  }
1321  }
1322  // Now write to database:
1323  $tce = $this->getNewTCE();
1324  $tce->isImporting = true;
1325  $this->callHook('before_processSoftReferences', [
1326  'tce' => $tce,
1327  'data' => &$inData
1328  ]);
1329  $tce->enableLogging = true;
1330  $tce->start($inData, []);
1331  $tce->process_datamap();
1332  $this->callHook('after_processSoftReferences', [
1333  'tce' => $tce
1334  ]);
1335  }
1336 
1349  public function processSoftReferences_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
1350  {
1351  // Extract parameters:
1352  list($table, $origUid, $field, $softRefCfgs) = $pParams;
1353  if (is_array($softRefCfgs)) {
1354  // First, find all soft reference configurations for this structure path (they are listed flat in the header):
1355  $thisSoftRefCfgList = [];
1356  foreach ($softRefCfgs as $sK => $sV) {
1357  if ($sV['structurePath'] === $path) {
1358  $thisSoftRefCfgList[$sK] = $sV;
1359  }
1360  }
1361  // If any was found, do processing:
1362  if (!empty($thisSoftRefCfgList)) {
1363  // Get tokenizedContent string and proceed only if that is not blank:
1364  $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$path]['tokenizedContent'];
1365  if (strlen($tokenizedContent)) {
1366  $dataValue = $this->processSoftReferences_substTokens($tokenizedContent, $thisSoftRefCfgList, $table, $origUid);
1367  }
1368  }
1369  }
1370  // Return
1371  return ['value' => $dataValue];
1372  }
1373 
1383  public function processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid)
1384  {
1385  // traverse each softref type for this field:
1386  foreach ($softRefCfgs as $cfg) {
1387  // Get token ID:
1388  $tokenID = $cfg['subst']['tokenID'];
1389  // Default is current token value:
1390  $insertValue = $cfg['subst']['tokenValue'];
1391  // Based on mode:
1392  switch ((string)$this->softrefCfg[$tokenID]['mode']) {
1393  case 'exclude':
1394  // Exclude is a simple passthrough of the value
1395  break;
1396  case 'editable':
1397  // Editable always picks up the value from this input array:
1398  $insertValue = $this->softrefInputValues[$tokenID];
1399  break;
1400  default:
1401  // Mapping IDs/creating files: Based on type, look up new value:
1402  switch ((string)$cfg['subst']['type']) {
1403  case 'file':
1404  // Create / Overwrite file:
1405  $insertValue = $this->processSoftReferences_saveFile($cfg['subst']['relFileName'], $cfg, $table, $uid);
1406  break;
1407  case 'db':
1408  default:
1409  // Trying to map database element if found in the mapID array:
1410  list($tempTable, $tempUid) = explode(':', $cfg['subst']['recordRef']);
1411  if (isset($this->import_mapId[$tempTable][$tempUid])) {
1412  $insertValue = BackendUtility::wsMapId($tempTable, $this->import_mapId[$tempTable][$tempUid]);
1413  // 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!
1414  if ($tempTable === 'pages' && !MathUtility::canBeInterpretedAsInteger($cfg['subst']['tokenValue'])) {
1415  $recWithUniqueValue = BackendUtility::getRecord($tempTable, $insertValue, 'alias');
1416  if ($recWithUniqueValue['alias']) {
1417  $insertValue = $recWithUniqueValue['alias'];
1418  }
1419  } elseif (strpos($cfg['subst']['tokenValue'], ':') !== false) {
1420  list($tokenKey) = explode(':', $cfg['subst']['tokenValue']);
1421  $insertValue = $tokenKey . ':' . $insertValue;
1422  }
1423  }
1424  }
1425  }
1426  // Finally, swap the soft reference token in tokenized content with the insert value:
1427  $tokenizedContent = str_replace('{softref:' . $tokenID . '}', $insertValue, $tokenizedContent);
1428  }
1429  return $tokenizedContent;
1430  }
1431 
1441  public function processSoftReferences_saveFile($relFileName, $cfg, $table, $uid)
1442  {
1443  if ($fileHeaderInfo = $this->dat['header']['files'][$cfg['file_ID']]) {
1444  // Initialize; Get directory prefix for file and find possible RTE filename
1445  $dirPrefix = PathUtility::dirname($relFileName) . '/';
1446  $rteOrigName = $this->getRTEoriginalFilename(PathUtility::basename($relFileName));
1447  // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
1448  if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/')) {
1449  // RTE:
1450  // First, find unique RTE file name:
1451  if (@is_dir((PATH_site . $dirPrefix))) {
1452  // From the "original" RTE filename, produce a new "original" destination filename which is unused.
1453  // Even if updated, the image should be unique. Currently the problem with this is that it leaves a lot of unused RTE images...
1454  $fileProcObj = $this->getFileProcObj();
1455  $origDestName = $fileProcObj->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
1456  // Create copy file name:
1457  $pI = pathinfo($relFileName);
1458  $copyDestName = PathUtility::dirname($origDestName) . '/RTEmagicC_' . substr(PathUtility::basename($origDestName), 10) . '.' . $pI['extension'];
1459  if (
1460  !@is_file($copyDestName) && !@is_file($origDestName)
1461  && $origDestName === GeneralUtility::getFileAbsFileName($origDestName)
1462  && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)
1463  ) {
1464  if ($this->dat['header']['files'][$fileHeaderInfo['RTE_ORIG_ID']]) {
1465  // Write the copy and original RTE file to the respective filenames:
1466  $this->writeFileVerify($copyDestName, $cfg['file_ID'], true);
1467  $this->writeFileVerify($origDestName, $fileHeaderInfo['RTE_ORIG_ID'], true);
1468  // Return the relative path of the copy file name:
1469  return PathUtility::stripPathSitePrefix($copyDestName);
1470  }
1471  $this->error('ERROR: Could not find original file ID');
1472  } else {
1473  $this->error('ERROR: The destination filenames "' . $copyDestName . '" and "' . $origDestName . '" either existed or have non-valid names');
1474  }
1475  } else {
1476  $this->error('ERROR: "' . PATH_site . $dirPrefix . '" was not a directory, so could not process file "' . $relFileName . '"');
1477  }
1478  } elseif (GeneralUtility::isFirstPartOfStr($dirPrefix, $this->fileadminFolderName . '/')) {
1479  // File in fileadmin/ folder:
1480  // Create file (and possible resources)
1481  $newFileName = $this->processSoftReferences_saveFile_createRelFile($dirPrefix, PathUtility::basename($relFileName), $cfg['file_ID'], $table, $uid);
1482  if (strlen($newFileName)) {
1483  $relFileName = $newFileName;
1484  } else {
1485  $this->error('ERROR: No new file created for "' . $relFileName . '"');
1486  }
1487  } else {
1488  $this->error('ERROR: Sorry, cannot operate on non-RTE files which are outside the fileadmin folder.');
1489  }
1490  } else {
1491  $this->error('ERROR: Could not find file ID in header.');
1492  }
1493  // Return (new) filename relative to PATH_site:
1494  return $relFileName;
1495  }
1496 
1507  public function processSoftReferences_saveFile_createRelFile($origDirPrefix, $fileName, $fileID, $table, $uid)
1508  {
1509  // If the fileID map contains an entry for this fileID then just return the relative filename of that entry;
1510  // we don't want to write another unique filename for this one!
1511  if (isset($this->fileIDMap[$fileID])) {
1512  return PathUtility::stripPathSitePrefix($this->fileIDMap[$fileID]);
1513  }
1514  // Verify FileMount access to dir-prefix. Returns the best alternative relative path if any
1515  $dirPrefix = $this->verifyFolderAccess($origDirPrefix);
1516  if ($dirPrefix && (!$this->update || $origDirPrefix === $dirPrefix) && $this->checkOrCreateDir($dirPrefix)) {
1517  $fileHeaderInfo = $this->dat['header']['files'][$fileID];
1518  $updMode = $this->update && $this->import_mapId[$table][$uid] === $uid && $this->import_mode[$table . ':' . $uid] !== 'as_new';
1519  // Create new name for file:
1520  // Must have same ID in map array (just for security, is not really needed) and NOT be set "as_new".
1521 
1522  // Write main file:
1523  if ($updMode) {
1524  $newName = PATH_site . $dirPrefix . $fileName;
1525  } else {
1526  // Create unique filename:
1527  $fileProcObj = $this->getFileProcObj();
1528  $newName = $fileProcObj->getUniqueName($fileName, PATH_site . $dirPrefix);
1529  }
1530  if ($this->writeFileVerify($newName, $fileID)) {
1531  // If the resource was an HTML/CSS file with resources attached, we will write those as well!
1532  if (is_array($fileHeaderInfo['EXT_RES_ID'])) {
1533  $tokenizedContent = $this->dat['files'][$fileID]['tokenizedContent'];
1534  $tokenSubstituted = false;
1535  $fileProcObj = $this->getFileProcObj();
1536  if ($updMode) {
1537  foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1538  if ($this->dat['files'][$res_fileID]['filename']) {
1539  // Resolve original filename:
1540  $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
1541  $absResourceFileName = GeneralUtility::resolveBackPath(PATH_site . $origDirPrefix . $relResourceFileName);
1542  $absResourceFileName = GeneralUtility::getFileAbsFileName($absResourceFileName);
1543  if ($absResourceFileName && GeneralUtility::isFirstPartOfStr($absResourceFileName, PATH_site . $this->fileadminFolderName . '/')) {
1544  $destDir = PathUtility::stripPathSitePrefix(PathUtility::dirname($absResourceFileName) . '/');
1545  if ($this->verifyFolderAccess($destDir, true) && $this->checkOrCreateDir($destDir)) {
1546  $this->writeFileVerify($absResourceFileName, $res_fileID);
1547  } else {
1548  $this->error('ERROR: Could not create file in directory "' . $destDir . '"');
1549  }
1550  } else {
1551  $this->error('ERROR: Could not resolve path for "' . $relResourceFileName . '"');
1552  }
1553  $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1554  $tokenSubstituted = true;
1555  }
1556  }
1557  } else {
1558  // Create the resouces directory name (filename without extension, suffixed "_FILES")
1559  $resourceDir = PathUtility::dirname($newName) . '/' . preg_replace('/\\.[^.]*$/', '', PathUtility::basename($newName)) . '_FILES';
1560  if (GeneralUtility::mkdir($resourceDir)) {
1561  foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1562  if ($this->dat['files'][$res_fileID]['filename']) {
1563  $absResourceFileName = $fileProcObj->getUniqueName($this->dat['files'][$res_fileID]['filename'], $resourceDir);
1564  $relResourceFileName = substr($absResourceFileName, strlen(PathUtility::dirname($resourceDir)) + 1);
1565  $this->writeFileVerify($absResourceFileName, $res_fileID);
1566  $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1567  $tokenSubstituted = true;
1568  }
1569  }
1570  }
1571  }
1572  // If substitutions has been made, write the content to the file again:
1573  if ($tokenSubstituted) {
1574  GeneralUtility::writeFile($newName, $tokenizedContent);
1575  }
1576  }
1577  return PathUtility::stripPathSitePrefix($newName);
1578  }
1579  }
1580  return null;
1581  }
1582 
1591  public function writeFileVerify($fileName, $fileID, $bypassMountCheck = false)
1592  {
1593  $fileProcObj = $this->getFileProcObj();
1594  if (!$fileProcObj->actionPerms['addFile']) {
1595  $this->error('ERROR: You did not have sufficient permissions to write the file "' . $fileName . '"');
1596  return false;
1597  }
1598  // Just for security, check again. Should actually not be necessary.
1599  if (!$bypassMountCheck) {
1600  try {
1601  ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier(dirname($fileName));
1603  $this->error('ERROR: Filename "' . $fileName . '" was not allowed in destination path!');
1604  return false;
1605  }
1606  }
1607  $fI = GeneralUtility::split_fileref($fileName);
1608  if (!$fileProcObj->checkIfAllowed($fI['fileext'], $fI['path'], $fI['file']) && (!$this->allowPHPScripts || !$this->getBackendUser()->isAdmin())) {
1609  $this->error('ERROR: Filename "' . $fileName . '" failed against extension check or deny-pattern!');
1610  return false;
1611  }
1612  if (!GeneralUtility::getFileAbsFileName($fileName)) {
1613  $this->error('ERROR: Filename "' . $fileName . '" was not a valid relative file path!');
1614  return false;
1615  }
1616  if (!$this->dat['files'][$fileID]) {
1617  $this->error('ERROR: File ID "' . $fileID . '" could not be found');
1618  return false;
1619  }
1620  GeneralUtility::writeFile($fileName, $this->dat['files'][$fileID]['content']);
1621  $this->fileIDMap[$fileID] = $fileName;
1622  if (md5(file_get_contents($fileName)) == $this->dat['files'][$fileID]['content_md5']) {
1623  return true;
1624  }
1625  $this->error('ERROR: File content "' . $fileName . '" was corrupted');
1626  return false;
1627  }
1628 
1635  public function checkOrCreateDir($dirPrefix)
1636  {
1637  // Split dir path and remove first directory (which should be "fileadmin")
1638  $filePathParts = explode('/', $dirPrefix);
1639  $firstDir = array_shift($filePathParts);
1640  if ($firstDir === $this->fileadminFolderName && GeneralUtility::getFileAbsFileName($dirPrefix)) {
1641  $pathAcc = '';
1642  foreach ($filePathParts as $dirname) {
1643  $pathAcc .= '/' . $dirname;
1644  if (strlen($dirname)) {
1645  if (!@is_dir((PATH_site . $this->fileadminFolderName . $pathAcc))) {
1646  if (!GeneralUtility::mkdir((PATH_site . $this->fileadminFolderName . $pathAcc))) {
1647  $this->error('ERROR: Directory could not be created....B');
1648  return false;
1649  }
1650  }
1651  } elseif ($dirPrefix === $this->fileadminFolderName . $pathAcc) {
1652  return true;
1653  } else {
1654  $this->error('ERROR: Directory could not be created....A');
1655  }
1656  }
1657  }
1658  return false;
1659  }
1660 
1661  /**************************
1662  * File Input
1663  *************************/
1664 
1672  public function loadFile($filename, $all = false)
1673  {
1674  if (!@is_file($filename)) {
1675  $this->error('Filename not found: ' . $filename);
1676  return false;
1677  }
1678  $fI = pathinfo($filename);
1679  if (@is_dir($filename . '.files')) {
1680  if (GeneralUtility::isAllowedAbsPath($filename . '.files')) {
1681  // copy the folder lowlevel to typo3temp, because the files would be deleted after import
1682  $temporaryFolderName = $this->getTemporaryFolderName();
1683  GeneralUtility::copyDirectory($filename . '.files', $temporaryFolderName);
1684  $this->filesPathForImport = $temporaryFolderName;
1685  } else {
1686  $this->error('External import files for the given import source is currently not supported.');
1687  }
1688  }
1689  if (strtolower($fI['extension']) === 'xml') {
1690  // XML:
1691  $xmlContent = file_get_contents($filename);
1692  if (strlen($xmlContent)) {
1693  $this->dat = GeneralUtility::xml2array($xmlContent, '', true);
1694  if (is_array($this->dat)) {
1695  if ($this->dat['_DOCUMENT_TAG'] === 'T3RecordDocument' && is_array($this->dat['header']) && is_array($this->dat['records'])) {
1696  $this->loadInit();
1697  return true;
1698  }
1699  $this->error('XML file did not contain proper XML for TYPO3 Import');
1700  } else {
1701  $this->error('XML could not be parsed: ' . $this->dat);
1702  }
1703  } else {
1704  $this->error('Error opening file: ' . $filename);
1705  }
1706  } else {
1707  // T3D
1708  if ($fd = fopen($filename, 'rb')) {
1709  $this->dat['header'] = $this->getNextFilePart($fd, 1, 'header');
1710  if ($all) {
1711  $this->dat['records'] = $this->getNextFilePart($fd, 1, 'records');
1712  $this->dat['files'] = $this->getNextFilePart($fd, 1, 'files');
1713  $this->dat['files_fal'] = $this->getNextFilePart($fd, 1, 'files_fal');
1714  }
1715  $this->loadInit();
1716  return true;
1717  }
1718  $this->error('Error opening file: ' . $filename);
1719 
1720  fclose($fd);
1721  }
1722  return false;
1723  }
1724 
1735  public function getNextFilePart($fd, $unserialize = false, $name = '')
1736  {
1737  $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
1738  // Getting header data
1739  $initStr = fread($fd, $initStrLen);
1740  if (empty($initStr)) {
1741  $this->error('File does not contain data for "' . $name . '"');
1742  return null;
1743  }
1744  $initStrDat = explode(':', $initStr);
1745  if (strstr($initStrDat[0], 'Warning')) {
1746  $this->error('File read error: Warning message in file. (' . $initStr . fgets($fd) . ')');
1747  return null;
1748  }
1749  if ((string)$initStrDat[3] !== '') {
1750  $this->error('File read error: InitString had a wrong length. (' . $name . ')');
1751  return null;
1752  }
1753  $datString = fread($fd, (int)$initStrDat[2]);
1754  fread($fd, 1);
1755  if (md5($datString) === $initStrDat[0]) {
1756  if ($initStrDat[1]) {
1757  if ($this->compress) {
1758  $datString = gzuncompress($datString);
1759  } else {
1760  $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.');
1761  return null;
1762  }
1763  }
1764  return $unserialize ? unserialize($datString, ['allowed_classes' => false]) : $datString;
1765  }
1766  $this->error('MD5 check failed (' . $name . ')');
1767 
1768  return null;
1769  }
1770 
1777  public function loadContent($filecontent)
1778  {
1779  $pointer = 0;
1780  $this->dat['header'] = $this->getNextContentPart($filecontent, $pointer, 1, 'header');
1781  $this->dat['records'] = $this->getNextContentPart($filecontent, $pointer, 1, 'records');
1782  $this->dat['files'] = $this->getNextContentPart($filecontent, $pointer, 1, 'files');
1783  $this->loadInit();
1784  }
1785 
1795  public function getNextContentPart($filecontent, &$pointer, $unserialize = false, $name = '')
1796  {
1797  $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
1798  // getting header data
1799  $initStr = substr($filecontent, $pointer, $initStrLen);
1800  $pointer += $initStrLen;
1801  $initStrDat = explode(':', $initStr);
1802  if ((string)$initStrDat[3] !== '') {
1803  $this->error('Content read error: InitString had a wrong length. (' . $name . ')');
1804  return null;
1805  }
1806  $datString = substr($filecontent, $pointer, (int)$initStrDat[2]);
1807  $pointer += (int)$initStrDat[2] + 1;
1808  if (md5($datString) === $initStrDat[0]) {
1809  if ($initStrDat[1]) {
1810  if ($this->compress) {
1811  $datString = gzuncompress($datString);
1812  return $unserialize ? unserialize($datString, ['allowed_classes' => false]) : $datString;
1813  }
1814  $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.');
1815  }
1816  } else {
1817  $this->error('MD5 check failed (' . $name . ')');
1818  }
1819  return null;
1820  }
1821 
1825  public function loadInit()
1826  {
1827  $this->relStaticTables = (array)$this->dat['header']['relStaticTables'];
1828  $this->excludeMap = (array)$this->dat['header']['excludeMap'];
1829  $this->softrefCfg = (array)$this->dat['header']['softrefCfg'];
1830  }
1831 }
processSoftReferences_saveFile_createRelFile($origDirPrefix, $fileName, $fileID, $table, $uid)
Definition: Import.php:1507
static unlink_tempfile($uploadedTempFileName)
processSoftReferences_saveFile($relFileName, $cfg, $table, $uid)
Definition: Import.php:1441
fixUidLocalInSysFileReferenceRecords($oldFileUid, $newFileUid)
Definition: Import.php:513
static isFirstPartOfStr($str, $partStr)
remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
Definition: Import.php:1232
writeRecords_records_order($mainPid)
Definition: Import.php:764
removeSysFileReferenceRecordsFromImportDataWithRelationToMissingFile()
Definition: Import.php:467
processSoftReferences_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
Definition: Import.php:1349
static getFileAbsFileName($filename, $_=null, $_2=null)
flatInversePageTree_pid($idH, $a=[], $pid=-1)
Definition: Import.php:683
addToMapId($substNEWwithIDs)
Definition: Import.php:927
static copyDirectory($source, $destination)
isFallbackStorage($storageId)
Definition: Import.php:493
static makeInstance($className,... $constructorArguments)
loadFile($filename, $all=false)
Definition: Import.php:1672
static split_fileref($fileNameWithPath)
processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid)
Definition: Import.php:1383
addSingle($table, $uid, $pid)
Definition: Import.php:816
static tempnam($filePrefix, $fileSuffix='')
getNextFilePart($fd, $unserialize=false, $name='')
Definition: Import.php:1735
doesRecordExist($table, $uid, $fields='')
setRelations_db($itemArray, $itemConfig)
Definition: Import.php:1060
loadContent($filecontent)
Definition: Import.php:1777
writeFileVerify($fileName, $fileID, $bypassMountCheck=false)
Definition: Import.php:1591
static xml2array($string, $NSprefix='', $reportDocTag=false)
checkOrCreateDir($dirPrefix)
Definition: Import.php:1635
getNextContentPart($filecontent, &$pointer, $unserialize=false, $name='')
Definition: Import.php:1795
import_addFileNameToBeCopied($fI)
Definition: Import.php:1104
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static writeFile($file, $content, $changePermissions=false)
verifyFolderAccess($dirPrefix, $noAlternative=false)
writeTemporaryFileFromData($fileId, $dataKey='files_fal')
Definition: Import.php:557
isEquivalentObjectStorage(ResourceStorage $storageObject, array $storageRecord)
Definition: Import.php:250