Import::writePages()   B
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 45
rs 8.8657
c 0
b 0
f 0
cc 6
nc 5
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Impexp;
19
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
22
use TYPO3\CMS\Core\Core\Environment;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\DataHandling\DataHandler;
25
use TYPO3\CMS\Core\Exception;
26
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
27
use TYPO3\CMS\Core\Resource\File;
28
use TYPO3\CMS\Core\Resource\ResourceFactory;
29
use TYPO3\CMS\Core\Resource\ResourceStorage;
30
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
31
use TYPO3\CMS\Core\Service\FlexFormService;
32
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Core\Utility\MathUtility;
35
use TYPO3\CMS\Core\Utility\PathUtility;
36
use TYPO3\CMS\Core\Utility\StringUtility;
37
use TYPO3\CMS\Impexp\Exception\ImportFailedException;
38
use TYPO3\CMS\Impexp\Exception\LoadingFileFailedException;
39
use TYPO3\CMS\Impexp\Exception\PrerequisitesNotMetException;
40
41
/**
42
 * T3D file Import library (TYPO3 Record Document)
43
 *
44
 * @internal This class is not considered part of the public TYPO3 API.
45
 */
46
class Import extends ImportExport
47
{
48
    public const IMPORT_MODE_FORCE_UID = 'force_uid';
49
    public const IMPORT_MODE_AS_NEW = 'as_new';
50
    public const IMPORT_MODE_EXCLUDE = 'exclude';
51
    public const IMPORT_MODE_IGNORE_PID = 'ignore_pid';
52
    public const IMPORT_MODE_RESPECT_PID = 'respect_pid';
53
54
    public const SOFTREF_IMPORT_MODE_EXCLUDE = 'exclude';
55
    public const SOFTREF_IMPORT_MODE_EDITABLE = 'editable';
56
57
    /**
58
     * @var string
59
     */
60
    protected $mode = 'import';
61
62
    /**
63
     * Used to register the forced UID values for imported records that we want
64
     * to create with the same UIDs as in the import file. Admin-only feature.
65
     *
66
     * @var array
67
     */
68
    protected $suggestedInsertUids = [];
69
70
    /**
71
     * Disable logging when importing
72
     *
73
     * @var bool
74
     */
75
    protected $enableLogging = false;
76
77
    /**
78
     * Keys are [tablename]:[new NEWxxx ids (or when updating it is uids)]
79
     * while values are arrays with table/uid of the original record it is based on.
80
     * With the array keys the new ids can be looked up inside DataHandler
81
     *
82
     * @var array
83
     */
84
    protected $importNewId = [];
85
86
    /**
87
     * Page id map for page tree (import)
88
     *
89
     * @var array
90
     */
91
    protected $importNewIdPids = [];
92
93
    /**
94
     * @var bool
95
     */
96
    protected $decompressionAvailable = false;
97
98
    /**
99
     * @var array
100
     */
101
    private $supportedFileExtensions = [];
102
103
    /**
104
     * @var bool
105
     */
106
    protected $isFilesSavedOutsideImportFile = false;
107
108
    /**
109
     * The constructor
110
     */
111
    public function __construct()
112
    {
113
        parent::__construct();
114
        $this->decompressionAvailable = function_exists('gzuncompress');
115
    }
116
117
    /**************************
118
     * File Input
119
     *************************/
120
121
    /**
122
     * Loads the TYPO3 import file $fileName into memory.
123
     *
124
     * @param string $fileName File path, has to be within the TYPO3's base folder
125
     * @param bool $all If set, all information is loaded (header, records and files). Otherwise the default is to read only the header information
126
     * @throws LoadingFileFailedException
127
     */
128
    public function loadFile(string $fileName, bool $all = false): void
129
    {
130
        $filePath = GeneralUtility::getFileAbsFileName($fileName);
131
132
        if (empty($filePath)) {
133
            $this->addError('File path is not valid: ' . $fileName);
134
        } elseif (!@is_file($filePath)) {
135
            $this->addError('File not found: ' . $filePath);
136
        }
137
138
        if ($this->hasErrors()) {
139
            throw new LoadingFileFailedException(
140
                sprintf('Loading of the import file "%s" failed.', $fileName),
141
                1484484619
142
            );
143
        }
144
145
        $pathInfo = pathinfo($filePath);
146
        $fileExtension = strtolower($pathInfo['extension']);
147
148
        if (!in_array($fileExtension, $this->getSupportedFileExtensions(), true)) {
149
            $this->addError(
150
                sprintf(
151
                    'File extension "%s" is not valid. Supported file extensions are %s.',
152
                    $fileExtension,
153
                    implode(', ', array_map(static function ($supportedFileExtension) {
154
                        return '"' . $supportedFileExtension . '"';
155
                    }, $this->getSupportedFileExtensions()))
156
                )
157
            );
158
        }
159
160
        if ($this->hasErrors() === false) {
0 ignored issues
show
introduced by
The condition $this->hasErrors() === false is always true.
Loading history...
161
            if (@is_dir($filePath . '.files')) {
162
                if (GeneralUtility::isAllowedAbsPath($filePath . '.files')) {
163
                    // copy the folder lowlevel to typo3temp, because the files would be deleted after import
164
                    GeneralUtility::copyDirectory($filePath . '.files', $this->getOrCreateTemporaryFolderName());
165
                } else {
166
                    $this->addError('External import files for the given import source is currently not supported.');
167
                }
168
                $this->isFilesSavedOutsideImportFile = true;
169
            } else {
170
                $this->isFilesSavedOutsideImportFile = false;
171
            }
172
            if ($fileExtension === 'xml') {
173
                $xmlContent = (string)file_get_contents($filePath);
174
                if (strlen($xmlContent)) {
175
                    $this->dat = GeneralUtility::xml2array($xmlContent, '', true);
176
                    if (is_array($this->dat)) {
177
                        if ($this->dat['_DOCUMENT_TAG'] === 'T3RecordDocument' && is_array($this->dat['header'] ?? null) && is_array($this->dat['records'] ?? null)) {
178
                            $this->loadInit();
179
                        } else {
180
                            $this->addError('XML file did not contain proper XML for TYPO3 Import');
181
                        }
182
                    } else {
183
                        $this->addError('XML could not be parsed: ' . $this->dat);
184
                    }
185
                } else {
186
                    $this->addError('Error opening file: ' . $filePath);
187
                }
188
            } elseif ($fileExtension === 't3d') {
189
                if ($fd = fopen($filePath, 'rb')) {
190
                    $this->dat['header'] = $this->getNextFilePart($fd, true, 'header');
191
                    if ($all) {
192
                        $this->dat['records'] = $this->getNextFilePart($fd, true, 'records');
193
                        $this->dat['files'] = $this->getNextFilePart($fd, true, 'files');
194
                        $this->dat['files_fal'] = $this->getNextFilePart($fd, true, 'files_fal');
195
                    }
196
                    $this->loadInit();
197
                    fclose($fd);
198
                } else {
199
                    $this->addError('Error opening file: ' . $filePath);
200
                }
201
            }
202
        }
203
204
        if ($this->hasErrors()) {
205
            throw new LoadingFileFailedException(
206
                sprintf('Loading of the import file "%s" failed.', $fileName),
207
                1484484620
208
            );
209
        }
210
    }
211
212
    /**
213
     * @return array
214
     */
215
    public function getSupportedFileExtensions(): array
216
    {
217
        if (empty($this->supportedFileExtensions)) {
218
            $supportedFileExtensions = [];
219
            $supportedFileExtensions[] = 'xml';
220
            $supportedFileExtensions[] = 't3d';
221
            $this->supportedFileExtensions = $supportedFileExtensions;
222
        }
223
        return $this->supportedFileExtensions;
224
    }
225
226
    /**
227
     * Extracts the next content part of the T3D file
228
     *
229
     * @param resource $fd Import file pointer
230
     * @param bool $unserialize If set, the returned content is deserialized into an array, otherwise you get the raw string
231
     * @param string $name For error messages this indicates the section of the problem.
232
     * @return array|string|null Data array if unserializing or
233
     *                              data string if not unserializing or
234
     *                              NULL in case of an error
235
     *
236
     * @see loadFile()
237
     */
238
    protected function getNextFilePart($fd, bool $unserialize = false, string $name = '')
239
    {
240
        $headerLength = 32 + 1 + 1 + 1 + 10 + 1;
241
        $headerString = fread($fd, $headerLength);
242
        if (empty($headerString)) {
243
            $this->addError('File does not contain data for "' . $name . '"');
244
            return null;
245
        }
246
247
        $header = explode(':', $headerString);
248
        if (str_contains($header[0], 'Warning')) {
249
            $this->addError('File read error: Warning message in file. (' . $headerString . fgets($fd) . ')');
250
            return null;
251
        }
252
        if ((string)$header[3] !== '') {
253
            $this->addError('File read error: InitString had a wrong length. (' . $name . ')');
254
            return null;
255
        }
256
257
        $dataString = (string)fread($fd, (int)$header[2]);
258
        $isDataCompressed = $header[1] === '1';
259
        fread($fd, 1);
260
        if (!hash_equals($header[0], md5($dataString))) {
261
            $this->addError('MD5 check failed (' . $name . ')');
262
            return null;
263
        }
264
265
        if ($isDataCompressed) {
266
            if ($this->decompressionAvailable) {
267
                $dataString = (string)gzuncompress($dataString);
268
            } else {
269
                $this->addError('Content read error: This file requires decompression, ' .
270
                    'but this server does not offer gzcompress()/gzuncompress() functions.');
271
                return null;
272
            }
273
        }
274
275
        return $unserialize ? unserialize($dataString, ['allowed_classes' => false]) : $dataString;
276
    }
277
278
    /**
279
     * Setting up the object based on the recently loaded ->dat array
280
     */
281
    protected function loadInit(): void
282
    {
283
        $this->relStaticTables = (array)($this->dat['header']['relStaticTables'] ?? []);
284
        $this->excludeMap = (array)($this->dat['header']['excludeMap'] ?? []);
285
        $this->softrefCfg = (array)($this->dat['header']['softrefCfg'] ?? []);
286
    }
287
288
    public function getMetaData(): array
289
    {
290
        return $this->dat['header']['meta'] ?? [];
291
    }
292
293
    /***********************
294
     * Import
295
     ***********************/
296
297
    /**
298
     * Checks all requirements that must be met before import.
299
     *
300
     * @throws PrerequisitesNotMetException
301
     */
302
    public function checkImportPrerequisites(): void
303
    {
304
        // Check #1: Extension dependencies
305
        $extKeysToInstall = [];
306
        if (isset($this->dat['header']['extensionDependencies'])) {
307
            foreach ($this->dat['header']['extensionDependencies'] as $extKey) {
308
                if (!empty($extKey) && !ExtensionManagementUtility::isLoaded($extKey)) {
309
                    $extKeysToInstall[] = $extKey;
310
                }
311
            }
312
        }
313
        if (!empty($extKeysToInstall)) {
314
            $this->addError(
315
                sprintf(
316
                    'Before you can import this file you need to install the extensions "%s".',
317
                    implode('", "', $extKeysToInstall)
318
                )
319
            );
320
        }
321
322
        // Check #2: Presence of imported storage paths
323
        if (!empty($this->dat['header']['records']['sys_file_storage'])) {
324
            foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) {
325
                $storageRecord = &$this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data'];
326
                if ($storageRecord['driver'] === 'Local'
327
                    && $storageRecord['is_writable']
328
                    && $storageRecord['is_online']
329
                ) {
330
                    $storageMapUid = -1;
331
                    foreach ($this->storages as $storage) {
332
                        if ($this->isEquivalentStorage($storage, $storageRecord)) {
333
                            $storageMapUid = $storage->getUid();
334
                            break;
335
                        }
336
                    }
337
                    // The storage from the import does not have an equivalent storage
338
                    // in the current instance (same driver, same path, etc.). Before
339
                    // the storage record can get inserted later on take care the path
340
                    // it points to really exists and is accessible.
341
                    if ($storageMapUid === -1) {
342
                        // Unset the storage record UID when trying to create the storage object
343
                        // as the record does not already exist in database. The constructor of the
344
                        // storage object will check whether the target folder exists and set the
345
                        // isOnline flag depending on the outcome.
346
                        $storageRecordWithUid0 = $storageRecord;
347
                        $storageRecordWithUid0['uid'] = 0;
348
                        $storageObject = $this->getStorageRepository()->createStorageObject($storageRecordWithUid0);
349
                        if (!$storageObject->isOnline()) {
350
                            $configuration = $storageObject->getConfiguration();
351
                            $this->addError(
352
                                sprintf(
353
                                    'The file storage "%s" does not exist. ' .
354
                                    'Please create the directory prior to starting the import!',
355
                                    $storageObject->getName() . $configuration['basePath']
356
                                )
357
                            );
358
                        }
359
                    }
360
                }
361
            }
362
        }
363
364
        if ($this->hasErrors()) {
365
            throw new PrerequisitesNotMetException(
366
                'Prerequisites for file import are not met.',
367
                1484484612
368
            );
369
        }
370
    }
