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