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