371
372
    /**
373
     * Imports the memory data into the TYPO3 database.
374
     *
375
     * @throws ImportFailedException
376
     */
377
    public function importData(): void
378
    {
379
        $this->initializeImport();
380
381
        // Write sys_file_storages first
382
        $this->writeSysFileStorageRecords();
383
        // Write sys_file records and write the binary file data
384
        $this->writeSysFileRecords();
385
        // Write records, first pages, then the rest
386
        // Fields with "hard" relations to database, files and flexform fields are kept empty during this run
387
        $this->writePages();
388
        $this->writeRecords();
389
        // Finally all the file and database record references must be fixed. This is done after all records have supposedly
390
        // been written to database. $this->importMapId will indicate two things:
391
        // 1) that a record WAS written to db and
392
        // 2) that it has got a new id-number.
393
        $this->setRelations();
394
        // And when all database relations are in place, we can fix file and database relations in flexform fields
395
        // - since data structures often depend on relations to a DS record:
396
        $this->setFlexFormRelations();
397
        // Finally, traverse all records and process soft references with substitution attributes.
398
        $this->processSoftReferences();
399
        // Cleanup
400
        $this->removeTemporaryFolderName();
401
402
        if ($this->hasErrors()) {
403
            throw new ImportFailedException('The import has failed.', 1484484613);
404
        }
405
    }
406
407
    /**
408
     * Initialize all settings for the import
409
     */
410
    protected function initializeImport(): void
411
    {
412
        $this->doesImport = true;
413
        $this->importMapId = [];
414
        $this->importNewId = [];
415
        $this->importNewIdPids = [];
416
    }
417
418
    /**
419
     * Imports the sys_file_storage records from memory data.
420
     */
421
    protected function writeSysFileStorageRecords(): void
422
    {
423
        if (!isset($this->dat['header']['records']['sys_file_storage'])) {
424
            return;
425
        }
426
427
        $importData = [];
428
429
        $storageUidsToBeResetToDefaultStorage = [];
430
        foreach ($this->dat['header']['records']['sys_file_storage'] as $sysFileStorageUid => $_) {
431
            $storageRecord = &$this->dat['records']['sys_file_storage:' . $sysFileStorageUid]['data'];
432
            if ($storageRecord['driver'] === 'Local'
433
                && $storageRecord['is_writable']
434
                && $storageRecord['is_online']
435
            ) {
436
                foreach ($this->storages as $storage) {
437
                    if ($this->isEquivalentStorage($storage, $storageRecord)) {
438
                        $this->importMapId['sys_file_storage'][$sysFileStorageUid] = $storage->getUid();
439
                        break;
440
                    }
441
                }
442
443
                if (!isset($this->importMapId['sys_file_storage'][$sysFileStorageUid])) {
444
                    // Local, writable and online storage. May be used later for writing files.
445
                    // Does not currently exist, mark the storage for import.
446
                    $this->addSingle($importData, 'sys_file_storage', $sysFileStorageUid, 0);
447
                }
448
            } else {
449
                // Storage with non-local drivers can be imported, but must not be used to save files as you cannot
450
                // be sure that this is supported. In this case the default storage is used. Non-writable and
451
                // non-online storage may be created as duplicates because you were unable to check the detailed
452
                // configuration options at that time.
453
                $this->addSingle($importData, 'sys_file_storage', $sysFileStorageUid, 0);
454
                $storageUidsToBeResetToDefaultStorage[] = $sysFileStorageUid;
455
            }
456
        }
457
458
        // Write new storages to the database
459
        $dataHandler = $this->createDataHandler();
460
        // Because all records are submitted in the correct order with positive pid numbers,
461
        // we should internally reverse the order of submission.
462
        $dataHandler->reverseOrder = true;
463
        $dataHandler->isImporting = true;
464
        $dataHandler->start($importData, []);
465
        $dataHandler->process_datamap();
466
        $this->addToMapId($importData, $dataHandler->substNEWwithIDs);
467
468
        // Refresh internal storage representation after potential storage import
469
        $this->fetchStorages();
470
471
        // Map references of non-local / non-writable / non-online storages to the default storage
472
        $defaultStorageUid = $this->defaultStorage !== null ? $this->defaultStorage->getUid() : null;
473
        foreach ($storageUidsToBeResetToDefaultStorage as $storageUidToBeResetToDefaultStorage) {
474
            $this->importMapId['sys_file_storage'][$storageUidToBeResetToDefaultStorage] = $defaultStorageUid;
475
        }
476
477
        // Unset the sys_file_storage records to prevent an import in writeRecords()
478
        unset($this->dat['header']['records']['sys_file_storage']);
479
    }
480
481
    /**
482
     * Determines whether the passed storage object and the storage record (sys_file_storage) can be considered
483
     * equivalent during the import.
484
     *
485
     * @param ResourceStorage $storageObject The storage object which should get compared
486
     * @param array $storageRecord The storage record which should get compared
487
     * @return bool Returns TRUE if both storage representations can be considered equal
488
     */
489
    protected function isEquivalentStorage(ResourceStorage &$storageObject, array &$storageRecord): bool
490
    {
491
        if ($storageObject->getDriverType() === $storageRecord['driver']
492
            && (bool)$storageObject->isWritable() === (bool)$storageRecord['is_writable']
493
            && (bool)$storageObject->isOnline() === (bool)$storageRecord['is_online']
494
        ) {
495
            $storageRecordConfiguration = GeneralUtility::makeInstance(FlexFormService::class)
496
                ->convertFlexFormContentToArray($storageRecord['configuration'] ?? '');
497
            $storageObjectConfiguration = $storageObject->getConfiguration();
498
            if ($storageRecordConfiguration['pathType'] === $storageObjectConfiguration['pathType']
499
                && $storageRecordConfiguration['basePath'] === $storageObjectConfiguration['basePath']
500
            ) {
501
                return true;
502
            }
503
        }
504
        return false;
505
    }
506
507
    /**
508
     * Imports the sys_file records and the binary files data from internal data array.
509
     */
510
    protected function writeSysFileRecords(): void
