[TASK] Remove unneeded parenthesis on array-access
[Packages/TYPO3.CMS.git] / typo3 / sysext / impexp / Classes / ImportExport.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\DataHandling\DataHandler;
19 use TYPO3\CMS\Core\Exception;
20 use TYPO3\CMS\Core\Imaging\Icon;
21 use TYPO3\CMS\Core\Imaging\IconFactory;
22 use TYPO3\CMS\Core\Resource\DuplicationBehavior;
23 use TYPO3\CMS\Core\Resource\ResourceFactory;
24 use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\PathUtility;
27 use TYPO3\CMS\Core\Utility\StringUtility;
28
29 /**
30 * EXAMPLE for using the impexp-class for exporting stuff:
31 *
32 * Create and initialize:
33 * $this->export = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Impexp\ImportExport::class);
34 * $this->export->init();
35 * Set which tables relations we will allow:
36 * $this->export->relOnlyTables[]="tt_news"; // exclusively includes. See comment in the class
37 *
38 * Adding records:
39 * $this->export->export_addRecord("pages", $this->pageinfo);
40 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 38));
41 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 39));
42 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 12));
43 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 74));
44 * $this->export->export_addRecord("sys_template", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("sys_template", 20));
45 *
46 * Adding all the relations (recursively in 5 levels so relations has THEIR relations registered as well)
47 * for($a=0;$a<5;$a++) {
48 * $addR = $this->export->export_addDBRelations($a);
49 * if (empty($addR)) break;
50 * }
51 *
52 * Finally load all the files.
53 * $this->export->export_addFilesFromRelations(); // MUST be after the DBrelations are set so that file from ALL added records are included!
54 *
55 * Write export
56 * $out = $this->export->compileMemoryToFileContent();
57 */
58
59 /**
60 * T3D file Import/Export library (TYPO3 Record Document)
61 */
62 class ImportExport
63 {
64 /**
65 * If set, static relations (not exported) will be shown in overview as well
66 *
67 * @var bool
68 */
69 public $showStaticRelations = false;
70
71 /**
72 * Name of the "fileadmin" folder where files for export/import should be located
73 *
74 * @var string
75 */
76 public $fileadminFolderName = '';
77
78 /**
79 * Whether "import" or "export" mode of object. Set through init() function
80 *
81 * @var string
82 */
83 public $mode = '';
84
85 /**
86 * Updates all records that has same UID instead of creating new!
87 *
88 * @var bool
89 */
90 public $update = false;
91
92 /**
93 * Is set by importData() when an import has been done.
94 *
95 * @var bool
96 */
97 public $doesImport = false;
98
99 /**
100 * If set to a page-record, then the preview display of the content will expect this page-record to be the target
101 * for the import and accordingly display validation information. This triggers the visual view of the
102 * import/export memory to validate if import is possible
103 *
104 * @var array
105 */
106 public $display_import_pid_record = array();
107
108 /**
109 * Used to register the forged UID values for imported records that we want
110 * to create with the same UIDs as in the import file. Admin-only feature.
111 *
112 * @var array
113 */
114 public $suggestedInsertUids = array();
115
116 /**
117 * Setting import modes during update state: as_new, exclude, force_uid
118 *
119 * @var array
120 */
121 public $import_mode = array();
122
123 /**
124 * If set, PID correct is ignored globally
125 *
126 * @var bool
127 */
128 public $global_ignore_pid = false;
129
130 /**
131 * If set, all UID values are forced! (update or import)
132 *
133 * @var bool
134 */
135 public $force_all_UIDS = false;
136
137 /**
138 * If set, a diff-view column is added to the overview.
139 *
140 * @var bool
141 */
142 public $showDiff = false;
143
144 /**
145 * If set, and if the user is admin, allow the writing of PHP scripts to fileadmin/ area.
146 *
147 * @var bool
148 */
149 public $allowPHPScripts = false;
150
151 /**
152 * Disable logging when importing
153 *
154 * @var bool
155 */
156 public $enableLogging = false;
157
158 /**
159 * Array of values to substitute in editable softreferences.
160 *
161 * @var array
162 */
163 public $softrefInputValues = array();
164
165 /**
166 * Mapping between the fileID from import memory and the final filenames they are written to.
167 *
168 * @var array
169 */
170 public $fileIDMap = array();
171
172 /**
173 * 1MB max file size
174 *
175 * @var int
176 */
177 public $maxFileSize = 1000000;
178
179 /**
180 * 1MB max record size
181 *
182 * @var int
183 */
184 public $maxRecordSize = 1000000;
185
186 /**
187 * 10MB max export size
188 *
189 * @var int
190 */
191 public $maxExportSize = 10000000;
192
193 /**
194 * Add table names here which are THE ONLY ones which will be included
195 * into export if found as relations. '_ALL' will allow all tables.
196 *
197 * @var array
198 */
199 public $relOnlyTables = array();
200
201 /**
202 * Add tables names here which should not be exported with the file.
203 * (Where relations should be mapped to same UIDs in target system).
204 *
205 * @var array
206 */
207 public $relStaticTables = array();
208
209 /**
210 * Exclude map. Keys are table:uid pairs and if set, records are not added to the export.
211 *
212 * @var array
213 */
214 public $excludeMap = array();
215
216 /**
217 * Soft Reference Token ID modes.
218 *
219 * @var array
220 */
221 public $softrefCfg = array();
222
223 /**
224 * Listing extension dependencies.
225 *
226 * @var array
227 */
228 public $extensionDependencies = array();
229
230 /**
231 * Set by user: If set, compression in t3d files is disabled
232 *
233 * @var bool
234 */
235 public $dontCompress = false;
236
237 /**
238 * If set, HTML file resources are included.
239 *
240 * @var bool
241 */
242 public $includeExtFileResources = false;
243
244 /**
245 * Files with external media (HTML/css style references inside)
246 *
247 * @var string
248 */
249 public $extFileResourceExtensions = 'html,htm,css';
250
251 /**
252 * After records are written this array is filled with [table][original_uid] = [new_uid]
253 *
254 * @var array
255 */
256 public $import_mapId = array();
257
258 /**
259 * Keys are [tablename]:[new NEWxxx ids (or when updating it is uids)]
260 * while values are arrays with table/uid of the original record it is based on.
261 * With the array keys the new ids can be looked up inside tcemain
262 *
263 * @var array
264 */
265 public $import_newId = array();
266
267 /**
268 * Page id map for page tree (import)
269 *
270 * @var array
271 */
272 public $import_newId_pids = array();
273
274 /**
275 * Internal data accumulation for writing records during import
276 *
277 * @var array
278 */
279 public $import_data = array();
280
281 /**
282 * Error log.
283 *
284 * @var array
285 */
286 public $errorLog = array();
287
288 /**
289 * Cache for record paths
290 *
291 * @var array
292 */
293 public $cache_getRecordPath = array();
294
295 /**
296 * Cache of checkPID values.
297 *
298 * @var array
299 */
300 public $checkPID_cache = array();
301
302 /**
303 * Set internally if the gzcompress function exists
304 * Used by ImportExportController
305 *
306 * @var bool
307 */
308 public $compress = false;
309
310 /**
311 * Internal import/export memory
312 *
313 * @var array
314 */
315 public $dat = array();
316
317 /**
318 * File processing object
319 *
320 * @var \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility
321 */
322 protected $fileProcObj = null;
323
324 /**
325 * Keys are [recordname], values are an array of fields to be included
326 * in the export
327 *
328 * @var array
329 */
330 protected $recordTypesIncludeFields = array();
331
332 /**
333 * Default array of fields to be included in the export
334 *
335 * @var array
336 */
337 protected $defaultRecordIncludeFields = array('uid', 'pid');
338
339 /**
340 * Array of current registered storage objects
341 *
342 * @var array
343 */
344 protected $storageObjects = array();
345
346 /**
347 * Is set, if the import file has a TYPO3 version below 6.0
348 *
349 * @var bool
350 */
351 protected $legacyImport = false;
352
353 /**
354 * @var \TYPO3\CMS\Core\Resource\Folder
355 */
356 protected $legacyImportFolder = null;
357
358 /**
359 * Related to the default storage root
360 *
361 * @var string
362 */
363 protected $legacyImportTargetPath = '_imported/';
364
365 /**
366 * Table fields to migrate
367 *
368 * @var array
369 */
370 protected $legacyImportMigrationTables = array(
371 'tt_content' => array(
372 'image' => array(
373 'titleTexts' => 'titleText',
374 'description' => 'imagecaption',
375 'links' => 'image_link',
376 'alternativeTexts' => 'altText'
377 ),
378 'media' => array(
379 'description' => 'imagecaption',
380 )
381 ),
382 'pages' => array(
383 'media' => array()
384 ),
385 'pages_language_overlay' => array(
386 'media' => array()
387 )
388 );
389
390
391 /**
392 * Records to be migrated after all
393 * Multidimensional array [table][uid][field] = array([related sys_file_reference uids])
394 *
395 * @var array
396 */
397 protected $legacyImportMigrationRecords = array();
398
399 /**
400 * @var bool
401 */
402 protected $saveFilesOutsideExportFile = false;
403
404 /**
405 * @var NULL|string
406 */
407 protected $temporaryFilesPathForExport = null;
408
409 /**
410 * @var NULL|string
411 */
412 protected $filesPathForImport = null;
413
414 /**
415 * @var array
416 */
417 protected $unlinkFiles = array();
418
419 /**
420 * @var array
421 */
422 protected $alternativeFileName = array();
423
424 /**
425 * @var array
426 */
427 protected $alternativeFilePath = array();
428
429 /**
430 * @var array
431 */
432 protected $filePathMap = array();
433
434 /**
435 * @var array
436 */
437 protected $remainHeader = array();
438
439 /**
440 * @var IconFactory
441 */
442 protected $iconFactory;
443
444 /**
445 * The constructor
446 */
447 public function __construct()
448 {
449 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
450 }
451
452 /**************************
453 * Initialize
454 *************************/
455
456 /**
457 * Init the object, both import and export
458 *
459 * @param bool $dontCompress If set, compression of t3d files is disabled
460 * @param string $mode Mode of usage, either "import" or "export
461 * @return void
462 */
463 public function init($dontCompress = false, $mode = '')
464 {
465 $this->compress = function_exists('gzcompress');
466 $this->dontCompress = $dontCompress;
467 $this->mode = $mode;
468 $this->fileadminFolderName = !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']) ? rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') : 'fileadmin';
469 }
470
471 /**************************
472 * Export / Init + Meta Data
473 *************************/
474
475 /**
476 * Set header basics
477 *
478 * @return void
479 */
480 public function setHeaderBasics()
481 {
482 // Initializing:
483 if (is_array($this->softrefCfg)) {
484 foreach ($this->softrefCfg as $key => $value) {
485 if (!strlen($value['mode'])) {
486 unset($this->softrefCfg[$key]);
487 }
488 }
489 }
490 // Setting in header memory:
491 // Version of file format
492 $this->dat['header']['XMLversion'] = '1.0';
493 // Initialize meta data array (to put it in top of file)
494 $this->dat['header']['meta'] = array();
495 // Add list of tables to consider static
496 $this->dat['header']['relStaticTables'] = $this->relStaticTables;
497 // The list of excluded records
498 $this->dat['header']['excludeMap'] = $this->excludeMap;
499 // Soft Reference mode for elements
500 $this->dat['header']['softrefCfg'] = $this->softrefCfg;
501 // List of extensions the import depends on.
502 $this->dat['header']['extensionDependencies'] = $this->extensionDependencies;
503 }
504
505 /**
506 * Set charset
507 *
508 * @param string $charset Charset for the content in the export. During import the character set will be converted if the target system uses another charset.
509 * @return void
510 */
511 public function setCharset($charset)
512 {
513 $this->dat['header']['charset'] = $charset;
514 }
515
516 /**
517 * Sets meta data
518 *
519 * @param string $title Title of the export
520 * @param string $description Description of the export
521 * @param string $notes Notes about the contents
522 * @param string $packager_username Backend Username of the packager (the guy making the export)
523 * @param string $packager_name Real name of the packager
524 * @param string $packager_email Email of the packager
525 * @return void
526 */
527 public function setMetaData($title, $description, $notes, $packager_username, $packager_name, $packager_email)
528 {
529 $this->dat['header']['meta'] = array(
530 'title' => $title,
531 'description' => $description,
532 'notes' => $notes,
533 'packager_username' => $packager_username,
534 'packager_name' => $packager_name,
535 'packager_email' => $packager_email,
536 'TYPO3_version' => TYPO3_version,
537 'created' => strftime('%A %e. %B %Y', $GLOBALS['EXEC_TIME'])
538 );
539 }
540
541 /**
542 * Option to enable having the files not included in the export file.
543 * The files are saved to a temporary folder instead.
544 *
545 * @param bool $saveFilesOutsideExportFile
546 * @see getTemporaryFilesPathForExport()
547 */
548 public function setSaveFilesOutsideExportFile($saveFilesOutsideExportFile)
549 {
550 $this->saveFilesOutsideExportFile = $saveFilesOutsideExportFile;
551 }
552
553 /**
554 * Sets a thumbnail image to the exported file
555 *
556 * @param string $imgFilepath Filename reference, gif, jpg, png. Absolute path.
557 * @return void
558 */
559 public function addThumbnail($imgFilepath)
560 {
561 if (@is_file($imgFilepath)) {
562 $imgInfo = @getimagesize($imgFilepath);
563 if (is_array($imgInfo)) {
564 $fileContent = GeneralUtility::getUrl($imgFilepath);
565 $this->dat['header']['thumbnail'] = array(
566 'imgInfo' => $imgInfo,
567 'content' => $fileContent,
568 'filesize' => strlen($fileContent),
569 'filemtime' => filemtime($imgFilepath),
570 'filename' => PathUtility::basename($imgFilepath)
571 );
572 }
573 }
574 }
575
576 /**************************
577 * Export / Init Page tree
578 *************************/
579
580 /**
581 * Sets the page-tree array in the export header and returns the array in a flattened version
582 *
583 * @param array $idH Hierarchy of ids, the page tree: array([uid] => array("uid" => [uid], "subrow" => array(.....)), [uid] => ....)
584 * @return array The hierarchical page tree converted to a one-dimensional list of pages
585 */
586 public function setPageTree($idH)
587 {
588 $this->dat['header']['pagetree'] = $this->unsetExcludedSections($idH);
589 return $this->flatInversePageTree($this->dat['header']['pagetree']);
590 }
591
592 /**
593 * Removes entries in the page tree which are found in ->excludeMap[]
594 *
595 * @param array $idH Page uid hierarchy
596 * @return array Modified input array
597 * @access private
598 * @see setPageTree()
599 */
600 public function unsetExcludedSections($idH)
601 {
602 if (is_array($idH)) {
603 foreach ($idH as $k => $v) {
604 if ($this->excludeMap['pages:' . $idH[$k]['uid']]) {
605 unset($idH[$k]);
606 } elseif (is_array($idH[$k]['subrow'])) {
607 $idH[$k]['subrow'] = $this->unsetExcludedSections($idH[$k]['subrow']);
608 }
609 }
610 }
611 return $idH;
612 }
613
614 /**
615 * Recursively flattening the idH array (for setPageTree() function)
616 *
617 * @param array $idH Page uid hierarchy
618 * @param array $a Accumulation array of pages (internal, don't set from outside)
619 * @return array Array with uid-uid pairs for all pages in the page tree.
620 * @see flatInversePageTree_pid()
621 */
622 public function flatInversePageTree($idH, $a = array())
623 {
624 if (is_array($idH)) {
625 $idH = array_reverse($idH);
626 foreach ($idH as $k => $v) {
627 $a[$v['uid']] = $v['uid'];
628 if (is_array($v['subrow'])) {
629 $a = $this->flatInversePageTree($v['subrow'], $a);
630 }
631 }
632 }
633 return $a;
634 }
635
636 /**
637 * Recursively flattening the idH array (for setPageTree() function), setting PIDs as values
638 *
639 * @param array $idH Page uid hierarchy
640 * @param array $a Accumulation array of pages (internal, don't set from outside)
641 * @param int $pid PID value (internal)
642 * @return array Array with uid-pid pairs for all pages in the page tree.
643 * @see flatInversePageTree()
644 */
645 public function flatInversePageTree_pid($idH, $a = array(), $pid = -1)
646 {
647 if (is_array($idH)) {
648 $idH = array_reverse($idH);
649 foreach ($idH as $v) {
650 $a[$v['uid']] = $pid;
651 if (is_array($v['subrow'])) {
652 $a = $this->flatInversePageTree_pid($v['subrow'], $a, $v['uid']);
653 }
654 }
655 }
656 return $a;
657 }
658
659 /**************************
660 * Export
661 *************************/
662
663 /**
664 * Sets the fields of record types to be included in the export
665 *
666 * @param array $recordTypesIncludeFields Keys are [recordname], values are an array of fields to be included in the export
667 * @throws \TYPO3\CMS\Core\Exception if an array value is not type of array
668 * @return void
669 */
670 public function setRecordTypesIncludeFields(array $recordTypesIncludeFields)
671 {
672 foreach ($recordTypesIncludeFields as $table => $fields) {
673 if (!is_array($fields)) {
674 throw new \TYPO3\CMS\Core\Exception('The include fields for record type ' . htmlspecialchars($table) . ' are not defined by an array.', 1391440658);
675 }
676 $this->setRecordTypeIncludeFields($table, $fields);
677 }
678 }
679
680 /**
681 * Sets the fields of a record type to be included in the export
682 *
683 * @param string $table The record type
684 * @param array $fields The fields to be included
685 * @return void
686 */
687 public function setRecordTypeIncludeFields($table, array $fields)
688 {
689 $this->recordTypesIncludeFields[$table] = $fields;
690 }
691
692 /**
693 * Adds the record $row from $table.
694 * No checking for relations done here. Pure data.
695 *
696 * @param string $table Table name
697 * @param array $row Record row.
698 * @param int $relationLevel (Internal) if the record is added as a relation, this is set to the "level" it was on.
699 * @return void
700 */
701 public function export_addRecord($table, $row, $relationLevel = 0)
702 {
703 BackendUtility::workspaceOL($table, $row);
704 if ((string)$table !== '' && is_array($row) && $row['uid'] > 0 && !$this->excludeMap[$table . ':' . $row['uid']]) {
705 if ($this->checkPID($table === 'pages' ? $row['uid'] : $row['pid'])) {
706 if (!isset($this->dat['records'][$table . ':' . $row['uid']])) {
707 // Prepare header info:
708 $row = $this->filterRecordFields($table, $row);
709 $headerInfo = array();
710 $headerInfo['uid'] = $row['uid'];
711 $headerInfo['pid'] = $row['pid'];
712 $headerInfo['title'] = GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $row), 40);
713 $headerInfo['size'] = strlen(serialize($row));
714 if ($relationLevel) {
715 $headerInfo['relationLevel'] = $relationLevel;
716 }
717 // If record content is not too large in size, set the header content and add the rest:
718 if ($headerInfo['size'] < $this->maxRecordSize) {
719 // Set the header summary:
720 $this->dat['header']['records'][$table][$row['uid']] = $headerInfo;
721 // Create entry in the PID lookup:
722 $this->dat['header']['pid_lookup'][$row['pid']][$table][$row['uid']] = 1;
723 // Initialize reference index object:
724 $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
725 // Yes to workspace overlays for exporting....
726 $refIndexObj->WSOL = true;
727 $relations = $refIndexObj->getRelations($table, $row);
728 $relations = $this->fixFileIDsInRelations($relations);
729 $relations = $this->removeSoftrefsHavingTheSameDatabaseRelation($relations);
730 // Data:
731 $this->dat['records'][$table . ':' . $row['uid']] = array();
732 $this->dat['records'][$table . ':' . $row['uid']]['data'] = $row;
733 $this->dat['records'][$table . ':' . $row['uid']]['rels'] = $relations;
734 $this->errorLog = array_merge($this->errorLog, $refIndexObj->errorLog);
735 // Merge error logs.
736 // Add information about the relations in the record in the header:
737 $this->dat['header']['records'][$table][$row['uid']]['rels'] = $this->flatDBrels($this->dat['records'][$table . ':' . $row['uid']]['rels']);
738 // Add information about the softrefs to header:
739 $this->dat['header']['records'][$table][$row['uid']]['softrefs'] = $this->flatSoftRefs($this->dat['records'][$table . ':' . $row['uid']]['rels']);
740 } else {
741 $this->error('Record ' . $table . ':' . $row['uid'] . ' was larger than maxRecordSize (' . GeneralUtility::formatSize($this->maxRecordSize) . ')');
742 }
743 } else {
744 $this->error('Record ' . $table . ':' . $row['uid'] . ' already added.');
745 }
746 } else {
747 $this->error('Record ' . $table . ':' . $row['uid'] . ' was outside your DB mounts!');
748 }
749 }
750 }
751
752 /**
753 * This changes the file reference ID from a hash based on the absolute file path
754 * (coming from ReferenceIndex) to a hash based on the relative file path.
755 *
756 * @param array $relations
757 * @return array
758 */
759 protected function fixFileIDsInRelations(array $relations)
760 {
761 foreach ($relations as $field => $relation) {
762 if (isset($relation['type']) && $relation['type'] === 'file') {
763 foreach ($relation['newValueFiles'] as $key => $fileRelationData) {
764 $absoluteFilePath = $fileRelationData['ID_absFile'];
765 if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, PATH_site)) {
766 $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
767 $relations[$field]['newValueFiles'][$key]['ID'] = md5($relatedFilePath);
768 }
769 }
770 }
771 if ($relation['type'] === 'flex') {
772 if (is_array($relation['flexFormRels']['file'])) {
773 foreach ($relation['flexFormRels']['file'] as $key => $subList) {
774 foreach ($subList as $subKey => $fileRelationData) {
775 $absoluteFilePath = $fileRelationData['ID_absFile'];
776 if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, PATH_site)) {
777 $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
778 $relations[$field]['flexFormRels']['file'][$key][$subKey]['ID'] = md5($relatedFilePath);
779 }
780 }
781 }
782 }
783 }
784 }
785 return $relations;
786 }
787
788 /**
789 * Relations could contain db relations to sys_file records. Some configuration combinations of TCA and
790 * SoftReferenceIndex create also softref relation entries for the identical file. This results
791 * in double included files, one in array "files" and one in array "file_fal".
792 * This function checks the relations for this double inclusions and removes the redundant softref relation.
793 *
794 * @param array $relations
795 * @return array
796 */
797 protected function removeSoftrefsHavingTheSameDatabaseRelation($relations)
798 {
799 $fixedRelations = array();
800 foreach ($relations as $field => $relation) {
801 $newRelation = $relation;
802 if (isset($newRelation['type']) && $newRelation['type'] === 'db') {
803 foreach ($newRelation['itemArray'] as $key => $dbRelationData) {
804 if ($dbRelationData['table'] === 'sys_file') {
805 if (isset($newRelation['softrefs']['keys']['typolink'])) {
806 foreach ($newRelation['softrefs']['keys']['typolink'] as $softrefKey => $softRefData) {
807 if ($softRefData['subst']['type'] === 'file') {
808 $file = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->retrieveFileOrFolderObject($softRefData['subst']['relFileName']);
809 if ($file instanceof \TYPO3\CMS\Core\Resource\File) {
810 if ($file->getUid() == $dbRelationData['id']) {
811 unset($newRelation['softrefs']['keys']['typolink'][$softrefKey]);
812 }
813 }
814 }
815 }
816 if (empty($newRelation['softrefs']['keys']['typolink'])) {
817 unset($newRelation['softrefs']);
818 }
819 }
820 }
821 }
822 }
823 $fixedRelations[$field] = $newRelation;
824 }
825 return $fixedRelations;
826 }
827
828
829 /**
830 * This analyses the existing added records, finds all database relations to records and adds these records to the export file.
831 * This function can be called repeatedly until it returns an empty array.
832 * In principle it should not allow to infinite recursivity, but you better set a limit...
833 * Call this BEFORE the ext_addFilesFromRelations (so files from added relations are also included of course)
834 *
835 * @param int $relationLevel Recursion level
836 * @return array overview of relations found and added: Keys [table]:[uid], values array with table and id
837 * @see export_addFilesFromRelations()
838 */
839 public function export_addDBRelations($relationLevel = 0)
840 {
841 // Traverse all "rels" registered for "records"
842 if (!is_array($this->dat['records'])) {
843 $this->error('There were no records available.');
844 return array();
845 }
846 $addR = array();
847 foreach ($this->dat['records'] as $k => $value) {
848 if (!is_array($this->dat['records'][$k])) {
849 continue;
850 }
851 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
852 // For all DB types of relations:
853 if ($vR['type'] == 'db') {
854 foreach ($vR['itemArray'] as $fI) {
855 $this->export_addDBRelations_registerRelation($fI, $addR);
856 }
857 }
858 // For all flex/db types of relations:
859 if ($vR['type'] == 'flex') {
860 // DB relations in flex form fields:
861 if (is_array($vR['flexFormRels']['db'])) {
862 foreach ($vR['flexFormRels']['db'] as $subList) {
863 foreach ($subList as $fI) {
864 $this->export_addDBRelations_registerRelation($fI, $addR);
865 }
866 }
867 }
868 // DB oriented soft references in flex form fields:
869 if (is_array($vR['flexFormRels']['softrefs'])) {
870 foreach ($vR['flexFormRels']['softrefs'] as $subList) {
871 foreach ($subList['keys'] as $spKey => $elements) {
872 foreach ($elements as $el) {
873 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
874 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
875 $fI = array(
876 'table' => $tempTable,
877 'id' => $tempUid
878 );
879 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
880 }
881 }
882 }
883 }
884 }
885 }
886 // In any case, if there are soft refs:
887 if (is_array($vR['softrefs']['keys'])) {
888 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
889 foreach ($elements as $el) {
890 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
891 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
892 $fI = array(
893 'table' => $tempTable,
894 'id' => $tempUid
895 );
896 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
897 }
898 }
899 }
900 }
901 }
902 }
903
904 // Now, if there were new records to add, do so:
905 if (!empty($addR)) {
906 foreach ($addR as $fI) {
907 // Get and set record:
908 $row = BackendUtility::getRecord($fI['table'], $fI['id']);
909 if (is_array($row)) {
910 $this->export_addRecord($fI['table'], $row, $relationLevel + 1);
911 }
912 // Set status message
913 // Relation pointers always larger than zero except certain "select" types with
914 // negative values pointing to uids - but that is not supported here.
915 if ($fI['id'] > 0) {
916 $rId = $fI['table'] . ':' . $fI['id'];
917 if (!isset($this->dat['records'][$rId])) {
918 $this->dat['records'][$rId] = 'NOT_FOUND';
919 $this->error('Relation record ' . $rId . ' was not found!');
920 }
921 }
922 }
923 }
924 // Return overview of relations found and added
925 return $addR;
926 }
927
928 /**
929 * Helper function for export_addDBRelations()
930 *
931 * @param array $fI Array with table/id keys to add
932 * @param array $addR Add array, passed by reference to be modified
933 * @param string $tokenID Softref Token ID, if applicable.
934 * @return void
935 * @see export_addDBRelations()
936 */
937 public function export_addDBRelations_registerRelation($fI, &$addR, $tokenID = '')
938 {
939 $rId = $fI['table'] . ':' . $fI['id'];
940 if (
941 isset($GLOBALS['TCA'][$fI['table']]) && !$this->isTableStatic($fI['table']) && !$this->isExcluded($fI['table'], $fI['id'])
942 && (!$tokenID || $this->includeSoftref($tokenID)) && $this->inclRelation($fI['table'])
943 ) {
944 if (!isset($this->dat['records'][$rId])) {
945 // Set this record to be included since it is not already.
946 $addR[$rId] = $fI;
947 }
948 }
949 }
950
951 /**
952 * This adds all files in relations.
953 * Call this method AFTER adding all records including relations.
954 *
955 * @return void
956 * @see export_addDBRelations()
957 */
958 public function export_addFilesFromRelations()
959 {
960 // Traverse all "rels" registered for "records"
961 if (!is_array($this->dat['records'])) {
962 $this->error('There were no records available.');
963 return;
964 }
965 foreach ($this->dat['records'] as $k => $value) {
966 if (!isset($this->dat['records'][$k]['rels']) || !is_array($this->dat['records'][$k]['rels'])) {
967 continue;
968 }
969 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
970 // For all file type relations:
971 if ($vR['type'] == 'file') {
972 foreach ($vR['newValueFiles'] as $key => $fI) {
973 $this->export_addFile($fI, $k, $fieldname);
974 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
975 unset($this->dat['records'][$k]['rels'][$fieldname]['newValueFiles'][$key]['ID_absFile']);
976 }
977 }
978 // For all flex type relations:
979 if ($vR['type'] == 'flex') {
980 if (is_array($vR['flexFormRels']['file'])) {
981 foreach ($vR['flexFormRels']['file'] as $key => $subList) {
982 foreach ($subList as $subKey => $fI) {
983 $this->export_addFile($fI, $k, $fieldname);
984 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
985 unset($this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['file'][$key][$subKey]['ID_absFile']);
986 }
987 }
988 }
989 // DB oriented soft references in flex form fields:
990 if (is_array($vR['flexFormRels']['softrefs'])) {
991 foreach ($vR['flexFormRels']['softrefs'] as $key => $subList) {
992 foreach ($subList['keys'] as $spKey => $elements) {
993 foreach ($elements as $subKey => $el) {
994 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
995 // Create abs path and ID for file:
996 $ID_absFile = GeneralUtility::getFileAbsFileName(PATH_site . $el['subst']['relFileName']);
997 $ID = md5($el['subst']['relFileName']);
998 if ($ID_absFile) {
999 if (!$this->dat['files'][$ID]) {
1000 $fI = array(
1001 'filename' => PathUtility::basename($ID_absFile),
1002 'ID_absFile' => $ID_absFile,
1003 'ID' => $ID,
1004 'relFileName' => $el['subst']['relFileName']
1005 );
1006 $this->export_addFile($fI, '_SOFTREF_');
1007 }
1008 $this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['softrefs'][$key]['keys'][$spKey][$subKey]['file_ID'] = $ID;
1009 }
1010 }
1011 }
1012 }
1013 }
1014 }
1015 }
1016 // In any case, if there are soft refs:
1017 if (is_array($vR['softrefs']['keys'])) {
1018 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
1019 foreach ($elements as $subKey => $el) {
1020 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
1021 // Create abs path and ID for file:
1022 $ID_absFile = GeneralUtility::getFileAbsFileName(PATH_site . $el['subst']['relFileName']);
1023 $ID = md5($el['subst']['relFileName']);
1024 if ($ID_absFile) {
1025 if (!$this->dat['files'][$ID]) {
1026 $fI = array(
1027 'filename' => PathUtility::basename($ID_absFile),
1028 'ID_absFile' => $ID_absFile,
1029 'ID' => $ID,
1030 'relFileName' => $el['subst']['relFileName']
1031 );
1032 $this->export_addFile($fI, '_SOFTREF_');
1033 }
1034 $this->dat['records'][$k]['rels'][$fieldname]['softrefs']['keys'][$spKey][$subKey]['file_ID'] = $ID;
1035 }
1036 }
1037 }
1038 }
1039 }
1040 }
1041 }
1042 }
1043
1044 /**
1045 * This adds all files from sys_file records
1046 *
1047 * @return void
1048 */
1049 public function export_addFilesFromSysFilesRecords()
1050 {
1051 if (!isset($this->dat['header']['records']['sys_file']) || !is_array($this->dat['header']['records']['sys_file'])) {
1052 return;
1053 }
1054 foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) {
1055 $recordData = $this->dat['records']['sys_file:' . $sysFileUid]['data'];
1056 $file = ResourceFactory::getInstance()->createFileObject($recordData);
1057 $this->export_addSysFile($file);
1058 }
1059 }
1060
1061 /**
1062 * Adds a files content from a sys file record to the export memory
1063 *
1064 * @param \TYPO3\CMS\Core\Resource\File $file
1065 * @return void
1066 */
1067 public function export_addSysFile(\TYPO3\CMS\Core\Resource\File $file)
1068 {
1069 if ($file->getProperty('size') >= $this->maxFileSize) {
1070 $this->error('File ' . $file->getPublicUrl() . ' was larger (' . GeneralUtility::formatSize($file->getProperty('size')) . ') than the maxFileSize (' . GeneralUtility::formatSize($this->maxFileSize) . ')! Skipping.');
1071 return;
1072 }
1073 $fileContent = '';
1074 try {
1075 if (!$this->saveFilesOutsideExportFile) {
1076 $fileContent = $file->getContents();
1077 } else {
1078 $file->checkActionPermission('read');
1079 }
1080 } catch (\Exception $e) {
1081 $this->error('Error when trying to add file ' . $file->getCombinedIdentifier() . ': ' . $e->getMessage());
1082 return;
1083 }
1084 $fileUid = $file->getUid();
1085 $fileInfo = $file->getStorage()->getFileInfo($file);
1086 // we sadly have to cast it to string here, because the size property is also returning a string
1087 $fileSize = (string)$fileInfo['size'];
1088 if ($fileSize !== $file->getProperty('size')) {
1089 $this->error('File size of ' . $file->getCombinedIdentifier() . ' is not up-to-date in index! File added with current size.');
1090 $this->dat['records']['sys_file:' . $fileUid]['data']['size'] = $fileSize;
1091 }
1092 $fileSha1 = $file->getStorage()->hashFile($file, 'sha1');
1093 if ($fileSha1 !== $file->getProperty('sha1')) {
1094 $this->error('File sha1 hash of ' . $file->getCombinedIdentifier() . ' is not up-to-date in index! File added on current sha1.');
1095 $this->dat['records']['sys_file:' . $fileUid]['data']['sha1'] = $fileSha1;
1096 }
1097
1098 $fileRec = array();
1099 $fileRec['filesize'] = $fileSize;
1100 $fileRec['filename'] = $file->getProperty('name');
1101 $fileRec['filemtime'] = $file->getProperty('modification_date');
1102
1103 // build unique id based on the storage and the file identifier
1104 $fileId = md5($file->getStorage()->getUid() . ':' . $file->getProperty('identifier_hash'));
1105
1106 // Setting this data in the header
1107 $this->dat['header']['files_fal'][$fileId] = $fileRec;
1108
1109 if (!$this->saveFilesOutsideExportFile) {
1110 // ... and finally add the heavy stuff:
1111 $fileRec['content'] = $fileContent;
1112 } else {
1113 GeneralUtility::upload_copy_move($file->getForLocalProcessing(false), $this->getTemporaryFilesPathForExport() . $file->getProperty('sha1'));
1114 }
1115 $fileRec['content_sha1'] = $fileSha1;
1116
1117 $this->dat['files_fal'][$fileId] = $fileRec;
1118 }
1119
1120
1121 /**
1122 * Adds a files content to the export memory
1123 *
1124 * @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". "relFileName" is optional for files attached to records, but mandatory for soft referenced files (since the relFileName determines where such a file should be stored!)
1125 * @param string $recordRef If the file is related to a record, this is the id on the form [table]:[id]. Information purposes only.
1126 * @param string $fieldname If the file is related to a record, this is the field name it was related to. Information purposes only.
1127 * @return void
1128 */
1129 public function export_addFile($fI, $recordRef = '', $fieldname = '')
1130 {
1131 if (!@is_file($fI['ID_absFile'])) {
1132 $this->error($fI['ID_absFile'] . ' was not a file! Skipping.');
1133 return;
1134 }
1135 if (filesize($fI['ID_absFile']) >= $this->maxFileSize) {
1136 $this->error($fI['ID_absFile'] . ' was larger (' . GeneralUtility::formatSize(filesize($fI['ID_absFile'])) . ') than the maxFileSize (' . GeneralUtility::formatSize($this->maxFileSize) . ')! Skipping.');
1137 return;
1138 }
1139 $fileInfo = stat($fI['ID_absFile']);
1140 $fileRec = array();
1141 $fileRec['filesize'] = $fileInfo['size'];
1142 $fileRec['filename'] = PathUtility::basename($fI['ID_absFile']);
1143 $fileRec['filemtime'] = $fileInfo['mtime'];
1144 //for internal type file_reference
1145 $fileRec['relFileRef'] = PathUtility::stripPathSitePrefix($fI['ID_absFile']);
1146 if ($recordRef) {
1147 $fileRec['record_ref'] = $recordRef . '/' . $fieldname;
1148 }
1149 if ($fI['relFileName']) {
1150 $fileRec['relFileName'] = $fI['relFileName'];
1151 }
1152 // Setting this data in the header
1153 $this->dat['header']['files'][$fI['ID']] = $fileRec;
1154 // ... and for the recordlisting, why not let us know WHICH relations there was...
1155 if ($recordRef && $recordRef !== '_SOFTREF_') {
1156 $refParts = explode(':', $recordRef, 2);
1157 if (!is_array($this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'])) {
1158 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'] = array();
1159 }
1160 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'][] = $fI['ID'];
1161 }
1162 $fileMd5 = md5_file($fI['ID_absFile']);
1163 if (!$this->saveFilesOutsideExportFile) {
1164 // ... and finally add the heavy stuff:
1165 $fileRec['content'] = GeneralUtility::getUrl($fI['ID_absFile']);
1166 } else {
1167 GeneralUtility::upload_copy_move($fI['ID_absFile'], $this->getTemporaryFilesPathForExport() . $fileMd5);
1168 }
1169 $fileRec['content_md5'] = $fileMd5;
1170 $this->dat['files'][$fI['ID']] = $fileRec;
1171 // For soft references, do further processing:
1172 if ($recordRef === '_SOFTREF_') {
1173 // RTE files?
1174 if ($RTEoriginal = $this->getRTEoriginalFilename(PathUtility::basename($fI['ID_absFile']))) {
1175 $RTEoriginal_absPath = PathUtility::dirname($fI['ID_absFile']) . '/' . $RTEoriginal;
1176 if (@is_file($RTEoriginal_absPath)) {
1177 $RTEoriginal_ID = md5($RTEoriginal_absPath);
1178 $fileInfo = stat($RTEoriginal_absPath);
1179 $fileRec = array();
1180 $fileRec['filesize'] = $fileInfo['size'];
1181 $fileRec['filename'] = PathUtility::basename($RTEoriginal_absPath);
1182 $fileRec['filemtime'] = $fileInfo['mtime'];
1183 $fileRec['record_ref'] = '_RTE_COPY_ID:' . $fI['ID'];
1184 $this->dat['header']['files'][$fI['ID']]['RTE_ORIG_ID'] = $RTEoriginal_ID;
1185 // Setting this data in the header
1186 $this->dat['header']['files'][$RTEoriginal_ID] = $fileRec;
1187 $fileMd5 = md5_file($RTEoriginal_absPath);
1188 if (!$this->saveFilesOutsideExportFile) {
1189 // ... and finally add the heavy stuff:
1190 $fileRec['content'] = GeneralUtility::getUrl($RTEoriginal_absPath);
1191 } else {
1192 GeneralUtility::upload_copy_move($RTEoriginal_absPath, $this->getTemporaryFilesPathForExport() . $fileMd5);
1193 }
1194 $fileRec['content_md5'] = $fileMd5;
1195 $this->dat['files'][$RTEoriginal_ID] = $fileRec;
1196 } else {
1197 $this->error('RTE original file "' . PathUtility::stripPathSitePrefix($RTEoriginal_absPath) . '" was not found!');
1198 }
1199 }
1200 // Files with external media?
1201 // This is only done with files grabbed by a softreference parser since it is deemed improbable that hard-referenced files should undergo this treatment.
1202 $html_fI = pathinfo(PathUtility::basename($fI['ID_absFile']));
1203 if ($this->includeExtFileResources && GeneralUtility::inList($this->extFileResourceExtensions, strtolower($html_fI['extension']))) {
1204 $uniquePrefix = '###' . md5($GLOBALS['EXEC_TIME']) . '###';
1205 if (strtolower($html_fI['extension']) === 'css') {
1206 $prefixedMedias = explode($uniquePrefix, preg_replace('/(url[[:space:]]*\\([[:space:]]*["\']?)([^"\')]*)(["\']?[[:space:]]*\\))/i', '\\1' . $uniquePrefix . '\\2' . $uniquePrefix . '\\3', $fileRec['content']));
1207 } else {
1208 // html, htm:
1209 $htmlParser = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Html\HtmlParser::class);
1210 $prefixedMedias = explode($uniquePrefix, $htmlParser->prefixResourcePath($uniquePrefix, $fileRec['content'], array(), $uniquePrefix));
1211 }
1212 $htmlResourceCaptured = false;
1213 foreach ($prefixedMedias as $k => $v) {
1214 if ($k % 2) {
1215 $EXTres_absPath = GeneralUtility::resolveBackPath(PathUtility::dirname($fI['ID_absFile']) . '/' . $v);
1216 $EXTres_absPath = GeneralUtility::getFileAbsFileName($EXTres_absPath);
1217 if ($EXTres_absPath && GeneralUtility::isFirstPartOfStr($EXTres_absPath, PATH_site . $this->fileadminFolderName . '/') && @is_file($EXTres_absPath)) {
1218 $htmlResourceCaptured = true;
1219 $EXTres_ID = md5($EXTres_absPath);
1220 $this->dat['header']['files'][$fI['ID']]['EXT_RES_ID'][] = $EXTres_ID;
1221 $prefixedMedias[$k] = '{EXT_RES_ID:' . $EXTres_ID . '}';
1222 // Add file to memory if it is not set already:
1223 if (!isset($this->dat['header']['files'][$EXTres_ID])) {
1224 $fileInfo = stat($EXTres_absPath);
1225 $fileRec = array();
1226 $fileRec['filesize'] = $fileInfo['size'];
1227 $fileRec['filename'] = PathUtility::basename($EXTres_absPath);
1228 $fileRec['filemtime'] = $fileInfo['mtime'];
1229 $fileRec['record_ref'] = '_EXT_PARENT_:' . $fI['ID'];
1230 // Media relative to the HTML file.
1231 $fileRec['parentRelFileName'] = $v;
1232 // Setting this data in the header
1233 $this->dat['header']['files'][$EXTres_ID] = $fileRec;
1234 // ... and finally add the heavy stuff:
1235 $fileRec['content'] = GeneralUtility::getUrl($EXTres_absPath);
1236 $fileRec['content_md5'] = md5($fileRec['content']);
1237 $this->dat['files'][$EXTres_ID] = $fileRec;
1238 }
1239 }
1240 }
1241 }
1242 if ($htmlResourceCaptured) {
1243 $this->dat['files'][$fI['ID']]['tokenizedContent'] = implode('', $prefixedMedias);
1244 }
1245 }
1246 }
1247 }
1248
1249 /**
1250 * If saveFilesOutsideExportFile is enabled, this function returns the path
1251 * where the files referenced in the export are copied to.
1252 *
1253 * @return string
1254 * @throws \RuntimeException
1255 * @see setSaveFilesOutsideExportFile()
1256 */
1257 public function getTemporaryFilesPathForExport()
1258 {
1259 if (!$this->saveFilesOutsideExportFile) {
1260 throw new \RuntimeException('You need to set saveFilesOutsideExportFile to TRUE before you want to get the temporary files path for export.', 1401205213);
1261 }
1262 if ($this->temporaryFilesPathForExport === null) {
1263 $temporaryFolderName = $this->getTemporaryFolderName();
1264 $this->temporaryFilesPathForExport = $temporaryFolderName . '/';
1265 }
1266 return $this->temporaryFilesPathForExport;
1267 }
1268
1269 /**
1270 *
1271 * @return string
1272 */
1273 protected function getTemporaryFolderName()
1274 {
1275 $temporaryPath = PATH_site . 'typo3temp/';
1276 do {
1277 $temporaryFolderName = $temporaryPath . 'export_temp_files_' . mt_rand(1, PHP_INT_MAX);
1278 } while (is_dir($temporaryFolderName));
1279 GeneralUtility::mkdir($temporaryFolderName);
1280 return $temporaryFolderName;
1281 }
1282
1283 /**
1284 * DB relations flattend to 1-dim array.
1285 * The list will be unique, no table/uid combination will appear twice.
1286 *
1287 * @param array $dbrels 2-dim Array of database relations organized by table key
1288 * @return array 1-dim array where entries are table:uid and keys are array with table/id
1289 */
1290 public function flatDBrels($dbrels)
1291 {
1292 $list = array();
1293 foreach ($dbrels as $dat) {
1294 if ($dat['type'] == 'db') {
1295 foreach ($dat['itemArray'] as $i) {
1296 $list[$i['table'] . ':' . $i['id']] = $i;
1297 }
1298 }
1299 if ($dat['type'] == 'flex' && is_array($dat['flexFormRels']['db'])) {
1300 foreach ($dat['flexFormRels']['db'] as $subList) {
1301 foreach ($subList as $i) {
1302 $list[$i['table'] . ':' . $i['id']] = $i;
1303 }
1304 }
1305 }
1306 }
1307 return $list;
1308 }
1309
1310 /**
1311 * Soft References flattend to 1-dim array.
1312 *
1313 * @param array $dbrels 2-dim Array of database relations organized by table key
1314 * @return array 1-dim array where entries are arrays with properties of the soft link found and keys are a unique combination of field, spKey, structure path if applicable and token ID
1315 */
1316 public function flatSoftRefs($dbrels)
1317 {
1318 $list = array();
1319 foreach ($dbrels as $field => $dat) {
1320 if (is_array($dat['softrefs']['keys'])) {
1321 foreach ($dat['softrefs']['keys'] as $spKey => $elements) {
1322 if (is_array($elements)) {
1323 foreach ($elements as $subKey => $el) {
1324 $lKey = $field . ':' . $spKey . ':' . $subKey;
1325 $list[$lKey] = array_merge(array('field' => $field, 'spKey' => $spKey), $el);
1326 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation
1327 // changes for the same value in $this->records[...] this will not work anymore!
1328 if ($el['subst'] && $el['subst']['relFileName']) {
1329 $list[$lKey]['file_ID'] = md5(PATH_site . $el['subst']['relFileName']);
1330 }
1331 }
1332 }
1333 }
1334 }
1335 if ($dat['type'] == 'flex' && is_array($dat['flexFormRels']['softrefs'])) {
1336 foreach ($dat['flexFormRels']['softrefs'] as $structurePath => $subSoftrefs) {
1337 if (is_array($subSoftrefs['keys'])) {
1338 foreach ($subSoftrefs['keys'] as $spKey => $elements) {
1339 foreach ($elements as $subKey => $el) {
1340 $lKey = $field . ':' . $structurePath . ':' . $spKey . ':' . $subKey;
1341 $list[$lKey] = array_merge(array('field' => $field, 'spKey' => $spKey, 'structurePath' => $structurePath), $el);
1342 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation
1343 // changes for the same value in $this->records[...] this will not work anymore!
1344 if ($el['subst'] && $el['subst']['relFileName']) {
1345 $list[$lKey]['file_ID'] = md5(PATH_site . $el['subst']['relFileName']);
1346 }
1347 }
1348 }
1349 }
1350 }
1351 }
1352 }
1353 return $list;
1354 }
1355
1356 /**
1357 * If include fields for a specific record type are set, the data
1358 * are filtered out with fields are not included in the fields.
1359 *
1360 * @param string $table The record type to be filtered
1361 * @param array $row The data to be filtered
1362 * @return array The filtered record row
1363 */
1364 protected function filterRecordFields($table, array $row)
1365 {
1366 if (isset($this->recordTypesIncludeFields[$table])) {
1367 $includeFields = array_unique(array_merge(
1368 $this->recordTypesIncludeFields[$table],
1369 $this->defaultRecordIncludeFields
1370 ));
1371 $newRow = array();
1372 foreach ($row as $key => $value) {
1373 if (in_array($key, $includeFields)) {
1374 $newRow[$key] = $value;
1375 }
1376 }
1377 } else {
1378 $newRow = $row;
1379 }
1380 return $newRow;
1381 }
1382
1383
1384 /**************************
1385 * File Output
1386 *************************/
1387
1388 /**
1389 * This compiles and returns the data content for an exported file
1390 *
1391 * @param string $type Type of output; "xml" gives xml, otherwise serialized array, possibly compressed.
1392 * @return string The output file stream
1393 */
1394 public function compileMemoryToFileContent($type = '')
1395 {
1396 if ($type == 'xml') {
1397 $out = $this->createXML();
1398 } else {
1399 $compress = $this->doOutputCompress();
1400 $out = '';
1401 // adding header:
1402 $out .= $this->addFilePart(serialize($this->dat['header']), $compress);
1403 // adding records:
1404 $out .= $this->addFilePart(serialize($this->dat['records']), $compress);
1405 // adding files:
1406 $out .= $this->addFilePart(serialize($this->dat['files']), $compress);
1407 // adding files_fal:
1408 $out .= $this->addFilePart(serialize($this->dat['files_fal']), $compress);
1409 }
1410 return $out;
1411 }
1412
1413 /**
1414 * Creates XML string from input array
1415 *
1416 * @return string XML content
1417 */
1418 public function createXML()
1419 {
1420 // Options:
1421 $options = array(
1422 'alt_options' => array(
1423 '/header' => array(
1424 'disableTypeAttrib' => true,
1425 'clearStackPath' => true,
1426 'parentTagMap' => array(
1427 'files' => 'file',
1428 'files_fal' => 'file',
1429 'records' => 'table',
1430 'table' => 'rec',
1431 'rec:rels' => 'relations',
1432 'relations' => 'element',
1433 'filerefs' => 'file',
1434 'pid_lookup' => 'page_contents',
1435 'header:relStaticTables' => 'static_tables',
1436 'static_tables' => 'tablename',
1437 'excludeMap' => 'item',
1438 'softrefCfg' => 'softrefExportMode',
1439 'extensionDependencies' => 'extkey',
1440 'softrefs' => 'softref_element'
1441 ),
1442 'alt_options' => array(
1443 '/pagetree' => array(
1444 'disableTypeAttrib' => true,
1445 'useIndexTagForNum' => 'node',
1446 'parentTagMap' => array(
1447 'node:subrow' => 'node'
1448 )
1449 ),
1450 '/pid_lookup/page_contents' => array(
1451 'disableTypeAttrib' => true,
1452 'parentTagMap' => array(
1453 'page_contents' => 'table'
1454 ),
1455 'grandParentTagMap' => array(
1456 'page_contents/table' => 'item'
1457 )
1458 )
1459 )
1460 ),
1461 '/records' => array(
1462 'disableTypeAttrib' => true,
1463 'parentTagMap' => array(
1464 'records' => 'tablerow',
1465 'tablerow:data' => 'fieldlist',
1466 'tablerow:rels' => 'related',
1467 'related' => 'field',
1468 'field:itemArray' => 'relations',
1469 'field:newValueFiles' => 'filerefs',
1470 'field:flexFormRels' => 'flexform',
1471 'relations' => 'element',
1472 'filerefs' => 'file',
1473 'flexform:db' => 'db_relations',
1474 'flexform:file' => 'file_relations',
1475 'flexform:softrefs' => 'softref_relations',
1476 'softref_relations' => 'structurePath',
1477 'db_relations' => 'path',
1478 'file_relations' => 'path',
1479 'path' => 'element',
1480 'keys' => 'softref_key',
1481 'softref_key' => 'softref_element'
1482 ),
1483 'alt_options' => array(
1484 '/records/tablerow/fieldlist' => array(
1485 'useIndexTagForAssoc' => 'field'
1486 )
1487 )
1488 ),
1489 '/files' => array(
1490 'disableTypeAttrib' => true,
1491 'parentTagMap' => array(
1492 'files' => 'file'
1493 )
1494 ),
1495 '/files_fal' => array(
1496 'disableTypeAttrib' => true,
1497 'parentTagMap' => array(
1498 'files_fal' => 'file'
1499 )
1500 )
1501 )
1502 );
1503 // Creating XML file from $outputArray:
1504 $charset = $this->dat['header']['charset'] ?: 'utf-8';
1505 $XML = '<?xml version="1.0" encoding="' . $charset . '" standalone="yes" ?>' . LF;
1506 $XML .= GeneralUtility::array2xml($this->dat, '', 0, 'T3RecordDocument', 0, $options);
1507 return $XML;
1508 }
1509
1510 /**
1511 * Returns TRUE if the output should be compressed.
1512 *
1513 * @return bool TRUE if compression is possible AND requested.
1514 */
1515 public function doOutputCompress()
1516 {
1517 return $this->compress && !$this->dontCompress;
1518 }
1519
1520 /**
1521 * Returns a content part for a filename being build.
1522 *
1523 * @param array $data Data to store in part
1524 * @param bool $compress Compress file?
1525 * @return string Content stream.
1526 */
1527 public function addFilePart($data, $compress = false)
1528 {
1529 if ($compress) {
1530 $data = gzcompress($data);
1531 }
1532 return md5($data) . ':' . ($compress ? '1' : '0') . ':' . str_pad(strlen($data), 10, '0', STR_PAD_LEFT) . ':' . $data . ':';
1533 }
1534
1535 /***********************
1536 * Import
1537 ***********************/
1538
1539 /**
1540 * Initialize all settings for the import
1541 *
1542 * @return void
1543 */
1544 protected function initializeImport()
1545 {
1546 // Set this flag to indicate that an import is being/has been done.
1547 $this->doesImport = 1;
1548 // Initialize:
1549 // 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.
1550 $this->import_mapId = array();
1551 $this->import_newId = array();
1552 $this->import_newId_pids = array();
1553 // Temporary files stack initialized:
1554 $this->unlinkFiles = array();
1555 $this->alternativeFileName = array();
1556 $this->alternativeFilePath = array();
1557
1558 $this->initializeStorageObjects();
1559 }
1560
1561 /**
1562 * Initialize the all present storage objects
1563 *
1564 * @return void
1565 */
1566 protected function initializeStorageObjects()
1567 {
1568 /** @var $storageRepository \TYPO3\CMS\Core\Resource\StorageRepository */
1569 $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class);
1570 $this->storageObjects = $storageRepository->findAll();
1571 }
1572
1573 /**
1574 * Imports the internal data array to $pid.
1575 *
1576 * @param int $pid Page ID in which to import the content
1577 * @return void
1578 */
1579 public function importData($pid)
1580 {
1581 $this->initializeImport();
1582
1583 // Write sys_file_storages first
1584 $this->writeSysFileStorageRecords();
1585 // Write sys_file records and write the binary file data
1586 $this->writeSysFileRecords();
1587 // Write records, first pages, then the rest
1588 // Fields with "hard" relations to database, files and flexform fields are kept empty during this run
1589 $this->writeRecords_pages($pid);
1590 $this->writeRecords_records($pid);
1591 // Finally all the file and DB record references must be fixed. This is done after all records have supposedly been written to database:
1592 // $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.
1593 $this->setRelations();
1594 // 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):
1595 $this->setFlexFormRelations();
1596 // Unlink temporary files:
1597 $this->unlinkTempFiles();
1598 // Finally, traverse all records and process softreferences with substitution attributes.
1599 $this->processSoftReferences();
1600 // After all migrate records using sys_file_reference now
1601 if ($this->legacyImport) {
1602 $this->migrateLegacyImportRecords();
1603 }
1604 }
1605
1606 /**
1607 * Imports the sys_file_storage records from internal data array.
1608 *
1609 * @return void
1610 */
1611 protected function writeSysFileStorageRecords()
1612 {
1613 if (!isset($this->dat['header']['records']['sys_file_storage'])) {
1614 return;
1615 }
1616 $sysFileStorageUidsToBeResetToDefaultStorage = array();
1617 foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) {
1618 $storageRecord = $this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data'];
1619 // continue with Local, writable and online storage only
1620 if ($storageRecord['driver'] === 'Local' && $storageRecord['is_writable'] && $storageRecord['is_online']) {
1621 $useThisStorageUidInsteadOfTheOneInImport = 0;
1622 /** @var $localStorage \TYPO3\CMS\Core\Resource\ResourceStorage */
1623 foreach ($this->storageObjects as $localStorage) {
1624 // check the available storage for Local, writable and online ones
1625 if ($localStorage->getDriverType() === 'Local' && $localStorage->isWritable() && $localStorage->isOnline()) {
1626 // check if there is already an identical storage present (same pathType and basePath)
1627 $storageRecordConfiguration = ResourceFactory::getInstance()->convertFlexFormDataToConfigurationArray($storageRecord['configuration']);
1628 $localStorageRecordConfiguration = $localStorage->getConfiguration();
1629 if (
1630 $storageRecordConfiguration['pathType'] === $localStorageRecordConfiguration['pathType']
1631 && $storageRecordConfiguration['basePath'] === $localStorageRecordConfiguration['basePath']
1632 ) {
1633 // same storage is already present
1634 $useThisStorageUidInsteadOfTheOneInImport = $localStorage->getUid();
1635 break;
1636 }
1637 }
1638 }
1639 if ($useThisStorageUidInsteadOfTheOneInImport > 0) {
1640 // same storage is already present; map the to be imported one to the present one
1641 $this->import_mapId['sys_file_storage'][$sysFileStorageUid] = $useThisStorageUidInsteadOfTheOneInImport;
1642 } else {
1643 // Local, writable and online storage. Is allowed to be used to later write files in.
1644 $this->addSingle('sys_file_storage', $sysFileStorageUid, 0);
1645 }
1646 } else {
1647 // Storage with non Local drivers could be imported but must not be used to saves files in, because you
1648 // could not be sure, that this is supported. The default storage will be used in this case.
1649 // It could happen that non writable and non online storage will be created as dupes because you could not
1650 // check the detailed configuration options at this point
1651 $this->addSingle('sys_file_storage', $sysFileStorageUid, 0);
1652 $sysFileStorageUidsToBeResetToDefaultStorage[] = $sysFileStorageUid;
1653 }
1654 }
1655
1656 // Importing the added ones
1657 $tce = $this->getNewTCE();
1658 // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
1659 $tce->reverseOrder = 1;
1660 $tce->isImporting = true;
1661 $tce->start($this->import_data, array());
1662 $tce->process_datamap();
1663 $this->addToMapId($tce->substNEWwithIDs);
1664
1665 $defaultStorageUid = null;
1666 // get default storage
1667 $defaultStorage = ResourceFactory::getInstance()->getDefaultStorage();
1668 if ($defaultStorage !== null) {
1669 $defaultStorageUid = $defaultStorage->getUid();
1670 }
1671 foreach ($sysFileStorageUidsToBeResetToDefaultStorage as $sysFileStorageUidToBeResetToDefaultStorage) {
1672 $this->import_mapId['sys_file_storage'][$sysFileStorageUidToBeResetToDefaultStorage] = $defaultStorageUid;
1673 }
1674
1675 // unset the sys_file_storage records to prevent an import in writeRecords_records
1676 unset($this->dat['header']['records']['sys_file_storage']);
1677 }
1678
1679 /**
1680 * Imports the sys_file records and the binary files data from internal data array.
1681 *
1682 * @return void
1683 */
1684 protected function writeSysFileRecords()
1685 {
1686 if (!isset($this->dat['header']['records']['sys_file'])) {
1687 return;
1688 }
1689 $this->addGeneralErrorsByTable('sys_file');
1690
1691 // fetch fresh storage records from database
1692 $storageRecords = $this->fetchStorageRecords();
1693
1694 $defaultStorage = ResourceFactory::getInstance()->getDefaultStorage();
1695
1696 $sanitizedFolderMappings = array();
1697
1698 foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) {
1699 $fileRecord = $this->dat['records']['sys_file:' . $sysFileUid]['data'];
1700
1701 $temporaryFile = null;
1702 // check if there is the right file already in the local folder
1703 if ($this->filesPathForImport !== null) {
1704 if (is_file($this->filesPathForImport . '/' . $fileRecord['sha1']) && sha1_file($this->filesPathForImport . '/' . $fileRecord['sha1']) === $fileRecord['sha1']) {
1705 $temporaryFile = $this->filesPathForImport . '/' . $fileRecord['sha1'];
1706 }
1707 }
1708
1709 // save file to disk
1710 if ($temporaryFile === null) {
1711 $fileId = md5($fileRecord['storage'] . ':' . $fileRecord['identifier_hash']);
1712 $temporaryFile = $this->writeTemporaryFileFromData($fileId);
1713 if ($temporaryFile === null) {
1714 // error on writing the file. Error message was already added
1715 continue;
1716 }
1717 }
1718
1719 $originalStorageUid = $fileRecord['storage'];
1720 $useStorageFromStorageRecords = false;
1721
1722 // replace storage id, if an alternative one was registered
1723 if (isset($this->import_mapId['sys_file_storage'][$fileRecord['storage']])) {
1724 $fileRecord['storage'] = $this->import_mapId['sys_file_storage'][$fileRecord['storage']];
1725 $useStorageFromStorageRecords = true;
1726 }
1727
1728 if (empty($fileRecord['storage']) && !$this->isFallbackStorage($fileRecord['storage'])) {
1729 // no storage for the file is defined, mostly because of a missing default storage.
1730 $this->error('Error: No storage for the file "' . $fileRecord['identifier'] . '" with storage uid "' . $originalStorageUid . '"');
1731 continue;
1732 }
1733
1734 // using a storage from the local storage is only allowed, if the uid is present in the
1735 // mapping. Only in this case we could be sure, that it's a local, online and writable storage.
1736 if ($useStorageFromStorageRecords && isset($storageRecords[$fileRecord['storage']])) {
1737 /** @var $storage \TYPO3\CMS\Core\Resource\ResourceStorage */
1738 $storage = ResourceFactory::getInstance()->getStorageObject($fileRecord['storage'], $storageRecords[$fileRecord['storage']]);
1739 } elseif ($this->isFallbackStorage($fileRecord['storage'])) {
1740 $storage = ResourceFactory::getInstance()->getStorageObject(0);
1741 } elseif ($defaultStorage !== null) {
1742 $storage = $defaultStorage;
1743 } else {
1744 $this->error('Error: No storage available for the file "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
1745 continue;
1746 }
1747
1748 $newFile = null;
1749
1750 // check, if there is an identical file
1751 try {
1752 if ($storage->hasFile($fileRecord['identifier'])) {
1753 $file = $storage->getFile($fileRecord['identifier']);
1754 if ($file->getSha1() === $fileRecord['sha1']) {
1755 $newFile = $file;
1756 }
1757 }
1758 } catch (Exception $e) {
1759 }
1760
1761 if ($newFile === null) {
1762 $folderName = PathUtility::dirname(ltrim($fileRecord['identifier'], '/'));
1763 if (in_array($folderName, $sanitizedFolderMappings)) {
1764 $folderName = $sanitizedFolderMappings[$folderName];
1765 }
1766 if (!$storage->hasFolder($folderName)) {
1767 try {
1768 $importFolder = $storage->createFolder($folderName);
1769 if ($importFolder->getIdentifier() !== $folderName && !in_array($folderName, $sanitizedFolderMappings)) {
1770 $sanitizedFolderMappings[$folderName] = $importFolder->getIdentifier();
1771 }
1772 } catch (Exception $e) {
1773 $this->error('Error: Folder could not be created for file "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
1774 continue;
1775 }
1776 } else {
1777 $importFolder = $storage->getFolder($folderName);
1778 }
1779
1780 try {
1781 /** @var $newFile \TYPO3\CMS\Core\Resource\File */
1782 $newFile = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']);
1783 } catch (Exception $e) {
1784 $this->error('Error: File could not be added to the storage: "' . $fileRecord['identifier'] . '" with storage uid "' . $fileRecord['storage'] . '"');
1785 continue;
1786 }
1787
1788 if ($newFile->getSha1() !== $fileRecord['sha1']) {
1789 $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'] . '"');
1790 }
1791 }
1792
1793 // save the new uid in the import id map
1794 $this->import_mapId['sys_file'][$fileRecord['uid']] = $newFile->getUid();
1795 $this->fixUidLocalInSysFileReferenceRecords($fileRecord['uid'], $newFile->getUid());
1796 }
1797
1798 // unset the sys_file records to prevent an import in writeRecords_records
1799 unset($this->dat['header']['records']['sys_file']);
1800 }
1801
1802 /**
1803 * Checks if the $storageId is the id of the fallback storage
1804 *
1805 * @param int|string $storageId
1806 * @return bool
1807 */
1808 protected function isFallbackStorage($storageId)
1809 {
1810 return $storageId === 0 || $storageId === '0';
1811 }
1812
1813 /**
1814 * Normally the importer works like the following:
1815 * Step 1: import the records with cleared field values of relation fields (see addSingle())
1816 * Step 2: update the records with the right relation ids (see setRelations())
1817 *
1818 * In step 2 the saving fields of type "relation to sys_file_reference" checks the related sys_file_reference
1819 * record (created in step 1) with the FileExtensionFilter for matching file extensions of the related file.
1820 * To make this work correct, the uid_local of sys_file_reference records has to be not empty AND has to
1821 * relate to the correct (imported) sys_file record uid!!!
1822 *
1823 * This is fixed here.
1824 *
1825 * @param int $oldFileUid
1826 * @param int $newFileUid
1827 * @return void
1828 */
1829 protected function fixUidLocalInSysFileReferenceRecords($oldFileUid, $newFileUid)
1830 {
1831 if (!isset($this->dat['header']['records']['sys_file_reference'])) {
1832 return;
1833 }
1834
1835 foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
1836 $fileReferenceRecord = $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data'];
1837 if ($fileReferenceRecord['uid_local'] == $oldFileUid) {
1838 $fileReferenceRecord['uid_local'] = $newFileUid;
1839 $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data'] = $fileReferenceRecord;
1840 }
1841 }
1842 }
1843
1844 /**
1845 * Initializes the folder for legacy imports as subfolder of backend users default upload folder
1846 *
1847 * @return void
1848 */
1849 protected function initializeLegacyImportFolder()
1850 {
1851 /** @var \TYPO3\CMS\Core\Resource\Folder $folder */
1852 $folder = $GLOBALS['BE_USER']->getDefaultUploadFolder();
1853 if ($folder === false) {
1854 $this->error('Error: the backend users default upload folder is missing! No files will be imported!');
1855 }
1856 if (!$folder->hasFolder($this->legacyImportTargetPath)) {
1857 try {
1858 $this->legacyImportFolder = $folder->createFolder($this->legacyImportTargetPath);
1859 } catch (\TYPO3\CMS\Core\Exception $e) {
1860 $this->error('Error: the import folder in the default upload folder could not be created! No files will be imported!');
1861 }
1862 } else {
1863 $this->legacyImportFolder = $folder->getSubFolder($this->legacyImportTargetPath);
1864 }
1865 }
1866
1867 /**
1868 * Fetched fresh storage records from database because the new imported
1869 * ones are not in cached data of the StorageRepository
1870 *
1871 * @return bool|array
1872 */
1873 protected function fetchStorageRecords()
1874 {
1875 $whereClause = BackendUtility::BEenableFields('sys_file_storage');
1876 $whereClause .= BackendUtility::deleteClause('sys_file_storage');
1877
1878 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
1879 '*',
1880 'sys_file_storage',
1881 '1=1' . $whereClause,
1882 '',
1883 '',
1884 '',
1885 'uid'
1886 );
1887
1888 return $rows;
1889 }
1890
1891 /**
1892 * Writes the file from import array to temp dir and returns the filename of it.
1893 *
1894 * @param string $fileId
1895 * @param string $dataKey
1896 * @return string Absolute filename of the temporary filename of the file
1897 */
1898 protected function writeTemporaryFileFromData($fileId, $dataKey = 'files_fal')
1899 {
1900 $temporaryFilePath = null;
1901 if (is_array($this->dat[$dataKey][$fileId])) {
1902 $temporaryFilePathInternal = GeneralUtility::tempnam('import_temp_');
1903 GeneralUtility::writeFile($temporaryFilePathInternal, $this->dat[$dataKey][$fileId]['content']);
1904 clearstatcache();
1905 if (@is_file($temporaryFilePathInternal)) {
1906 $this->unlinkFiles[] = $temporaryFilePathInternal;
1907 if (filesize($temporaryFilePathInternal) == $this->dat[$dataKey][$fileId]['filesize']) {
1908 $temporaryFilePath = $temporaryFilePathInternal;
1909 } else {
1910 $this->error('Error: temporary file ' . $temporaryFilePathInternal . ' had a size (' . filesize($temporaryFilePathInternal) . ') different from the original (' . $this->dat[$dataKey][$fileId]['filesize'] . ')', 1);
1911 }
1912 } else {
1913 $this->error('Error: temporary file ' . $temporaryFilePathInternal . ' was not written as it should have been!', 1);
1914 }
1915 } else {
1916 $this->error('Error: No file found for ID ' . $fileId, 1);
1917 }
1918 return $temporaryFilePath;
1919 }
1920
1921 /**
1922 * Writing pagetree/pages to database:
1923 *
1924 * @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
1925 * @return void
1926 * @see writeRecords_records()
1927 */
1928 public function writeRecords_pages($pid)
1929 {
1930 // First, write page structure if any:
1931 if (is_array($this->dat['header']['records']['pages'])) {
1932 $this->addGeneralErrorsByTable('pages');
1933 // $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.
1934 $pageRecords = $this->dat['header']['records']['pages'];
1935 $this->import_data = array();
1936 // First add page tree if any
1937 if (is_array($this->dat['header']['pagetree'])) {
1938 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
1939 foreach ($pagesFromTree as $uid) {
1940 $thisRec = $this->dat['header']['records']['pages'][$uid];
1941 // PID: Set the main $pid, unless a NEW-id is found
1942 $setPid = isset($this->import_newId_pids[$thisRec['pid']]) ? $this->import_newId_pids[$thisRec['pid']] : $pid;
1943 $this->addSingle('pages', $uid, $setPid);
1944 unset($pageRecords[$uid]);
1945 }
1946 }
1947 // Then add all remaining pages not in tree on root level:
1948 if (!empty($pageRecords)) {
1949 $remainingPageUids = array_keys($pageRecords);
1950 foreach ($remainingPageUids as $pUid) {
1951 $this->addSingle('pages', $pUid, $pid);
1952 }
1953 }
1954 // Now write to database:
1955 $tce = $this->getNewTCE();
1956 $tce->isImporting = true;
1957 $this->callHook('before_writeRecordsPages', array(
1958 'tce' => &$tce,
1959 'data' => &$this->import_data
1960 ));
1961 $tce->suggestedInsertUids = $this->suggestedInsertUids;
1962 $tce->start($this->import_data, array());
1963 $tce->process_datamap();
1964 $this->callHook('after_writeRecordsPages', array(
1965 'tce' => &$tce
1966 ));
1967 // post-processing: Registering new ids (end all tcemain sessions with this)
1968 $this->addToMapId($tce->substNEWwithIDs);
1969 // In case of an update, order pages from the page tree correctly:
1970 if ($this->update && is_array($this->dat['header']['pagetree'])) {
1971 $this->writeRecords_pages_order($pid);
1972 }
1973 }
1974 }
1975
1976 /**
1977 * Organize all updated pages in page tree so they are related like in the import file
1978 * Only used for updates and when $this->dat['header']['pagetree'] is an array.
1979 *
1980 * @param int $pid Page id in which to import
1981 * @return void
1982 * @access private
1983 * @see writeRecords_pages(), writeRecords_records_order()
1984 */
1985 public function writeRecords_pages_order($pid)
1986 {
1987 $cmd_data = array();
1988 // Get uid-pid relations and traverse them in order to map to possible new IDs
1989 $pidsFromTree = $this->flatInversePageTree_pid($this->dat['header']['pagetree']);
1990 foreach ($pidsFromTree as $origPid => $newPid) {
1991 if ($newPid >= 0 && $this->dontIgnorePid('pages', $origPid)) {
1992 // If the page had a new id (because it was created) use that instead!
1993 if (substr($this->import_newId_pids[$origPid], 0, 3) === 'NEW') {
1994 if ($this->import_mapId['pages'][$origPid]) {
1995 $mappedPid = $this->import_mapId['pages'][$origPid];
1996 $cmd_data['pages'][$mappedPid]['move'] = $newPid;
1997 }
1998 } else {
1999 $cmd_data['pages'][$origPid]['move'] = $newPid;
2000 }
2001 }
2002 }
2003 // Execute the move commands if any:
2004 if (!empty($cmd_data)) {
2005 $tce = $this->getNewTCE();
2006 $this->callHook('before_writeRecordsPagesOrder', array(
2007 'tce' => &$tce,
2008 'data' => &$cmd_data
2009 ));
2010 $tce->start(array(), $cmd_data);
2011 $tce->process_cmdmap();
2012 $this->callHook('after_writeRecordsPagesOrder', array(
2013 'tce' => &$tce
2014 ));
2015 }
2016 }
2017
2018 /**
2019 * Write all database records except pages (writtein in writeRecords_pages())
2020 *
2021 * @param int $pid Page id in which to import
2022 * @return void
2023 * @see writeRecords_pages()
2024 */
2025 public function writeRecords_records($pid)
2026 {
2027 // Write the rest of the records
2028 $this->import_data = array();
2029 if (is_array($this->dat['header']['records'])) {
2030 foreach ($this->dat['header']['records'] as $table => $recs) {
2031 $this->addGeneralErrorsByTable($table);
2032 if ($table != 'pages') {
2033 foreach ($recs as $uid => $thisRec) {
2034 // PID: Set the main $pid, unless a NEW-id is found
2035 $setPid = isset($this->import_mapId['pages'][$thisRec['pid']])
2036 ? (int)$this->import_mapId['pages'][$thisRec['pid']]
2037 : (int)$pid;
2038 if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel'])) {
2039 $rootLevelSetting = (int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'];
2040 if ($rootLevelSetting === 1) {
2041 $setPid = 0;
2042 } elseif ($rootLevelSetting === 0 && $setPid === 0) {
2043 $this->error('Error: Record type ' . $table . ' is not allowed on pid 0');
2044 continue;
2045 }
2046 }
2047 // Add record:
2048 $this->addSingle($table, $uid, $setPid);
2049 }
2050 }
2051 }
2052 } else {
2053 $this->error('Error: No records defined in internal data array.');
2054 }
2055 // Now write to database:
2056 $tce = $this->getNewTCE();
2057 $this->callHook('before_writeRecordsRecords', array(
2058 'tce' => &$tce,
2059 'data' => &$this->import_data
2060 ));
2061 $tce->suggestedInsertUids = $this->suggestedInsertUids;
2062 // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
2063 $tce->reverseOrder = 1;
2064 $tce->isImporting = true;
2065 $tce->start($this->import_data, array());
2066 $tce->process_datamap();
2067 $this->callHook('after_writeRecordsRecords', array(
2068 'tce' => &$tce
2069 ));
2070 // post-processing: Removing files and registering new ids (end all tcemain sessions with this)
2071 $this->addToMapId($tce->substNEWwithIDs);
2072 // In case of an update, order pages from the page tree correctly:
2073 if ($this->update) {
2074 $this->writeRecords_records_order($pid);
2075 }
2076 }
2077
2078 /**
2079 * Organize all updated record to their new positions.
2080 * Only used for updates
2081 *
2082 * @param int $mainPid Main PID into which we import.
2083 * @return void
2084 * @access private
2085 * @see writeRecords_records(), writeRecords_pages_order()
2086 */
2087 public function writeRecords_records_order($mainPid)
2088 {
2089 $cmd_data = array();
2090 if (is_array($this->dat['header']['pagetree'])) {
2091 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
2092 } else {
2093 $pagesFromTree = array();
2094 }
2095 if (is_array($this->dat['header']['pid_lookup'])) {
2096 foreach ($this->dat['header']['pid_lookup'] as $pid => $recList) {
2097 $newPid = isset($this->import_mapId['pages'][$pid]) ? $this->import_mapId['pages'][$pid] : $mainPid;
2098 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($newPid)) {
2099 foreach ($recList as $tableName => $uidList) {
2100 // If $mainPid===$newPid then we are on root level and we can consider to move pages as well!
2101 // (they will not be in the page tree!)
2102 if (($tableName != 'pages' || !$pagesFromTree[$pid]) && is_array($uidList)) {
2103 $uidList = array_reverse(array_keys($uidList));
2104 foreach ($uidList as $uid) {
2105 if ($this->dontIgnorePid($tableName, $uid)) {
2106 $cmd_data[$tableName][$uid]['move'] = $newPid;
2107 } else {
2108 }
2109 }
2110 }
2111 }
2112 }
2113 }
2114 }
2115 // Execute the move commands if any:
2116 if (!empty($cmd_data)) {
2117 $tce = $this->getNewTCE();
2118 $this->callHook('before_writeRecordsRecordsOrder', array(
2119 'tce' => &$tce,
2120 'data' => &$cmd_data
2121 ));
2122 $tce->start(array(), $cmd_data);
2123 $tce->process_cmdmap();
2124 $this->callHook('after_writeRecordsRecordsOrder', array(
2125 'tce' => &$tce
2126 ));
2127 }
2128 }
2129
2130 /**
2131 * Adds a single record to the $importData array. Also copies files to tempfolder.
2132 * However all File/DB-references and flexform field contents are set to blank for now!
2133 * That is done with setRelations() later
2134 *
2135 * @param string $table Table name (from import memory)
2136 * @param int $uid Record UID (from import memory)
2137 * @param int $pid Page id
2138 * @return void
2139 * @see writeRecords()
2140 */
2141 public function addSingle($table, $uid, $pid)
2142 {
2143 if ($this->import_mode[$table . ':' . $uid] === 'exclude') {
2144 return;
2145 }
2146 $record = $this->dat['records'][$table . ':' . $uid]['data'];
2147 if (is_array($record)) {
2148 if ($this->update && $this->doesRecordExist($table, $uid) && $this->import_mode[$table . ':' . $uid] !== 'as_new') {
2149 $ID = $uid;
2150 } elseif ($table === 'sys_file_metadata' && $record['sys_language_uid'] == '0' && $this->import_mapId['sys_file'][$record['file']]) {
2151 // on adding sys_file records the belonging sys_file_metadata record was also created
2152 // if there is one the record need to be overwritten instead of creating a new one.
2153 $recordInDatabase = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
2154 'uid',
2155 'sys_file_metadata',
2156 'file = ' . $this->import_mapId['sys_file'][$record['file']] . ' AND sys_language_uid = 0 AND pid = 0'
2157 );
2158 // if no record could be found, $this->import_mapId['sys_file'][$record['file']] is pointing
2159 // to a file, that was already there, thus a new metadata record should be created
2160 if (is_array($recordInDatabase)) {
2161 $this->import_mapId['sys_file_metadata'][$record['uid']] = $recordInDatabase['uid'];
2162 $ID = $recordInDatabase['uid'];
2163 } else {
2164 $ID = StringUtility::getUniqueId('NEW');
2165 }
2166 } else {
2167 $ID = StringUtility::getUniqueId('NEW');
2168 }
2169 $this->import_newId[$table . ':' . $ID] = array('table' => $table, 'uid' => $uid);
2170 if ($table == 'pages') {
2171 $this->import_newId_pids[$uid] = $ID;
2172 }
2173 // Set main record data:
2174 $this->import_data[$table][$ID] = $record;
2175 $this->import_data[$table][$ID]['tx_impexp_origuid'] = $this->import_data[$table][$ID]['uid'];
2176 // Reset permission data:
2177 if ($table === 'pages') {
2178 // Have to reset the user/group IDs so pages are owned by importing user. Otherwise strange things may happen for non-admins!
2179 unset($this->import_data[$table][$ID]['perms_userid']);
2180 unset($this->import_data[$table][$ID]['perms_groupid']);
2181 }
2182 // PID and UID:
2183 unset($this->import_data[$table][$ID]['uid']);
2184 // Updates:
2185 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($ID)) {
2186 unset($this->import_data[$table][$ID]['pid']);
2187 } else {
2188 // Inserts:
2189 $this->import_data[$table][$ID]['pid'] = $pid;
2190 if (($this->import_mode[$table . ':' . $uid] === 'force_uid' && $this->update || $this->force_all_UIDS) && $GLOBALS['BE_USER']->isAdmin()) {
2191 $this->import_data[$table][$ID]['uid'] = $uid;
2192 $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE';
2193 }
2194 }
2195 // Setting db/file blank:
2196 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
2197 switch ((string)$config['type']) {
2198 case 'db':
2199
2200 case 'file':
2201 // Fixed later in ->setRelations() [because we need to know ALL newly created IDs before we can map relations!]
2202 // In the meantime we set NO values for relations.
2203 //
2204 // BUT for field uid_local of table sys_file_reference the relation MUST not be cleared here,
2205 // because the value is already the uid of the right imported sys_file record.
2206 // @see fixUidLocalInSysFileReferenceRecords()
2207 // If it's empty or a uid to another record the FileExtensionFilter will throw an exception or
2208 // delete the reference record if the file extension of the related record doesn't match.
2209 if ($table !== 'sys_file_reference' && $field !== 'uid_local') {
2210 $this->import_data[$table][$ID][$field] = '';
2211 }
2212 break;
2213 case 'flex':
2214 // Fixed later in setFlexFormRelations()
2215 // In the meantime we set NO value for flexforms - this is mainly because file references
2216 // inside will not be processed properly; In fact references will point to no file
2217 // or existing files (in which case there will be double-references which is a big problem of course!)
2218 $this->import_data[$table][$ID][$field] = '';
2219 break;
2220 }
2221 }
2222 } elseif ($table . ':' . $uid != 'pages:0') {
2223 // On root level we don't want this error message.
2224 $this->error('Error: no record was found in data array!', 1);
2225 }
2226 }
2227
2228 /**
2229 * Registers the substNEWids in memory.
2230 *
2231 * @param array $substNEWwithIDs From tcemain to be merged into internal mapping variable in this object
2232 * @return void
2233 * @see writeRecords()
2234 */
2235 public function addToMapId($substNEWwithIDs)
2236 {
2237 foreach ($this->import_data as $table => $recs) {
2238 foreach ($recs as $id => $value) {
2239 $old_uid = $this->import_newId[$table . ':' . $id]['uid'];
2240 if (isset($substNEWwithIDs[$id])) {
2241 $this->import_mapId[$table][$old_uid] = $substNEWwithIDs[$id];
2242 } elseif ($this->update) {
2243 // Map same ID to same ID....
2244 $this->import_mapId[$table][$old_uid] = $id;
2245 } else {
2246 // if $this->import_mapId contains already the right mapping, skip the error msg.
2247 // See special handling of sys_file_metadata in addSingle() => nothing to do
2248 if (!($table === 'sys_file_metadata' && isset($this->import_mapId[$table][$old_uid]) && $this->import_mapId[$table][$old_uid] == $id)) {
2249 $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!', 1);
2250 }
2251 }
2252 }
2253 }
2254 }
2255
2256 /**
2257 * Returns a new $TCE object
2258 *
2259 * @return DataHandler $TCE object
2260 */
2261 public function getNewTCE()
2262 {
2263 $tce = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
2264 $tce->stripslashes_values = 0;
2265 $tce->dontProcessTransformations = 1;
2266 $tce->enableLogging = $this->enableLogging;
2267 $tce->alternativeFileName = $this->alternativeFileName;
2268 $tce->alternativeFilePath = $this->alternativeFilePath;
2269 return $tce;
2270 }
2271
2272 /**
2273 * Cleaning up all the temporary files stored in typo3temp/ folder
2274 *
2275 * @return void
2276 */
2277 public function unlinkTempFiles()
2278 {
2279 foreach ($this->unlinkFiles as $fileName) {
2280 if (GeneralUtility::isFirstPartOfStr($fileName, PATH_site . 'typo3temp/')) {
2281 GeneralUtility::unlink_tempfile($fileName);
2282 clearstatcache();
2283 if (is_file($fileName)) {
2284 $this->error('Error: ' . $fileName . ' was NOT unlinked as it should have been!', 1);
2285 }
2286 } else {
2287 $this->error('Error: ' . $fileName . ' was not in temp-path. Not removed!', 1);
2288 }
2289 }
2290 $this->unlinkFiles = array();
2291 }
2292
2293 /***************************
2294 * Import / Relations setting
2295 ***************************/
2296
2297 /**
2298 * At the end of the import process all file and DB relations should be set properly (that is relations
2299 * to imported records are all re-created so imported records are correctly related again)
2300 * Relations in flexform fields are processed in setFlexFormRelations() after this function
2301 *
2302 * @return void
2303 * @see setFlexFormRelations()
2304 */
2305 public function setRelations()
2306 {
2307 $updateData = array();
2308 // import_newId contains a register of all records that was in the import memorys "records" key
2309 foreach ($this->import_newId as $nId => $dat) {
2310 $table = $dat['table'];
2311 $uid = $dat['uid'];
2312 // original UID - NOT the new one!
2313 // If the record has been written and received a new id, then proceed:
2314 if (is_array($this->import_mapId[$table]) && isset($this->import_mapId[$table][$uid])) {
2315 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
2316 if (is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
2317 $thisNewPageUid = 0;
2318 if ($this->legacyImport) {
2319 if ($table != 'pages') {
2320 $oldPid = $this->dat['records'][$table . ':' . $uid]['data']['pid'];
2321 $thisNewPageUid = BackendUtility::wsMapId($table, $this->import_mapId['pages'][$oldPid]);
2322 } else {
2323 $thisNewPageUid = $thisNewUid;
2324 }
2325 }
2326 // Traverse relation fields of each record
2327 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
2328 // uid_local of sys_file_reference needs no update because the correct reference uid was already written
2329 // @see ImportExport::fixUidLocalInSysFileReferenceRecords()
2330 if ($table === 'sys_file_reference' && $field === 'uid_local') {
2331 continue;
2332 }
2333 switch ((string)$config['type']) {
2334 case 'db':
2335 if (is_array($config['itemArray']) && !empty($config['itemArray'])) {
2336 $itemConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
2337 $valArray = $this->setRelations_db($config['itemArray'], $itemConfig);
2338 $updateData[$table][$thisNewUid][$field] = implode(',', $valArray);
2339 }
2340 break;
2341 case 'file':
2342 if (is_array($config['newValueFiles']) && !empty($config['newValueFiles'])) {
2343 $valArr = array();
2344 foreach ($config['newValueFiles'] as $fI) {
2345 $valArr[] = $this->import_addFileNameToBeCopied($fI);
2346 }
2347 if ($this->legacyImport && $this->legacyImportFolder === null && isset($this->legacyImportMigrationTables[$table][$field])) {
2348 // Do nothing - the legacy import folder is missing
2349 } elseif ($this->legacyImport && $this->legacyImportFolder !== null && isset($this->legacyImportMigrationTables[$table][$field])) {
2350 $refIds = array();
2351 foreach ($valArr as $tempFile) {
2352 $fileName = $this->alternativeFileName[$tempFile];
2353 $fileObject = null;
2354
2355 try {
2356 // check, if there is alreay the same file in the folder
2357 if ($this->legacyImportFolder->hasFile($fileName)) {
2358 $fileStorage = $this->legacyImportFolder->getStorage();
2359 $file = $fileStorage->getFile($this->legacyImportFolder->getIdentifier() . $fileName);
2360 if ($file->getSha1() === sha1_file($tempFile)) {
2361 $fileObject = $file;
2362 }
2363 }
2364 } catch (Exception $e) {
2365 }
2366
2367 if ($fileObject === null) {
2368 try {
2369 $fileObject = $this->legacyImportFolder->addFile($tempFile, $fileName, DuplicationBehavior::RENAME);
2370 } catch (\TYPO3\CMS\Core\Exception $e) {
2371 $this->error('Error: no file could be added to the storage for file name' . $this->alternativeFileName[$tempFile]);
2372 }
2373 }
2374 if ($fileObject !== null) {
2375 $refId = StringUtility::getUniqueId('NEW');
2376 $refIds[] = $refId;
2377 $updateData['sys_file_reference'][$refId] = array(
2378 'uid_local' => $fileObject->getUid(),
2379 'uid_foreign' => $thisNewUid, // uid of your content record
2380 'tablenames' => $table,
2381 'fieldname' => $field,
2382 'pid' => $thisNewPageUid, // parent id of the parent page
2383 'table_local' => 'sys_file',
2384 );
2385 }
2386 }
2387 $updateData[$table][$thisNewUid][$field] = implode(',', $refIds);
2388 if (!empty($this->legacyImportMigrationTables[$table][$field])) {
2389 $this->legacyImportMigrationRecords[$table][$thisNewUid][$field] = $refIds;
2390 }
2391 } else {
2392 $updateData[$table][$thisNewUid][$field] = implode(',', $valArr);
2393 }
2394 }
2395 break;
2396 }
2397 }
2398 } else {
2399 $this->error('Error: no record was found in data array!', 1);
2400 }
2401 } else {
2402 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')', 1);
2403 }
2404 }
2405 if (!empty($updateData)) {
2406 $tce = $this->getNewTCE();
2407 $tce->isImporting = true;
2408 $this->callHook('before_setRelation', array(
2409 'tce' => &$tce,
2410 'data' => &$updateData
2411 ));
2412 $tce->start($updateData, array());
2413 $tce->process_datamap();
2414 // Replace the temporary "NEW" ids with the final ones.
2415 foreach ($this->legacyImportMigrationRecords as $table => $records) {
2416 foreach ($records as $uid => $fields) {
2417 foreach ($fields as $field => $referenceIds) {
2418 foreach ($referenceIds as $key => $referenceId) {
2419 $this->legacyImportMigrationRecords[$table][$uid][$field][$key] = $tce->substNEWwithIDs[$referenceId];
2420 }
2421 }
2422 }
2423 }
2424 $this->callHook('after_setRelations', array(
2425 'tce' => &$tce
2426 ));
2427 }
2428 }
2429
2430 /**
2431 * Maps relations for database
2432 *
2433 * @param array $itemArray Array of item sets (table/uid) from a dbAnalysis object
2434 * @param array $itemConfig Array of TCA config of the field the relation to be set on
2435 * @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.
2436 */
2437 public function setRelations_db($itemArray, $itemConfig)
2438 {
2439 $valArray = array();
2440 foreach ($itemArray as $relDat) {
2441 if (is_array($this->import_mapId[$relDat['table']]) && isset($this->import_mapId[$relDat['table']][$relDat['id']])) {
2442 // Since non FAL file relation type group internal_type file_reference are handled as reference to
2443 // sys_file records Datahandler requires the value as uid of the the related sys_file record only
2444 if ($itemConfig['type'] === 'group' && $itemConfig['internal_type'] === 'file_reference') {
2445 $value = $this->import_mapId[$relDat['table']][$relDat['id']];
2446 } elseif ($itemConfig['type'] === 'input' && isset($itemConfig['wizards']['link'])) {
2447 // If an input field has a relation to a sys_file record this need to be converted back to
2448 // the public path. But use getPublicUrl here, because could normally only be a local file path.
2449 $fileUid = $this->import_mapId[$relDat['table']][$relDat['id']];
2450 // Fallback value
2451 $value = 'file:' . $fileUid;
2452 try {
2453 $file = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->retrieveFileOrFolderObject($fileUid);
2454 } catch (\Exception $e) {
2455 }
2456 if ($file instanceof \TYPO3\CMS\Core\Resource\FileInterface) {
2457 $value = $file->getPublicUrl();
2458 }
2459 } else {
2460 $value = $relDat['table'] . '_' . $this->import_mapId[$relDat['table']][$relDat['id']];
2461 }
2462 $valArray[] = $value;
2463 } elseif ($this->isTableStatic($relDat['table']) || $this->isExcluded($relDat['table'], $relDat['id']) || $relDat['id'] < 0) {
2464 // Checking for less than zero because some select types could contain negative values,
2465 // eg. fe_groups (-1, -2) and sys_language (-1 = ALL languages). This must be handled on both export and import.
2466 $valArray[] = $relDat['table'] . '_' . $relDat['id'];
2467 } else {
2468 $this->error('Lost relation: ' . $relDat['table'] . ':' . $relDat['id'], 1);
2469 }
2470 }
2471 return $valArray;
2472 }
2473
2474 /**
2475 * Writes the file from import array to temp dir and returns the filename of it.
2476 *
2477 * @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
2478 * @return string|NULL Absolute filename of the temporary filename of the file. In ->alternativeFileName the original name is set.
2479 */
2480 public function import_addFileNameToBeCopied($fI)
2481 {
2482 if (is_array($this->dat['files'][$fI['ID']])) {
2483 $tmpFile = null;
2484 // check if there is the right file already in the local folder
2485 if ($this->filesPathForImport !== null) {
2486 if (is_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) &&
2487 md5_file($this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5']) === $this->dat['files'][$fI['ID']]['content_md5']) {
2488 $tmpFile = $this->filesPathForImport . '/' . $this->dat['files'][$fI['ID']]['content_md5'];
2489 }
2490 }
2491 if ($tmpFile === null) {
2492 $tmpFile = GeneralUtility::tempnam('import_temp_');
2493 GeneralUtility::writeFile($tmpFile, $this->dat['files'][$fI['ID']]['content']);
2494 }
2495 clearstatcache();
2496 if (@is_file($tmpFile)) {
2497 $this->unlinkFiles[] = $tmpFile;
2498 if (filesize($tmpFile) == $this->dat['files'][$fI['ID']]['filesize']) {
2499 $this->alternativeFileName[$tmpFile] = $fI['filename'];
2500 $this->alternativeFilePath[$tmpFile] = $this->dat['files'][$fI['ID']]['relFileRef'];
2501 return $tmpFile;
2502 } else {
2503 $this->error('Error: temporary file ' . $tmpFile . ' had a size (' . filesize($tmpFile) . ') different from the original (' . $this->dat['files'][$fI['ID']]['filesize'] . ')', 1);
2504 }
2505 } else {
2506 $this->error('Error: temporary file ' . $tmpFile . ' was not written as it should have been!', 1);
2507 }
2508 } else {
2509 $this->error('Error: No file found for ID ' . $fI['ID'], 1);
2510 }
2511 return null;
2512 }
2513
2514 /**
2515 * 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.
2516 * 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!
2517 *
2518 * @return void
2519 * @see setRelations()
2520 */
2521 public function setFlexFormRelations()
2522 {
2523 $updateData = array();
2524 // import_newId contains a register of all records that was in the import memorys "records" key
2525 foreach ($this->import_newId as $nId => $dat) {
2526 $table = $dat['table'];
2527 $uid = $dat['uid'];
2528 // original UID - NOT the new one!
2529 // If the record has been written and received a new id, then proceed:
2530 if (!isset($this->import_mapId[$table][$uid])) {
2531 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')', 1);
2532 continue;
2533 }
2534
2535 if (!is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
2536 $this->error('Error: no record was found in data array!', 1);
2537 continue;
2538 }
2539 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
2540 // Traverse relation fields of each record
2541 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
2542 switch ((string)$config['type']) {
2543 case 'flex':
2544 // Get XML content and set as default value (string, non-processed):
2545 $updateData[$table][$thisNewUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field];
2546 // If there has been registered relations inside the flex form field, run processing on the content:
2547 if (!empty($config['flexFormRels']['db']) || !empty($config['flexFormRels']['file'])) {
2548 $origRecordRow = BackendUtility::getRecord($table, $thisNewUid, '*');
2549 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
2550 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
2551 if (is_array($origRecordRow) && is_array($conf) && $conf['type'] === 'flex') {
2552 // Get current data structure and value array:
2553 $dataStructArray = BackendUtility::getFlexFormDS($conf, $origRecordRow, $table, $field);
2554 $currentValueArray = GeneralUtility::xml2array($updateData[$table][$thisNewUid][$field]);
2555 // Do recursive processing of the XML data:
2556 $iteratorObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
2557 $iteratorObj->callBackObj = $this;
2558 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData(
2559 $currentValueArray['data'],
2560 array(),
2561 array(),
2562 $dataStructArray,
2563 array($table, $thisNewUid, $field, $config),
2564 'remapListedDBRecords_flexFormCallBack'
2565 );
2566 // The return value is set as an array which means it will be processed by tcemain for file and DB references!
2567 if (is_array($currentValueArray['data'])) {
2568 $updateData[$table][$thisNewUid][$field] = $currentValueArray;
2569 }
2570 }
2571 }
2572 break;
2573 }
2574 }
2575 }
2576 if (!empty($updateData)) {
2577 $tce = $this->getNewTCE();
2578 $tce->isImporting = true;
2579 $this->callHook('before_setFlexFormRelations', array(
2580 'tce' => &$tce,
2581 'data' => &$updateData
2582 ));
2583 $tce->start($updateData, array());
2584 $tce->process_datamap();
2585 $this->callHook('after_setFlexFormRelations', array(
2586 'tce' => &$tce
2587 ));
2588 }
2589 }
2590
2591 /**
2592 * Callback function for traversing the FlexForm structure in relation to remapping database relations
2593 *
2594 * @param array $pParams Set of parameters in numeric array: table, uid, field
2595 * @param array $dsConf TCA config for field (from Data Structure of course)
2596 * @param string $dataValue Field value (from FlexForm XML)
2597 * @param string $dataValue_ext1 Not used
2598 * @param string $dataValue_ext2 Not used
2599 * @param string $path Path of where the data structure of the element is found
2600 * @return array Array where the "value" key carries the value.
2601 * @see setFlexFormRelations()
2602 */
2603 public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
2604 {
2605 // Extract parameters:
2606 list($table, $uid, $field, $config) = $pParams;
2607 // In case the $path is used as index without a trailing slash we will remove that
2608 if (!is_array($config['flexFormRels']['db'][$path]) && is_array($config['flexFormRels']['db'][rtrim($path, '/')])) {
2609 $path = rtrim($path, '/');
2610 }
2611 if (is_array($config['flexFormRels']['db'][$path])) {
2612 $valArray = $this->setRelations_db($config['flexFormRels']['db'][$path], $dsConf);
2613 $dataValue = implode(',', $valArray);
2614 }
2615 if (is_array($config['flexFormRels']['file'][$path])) {
2616 $valArr = array();
2617 foreach ($config['flexFormRels']['file'][$path] as $fI) {
2618 $valArr[] = $this->import_addFileNameToBeCopied($fI);
2619 }
2620 $dataValue = implode(',', $valArr);
2621 }
2622 return array('value' => $dataValue);
2623 }
2624
2625 /**************************
2626 * Import / Soft References
2627 *************************/
2628
2629 /**
2630 * Processing of soft references
2631 *
2632 * @return void
2633 */
2634 public function processSoftReferences()
2635 {
2636 // Initialize:
2637 $inData = array();
2638 // Traverse records:
2639 if (is_array($this->dat['header']['records'])) {
2640 foreach ($this->dat['header']['records'] as $table => $recs) {
2641 foreach ($recs as $uid => $thisRec) {
2642 // If there are soft references defined, traverse those:
2643 if (isset($GLOBALS['TCA'][$table]) && is_array($thisRec['softrefs'])) {
2644 // First traversal is to collect softref configuration and split them up based on fields.
2645 // This could probably also have been done with the "records" key instead of the header.
2646 $fieldsIndex = array();
2647 foreach ($thisRec['softrefs'] as $softrefDef) {
2648 // If a substitution token is set:
2649 if ($softrefDef['field'] && is_array($softrefDef['subst']) && $softrefDef['subst']['tokenID']) {
2650 $fieldsIndex[$softrefDef['field']][$softrefDef['subst']['tokenID']] = $softrefDef;
2651 }
2652 }
2653 // The new id:
2654 $thisNewUid = BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
2655 // Now, if there are any fields that require substitution to be done, lets go for that:
2656 foreach ($fieldsIndex as $field => $softRefCfgs) {
2657 if (is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
2658 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
2659 if ($conf['type'] === 'flex') {
2660 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
2661 $origRecordRow = BackendUtility::getRecord($table, $thisNewUid, '*');
2662 if (is_array($origRecordRow)) {
2663 // Get current data structure and value array:
2664 $dataStructArray = BackendUtility::getFlexFormDS($conf, $origRecordRow, $table, $field);
2665 $currentValueArray = GeneralUtility::xml2array($origRecordRow[$field]);
2666 // Do recursive processing of the XML data:
2667 /** @var $iteratorObj \TYPO3\CMS\Core\DataHandling\DataHandler */
2668 $iteratorObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
2669 $iteratorObj->callBackObj = $this;
2670 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $uid, $field, $softRefCfgs), 'processSoftReferences_flexFormCallBack');
2671 // The return value is set as an array which means it will be processed by tcemain for file and DB references!
2672 if (is_array($currentValueArray['data'])) {
2673 $inData[$table][$thisNewUid][$field] = $currentValueArray;
2674 }
2675 }
2676 } else {
2677 // Get tokenizedContent string and proceed only if that is not blank:
2678 $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'];
2679 if (strlen($tokenizedContent) && is_array($softRefCfgs)) {
2680 $inData[$table][$thisNewUid][$field] = $this->processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid);
2681 }
2682 }
2683 }
2684 }
2685 }
2686 }
2687 }
2688 }
2689 // Now write to database:
2690 $tce = $this->getNewTCE();
2691 $tce->isImporting = true;
2692 $this->callHook('before_processSoftReferences', array(
2693 'tce' => $tce,
2694 'data' => &$inData
2695 ));
2696 $tce->enableLogging = true;
2697 $tce->start($inData, array());
2698 $tce->process_datamap();
2699 $this->callHook('after_processSoftReferences', array(
2700 'tce' => $tce
2701 ));
2702 }
2703
2704 /**
2705 * Callback function for traversing the FlexForm structure in relation to remapping softreference relations
2706 *
2707 * @param array $pParams Set of parameters in numeric array: table, uid, field
2708 * @param array $dsConf TCA config for field (from Data Structure of course)
2709 * @param string $dataValue Field value (from FlexForm XML)
2710 * @param string $dataValue_ext1 Not used
2711 * @param string $dataValue_ext2 Not used
2712 * @param string $path Path of where the data structure where the element is found
2713 * @return array Array where the "value" key carries the value.
2714 * @see setFlexFormRelations()
2715 */
2716 public function processSoftReferences_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path)
2717 {
2718 // Extract parameters:
2719 list($table, $origUid, $field, $softRefCfgs) = $pParams;
2720 if (is_array($softRefCfgs)) {
2721 // First, find all soft reference configurations for this structure path (they are listed flat in the header):
2722 $thisSoftRefCfgList = array();
2723 foreach ($softRefCfgs as $sK => $sV) {
2724 if ($sV['structurePath'] === $path) {
2725 $thisSoftRefCfgList[$sK] = $sV;
2726 }
2727 }
2728 // If any was found, do processing:
2729 if (!empty($thisSoftRefCfgList)) {
2730 // Get tokenizedContent string and proceed only if that is not blank:
2731 $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$path]['tokenizedContent'];
2732 if (strlen($tokenizedContent)) {
2733 $dataValue = $this->processSoftReferences_substTokens($tokenizedContent, $thisSoftRefCfgList, $table, $origUid);
2734 }
2735 }
2736 }
2737 // Return
2738 return array('value' => $dataValue);
2739 }
2740
2741 /**
2742 * Substition of softreference tokens
2743 *
2744 * @param string $tokenizedContent Content of field with soft reference tokens in.
2745 * @param array $softRefCfgs Soft reference configurations
2746 * @param string $table Table for which the processing occurs
2747 * @param string $uid UID of record from table
2748 * @return string The input content with tokens substituted according to entries in softRefCfgs
2749 */
2750 public function processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid)
2751 {
2752 // traverse each softref type for this field:
2753 foreach ($softRefCfgs as $cfg) {
2754 // Get token ID:
2755 $tokenID = $cfg['subst']['tokenID'];
2756 // Default is current token value:
2757 $insertValue = $cfg['subst']['tokenValue'];
2758 // Based on mode:
2759 switch ((string)$this->softrefCfg[$tokenID]['mode']) {
2760 case 'exclude':
2761 // Exclude is a simple passthrough of the value
2762 break;
2763 case 'editable':
2764 // Editable always picks up the value from this input array:
2765 $insertValue = $this->softrefInputValues[$tokenID];
2766 break;
2767 default:
2768 // Mapping IDs/creating files: Based on type, look up new value:
2769 switch ((string)$cfg['subst']['type']) {
2770 case 'file':
2771 // Create / Overwrite file:
2772 $insertValue = $this->processSoftReferences_saveFile($cfg['subst']['relFileName'], $cfg, $table, $uid);
2773 break;
2774 case 'db':
2775 default:
2776 // Trying to map database element if found in the mapID array:
2777 list($tempTable, $tempUid) = explode(':', $cfg['subst']['recordRef']);
2778 if (isset($this->import_mapId[$tempTable][$tempUid])) {
2779 $insertValue = BackendUtility::wsMapId($tempTable, $this->import_mapId[$tempTable][$tempUid]);
2780 // 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!
2781 if ($tempTable === 'pages' && !\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($cfg['subst']['tokenValue'])) {
2782 $recWithUniqueValue = BackendUtility::getRecord($tempTable, $insertValue, 'alias');
2783 if ($recWithUniqueValue['alias']) {
2784 $insertValue = $recWithUniqueValue['alias'];
2785 }
2786 } elseif (strpos($cfg['subst']['tokenValue'], ':') !== false) {
2787 list($tokenKey, $tokenId) = explode(':', $cfg['subst']['tokenValue']);
2788 $insertValue = $tokenKey . ':' . $insertValue;
2789 }
2790 }
2791 }
2792 }
2793 // Finally, swap the soft reference token in tokenized content with the insert value:
2794 $tokenizedContent = str_replace('{softref:' . $tokenID . '}', $insertValue, $tokenizedContent);
2795 }
2796 return $tokenizedContent;
2797 }
2798
2799 /**
2800 * Process a soft reference file
2801 *
2802 * @param string $relFileName Old Relative filename
2803 * @param array $cfg soft reference configuration array
2804 * @param string $table Table for which the processing occurs
2805 * @param string $uid UID of record from table
2806 * @return string New relative filename (value to insert instead of the softref token)
2807 */
2808 public function processSoftReferences_saveFile($relFileName, $cfg, $table, $uid)
2809 {
2810 if ($fileHeaderInfo = $this->dat['header']['files'][$cfg['file_ID']]) {
2811 // Initialize; Get directory prefix for file and find possible RTE filename
2812 $dirPrefix = PathUtility::dirname($relFileName) . '/';
2813 $rteOrigName = $this->getRTEoriginalFilename(PathUtility::basename($relFileName));
2814 // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
2815 if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/')) {
2816 // RTE:
2817 // First, find unique RTE file name:
2818 if (@is_dir((PATH_site . $dirPrefix))) {
2819 // From the "original" RTE filename, produce a new "original" destination filename which is unused.
2820 // Even if updated, the image should be unique. Currently the problem with this is that it leaves a lot of unused RTE images...
2821 $fileProcObj = $this->getFileProcObj();
2822 $origDestName = $fileProcObj->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
2823 // Create copy file name:
2824 $pI = pathinfo($relFileName);
2825 $copyDestName = PathUtility::dirname($origDestName) . '/RTEmagicC_' . substr(PathUtility::basename($origDestName), 10) . '.' . $pI['extension'];
2826 if (
2827 !@is_file($copyDestName) && !@is_file($origDestName)
2828 && $origDestName === GeneralUtility::getFileAbsFileName($origDestName)
2829 && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)
2830 ) {
2831 if ($this->dat['header']['files'][$fileHeaderInfo['RTE_ORIG_ID']]) {
2832 if ($this->legacyImport) {
2833 $fileName = PathUtility::basename($copyDestName);
2834 $this->writeSysFileResourceForLegacyImport($fileName, $cfg['file_ID']);
2835 $relFileName = $this->filePathMap[$cfg['file_ID']] . '" data-htmlarea-file-uid="' . $fileName . '" data-htmlarea-file-table="sys_file';
2836 // Also save the original file
2837 $originalFileName = PathUtility::basename($origDestName);
2838 $this->writeSysFileResourceForLegacyImport($originalFileName, $fileHeaderInfo['RTE_ORIG_ID']);
2839 } else {
2840 // Write the copy and original RTE file to the respective filenames:
2841 $this->writeFileVerify($copyDestName, $cfg['file_ID'], true);
2842 $this->writeFileVerify($origDestName, $fileHeaderInfo['RTE_ORIG_ID'], true);
2843 // Return the relative path of the copy file name:
2844 return PathUtility::stripPathSitePrefix($copyDestName);
2845 }
2846 } else {
2847 $this->error('ERROR: Could not find original file ID');
2848 }
2849 } else {
2850 $this->error('ERROR: The destination filenames "' . $copyDestName . '" and "' . $origDestName . '" either existed or have non-valid names');
2851 }
2852 } else {
2853 $this->error('ERROR: "' . PATH_site . $dirPrefix . '" was not a directory, so could not process file "' . $relFileName . '"');
2854 }
2855 } elseif (GeneralUtility::isFirstPartOfStr($dirPrefix, $this->fileadminFolderName . '/')) {
2856 // File in fileadmin/ folder:
2857 // Create file (and possible resources)
2858 $newFileName = $this->processSoftReferences_saveFile_createRelFile($dirPrefix, PathUtility::basename($relFileName), $cfg['file_ID'], $table, $uid);
2859 if (strlen($newFileName)) {
2860 $relFileName = $newFileName;
2861 } else {
2862 $this->error('ERROR: No new file created for "' . $relFileName . '"');
2863 }
2864 } else {
2865 $this->error('ERROR: Sorry, cannot operate on non-RTE files which are outside the fileadmin folder.');
2866 }
2867 } else {
2868 $this->error('ERROR: Could not find file ID in header.');
2869 }
2870 // Return (new) filename relative to PATH_site:
2871 return $relFileName;
2872 }
2873
2874 /**
2875 * Create file in directory and return the new (unique) filename
2876 *
2877 * @param string $origDirPrefix Directory prefix, relative, with trailing slash
2878 * @param string $fileName Filename (without path)
2879 * @param string $fileID File ID from import memory
2880 * @param string $table Table for which the processing occurs
2881 * @param string $uid UID of record from table
2882 * @return string|NULL New relative filename, if any
2883 */
2884 public function processSoftReferences_saveFile_createRelFile($origDirPrefix, $fileName, $fileID, $table, $uid)
2885 {
2886 // If the fileID map contains an entry for this fileID then just return the relative filename of that entry;
2887 // we don't want to write another unique filename for this one!
2888 if (isset($this->fileIDMap[$fileID])) {
2889 return PathUtility::stripPathSitePrefix($this->fileIDMap[$fileID]);
2890 }
2891 if ($this->legacyImport) {
2892 // set dirPrefix to fileadmin because the right target folder is set and checked for permissions later
2893 $dirPrefix = $this->fileadminFolderName . '/';
2894 } else {
2895 // Verify FileMount access to dir-prefix. Returns the best alternative relative path if any
2896 $dirPrefix = $this->verifyFolderAccess($origDirPrefix);
2897 }
2898 if ($dirPrefix && (!$this->update || $origDirPrefix === $dirPrefix) && $this->checkOrCreateDir($dirPrefix)) {
2899 $fileHeaderInfo = $this->dat['header']['files'][$fileID];
2900 $updMode = $this->update && $this->import_mapId[$table][$uid] === $uid && $this->import_mode[$table . ':' . $uid] !== 'as_new';
2901 // Create new name for file:
2902 // Must have same ID in map array (just for security, is not really needed) and NOT be set "as_new".
2903
2904 // Write main file:
2905 if ($this->legacyImport) {
2906 $fileWritten = $this->writeSysFileResourceForLegacyImport($fileName, $fileID);
2907 if ($fileWritten) {
2908 $newName = 'file:' . $fileName;
2909 return $newName;
2910 // no support for HTML/CSS file resources attached ATM - see below
2911 }
2912 } else {
2913 if ($updMode) {
2914 $newName = PATH_site . $dirPrefix . $fileName;
2915 } else {
2916 // Create unique filename:
2917 $fileProcObj = $this->getFileProcObj();
2918 $newName = $fileProcObj->getUniqueName($fileName, PATH_site . $dirPrefix);
2919 }
2920 if ($this->writeFileVerify($newName, $fileID)) {
2921 // If the resource was an HTML/CSS file with resources attached, we will write those as well!
2922 if (is_array($fileHeaderInfo['EXT_RES_ID'])) {
2923 $tokenizedContent = $this->dat['files'][$fileID]['tokenizedContent'];
2924 $tokenSubstituted = false;
2925 $fileProcObj = $this->getFileProcObj();
2926 if ($updMode) {
2927 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
2928 if ($this->dat['files'][$res_fileID]['filename']) {
2929 // Resolve original filename:
2930 $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
2931 $absResourceFileName = GeneralUtility::resolveBackPath(PATH_site . $origDirPrefix . $relResourceFileName);
2932 $absResourceFileName = GeneralUtility::getFileAbsFileName($absResourceFileName);
2933 if ($absResourceFileName && GeneralUtility::isFirstPartOfStr($absResourceFileName, PATH_site . $this->fileadminFolderName . '/')) {
2934 $destDir = PathUtility::stripPathSitePrefix(PathUtility::dirname($absResourceFileName) . '/');
2935 if ($this->verifyFolderAccess($destDir, true) && $this->checkOrCreateDir($destDir)) {
2936 $this->writeFileVerify($absResourceFileName, $res_fileID);
2937 } else {
2938 $this->error('ERROR: Could not create file in directory "' . $destDir . '"');
2939 }
2940 } else {
2941 $this->error('ERROR: Could not resolve path for "' . $relResourceFileName . '"');
2942 }
2943 $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
2944 $tokenSubstituted = true;
2945 }
2946 }
2947 } else {
2948 // Create the resouces directory name (filename without extension, suffixed "_FILES")
2949 $resourceDir = PathUtility::dirname($newName) . '/' . preg_replace('/\\.[^.]*$/', '', PathUtility::basename($newName)) . '_FILES';
2950 if (GeneralUtility::mkdir($resourceDir)) {
2951 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
2952 if ($this->dat['files'][$res_fileID]['filename']) {
2953 $absResourceFileName = $fileProcObj->getUniqueName($this->dat['files'][$res_fileID]['filename'], $resourceDir);
2954 $relResourceFileName = substr($absResourceFileName, strlen(PathUtility::dirname($resourceDir)) + 1);
2955 $this->writeFileVerify($absResourceFileName, $res_fileID);
2956 $tokenizedContent =