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