511
    {
512
        if (!isset($this->dat['header']['records']['sys_file'])) {
513
            return;
514
        }
515
516
        $this->addGeneralErrorsByTable('sys_file');
517
518
        $temporaryFolder = $this->getOrCreateTemporaryFolderName();
519
        $sanitizedFolderMappings = [];
520
521
        foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) {
522
            $fileRecord = &$this->dat['records']['sys_file:' . $sysFileUid]['data'];
523
524
            $temporaryFile = null;
525
            $temporaryFilePath = $temporaryFolder . '/' . $fileRecord['sha1'];
526
527
            if ($this->isFilesSavedOutsideImportFile) {
528
                if (is_file($temporaryFilePath) && sha1_file($temporaryFilePath) === $fileRecord['sha1']) {
529
                    $temporaryFile = $temporaryFilePath;
530
                } else {
531
                    $this->addError(sprintf(
532
                        'Error: Temporary file %s could not be found or does not match the checksum!',
533
                        $temporaryFilePath
534
                    ));
535
                    continue;
536
                }
537
            } else {
538
                $fileId = md5($fileRecord['storage'] . ':' . $fileRecord['identifier_hash']);
539
                if (isset($this->dat['files_fal'][$fileId]['content'])) {
540
                    $fileInfo = &$this->dat['files_fal'][$fileId];
541
                    if (GeneralUtility::writeFile($temporaryFilePath, $fileInfo['content'])) {
542
                        clearstatcache();
543
                        $temporaryFile = $temporaryFilePath;
544
                    } else {
545
                        $this->addError(sprintf(
546
                            'Error: Temporary file %s was not written as it should have been!',
547
                            $temporaryFilePath
548
                        ));
549
                        continue;
550
                    }
551
                } else {
552
                    $this->addError(sprintf('Error: No file found for ID %s', $fileId));
553
                    continue;
554
                }
555
            }
556
557
            $storageUid = $this->importMapId['sys_file_storage'][$fileRecord['storage']] ?? $fileRecord['storage'];
558
            if (isset($this->storagesAvailableForImport[$storageUid])) {
559
                $storage = $this->storagesAvailableForImport[$storageUid];
560
            } elseif ($storageUid === 0 || $storageUid === '0') {
561
                $storage = $this->getStorageRepository()->findByUid(0);
562
            } elseif ($this->defaultStorage !== null) {
563
                $storage = $this->defaultStorage;
564
            } else {
565
                $this->addError(sprintf(
566
                    'Error: No storage available for the file "%s" with storage uid "%s"',
567
                    $fileRecord['identifier'],
568
                    $fileRecord['storage']
569
                ));
570
                continue;
571
            }
572
573
            /** @var File $file */
574
            $file = null;
575
            try {
576
                if ($storage->hasFile($fileRecord['identifier'])) {
577
                    $file = $storage->getFile($fileRecord['identifier']);
578
                    if ($file->getSha1() !== $fileRecord['sha1']) {
579
                        $file = null;
580
                    }
581
                }
582
            } catch (Exception $e) {
583
                // @todo: Can this exception be thrown anywhere?
584
                $file = null;
585
            }
586
587
            if ($file === null) {
588
                $folderName = PathUtility::dirname(ltrim($fileRecord['identifier'], '/'));
589
                if (in_array($folderName, $sanitizedFolderMappings, true)) {
590
                    $folderName = $sanitizedFolderMappings[$folderName];
591
                }
592
                if (!$storage->hasFolder($folderName)) {
593
                    try {
594
                        $importFolder = $storage->createFolder($folderName);
595
                        if ($importFolder->getIdentifier() !== $folderName && !in_array($folderName, $sanitizedFolderMappings, true)) {
596
                            $sanitizedFolderMappings[$folderName] = $importFolder->getIdentifier();
597
                        }
598
                    } catch (Exception $e) {
599
                        $this->addError(sprintf(
600
                            'Error: Folder "%s" could not be created for file "%s" with storage uid "%s"',
601
                            $folderName,
602
                            $fileRecord['identifier'],
603
                            $fileRecord['storage']
604
                        ));
605
                        continue;
606
                    }
607
                } else {
608
                    $importFolder = $storage->getFolder($folderName);
609
                }
610
611
                $this->callHook('before_addSysFileRecord', [
612
                    'fileRecord' => $fileRecord,
613
                    'importFolder' => $importFolder,
614
                    'temporaryFile' => $temporaryFile,
615
                ]);
616
617
                try {
618
                    $file = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']);
619
                } catch (Exception $e) {
620
                    $this->addError(sprintf(
621
                        'Error: File could not be added to the storage: "%s" with storage uid "%s"',
622
                        $fileRecord['identifier'],
623
                        $fileRecord['storage']
624
                    ));
625
                    continue;
626
                }
627
628
                if ($file->getSha1() !== $fileRecord['sha1']) {
629
                    $this->addError(sprintf(
630
                        'Error: The hash of the written file is not identical to the import data! ' .
631
                        'File could be corrupted! File: "%s" with storage uid "%s"',
632
                        $fileRecord['identifier'],
633
                        $fileRecord['storage']
634
                    ));
635
                }
636
            }
637
638
            // save the new uid in the import id map
639
            $this->importMapId['sys_file'][$fileRecord['uid']] = $file->getUid();
640
            $this->fixUidLocalInSysFileReferenceRecords((int)$fileRecord['uid'], $file->getUid());
641
        }
642
643
        // unset the sys_file records to prevent an import in writeRecords()
644
        unset($this->dat['header']['records']['sys_file']);
645
        // remove all sys_file_reference records that point to file records which are unknown
646
        // in the system to prevent exceptions
647
        $this->removeSysFileReferenceRecordsWithRelationToMissingFile();
648
    }
649
650
    /**
651
     * Normally the importer works like the following:
652
     * Step 1: import the records with cleared field values of relation fields (see addSingle())
653
     * Step 2: update the records with the right relation ids (see setRelations())
654
     *
655
     * In step 2 the saving fields of type "relation to sys_file_reference" checks the related sys_file_reference
656
     * record (created in step 1) with the FileExtensionFilter for matching file extensions of the related file.
657
     * To make this work correct, the uid_local of sys_file_reference records has to be not empty AND has to
658
     * relate to the correct (imported) sys_file record uid!
659
     *
660
     * This is fixed here.
661
     *
662
     * @param int $oldFileUid
663
     * @param int $newFileUid
664
     */
665
    protected function fixUidLocalInSysFileReferenceRecords(int $oldFileUid, int $newFileUid): void
666
    {
667
        if (!isset($this->dat['header']['records']['sys_file_reference'])) {
668
            return;
669
        }
670
671
        foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
672
            if (!isset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'])
673
                && $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] == $oldFileUid
674
            ) {
675
                $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['hasBeenMapped'] = true;
676
                $this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data']['uid_local'] = $newFileUid;
677
            }
678
        }
679
    }
680
681
    /**
682
     * Removes all sys_file_reference records from the import data array that are pointing to sys_file records which
683
     * are missing in the import data to prevent exceptions on checking the related file started by the DataHandler.
684
     */
685
    protected function removeSysFileReferenceRecordsWithRelationToMissingFile(): void
686
    {
687
        if (!isset($this->dat['header']['records']['sys_file_reference'])) {
688
            return;
689
        }
690
691
        foreach ($this->dat['header']['records']['sys_file_reference'] as $sysFileReferenceUid => $_) {
692
            $fileReferenceRecord = &$this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]['data'];
693
            if (!in_array($fileReferenceRecord['uid_local'], (array)($this->importMapId['sys_file'] ?? []))) {
694
                unset($this->dat['header']['records']['sys_file_reference'][$sysFileReferenceUid]);
695
                unset($this->dat['records']['sys_file_reference:' . $sysFileReferenceUid]);
696
                $this->addError(sprintf(
697
                    'Error: sys_file_reference record "%s" with relation to sys_file record "%s"'
698
                    . ', which is not part of the import data, was not imported.',
699
                    $sysFileReferenceUid,
700
                    $fileReferenceRecord['uid_local']
701
                ));
702
            }
703
        }
704
    }
705
706
    /**
707
     * Writing page tree / pages to database:
708
     * If the operation is an update operation, the root of the page tree inside will be moved to $this->pid
709
     * unless it is the same as the root page from the import.
710
     *
711
     * @see writeRecords()
712
     */
713
    protected function writePages(): void
714
    {
715
        if (!isset($this->dat['header']['records']['pages'])) {
716
            return;
717
        }
718
719
        $importData = [];
720
721
        // Add page tree
722
        $remainingPages = $this->dat['header']['records']['pages'];
723
        if (is_array($this->dat['header']['pagetree'] ?? null)) {
724
            $pageList = [];
725
            $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList);
726
            foreach ($pageList as $pageUid => $_) {
727
                $pid = $this->dat['header']['records']['pages'][$pageUid]['pid'] ?? null;
728
                $pid = $this->importNewIdPids[$pid] ?? $this->pid;
729
                $this->addSingle($importData, 'pages', (int)$pageUid, $pid);
730
                unset($remainingPages[$pageUid]);
731
            }
732
        }
733
734
        // Add remaining pages on root level
735
        if (!empty($remainingPages)) {
736
            foreach ($remainingPages as $pageUid => $_) {
737
                $this->addSingle($importData, 'pages', (int)$pageUid, $this->pid);
738
            }
739
        }
740
741
        // Write pages to the database
742
        $dataHandler = $this->createDataHandler();
743
        $dataHandler->isImporting = true;
744
        $this->callHook('before_writeRecordsPages', [
745
            'tce' => &$dataHandler,
746
            'data' => &$importData,
747
        ]);
748
        $dataHandler->suggestedInsertUids = $this->suggestedInsertUids;
749
        $dataHandler->start($importData, []);
750
        $dataHandler->process_datamap();
751
        $this->callHook('after_writeRecordsPages', [
752
            'tce' => &$dataHandler,
753
        ]);
754
        $this->addToMapId($importData, $dataHandler->substNEWwithIDs);
755
756
        // Sort pages
757
        $this->writePagesOrder();
758
    }
759
760
    /**
761
     * Organize all updated pages in page tree so they are related like in the import file.
762
     * Only used for updates.
763
     *
764
     * @see writePages()
765
     * @see writeRecordsOrder()
766
     */
767
    protected function writePagesOrder(): void
768
    {
769
        if (!$this->update || !is_array($this->dat['header']['pagetree'] ?? null)) {
770
            return;
771
        }
772
773
        $importCmd = [];
774
775
        // Get uid-pid relations and traverse them in order to map to possible new IDs
776
        $pageList = [];
777
        $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList);
778
        foreach ($pageList as $pageUid => $pagePid) {
779
            if ($pagePid >= 0 && $this->doRespectPid('pages', $pageUid)) {
780
                // If the page has been assigned a new ID (because it was created), use that instead!
781
                if (!MathUtility::canBeInterpretedAsInteger($this->importNewIdPids[$pageUid])) {
782
                    if ($this->importMapId['pages'][$pageUid]) {
783
                        $mappedUid = $this->importMapId['pages'][$pageUid];
784
                        $importCmd['pages'][$mappedUid]['move'] = $pagePid;
785
                    }
786
                } else {
787
                    $importCmd['pages'][$pageUid]['move'] = $pagePid;
788
                }
789
            }
790
        }
791
792
        // Move pages in the database
793
        if (!empty($importCmd)) {
794
            $dataHandler = $this->createDataHandler();
795
            $this->callHook('before_writeRecordsPagesOrder', [
796
                'tce' => &$dataHandler,
797
                'data' => &$importCmd,
798
            ]);
799
            $dataHandler->start([], $importCmd);
800
            $dataHandler->process_cmdmap();
801
            $this->callHook('after_writeRecordsPagesOrder', [
802
                'tce' => &$dataHandler,
803
            ]);
804
        }
805
    }
806
807
    /**
808
     * Checks if the position of an updated record is configured to be corrected.
809
     * This can be disabled globally and changed individually for elements.
810
     *
811
     * @param string $table Table name
812
     * @param int $uid Record UID
813
     * @return bool TRUE if the position of the record should be updated to match the one in the import structure
814
     */
815
    protected function doRespectPid(string $table, int $uid): bool
816
    {
817
        return ($this->importMode[$table . ':' . $uid] ?? '') !== self::IMPORT_MODE_IGNORE_PID &&
818
            (!$this->globalIgnorePid || ($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_RESPECT_PID);
819
    }
820
821
    /**
822
     * Write all database records except pages (written in writePages())
823
     *
824
     * @see writePages()
825
     */
826
    protected function writeRecords(): void
827
    {
828
        $importData = [];
829
830
        // Write the rest of the records
831
        if (is_array($this->dat['header']['records'] ?? null)) {
832
            foreach ($this->dat['header']['records'] as $table => $records) {
833
                $this->addGeneralErrorsByTable($table);
834
                if ($table !== 'pages') {
835
                    foreach ($records as $uid => $record) {
836
                        // PID: Set the main $this->pid, unless a NEW-id is found
837
                        $pid = isset($this->importMapId['pages'][$record['pid']])
838
                            ? (int)$this->importMapId['pages'][$record['pid']]
839
                            : $this->pid;
840
                        if (isset($GLOBALS['TCA'][$table]['ctrl']['rootLevel'])) {
841
                            $rootLevelSetting = (int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'];
842
                            if ($rootLevelSetting === 1) {
843
                                $pid = 0;
844
                            } elseif ($rootLevelSetting === 0 && $pid === 0) {
845
                                $this->addError('Error: Record type ' . $table . ' is not allowed on pid 0');
846
                                continue;
847
                            }
848
                        }
849
                        // Add record
850
                        $this->addSingle($importData, $table, $uid, $pid);
851
                    }
852
                }
853
            }
854
        } else {
855
            $this->addError('Error: No records defined in internal data array.');
856
        }
857
858
        // Write records to the database
859
        $dataHandler = $this->createDataHandler();
860
        $this->callHook('before_writeRecordsRecords', [
861
            'tce' => &$dataHandler,
862
            'data' => &$importData,
863
        ]);
864
        $dataHandler->suggestedInsertUids = $this->suggestedInsertUids;
865
        // Because all records are submitted in the correct order with positive pid numbers,
866
        // we should internally reverse the order of submission.
867
        $dataHandler->reverseOrder = true;
868
        $dataHandler->isImporting = true;
869
        $dataHandler->start($importData, []);
870
        $dataHandler->process_datamap();
871
        $this->callHook('after_writeRecordsRecords', [
872
            'tce' => &$dataHandler,
873
        ]);
874
        $this->addToMapId($importData, $dataHandler->substNEWwithIDs);
875
876
        // Sort records
877
        $this->writeRecordsOrder();
878
    }
879
880
    /**
881
     * Organize all updated records so they are related like in the import file.
882
     * Only used for updates.
883
     *
884
     * @see writeRecords()
885
     * @see writePagesOrder()
886
     */
887
    protected function writeRecordsOrder(): void
888
    {
889
        if (!$this->update) {
890
            return;
891
        }
892
893
        $importCmd = [];
894
895
        $pageList = [];
896
        if (is_array($this->dat['header']['pagetree'] ?? null)) {
897
            $this->flatInversePageTree($this->dat['header']['pagetree'], $pageList);
898
        }
899
        // @todo: drop by-reference and write final $this->dat at the end of method?!
900
        if (is_array($this->dat['header']['pid_lookup'] ?? null)) {
901
            foreach ($this->dat['header']['pid_lookup'] as $pid => &$recordsByPid) {
902
                $mappedPid = $this->importMapId['pages'][$pid] ?? $this->pid;
903
                if (MathUtility::canBeInterpretedAsInteger($mappedPid)) {
904
                    foreach ($recordsByPid as $table => &$records) {
905
                        // If $mappedPid === $this->pid then we are on root level and we can consider to move pages as well!
906
                        // (they will not be in the page tree!)
907
                        if ($table !== 'pages' || !isset($pageList[$pid])) {
908
                            foreach (array_reverse(array_keys($records)) as $uid) {
909
                                if ($this->doRespectPid($table, (int)$uid)) {
910
                                    if (isset($this->importMapId[$table][$uid])) {
911
                                        $mappedUid = $this->importMapId[$table][$uid];
912
                                        $importCmd[$table][$mappedUid]['move'] = $mappedPid;
913
                                    }
914
                                }
915
                            }
916
                        }
917
                    }
918
                }
919
            }
920
        }
921
922
        // Move records in the database
923
        if (!empty($importCmd)) {
924
            $dataHandler = $this->createDataHandler();
925
            $this->callHook('before_writeRecordsRecordsOrder', [
926
                'tce' => &$dataHandler,
927
                'data' => &$importCmd,
928
            ]);
929
            $dataHandler->start([], $importCmd);
930
            $dataHandler->process_cmdmap();
931
            $this->callHook('after_writeRecordsRecordsOrder', [
932
                'tce' => &$dataHandler,
933
            ]);
934
        }
935
    }
936
937
    /**
938
     * Adds a single record to the $importData array. Also copies files to the temporary folder.
939
     * However all file and database references and flexform fields are set to blank for now!
940
     * That is processed with setRelations() later.
941
     *
942
     * @param array $importData Data to be modified or inserted in the database during import
943
     * @param string $table Table name
944
     * @param int $uid Record UID
945
     * @param int|string $pid Page id or NEW-id, e.g. "NEW5fb3c2641281c885267727"
946
     * @see setRelations()
947
     */
948
    protected function addSingle(array &$importData, string $table, int $uid, $pid): void
949
    {
950
        // @todo return modified $importData instead of by-reference.
951
        if (($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_EXCLUDE) {
952
            return;
953
        }
954
955
        $record = $this->dat['records'][$table . ':' . $uid]['data'] ?? null;
956
957
        if (!is_array($record)) {
958
            if (!($table === 'pages' && $uid === 0)) {
959
                // On root level we don't want this error message.
960
                $this->addError('Error: No record was found in data array!');
961
            }
962
            return;
963
        }
964
965
        // Generate record ID
966
        $ID = StringUtility::getUniqueId('NEW');
967
        if ($this->update
968
            && $this->getRecordFromDatabase($table, $uid) !== null
969
            && ($this->importMode[$table . ':' . $uid] ?? '') !== self::IMPORT_MODE_AS_NEW
970
        ) {
971
            $ID = $uid;
972
        } elseif ($table === 'sys_file_metadata'
973
            && $record['sys_language_uid'] === '0'
974
            && isset($this->importMapId['sys_file'][$record['file']])
975
        ) {
976
            // On adding sys_file records the belonging sys_file_metadata record was also created:
977
            // If there is one, the record needs to be overwritten instead of a new one created.
978
            $databaseRecord = $this->getSysFileMetaDataFromDatabase(
979
                0,
980
                $this->importMapId['sys_file'][$record['file']],
981
                0
982
            );
983
            if (is_array($databaseRecord)) {
984
                $this->importMapId['sys_file_metadata'][$record['uid']] = $databaseRecord['uid'];
985
                $ID = $databaseRecord['uid'];
986
            }
987
        }
988
989
        // Mapping of generated record ID to original record UID
990
        $this->importNewId[$table . ':' . $ID] = ['table' => $table, 'uid' => $uid];
991
        if ($table === 'pages') {
992
            $this->importNewIdPids[$uid] = $ID;
993
        }
994
995
        // Record data
996
        $importData[$table][$ID] = $record;
997
        $importData[$table][$ID]['tx_impexp_origuid'] = $importData[$table][$ID]['uid'];
998
999
        // Record permissions
1000
        if ($table === 'pages') {
1001
            // Have to reset the user/group IDs so pages are owned by the importing user.
1002
            // Otherwise strange things may happen for non-admins!
1003
            unset($importData[$table][$ID]['perms_userid']);
1004
            unset($importData[$table][$ID]['perms_groupid']);
1005
        }
1006
1007
        // Record UID and PID
1008
        unset($importData[$table][$ID]['uid']);
1009
        // - for existing record
1010
        if (MathUtility::canBeInterpretedAsInteger($ID)) {
1011
            unset($importData[$table][$ID]['pid']);
1012
        }
1013
        // - for new record
1014
        else {
1015
            $importData[$table][$ID]['pid'] = $pid;
1016
            if ((($this->importMode[$table . ':' . $uid] ?? '') === self::IMPORT_MODE_FORCE_UID && $this->update
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->importMode[$tabl...ackendUser()->isAdmin(), Probably Intended Meaning: $this->importMode[$table...ckendUser()->isAdmin())
Loading history...
1017
                || $this->forceAllUids)
1018
                && $this->getBackendUser()->isAdmin()
1019
            ) {
1020
                $importData[$table][$ID]['uid'] = $uid;
1021
                $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE';
1022
            }
1023
        }
1024
1025
        // Record relations
1026
        foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => &$relation) {
1027
            if (isset($relation['type'])) {
1028
                switch ($relation['type']) {
1029
                    case 'db':
1030
                    case 'file':
1031
                        // Set blank now, fix later in setRelations(),
1032
                        // because we need to know ALL newly created IDs before we can map relations!
1033
                        // In the meantime we set NO values for relations.
1034
                        //
1035
                        // BUT for field uid_local of table sys_file_reference the relation MUST not be cleared here,
1036
                        // because the value is already the uid of the right imported sys_file record.
1037
                        // @see fixUidLocalInSysFileReferenceRecords()
1038
                        // If it's empty or a uid to another record the FileExtensionFilter will throw an exception or
1039
                        // delete the reference record if the file extension of the related record doesn't match.
1040
                        if (!($table === 'sys_file_reference' && $field === 'uid_local')) {
1041
                            $importData[$table][$ID][$field] = '';
1042
                        }
1043
                        break;
1044
                    case 'flex':
1045
                        // Set blank now, fix later in setFlexFormRelations().
1046
                        // In the meantime we set NO values for flexforms - this is mainly because file references
1047
                        // inside will not be processed properly. In fact references will point to no file
1048
                        // or existing files (in which case there will be double-references which is a big problem of
1049
                        // course!).
1050
                        //
1051
                        // BUT for the field "configuration" of the table "sys_file_storage" the relation MUST NOT be
1052
                        // cleared, because the configuration array contains only string values, which are furthermore
1053
                        // important for the further import, e.g. the base path.
1054
                        if (!($table === 'sys_file_storage' && $field === 'configuration')) {
1055
                            $importData[$table][$ID][$field] = '';
1056
                        }
1057
                        break;
1058
                }
1059
            }
1060
        }
1061
    }
1062
1063
    /**
1064
     * Selects sys_file_metadata database record.
1065
     *
1066
     * @param int $pid
1067
     * @param int $file
1068
     * @param int $sysLanguageUid
1069
     * @return array|null
1070
     */
1071
    protected function getSysFileMetaDataFromDatabase(int $pid, int $file, int $sysLanguageUid): ?array
1072
    {
1073
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1074
            ->getQueryBuilderForTable('sys_file_metadata');
1075
1076
        $databaseRecord = $queryBuilder->select('uid')
1077
            ->from('sys_file_metadata')
1078
            ->where(
1079
                $queryBuilder->expr()->eq(
1080
                    'file',
1081
                    $queryBuilder->createNamedParameter($file, \PDO::PARAM_INT)
1082
                ),
1083
                $queryBuilder->expr()->eq(
1084
                    'sys_language_uid',
1085
                    $queryBuilder->createNamedParameter($sysLanguageUid, \PDO::PARAM_INT)
1086
                ),
1087
                $queryBuilder->expr()->eq(
1088
                    'pid',
1089
                    $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
1090
                )
1091
            )
1092
            ->execute()
1093
            ->fetchAssociative();
1094
1095
        return is_array($databaseRecord) ? $databaseRecord : null;
1096
    }
1097
1098
    /**
1099
     * Store the mapping between the import file record UIDs and the final record UIDs in the database after import.
1100
     *
1101
     * @param array $importData Data to be modified or inserted in the database during import
1102
     * @param array $substNEWwithIDs A map between the "NEW..." string IDs and the eventual record UID in database
1103
     * @see writeRecords()
1104
     */
1105
    protected function addToMapId(array $importData, array $substNEWwithIDs): void
1106
    {
1107
        foreach ($importData as $table => &$records) {
1108
            foreach ($records as $ID => &$_) {
1109
                $uid = $this->importNewId[$table . ':' . $ID]['uid'];
1110
                if (isset($substNEWwithIDs[$ID])) {
1111
                    $this->importMapId[$table][$uid] = $substNEWwithIDs[$ID];
1112
                } elseif ($this->update) {
1113
                    // Map same ID to same ID....
1114
                    $this->importMapId[$table][$uid] = $ID;
1115
                } else {
1116
                    // If $this->importMapId contains already the right mapping, skip the error message.
1117
                    // See special handling of sys_file_metadata in addSingle() => nothing to do.
1118
                    if (!($table === 'sys_file_metadata'
1119
                        && isset($this->importMapId[$table][$uid])
1120
                        && $this->importMapId[$table][$uid] == $ID)
1121
                    ) {
1122
                        $this->addError(
1123
                            'Possible error: ' . $table . ':' . $uid . ' had no new id assigned to it. ' .
1124
                            'This indicates that the record was not added to database during import. ' .
1125
                            'Please check changelog!'
1126
                        );
1127
                    }
1128
                }
1129
            }
1130
        }
1131
    }
1132
1133
    /**
1134
     * Returns a new DataHandler object
1135
     *
1136
     * @return DataHandler DataHandler object
1137
     */
1138
    protected function createDataHandler(): DataHandler
1139
    {
1140
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
1141
        $dataHandler->dontProcessTransformations = true;
1142
        $dataHandler->enableLogging = $this->enableLogging;
1143
        return $dataHandler;
1144
    }
1145
1146
    /********************
1147
     * Import relations
1148
     *******************/
1149
1150
    /**
1151
     * At the end of the import process all file and database relations should be set properly.
1152
     * This means that the relations to imported records are all recreated so that the imported
1153
     * records are correctly related again.
1154
     * Relations in flexform fields are processed in setFlexFormRelations() after this function.
1155
     *
1156
     * @see setFlexFormRelations()
1157
     */
1158
    protected function setRelations(): void
1159
    {
1160
        $updateData = [];
1161
1162
        foreach ($this->importNewId as $original) {
1163
            $table = $original['table'];
1164
            $uid = $original['uid'];
1165
1166
            if (isset($this->importMapId[$table][$uid])) {
1167
                if (is_array($this->dat['records'][$table . ':' . $uid]['rels'] ?? null)) {
1168
                    $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid]);
1169
                    foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $relation) {
1170
                        // Field "uid_local" of sys_file_reference needs no update because the correct reference uid
1171
                        // was already written.
1172
                        // @see ImportExport::fixUidLocalInSysFileReferenceRecords()
1173
                        if (isset($relation['type']) && !($table === 'sys_file_reference' && $field === 'uid_local')) {
1174
                            switch ($relation['type']) {
1175
                                case 'db':
1176
                                    if (is_array($relation['itemArray'] ?? null) && !empty($relation['itemArray'])) {
1177
                                        $fieldTca = &$GLOBALS['TCA'][$table]['columns'][$field];
1178
                                        $actualRelations = $this->remapRelationsOfField($relation['itemArray'], $fieldTca['config']);
1179
                                        $updateData[$table][$actualUid][$field] = implode(',', $actualRelations);
1180
                                    }
1181
                                    break;
1182
                                case 'file':
1183
                                    if (is_array($relation['newValueFiles'] ?? null) && !empty($relation['newValueFiles'])) {
1184
                                        $temporaryFiles = $this->writeFilesToTemporaryFolder($relation['newValueFiles']);
1185
                                        $updateData[$table][$actualUid][$field] = implode(',', $temporaryFiles);
1186
                                    }
1187
                                    break;
1188
                            }
1189
                        }
1190
                    }
1191
                } else {
1192
                    $this->addError(sprintf('Error: This record does not appear to have a relation array! (%s:%s)', $table, $uid));
1193
                }
1194
            } else {
1195
                $this->addError(sprintf('Error: This record does not appear to have been created! (%s:%s)', $table, $uid));
1196
            }
1197
        }
1198
1199
        if (!empty($updateData)) {
1200
            $dataHandler = $this->createDataHandler();
1201
            $dataHandler->isImporting = true;
1202
            $this->callHook('before_setRelation', [
1203
                'tce' => &$dataHandler,
1204
                'data' => &$updateData,
1205
            ]);
1206
            $dataHandler->start($updateData, []);
1207
            $dataHandler->process_datamap();
1208
            $this->callHook('after_setRelations', [
1209
                'tce' => &$dataHandler,
1210
            ]);
1211
        }
1212
    }
1213
1214
    /**
1215
     * Maps the original record UIDs of the relations to the actual UIDs of the imported records and returns relations
1216
     * as strings of type [table]_[uid] - or file:[uid] or [public url] for field of type "group" and internal_type
1217
     * "file_reference". These strings have the regular DataHandler input group/select type format which means
1218
     * they will automatically be processed into a list of UIDs or MM relations.
1219
     *
1220
     * @param array $fieldRelations Relations with original record UIDs
1221
     * @param array $fieldConfig TCA configuration of the record field the relations belong to
1222
     * @return array Array of relation strings with actual record UIDs
1223
     */
1224
    protected function remapRelationsOfField(array &$fieldRelations, array $fieldConfig): array
1225
    {
1226
        $actualRelations = [];
1227
1228
        foreach ($fieldRelations as $relation) {
1229
            if (isset($this->importMapId[$relation['table']][$relation['id']])) {
1230
                $actualUid = $this->importMapId[$relation['table']][$relation['id']];
1231
                if ($fieldConfig['type'] === 'input' && isset($fieldConfig['wizards']['link'])) {
1232
                    // If an input field has a relation to a sys_file record this need to be converted back to
1233
                    // the public path. But use getPublicUrl() here, because could normally only be a local file path.
1234
                    try {
1235
                        $file = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($actualUid);
1236
                        $actualRelations[] = $file->getPublicUrl();
1237
                    } catch (\Exception $e) {
1238
                        $actualRelations[] = 'file:' . $actualUid;
1239
                    }
1240
                } else {
1241
                    $actualRelations[] = $relation['table'] . '_' . $actualUid;
1242
                }
1243
            } elseif ($this->isTableStatic($relation['table']) || $this->isRecordExcluded($relation['table'], (int)$relation['id']) || $relation['id'] < 0) {
1244
                // Some select types could contain negative values,
1245
                // e.g. fe_groups (-1, -2) and sys_language (-1 = ALL languages).
1246
                // This must be handled on both export and import.
1247
                $actualRelations[] = $relation['table'] . '_' . $relation['id'];
1248
            } else {
1249
                $this->addError('Lost relation: ' . $relation['table'] . ':' . $relation['id']);
1250
            }
1251
        }
1252
1253
        return $actualRelations;
1254
    }
1255
1256
    /**
1257
     * Writes the files from the import array to the temporary folder and returns the actual filenames.
1258
     *
1259
     * @param array $files Files of file information with three keys:
1260
     *                          "filename" = filename without path,
1261
     *                          "ID_absFile" = absolute filepath to the file (including the filename),
1262
     *                          "ID" = md5 hash of "ID_absFile
1263
     * @return array Absolute file paths of the temporary files.
1264
     */
1265
    public function writeFilesToTemporaryFolder(array $files): array
1266
    {
1267
        $temporaryFiles = [];
1268
1269
        foreach ($files as $fileInfo) {
1270
            if (is_array($this->dat['files'][$fileInfo['ID']] ?? null)) {
1271
                $fileRecord = &$this->dat['files'][$fileInfo['ID']];
1272
1273
                $temporaryFolder = $this->getOrCreateTemporaryFolderName();
1274
                $temporaryFilePath = $temporaryFolder . '/' . $fileRecord['content_md5'];
1275
1276
                if (is_file($temporaryFilePath) && md5_file($temporaryFilePath) === $fileRecord['content_md5']) {
1277
                    $temporaryFiles[] = $temporaryFilePath;
1278
                } else {
1279
                    if (GeneralUtility::writeFile($temporaryFilePath, $fileRecord['content'])) {
1280
                        clearstatcache();
1281
                        $temporaryFiles[] = $temporaryFilePath;
1282
                    } else {
1283
                        $this->addError(sprintf(
1284
                            'Error: Temporary file %s was not written as it should have been!',
1285
                            $temporaryFilePath
1286
                        ));
1287
                    }
1288
                }
1289
            } else {
1290
                $this->addError(sprintf('Error: No file found for ID %s', $fileInfo['ID']));
1291
            }
1292
        }
1293
1294
        return $temporaryFiles;
1295
    }
1296
1297
    /**
1298
     * After all database relations have been set in the end of the import (see setRelations()) then it is time to
1299
     * correct all relations inside of FlexForm fields. The reason for doing this after is that the setting of relations
1300
     * may affect (quite often!) which data structure is used for the FlexForm field!
1301
     *
1302
     * @see setRelations()
1303
     */
1304
    protected function setFlexFormRelations(): void
1305
    {
1306
        $updateData = [];
1307
1308
        foreach ($this->importNewId as $original) {
1309
            $table = $original['table'];
1310
            $uid = $original['uid'];
1311
1312
            if (isset($this->importMapId[$table][$uid])) {
1313
                if (is_array($this->dat['records'][$table . ':' . $uid]['rels'] ?? null)) {
1314
                    $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid]);
1315
                    foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $relation) {
1316
                        // Field "configuration" of sys_file_storage needs no update because it has not been removed
1317
                        // and has no relations.
1318
                        // @see Import::addSingle()
1319
                        if (isset($relation['type']) && !($table === 'sys_file_storage' && $field === 'configuration')) {
1320
                            switch ($relation['type']) {
1321
                                case 'flex':
1322
                                    // Re-insert temporarily removed original FlexForm data as fallback
1323
                                    // @see Import::addSingle()
1324
                                    $updateData[$table][$actualUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field];
1325
1326
                                    if (!empty($relation['flexFormRels']['db']) || !empty($relation['flexFormRels']['file'])) {
1327
                                        $actualRecord = BackendUtility::getRecord($table, $actualUid, '*');
1328
                                        $fieldTca = &$GLOBALS['TCA'][$table]['columns'][$field];
1329
                                        if (is_array($actualRecord) && is_array($fieldTca['config'] ?? null) && $fieldTca['config']['type'] === 'flex') {
1330
                                            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1331
                                            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1332
                                                $fieldTca,
1333
                                                $table,
1334
                                                $field,
1335
                                                $actualRecord
1336
                                            );
1337
                                            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1338
                                            $flexFormData = GeneralUtility::xml2array($this->dat['records'][$table . ':' . $uid]['data'][$field]);
1339
                                            $flexFormIterator = GeneralUtility::makeInstance(DataHandler::class);
1340
                                            $flexFormIterator->callBackObj = $this;
1341
                                            $flexFormData['data'] = $flexFormIterator->checkValue_flex_procInData(
1342
                                                $flexFormData['data'],
1343
                                                [],
1344
                                                $dataStructure,
1345
                                                [$relation],
1346
                                                'remapRelationsOfFlexFormCallBack'
1347
                                            );
1348
                                            if (is_array($flexFormData['data'] ?? null)) {
1349
                                                $updateData[$table][$actualUid][$field] = $flexFormData;
1350
                                            }
1351
                                        }
1352
                                    }
1353
                                    break;
1354
                            }
1355
                        }
1356
                    }
1357
                } else {
1358
                    $this->addError(sprintf('Error: This record does not appear to have a relation array! (%s:%s)', $table, $uid));
1359
                }
1360
            } else {
1361
                $this->addError(sprintf('Error: This record does not appear to have been created! (%s:%s)', $table, $uid));
1362
            }
1363
        }
1364
1365
        if (!empty($updateData)) {
1366
            $dataHandler = $this->createDataHandler();
1367
            $dataHandler->isImporting = true;
1368
            $this->callHook('before_setFlexFormRelations', [
1369
                'tce' => &$dataHandler,
1370
                'data' => &$updateData,
1371
            ]);
1372
            $dataHandler->start($updateData, []);
1373
            $dataHandler->process_datamap();
1374
            $this->callHook('after_setFlexFormRelations', [
1375
                'tce' => &$dataHandler,
1376
            ]);
1377
        }
1378
    }
1379
1380
    /**
1381
     * Callback function to remap relations in FlexForm data
1382
     *
1383
     * @param array $pParams Set of parameters passed through by calling method setFlexFormRelations()
1384
     * @param array $dsConf TCA config for field (from Data Structure of course)
1385
     * @param string $dataValue Field value (from FlexForm XML)
1386
     * @param string $dataValue_ext1 Not used
1387
     * @param string $path Path of where the data structure of the element is found
1388
     * @return array Array where the "value" key carries the mapped relation string.
1389
     *
1390
     * @see setFlexFormRelations()
1391
     */
1392
    public function remapRelationsOfFlexFormCallBack(array $pParams, array $dsConf, string $dataValue, $dataValue_ext1, string $path): array
1393
    {
1394
        [$relation] = $pParams;
1395
        // In case the $path is used as index without a trailing slash we will remove that
1396
        if (!is_array($relation['flexFormRels']['db'][$path] ?? null) && is_array($relation['flexFormRels']['db'][rtrim($path, '/')] ?? false)) {
1397
            $path = rtrim($path, '/');
1398
        }
1399
        if (is_array($relation['flexFormRels']['db'][$path] ?? null)) {
1400
            $actualRelations = $this->remapRelationsOfField($relation['flexFormRels']['db'][$path], $dsConf);
1401
            $dataValue = implode(',', $actualRelations);
1402
        }
1403
        if (is_array($relation['flexFormRels']['file'][$path] ?? null)) {
1404
            $temporaryFiles = $this->writeFilesToTemporaryFolder($relation['flexFormRels']['file'][$path]);
1405
            $dataValue = implode(',', $temporaryFiles);
1406
        }
1407
        return ['value' => $dataValue];
1408
    }
1409
1410
    /**************************
1411
     * Import soft references
1412
     *************************/
1413
1414
    /**
1415
     * Processing of soft references
1416
     */
1417
    protected function processSoftReferences(): void
1418
    {
1419
        $updateData = [];
1420
1421
        if (is_array($this->dat['header']['records'] ?? null)) {
1422
            foreach ($this->dat['header']['records'] as $table => $records) {
1423
                if (isset($GLOBALS['TCA'][$table])) {
1424
                    foreach ($records as $uid => $record) {
1425
                        if (is_array($record['softrefs'] ?? null)) {
1426
                            $actualUid = BackendUtility::wsMapId($table, $this->importMapId[$table][$uid]);
1427
                            // First, group soft references by record field ...
1428
                            // (this could probably also have been done with $this->dat['records'] instead of $this->dat['header'])
1429
                            $softrefs = [];
1430
                            foreach ($record['softrefs'] as $softref) {
1431
                                if ($softref['field'] && is_array($softref['subst'] ?? null) && $softref['subst']['tokenID']) {
1432
                                    $softrefs[$softref['field']][$softref['subst']['tokenID']] = $softref;
1433
                                }
1434
                            }
1435
                            // ... then process only fields which require substitution.
1436
                            foreach ($softrefs as $field => $softrefsByField) {
1437
                                if (is_array($GLOBALS['TCA'][$table]['columns'][$field] ?? null)) {
1438
                                    $fieldTca = &$GLOBALS['TCA'][$table]['columns'][$field];
1439
                                    if ($fieldTca['config']['type'] === 'flex') {
1440
                                        $actualRecord = BackendUtility::getRecord($table, $actualUid, '*');
1441
                                        if (is_array($actualRecord)) {
1442
                                            $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1443
                                            $dataStructureIdentifier = $flexFormTools->getDataStructureIdentifier(
1444
                                                $fieldTca,
1445
                                                $table,
1446
                                                $field,
1447
                                                $actualRecord
1448
                                            );
1449
                                            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
1450
                                            $flexFormData = GeneralUtility::xml2array($actualRecord[$field]);
1451
                                            $flexFormIterator = GeneralUtility::makeInstance(DataHandler::class);
1452
                                            $flexFormIterator->callBackObj = $this;
1453
                                            $flexFormData['data'] = $flexFormIterator->checkValue_flex_procInData(
1454
                                                $flexFormData['data'],
1455
                                                [],
1456
                                                $dataStructure,
1457
                                                [$table, $uid, $field, $softrefsByField],
1458
                                                'processSoftReferencesFlexFormCallBack'
1459
                                            );
1460
                                            if (is_array($flexFormData['data'] ?? null)) {
1461
                                                $updateData[$table][$actualUid][$field] = $flexFormData;
1462
                                            }
1463
                                        }
1464
                                    } else {
1465
                                        // Get tokenizedContent string and proceed only if that is not blank:
1466
                                        $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'];
1467
                                        if (strlen($tokenizedContent) && is_array($softrefsByField)) {
1468
                                            $updateData[$table][$actualUid][$field] = $this->processSoftReferencesSubstTokens($tokenizedContent, $softrefsByField, $table, (string)$uid);
1469
                                        }
1470
                                    }
1471
                                }
1472
                            }
1473
                        }
1474
                    }
1475
                }
1476
            }
1477
        }
1478
1479
        // Update soft references in the database
1480
        $dataHandler = $this->createDataHandler();
1481
        $dataHandler->isImporting = true;
1482
        $this->callHook('before_processSoftReferences', [
1483
            'tce' => $dataHandler,
1484
            'data' => &$updateData,
1485
        ]);
1486
        $dataHandler->enableLogging = true;
1487
        $dataHandler->start($updateData, []);
1488
        $dataHandler->process_datamap();
1489
        $this->callHook('after_processSoftReferences', [
1490
            'tce' => $dataHandler,
1491
        ]);
1492
    }
1493
1494
    /**
1495
     * Callback function to traverse the FlexForm structure and remap its soft reference relations.
1496
     *
1497
     * @param array $pParams Set of parameters in numeric array: table, uid, field, soft references
1498
     * @param array $dsConf TCA config for field (from Data Structure of course)
1499
     * @param string $dataValue Field value (from FlexForm XML)
1500
     * @param string $dataValue_ext1 Not used
1501
     * @param string $path Path of where the data structure where the element is found
1502
     * @return array Array where the "value" key carries the value.
1503
     * @see setFlexFormRelations()
1504
     */
1505
    public function processSoftReferencesFlexFormCallBack(array $pParams, array $dsConf, string $dataValue, $dataValue_ext1, string $path): array
1506
    {
1507
        [$table, $origUid, $field, $softrefs] = $pParams;
1508
        if (is_array($softrefs)) {
1509
            // Filter for soft references of this path ...
1510
            $softrefsByPath = [];
1511
            foreach ($softrefs as $tokenID => $softref) {
1512
                if ($softref['structurePath'] === $path) {
1513
                    $softrefsByPath[$tokenID] = $softref;
1514
                }
1515
            }
1516
            // ... and perform the processing.
1517
            if (!empty($softrefsByPath)) {
1518
                $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$path]['tokenizedContent'];
1519
                if (strlen($tokenizedContent)) {
1520
                    $dataValue = $this->processSoftReferencesSubstTokens($tokenizedContent, $softrefsByPath, $table, (string)$origUid);
1521
                }
1522
            }
1523
        }
1524
        return ['value' => $dataValue];
1525
    }
1526
1527
    /**
1528
     * Substitution of soft reference tokens
1529
     *
1530
     * @param string $tokenizedContent Content of field with soft reference tokens in.
1531
     * @param array $softrefs Soft references
1532
     * @param string $table Table of record for which the processing occurs
1533
     * @param string $uid UID of record from table
1534
     * @return string The input content with tokens substituted according to entries in $softrefs
1535
     */
1536
    protected function processSoftReferencesSubstTokens(string $tokenizedContent, array $softrefs, string $table, string $uid): string
1537
    {
1538
        foreach ($softrefs as &$softref) {
1539
            $tokenID = $softref['subst']['tokenID'];
1540
            $insertValue = $softref['subst']['tokenValue'];
1541
            switch ((string)($this->softrefCfg[$tokenID]['mode'] ?? '')) {
1542
                case self::SOFTREF_IMPORT_MODE_EXCLUDE:
1543
                    // This is the same as handling static relations:
1544
                    // Do not create or update the related file or record and do not change the link in any way,
1545
                    // but use the link as it was when exported.
1546
                    break;
1547
                case self::SOFTREF_IMPORT_MODE_EDITABLE:
1548
                    // This is the same as "exclude" with the option to manually edit the link before importing.
1549
                    $insertValue = $this->softrefInputValues[$tokenID];
1550
                    break;
1551
                default:
1552
                    // This is almost the same as handling relations:
1553
                    // - Creating or updating related files and adjusting the file reference to link to the new file.
1554
                    // - Adjusting the record reference to link to the already imported record - if any.
1555
                    switch ((string)$softref['subst']['type']) {
1556
                        case 'file':
1557
                            $insertValue = $this->processSoftReferencesSaveFile($softref['subst']['relFileName'], $softref, $table, $uid);
1558
                            break;
1559
                        case 'db':
1560
                        default:
1561
                            [$tempTable, $tempUid] = explode(':', (string)($softref['subst']['recordRef'] ?? ''));
1562
                            if (isset($this->importMapId[$tempTable][$tempUid])) {
1563
                                $insertValue = BackendUtility::wsMapId($tempTable, $this->importMapId[$tempTable][$tempUid]);
1564
                                $tokenValue = (string)$softref['subst']['tokenValue'];
1565
                                if (str_contains($tokenValue, ':')) {
1566
                                    [$tokenKey] = explode(':', $tokenValue);
1567
                                    $insertValue = $tokenKey . ':' . $insertValue;
1568
                                }
1569
                            }
1570
                    }
1571
            }
1572
            // Finally, replace the soft reference token in tokenized content
1573
            $tokenizedContent = str_replace('{softref:' . $tokenID . '}', (string)$insertValue, $tokenizedContent);
1574
        }
1575
        return $tokenizedContent;
1576
    }
1577
1578
    /**
1579
     * Process a soft reference file
1580
     *
1581
     * @param string $relFileName Old Relative filename
1582
     * @param array $softref Soft reference
1583
     * @param string $table Table for which the processing occurs
1584
     * @param string $uid UID of record from table
1585
     * @return string New relative filename (value to insert instead of the softref token)
1586
     */
1587
    protected function processSoftReferencesSaveFile(string $relFileName, array $softref, string $table, string $uid): string
1588
    {
1589
        if ($this->dat['header']['files'][$softref['file_ID']]) {
1590
            // Initialize; Get directory prefix for file and find possible RTE filename
1591
            $dirPrefix = PathUtility::dirname($relFileName) . '/';
1592
            if (str_starts_with($dirPrefix, $this->getFileadminFolderName() . '/')) {
1593
                // File in fileadmin/ folder:
1594
                // Create file (and possible resources)
1595
                $newFileName = $this->processSoftReferencesSaveFileCreateRelFile($dirPrefix, PathUtility::basename($relFileName), $softref['file_ID'], $table, $uid) ?: '';
1596
                if (strlen($newFileName)) {
1597
                    $relFileName = $newFileName;
1598
                } else {
1599
                    $this->addError('ERROR: No new file created for "' . $relFileName . '"');
1600
                }
1601
            } else {
1602
                $this->addError('ERROR: Sorry, cannot operate on non-RTE files which are outside the fileadmin folder.');
1603
            }
1604
        } else {
1605
            $this->addError('ERROR: Could not find file ID in header.');
1606
        }
1607
        // Return (new) filename relative to public web path
1608
        return $relFileName;
1609
    }
1610
1611
    /**
1612
     * Create file in directory and return the new (unique) filename
1613
     *
1614
     * @param string $origDirPrefix Directory prefix, relative, with trailing slash
1615
     * @param string $fileName Filename (without path)
1616
     * @param string $fileID File ID from import memory
1617
     * @param string $table Table for which the processing occurs
1618
     * @param string $uid UID of record from table
1619
     * @return string|null New relative filename, if any
1620
     */
1621
    protected function processSoftReferencesSaveFileCreateRelFile(string $origDirPrefix, string $fileName, string $fileID, string $table, string $uid): ?string
1622
    {
1623
        // If the fileID map contains an entry for this fileID then just return the relative filename of that entry;
1624
        // we don't want to write another unique filename for this one!
1625
        if (isset($this->fileIdMap[$fileID])) {
1626
            return PathUtility::stripPathSitePrefix($this->fileIdMap[$fileID]);
1627
        }
1628
        // Verify FileMount access to dir-prefix. Returns the best alternative relative path if any
1629
        $dirPrefix = $this->resolveStoragePath($origDirPrefix);
1630
        if ($dirPrefix !== null && (!$this->update || $origDirPrefix === $dirPrefix) && $this->checkOrCreateDir($dirPrefix)) {
1631
            $fileHeaderInfo = $this->dat['header']['files'][$fileID];
1632
            $updMode = $this->update && $this->importMapId[$table][$uid] === $uid && ($this->importMode[$table . ':' . $uid] ?? '') !== self::IMPORT_MODE_AS_NEW;
1633
            // Create new name for file:
1634
            // Must have same ID in map array (just for security, is not really needed) and NOT be set "as_new".
1635
1636
            // Write main file:
1637
            if ($updMode) {
1638
                $newName = Environment::getPublicPath() . '/' . $dirPrefix . $fileName;
1639
            } else {
1640
                // Create unique filename:
1641
                $fileProcObj = $this->getFileProcObj();
1642
                $newName = (string)$fileProcObj->getUniqueName($fileName, Environment::getPublicPath() . '/' . $dirPrefix);
1643
            }
1644
            if ($this->writeFileVerify($newName, $fileID)) {
1645
                // If the resource was an HTML/CSS file with resources attached, we will write those as well!
1646
                if (is_array($fileHeaderInfo['EXT_RES_ID'] ?? null)) {
1647
                    $tokenizedContent = $this->dat['files'][$fileID]['tokenizedContent'];
1648
                    $tokenSubstituted = false;
1649
                    $fileProcObj = $this->getFileProcObj();
1650
                    if ($updMode) {
1651
                        foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1652
                            if ($this->dat['files'][$res_fileID]['filename']) {
1653
                                // Resolve original filename:
1654
                                $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
1655
                                $absResourceFileName = GeneralUtility::resolveBackPath(Environment::getPublicPath() . '/' . $origDirPrefix . $relResourceFileName);
1656
                                $absResourceFileName = GeneralUtility::getFileAbsFileName($absResourceFileName);
1657
                                if ($absResourceFileName && str_starts_with($absResourceFileName, Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . '/')) {
1658
                                    $destDir = PathUtility::stripPathSitePrefix(PathUtility::dirname($absResourceFileName) . '/');
1659
                                    if ($this->resolveStoragePath($destDir, false) !== null && $this->checkOrCreateDir($destDir)) {
1660
                                        $this->writeFileVerify($absResourceFileName, $res_fileID);
1661
                                    } else {
1662
                                        $this->addError('ERROR: Could not create file in directory "' . $destDir . '"');
1663
                                    }
1664
                                } else {
1665
                                    $this->addError('ERROR: Could not resolve path for "' . $relResourceFileName . '"');
1666
                                }
1667
                                $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1668
                                $tokenSubstituted = true;
1669
                            }
1670
                        }
1671
                    } else {
1672
                        // Create the ressource's directory name (filename without extension, suffixed "_FILES")
1673
                        $resourceDir = PathUtility::dirname($newName) . '/' . preg_replace('/\\.[^.]*$/', '', PathUtility::basename($newName)) . '_FILES';
1674
                        if (GeneralUtility::mkdir($resourceDir)) {
1675
                            foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1676
                                if ($this->dat['files'][$res_fileID]['filename']) {
1677
                                    $absResourceFileName = (string)$fileProcObj->getUniqueName($this->dat['files'][$res_fileID]['filename'], $resourceDir);
1678
                                    $relResourceFileName = substr($absResourceFileName, strlen(PathUtility::dirname($resourceDir)) + 1);
1679
                                    $this->writeFileVerify($absResourceFileName, $res_fileID);
1680
                                    $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1681
                                    $tokenSubstituted = true;
1682
                                }
1683
                            }
1684
                        }
1685
                    }
1686
                    // If substitutions has been made, write the content to the file again:
1687
                    if ($tokenSubstituted) {
1688
                        GeneralUtility::writeFile($newName, $tokenizedContent);
1689
                    }
1690
                }
1691
                return PathUtility::stripPathSitePrefix($newName);
1692
            }
1693
        }
1694
        return null;
1695
    }
1696
1697
    /**
1698
     * Writes a file from the import memory having $fileID to file name $fileName which must be an absolute path inside public web path
1699
     *
1700
     * @param string $fileName Absolute filename inside public web path to write to
1701
     * @param string $fileID File ID from import memory
1702
     * @param bool $bypassMountCheck Bypasses the checking against file mounts - only for RTE files!
1703
     * @return bool Returns TRUE if it went well. Notice that the content of the file is read again, and md5 from import memory is validated.
1704
     */
1705
    protected function writeFileVerify(string $fileName, string $fileID, bool $bypassMountCheck = false): bool
1706
    {
1707
        $fileProcObj = $this->getFileProcObj();
1708
        if (!$fileProcObj->actionPerms['addFile']) {
1709
            $this->addError('ERROR: You did not have sufficient permissions to write the file "' . $fileName . '"');
1710
            return false;
1711
        }
1712
        // Just for security, check again. Should actually not be necessary.
1713
        if (!$bypassMountCheck) {
1714
            try {
1715
                GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier(PathUtility::dirname($fileName));
1716
            } catch (InsufficientFolderAccessPermissionsException $e) {
1717
                $this->addError('ERROR: Filename "' . $fileName . '" was not allowed in destination path!');
1718
                return false;
1719
            }
1720
        }
1721
        $pathInfo = GeneralUtility::split_fileref($fileName);
1722
        if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($pathInfo['file'])) {
1723
            $this->addError('ERROR: Filename "' . $fileName . '" failed against extension check or deny-pattern!');
1724
            return false;
1725
        }
1726
        if (!GeneralUtility::getFileAbsFileName($fileName)) {
1727
            $this->addError('ERROR: Filename "' . $fileName . '" was not a valid relative file path!');
1728
            return false;
1729
        }
1730
        if (!$this->dat['files'][$fileID]) {
1731
            $this->addError('ERROR: File ID "' . $fileID . '" could not be found');
1732
            return false;
1733
        }
1734
        GeneralUtility::writeFile($fileName, $this->dat['files'][$fileID]['content']);
1735
        $this->fileIdMap[$fileID] = $fileName;
1736
        if (hash_equals(md5((string)file_get_contents($fileName)), $this->dat['files'][$fileID]['content_md5'])) {
1737
            return true;
1738
        }
1739
        $this->addError('ERROR: File content "' . $fileName . '" was corrupted');
1740
        return false;
1741
    }
1742
1743
    /**
1744
     * Returns TRUE if directory exists  and if it doesn't it will create directory and return TRUE if that succeeded.
1745
     *
1746
     * @param string $dirPrefix Directory to create. Having a trailing slash. Must be in fileadmin/. Relative to public web path
1747
     * @return bool TRUE, if directory exists (was created)
1748
     */
1749
    protected function checkOrCreateDir(string $dirPrefix): bool
1750
    {
1751
        // Split dir path and remove first directory (which should be "fileadmin")
1752
        $filePathParts = explode('/', $dirPrefix);
1753
        $firstDir = array_shift($filePathParts);
1754
        if ($firstDir === $this->getFileadminFolderName() && GeneralUtility::getFileAbsFileName($dirPrefix)) {
1755
            $pathAcc = '';
1756
            foreach ($filePathParts as $dirname) {
1757
                $pathAcc .= '/' . $dirname;
1758
                if (strlen($dirname)) {
1759
                    if (!@is_dir(Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . $pathAcc)) {
1760
                        if (!GeneralUtility::mkdir(Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . $pathAcc)) {
1761
                            $this->addError('ERROR: Directory could not be created....B');
1762
                            return false;
1763
                        }
1764
                    }
1765
                } elseif ($dirPrefix === $this->getFileadminFolderName() . $pathAcc) {
1766
                    return true;
1767
                } else {
1768
                    $this->addError('ERROR: Directory could not be created....A');
1769
                }
1770
            }
1771
        }
1772
        return false;
1773
    }
1774
1775
    /**
1776
     * Call Hook
1777
     *
1778
     * @param string $name Name of the hook
1779
     * @param array $params Array with params
1780
     */
1781
    protected function callHook(string $name, array $params): void
1782
    {
1783
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/impexp/class.tx_impexp.php'][$name] ?? [] as $hook) {
1784
            GeneralUtility::callUserFunction($hook, $params, $this);
1785
        }
1786
    }
1787
1788
    /**************************
1789
     * Getters and Setters
1790
     *************************/
1791
1792
    /**
1793
     * @return bool
1794
     */
1795
    public function isEnableLogging(): bool
1796
    {
1797
        return $this->enableLogging;
1798
    }
1799
1800
    /**
1801
     * @param bool $enableLogging
1802
     */
1803
    public function setEnableLogging(bool $enableLogging): void
1804
    {
1805
        $this->enableLogging = $enableLogging;
1806
    }
1807
1808
    /**
1809
     * @return bool
1810
     */
1811
    public function isDecompressionAvailable(): bool
1812
    {
1813
        return $this->decompressionAvailable;
1814
    }
1815
}
1816