ImportExport::getPidRecord()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 14
rs 9.6111
c 0
b 0
f 0
cc 5
nc 4
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\Routing\PreviewUriBuilder;
21
use TYPO3\CMS\Backend\Utility\BackendUtility;
22
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23
use TYPO3\CMS\Core\Core\Environment;
24
use TYPO3\CMS\Core\Imaging\Icon;
25
use TYPO3\CMS\Core\Imaging\IconFactory;
26
use TYPO3\CMS\Core\Localization\LanguageService;
27
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
28
use TYPO3\CMS\Core\Resource\Folder;
29
use TYPO3\CMS\Core\Resource\ResourceFactory;
30
use TYPO3\CMS\Core\Resource\ResourceStorage;
31
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
32
use TYPO3\CMS\Core\Resource\StorageRepository;
33
use TYPO3\CMS\Core\Type\Bitmask\Permission;
34
use TYPO3\CMS\Core\Utility\DiffUtility;
35
use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Core\Utility\PathUtility;
38
39
/**
40
 * T3D file Import / Export library (TYPO3 Record Document)
41
 *
42
 * @internal This class is not considered part of the public TYPO3 API.
43
 */
44
abstract class ImportExport
45
{
46
    /**
47
     * Whether "import" or "export" mode of object.
48
     *
49
     * @var string
50
     */
51
    protected $mode = '';
52
53
    /**
54
     * A WHERE clause for selection records from the pages table based on read-permissions of the current backend user.
55
     *
56
     * @var string
57
     */
58
    protected $permsClause;
59
60
    /**
61
     * Root page of import or export page tree
62
     *
63
     * @var int
64
     */
65
    protected $pid = -1;
66
67
    /**
68
     * Root page record of import or of export page tree
69
     */
70
    protected ?array $pidRecord = null;
71
72
    /**
73
     * If set, static relations (not exported) will be shown in preview as well
74
     *
75
     * @var bool
76
     */
77
    protected $showStaticRelations = false;
78
79
    /**
80
     * Updates all records that has same UID instead of creating new!
81
     *
82
     * @var bool
83
     */
84
    protected $update = false;
85
86
    /**
87
     * Set by importData() when an import is started.
88
     *
89
     * @var bool
90
     */
91
    protected $doesImport = false;
92
93
    /**
94
     * Setting the import mode for specific import records.
95
     * Available options are: force_uid, as_new, exclude, ignore_pid, respect_pid
96
     *
97
     * @var array
98
     */
99
    protected $importMode = [];
100
101
    /**
102
     * If set, PID correct is ignored globally
103
     *
104
     * @var bool
105
     */
106
    protected $globalIgnorePid = false;
107
108
    /**
109
     * If set, all UID values are forced! (update or import)
110
     *
111
     * @var bool
112
     */
113
    protected $forceAllUids = false;
114
115
    /**
116
     * If set, a diff-view column is added to the preview.
117
     *
118
     * @var bool
119
     */
120
    protected $showDiff = false;
121
122
    /**
123
     * Array of values to substitute in editable soft references.
124
     *
125
     * @var array
126
     */
127
    protected $softrefInputValues = [];
128
129
    /**
130
     * Mapping between the fileID from import memory and the final filenames they are written to.
131
     *
132
     * @var array
133
     */
134
    protected $fileIdMap = [];
135
136
    /**
137
     * Add tables names here which should not be exported with the file.
138
     * (Where relations should be mapped to same UIDs in target system).
139
     *
140
     * @var array
141
     */
142
    protected $relStaticTables = [];
143
144
    /**
145
     * Exclude map. Keys are table:uid pairs and if set, records are not added to the export.
146
     *
147
     * @var array
148
     */
149
    protected $excludeMap = [];
150
151
    /**
152
     * Soft reference token ID modes.
153
     *
154
     * @var array
155
     */
156
    protected $softrefCfg = [];
157
158
    /**
159
     * Listing extension dependencies.
160
     *
161
     * @var array
162
     */
163
    protected $extensionDependencies = [];
164
165
    /**
166
     * After records are written this array is filled with [table][original_uid] = [new_uid]
167
     *
168
     * @var array
169
     */
170
    protected $importMapId = [];
171
172
    /**
173
     * Error log.
174
     *
175
     * @var array
176
     */
177
    protected $errorLog = [];
178
179
    /**
180
     * Cache for record paths
181
     *
182
     * @var array
183
     */
184
    protected $cacheGetRecordPath = [];
185
186
    /**
187
     * Internal import/export memory
188
     *
189
     * @var array
190
     */
191
    protected $dat = [];
192
193
    /**
194
     * File processing object
195
     *
196
     * @var ExtendedFileUtility
197
     */
198
    protected $fileProcObj;
199
200
    /**
201
     * @var DiffUtility
202
     */
203
    protected $diffUtility;
204
205
    /**
206
     * @var array
207
     */
208
    protected $remainHeader = [];
209
210
    /**
211
     * @var LanguageService
212
     */
213
    protected $lang;
214
215
    /**
216
     * @var IconFactory
217
     */
218
    protected $iconFactory;
219
220
    /**
221
     * Name of the "fileadmin" folder where files for export/import should be located
222
     *
223
     * @var string
224
     */
225
    protected $fileadminFolderName = '';
226
227
    protected ?string $temporaryFolderName = null;
228
    protected ?Folder $defaultImportExportFolder = null;
229
230
    /**
231
     * Flag to control whether all disabled records and their children are excluded (true) or included (false). Defaults
232
     * to the old behaviour of including everything.
233
     *
234
     * @var bool
235
     */
236
    protected $excludeDisabledRecords = false;
237
238
    /**
239
     * Array of currently registered storage objects
240
     *
241
     * @var ResourceStorage[]
242
     */
243
    protected $storages = [];
244
245
    /**
246
     * Array of currently registered storage objects available for importing files to
247
     *
248
     * @var ResourceStorage[]
249
     */
250
    protected $storagesAvailableForImport = [];
251
252
    /**
253
     * Currently registered default storage object
254
     */
255
    protected ?ResourceStorage $defaultStorage = null;
256
257
    /**
258
     * @var StorageRepository
259
     */
260
    protected $storageRepository;
261
262
    /**
263
     * The constructor
264
     */
265
    public function __construct()
266
    {
267
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
268
        $this->lang = $this->getLanguageService();
269
        $this->lang->includeLLFile('EXT:impexp/Resources/Private/Language/locallang.xlf');
270
        $this->permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
271
272
        $this->fetchStorages();
273
    }
274
275
    /**
276
     * Fetch all available file storages and index by storage UID
277
     *
278
     * Note: It also creates a default storage record if the database table sys_file_storage is empty,
279
     * e.g. during tests.
280
     */
281
    protected function fetchStorages(): void
282
    {
283
        $this->storages = [];
284
        $this->storagesAvailableForImport = [];
285
        $this->defaultStorage = null;
286
287
        $this->getStorageRepository()->flush();
288
289
        $storages = $this->getStorageRepository()->findAll();
290
        // @todo: Why by reference here? Test ImagesWithStoragesTest::importMultipleImagesWithMultipleStorages fails otherwise
291
        foreach ($storages as &$storage) {
292
            $this->storages[$storage->getUid()] = $storage;
293
            if ($storage->isOnline() && $storage->isWritable() && $storage->getDriverType() === 'Local') {
294
                $this->storagesAvailableForImport[$storage->getUid()] = $storage;
295
            }
296
            if ($this->defaultStorage === null && $storage->isDefault()) {
297
                $this->defaultStorage = $storage;
298
            }
299
        }
300
    }
301
302
    /********************************************************
303
     * Visual rendering of import/export memory, $this->dat
304
     ********************************************************/
305
306
    /**
307
     * Displays a preview of the import or export.
308
     *
309
     * @return array The preview data
310
     */
311
    public function renderPreview(): array
312
    {
313
        // @todo: Why is this done?
314
        unset($this->dat['files']);
315
316
        $previewData = [
317
            'update' => $this->update,
318
            'showDiff' => $this->showDiff,
319
            'insidePageTree' => [],
320
            'outsidePageTree' => [],
321
        ];
322
323
        if (!isset($this->dat['header']['pagetree']) && !isset($this->dat['header']['records'])) {
324
            return $previewData;
325
        }
326
327
        // Traverse header:
328
        $this->remainHeader = $this->dat['header'];
329
330
        // Preview of the page tree to be exported
331
        if (is_array($this->dat['header']['pagetree'] ?? null)) {
332
            $this->traversePageTree($this->dat['header']['pagetree'], $previewData['insidePageTree']);
333
            foreach ($previewData['insidePageTree'] as &$line) {
334
                $line['controls'] = $this->renderControls($line);
335
                $line['message'] = (($line['msg'] ?? '') && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($line['msg']) . '</span>' : '');
336
            }
337
        }
338
339
        // Preview the remaining records that were not included in the page tree
340
        if (is_array($this->remainHeader['records'] ?? null)) {
341
            if (is_array($this->remainHeader['records']['pages'] ?? null)) {
342
                $this->traversePageRecords($this->remainHeader['records']['pages'], $previewData['outsidePageTree']);
343
            }
344
            $this->traverseAllRecords($this->remainHeader['records'], $previewData['outsidePageTree']);
345
            foreach ($previewData['outsidePageTree'] as &$line) {
346
                $line['controls'] = $this->renderControls($line);
347
                $line['message'] = (($line['msg'] ?? '') && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($line['msg']) . '</span>' : '');
348
            }
349
        }
350
351
        return $previewData;
352
    }
353
354
    /**
355
     * Go through page tree for display
356
     *
357
     * @param array<int, array> $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree])
358
     * @param array $lines Output lines array
359
     * @param int $indent Indentation level
360
     */
361
    protected function traversePageTree(array $pageTree, array &$lines, int $indent = 0): void
362
    {
363
        foreach ($pageTree as $pageUid => $page) {
364
            if ($this->excludeDisabledRecords === true && $this->isRecordDisabled('pages', $pageUid)) {
365
                $this->excludePageAndRecords($pageUid, $page);
366
                continue;
367
            }
368
369
            // Add page
370
            $this->addRecord('pages', $pageUid, $lines, $indent);
371
372
            // Add records
373
            if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) {
374
                foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) {
375
                    $table = (string)$table;
376
                    if ($table !== 'pages') {
377
                        foreach (array_keys($records) as $uid) {
378
                            $this->addRecord($table, (int)$uid, $lines, $indent + 2);
379
                        }
380
                    }
381
                }
382
                unset($this->remainHeader['pid_lookup'][$pageUid]);
383
            }
384
385
            // Add subtree
386
            if (is_array($page['subrow'] ?? null)) {
387
                $this->traversePageTree($page['subrow'], $lines, $indent + 2);
388
            }
389
        }
390
    }
391
392
    /**
393
     * Test whether a record is disabled (e.g. hidden)
394
     *
395
     * @param string $table Name of the records' database table
396
     * @param int $uid Database uid of the record
397
     * @return bool true if the record is disabled, false otherwise
398
     */
399
    protected function isRecordDisabled(string $table, int $uid): bool
400
    {
401
        return (bool)($this->dat['records'][$table . ':' . $uid]['data'][
402
            $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] ?? ''
403
        ] ?? false);
404
    }
405
406
    /**
407
     * Exclude a page, its sub pages (recursively) and records placed in them from this import/export
408
     *
409
     * @param int $pageUid Uid of the page to exclude
410
     * @param array $page Page array with uid/subrow (from ->dat[header][pagetree])
411
     */
412
    protected function excludePageAndRecords(int $pageUid, array $page): void
413
    {
414
        // Exclude page
415
        unset($this->remainHeader['records']['pages'][$pageUid]);
416
417
        // Exclude records
418
        if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) {
419
            foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) {
420
                if ($table !== 'pages') {
421
                    foreach (array_keys($records) as $uid) {
422
                        unset($this->remainHeader['records'][$table][$uid]);
423
                    }
424
                }
425
            }
426
            unset($this->remainHeader['pid_lookup'][$pageUid]);
427
        }
428
429
        // Exclude subtree
430
        if (is_array($page['subrow'] ?? null)) {
431
            foreach ($page['subrow'] as $subPageUid => $subPage) {
432
                $this->excludePageAndRecords($subPageUid, $subPage);
433
            }
434
        }
435
    }
436
437
    /**
438
     * Go through remaining pages (not in tree)
439
     *
440
     * @param array<int, array> $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree])
441
     * @param array $lines Output lines array
442
     */
443
    protected function traversePageRecords(array $pageTree, array &$lines): void
444
    {
445
        foreach ($pageTree as $pageUid => $_) {
446
            // Add page
447
            $this->addRecord('pages', (int)$pageUid, $lines, 0, true);
448
449
            // Add records
450
            if (is_array($this->dat['header']['pid_lookup'][$pageUid] ?? null)) {
451
                foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $records) {
452
                    if ($table !== 'pages') {
453
                        foreach (array_keys($records) as $uid) {
454
                            $this->addRecord((string)$table, (int)$uid, $lines, 2);
455
                        }
456
                    }
457
                }
458
                unset($this->remainHeader['pid_lookup'][$pageUid]);
459
            }
460
        }
461
    }
462
463
    /**
464
     * Go through ALL records (if the pages are displayed first, those will not be among these!)
465
     *
466
     * @param array $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree])
467
     * @param array $lines Output lines array
468
     */
469
    protected function traverseAllRecords(array $pageTree, array &$lines): void
470
    {
471
        foreach ($pageTree as $table => $records) {
472
            $this->addGeneralErrorsByTable($table);
473
            if ($table !== 'pages') {
474
                foreach (array_keys($records) as $uid) {
475
                    $this->addRecord((string)$table, (int)$uid, $lines, 0, true);
476
                }
477
            }
478
        }
479
    }
480
481
    /**
482
     * Log general error message for a given table
483
     *
484
     * @param string $table database table name
485
     */
486
    protected function addGeneralErrorsByTable(string $table): void
487
    {
488
        if ($this->update && $table === 'sys_file') {
489
            $this->addError('Updating sys_file records is not supported! They will be imported as new records!');
490
        }
491
        if ($this->forceAllUids && $table === 'sys_file') {
492
            $this->addError('Forcing uids of sys_file records is not supported! They will be imported as new records!');
493
        }
494
    }
495
496
    /**
497
     * Add a record, its relations and soft references, to the preview
498
     *
499
     * @param string $table Table name
500
     * @param int $uid Record uid
501
     * @param array $lines Output lines array
502
     * @param int $indent Indentation level
503
     * @param bool $checkImportInPidRecord If you want import validation, you can set this so it checks if the import can take place on the specified page.
504
     */
505
    protected function addRecord(string $table, int $uid, array &$lines, int $indent, bool $checkImportInPidRecord = false): void
506
    {
507
        $record = $this->dat['header']['records'][$table][$uid] ?? null;
508
        unset($this->remainHeader['records'][$table][$uid]);
509
        if (!is_array($record) && !($table === 'pages' && $uid === 0)) {
510
            $this->addError('MISSING RECORD: ' . $table . ':' . $uid);
511
        }
512
513
        // Create record information for preview
514
        $line = [];
515
        $line['ref'] = $table . ':' . $uid;
516
        $line['type'] = 'record';
517
        $line['msg'] = '';
518
        if ($table === '_SOFTREF_') {
519
            // Record is a soft reference
520
            $line['preCode'] = $this->renderIndent($indent);
521
            $line['title'] = '<em>' . htmlspecialchars($this->lang->getLL('impexpcore_singlereco_softReferencesFiles')) . '</em>';
522
        } elseif (!isset($GLOBALS['TCA'][$table])) {
523
            // Record is of unknown table
524
            $line['preCode'] = $this->renderIndent($indent);
525
            $line['title'] = '<em>' . htmlspecialchars((string)$record['title']) . '</em>';
526
            $line['msg'] = 'UNKNOWN TABLE "' . $line['ref'] . '"';
527
        } else {
528
            $pidRecord = $this->getPidRecord();
529
            $icon = $this->iconFactory->getIconForRecord(
530
                $table,
531
                (array)($this->dat['records'][$table . ':' . $uid]['data'] ?? []),
532
                Icon::SIZE_SMALL
533
            )->render();
534
            $line['preCode'] = sprintf(
535
                '%s<span title="%s">%s</span>',
536
                $this->renderIndent($indent),
537
                htmlspecialchars($line['ref']),
538
                $icon
539
            );
540
            $line['title'] = htmlspecialchars($record['title'] ?? '');
541
            // Link to page view
542
            if ($table === 'pages') {
543
                $viewID = $this->mode === 'export' ? $uid : ($this->doesImport ? ($this->importMapId['pages'][$uid] ?? 0) : 0);
544
                if ($viewID) {
545
                    $attributes = PreviewUriBuilder::create($viewID)->serializeDispatcherAttributes();
546
                    $line['title'] = sprintf('<a href="#" %s>%s</a>', $attributes, $line['title']);
547
                }
548
            }
549
            $line['active'] = !$this->isRecordDisabled($table, $uid) ? 'active' : 'hidden';
550
            if ($this->mode === 'import' && $pidRecord !== null) {
551
                if ($checkImportInPidRecord) {
552
                    if (!$this->getBackendUser()->doesUserHaveAccess($pidRecord, ($table === 'pages' ? 8 : 16))) {
553
                        $line['msg'] .= '"' . $line['ref'] . '" cannot be INSERTED on this page! ';
554
                    }
555
                    if ($this->pid > 0 && !$this->checkDokType($table, $pidRecord['doktype']) && !$GLOBALS['TCA'][$table]['ctrl']['rootLevel']) {
556
                        $line['msg'] .= '"' . $table . '" cannot be INSERTED on this page type (change page type to "Folder".) ';
557
                    }
558
                }
559
                if (!$this->getBackendUser()->check('tables_modify', $table)) {
560
                    $line['msg'] .= 'You are not allowed to CREATE "' . $table . '" tables! ';
561
                }
562
                if ($GLOBALS['TCA'][$table]['ctrl']['readOnly'] ?? false) {
563
                    $line['msg'] .= 'TABLE "' . $table . '" is READ ONLY! ';
564
                }
565
                if (($GLOBALS['TCA'][$table]['ctrl']['adminOnly'] ?? false) && !$this->getBackendUser()->isAdmin()) {
566
                    $line['msg'] .= 'TABLE "' . $table . '" is ADMIN ONLY! ';
567
                }
568
                if ($GLOBALS['TCA'][$table]['ctrl']['is_static'] ?? false) {
569
                    $line['msg'] .= 'TABLE "' . $table . '" is a STATIC TABLE! ';
570
                }
571
                if ((int)($GLOBALS['TCA'][$table]['ctrl']['rootLevel'] ?? 0) === 1) {
572
                    $line['msg'] .= 'TABLE "' . $table . '" will be inserted on ROOT LEVEL! ';
573
                }
574
                $databaseRecord = null;
575
                if ($this->update) {
576
                    $databaseRecord = $this->getRecordFromDatabase($table, $uid, $this->showDiff ? '*' : 'uid,pid');
577
                    if ($databaseRecord === null) {
578
                        $line['updatePath'] = '<strong>NEW!</strong>';
579
                    } else {
580
                        $line['updatePath'] = htmlspecialchars($this->getRecordPath((int)($databaseRecord['pid'] ?? 0)));
581
                    }
582
                    if ($table === 'sys_file') {
583
                        $line['updateMode'] = '';
584
                    } else {
585
                        $line['updateMode'] = $this->renderImportModeSelector(
586
                            $table,
587
                            $uid,
588
                            $databaseRecord !== null
589
                        );
590
                    }
591
                }
592
                // Diff view
593
                if ($this->showDiff) {
594
                    $diffInverse = $this->update ? true : false;
595
                    // For imports, get new id:
596
                    if (isset($this->importMapId[$table][$uid]) && $newUid = $this->importMapId[$table][$uid]) {
597
                        $diffInverse = false;
598
                        $databaseRecord = $this->getRecordFromDatabase($table, $newUid, '*');
599
                        BackendUtility::workspaceOL($table, $databaseRecord);
600
                    }
601
                    $importRecord = $this->dat['records'][$table . ':' . $uid]['data'] ?? null;
602
                    if (is_array($databaseRecord) && is_array($importRecord)) {
603
                        $line['showDiffContent'] = $this->compareRecords($databaseRecord, $importRecord, $table, $diffInverse);
604
                    } else {
605
                        $line['showDiffContent'] = 'ERROR: One of the inputs were not an array!';
606
                    }
607
                }
608
            }
609
        }
610
        $lines[] = $line;
611
612
        // File relations
613
        if (is_array($record['filerefs'] ?? null)) {
614
            $this->addFiles($record['filerefs'], $lines, $indent);
615
        }
616
        // Database relations
617
        if (is_array($record['rels'] ?? null)) {
618
            $this->addRelations($record['rels'], $lines, $indent);
619
        }
620
        // Soft references
621
        if (is_array($record['softrefs'] ?? null)) {
622
            $this->addSoftRefs($record['softrefs'], $lines, $indent);
623
        }
624
    }
625
626
    /**
627
     * Add database relations of a record to the preview
628
     *
629
     * @param array $relations Array of relations
630
     * @param array $lines Output lines array
631
     * @param int $indent Indentation level
632
     * @param array $recursionCheck Recursion check stack
633
     *
634
     * @see addRecord()
635
     */
636
    protected function addRelations(array $relations, array &$lines, int $indent, array $recursionCheck = []): void
637
    {
638
        foreach ($relations as $relation) {
639
            $table = $relation['table'];
640
            $uid = $relation['id'];
641
            $line = [];
642
            $line['ref'] = $table . ':' . $uid;
643
            $line['type'] = 'rel';
644
            $line['msg'] = '';
645
            if (in_array($line['ref'], $recursionCheck, true)) {
646
                continue;
647
            }
648
            $iconName = 'status-status-checked';
649
            $iconClass = '';
650
            $staticFixed = false;
651
            $record = null;
652
            if ($uid > 0) {
653
                $record = $this->dat['header']['records'][$table][$uid] ?? null;
654
                if (!is_array($record)) {
655
                    if ($this->isTableStatic($table) || $this->isRecordExcluded($table, (int)$uid)
656
                        || ($relation['tokenID'] ?? '') && !$this->isSoftRefIncluded($relation['tokenID'] ?? '')) {
657
                        $line['title'] = htmlspecialchars('STATIC: ' . $line['ref']);
658
                        $iconClass = 'text-info';
659
                        $staticFixed = true;
660
                    } else {
661
                        $databaseRecord = $this->getRecordFromDatabase($table, (int)$uid);
662
                        $recordPath = $this->getRecordPath($databaseRecord === null ? 0 : ($table === 'pages' ? (int)$databaseRecord['uid'] : (int)$databaseRecord['pid']));
663
                        $line['title'] = sprintf(
664
                            '<span title="%s">%s</span>',
665
                            htmlspecialchars($recordPath),
666
                            htmlspecialchars($line['ref'])
667
                        );
668
                        $line['msg'] = 'LOST RELATION' . ($databaseRecord === null ? ' (Record not found!)' : ' (Path: ' . $recordPath . ')');
669
                        $iconClass = 'text-danger';
670
                        $iconName = 'status-dialog-warning';
671
                    }
672
                } else {
673
                    $recordPath = $this->getRecordPath($table === 'pages' ? (int)$record['uid'] : (int)$record['pid']);
674
                    $line['title'] = sprintf(
675
                        '<span title="%s">%s</span>',
676
                        htmlspecialchars($recordPath),
677
                        htmlspecialchars((string)$record['title'])
678
                    );
679
                }
680
            } else {
681
                // Negative values in relation fields. These are typically fields of sys_language, fe_users etc.
682
                // They are static values. They CAN theoretically be negative pointers to uids in other tables,
683
                // but this is so rarely used that it is not supported.
684
                $line['title'] = htmlspecialchars('FIXED: ' . $line['ref']);
685
                $staticFixed = true;
686
            }
687
688
            $icon = $this->iconFactory->getIcon($iconName, Icon::SIZE_SMALL)->render();
689
            $line['preCode'] = sprintf(
690
                '%s<span class="%s" title="%s">%s</span>',
691
                $this->renderIndent($indent + 2),
692
                $iconClass,
693
                htmlspecialchars($line['ref']),
694
                $icon
695
            );
696
            if (!$staticFixed || $this->showStaticRelations) {
697
                $lines[] = $line;
698
                if (is_array($record) && is_array($record['rels'] ?? null)) {
699
                    $this->addRelations($record['rels'], $lines, $indent + 1, array_merge($recursionCheck, [$line['ref']]));
700
                }
701
            }
702
        }
703
    }
704
705
    /**
706
     * Add file relations of a record to the preview.
707
     *
708
     * Public access for testing purpose only.
709
     *
710
     * @param array $relations Array of file IDs
711
     * @param array $lines Output lines array
712
     * @param int $indent Indentation level
713
     * @param string $tokenID Token ID if this is a soft reference (in which case it only makes sense with a single element in the $relations array!)
714
     *
715
     * @see addRecord()
716
     */
717
    public function addFiles(array $relations, array &$lines, int $indent, string $tokenID = ''): void
718
    {
719
        foreach ($relations as $ID) {
720
            $line = [];
721
            $line['msg'] = '';
722
            $fileInfo = $this->dat['header']['files'][$ID];
723
            if (!is_array($fileInfo)) {
724
                if ($tokenID !== '' || $this->isSoftRefIncluded($tokenID)) {
725
                    $line['msg'] = 'MISSING FILE: ' . $ID;
726
                    $this->addError('MISSING FILE: ' . $ID);
727
                } else {
728
                    return;
729
                }
730
            }
731
            $line['ref'] = 'FILE';
732
            $line['type'] = 'file';
733
            $line['preCode'] = sprintf(
734
                '%s<span title="%s">%s</span>',
735
                $this->renderIndent($indent + 2),
736
                htmlspecialchars($line['ref']),
737
                $this->iconFactory->getIcon('status-reference-hard', Icon::SIZE_SMALL)->render()
738
            );
739
            $line['title'] = htmlspecialchars($fileInfo['filename']);
740
            $line['showDiffContent'] = PathUtility::stripPathSitePrefix((string)($this->fileIdMap[$ID] ?? ''));
741
            // If import mode and there is a non-RTE soft reference, check the destination directory.
742
            if ($this->mode === 'import' && $tokenID !== '' && !($fileInfo['RTE_ORIG_ID'] ?? false)) {
743
                // Check folder existence
744
                if (isset($fileInfo['parentRelFileName'])) {
745
                    $line['msg'] = 'Seems like this file is already referenced from within an HTML/CSS file. That takes precedence. ';
746
                } else {
747
                    $origDirPrefix = PathUtility::dirname($fileInfo['relFileName']) . '/';
748
                    $dirPrefix = $this->resolveStoragePath($origDirPrefix);
749
                    if ($dirPrefix === null) {
750
                        $line['msg'] = 'ERROR: There are no available file mounts to write file in! ';
751
                    } elseif ($origDirPrefix !== $dirPrefix) {
752
                        $line['msg'] = 'File will be attempted written to "' . $dirPrefix . '". ';
753
                    }
754
                }
755
                // Check file existence
756
                if (file_exists(Environment::getPublicPath() . '/' . $fileInfo['relFileName'])) {
757
                    if ($this->update) {
758
                        $line['updatePath'] .= 'File exists.';
759
                    } else {
760
                        $line['msg'] .= 'File already exists! ';
761
                    }
762
                }
763
                // Check file extension
764
                $fileProcObj = $this->getFileProcObj();
765
                if ($fileProcObj->actionPerms['addFile']) {
766
                    $pathInfo = GeneralUtility::split_fileref(Environment::getPublicPath() . '/' . $fileInfo['relFileName']);
767
                    if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($pathInfo['file'])) {
768
                        $line['msg'] .= 'File extension was not allowed!';
769
                    }
770
                } else {
771
                    $line['msg'] = 'Your user profile does not allow you to create files on the server!';
772
                }
773
            }
774
            $lines[] = $line;
775
            unset($this->remainHeader['files'][$ID]);
776
777
            // RTE originals
778
            if ($fileInfo['RTE_ORIG_ID'] ?? false) {
779
                $ID = $fileInfo['RTE_ORIG_ID'];
780
                $line = [];
781
                $fileInfo = $this->dat['header']['files'][$ID];
782
                if (!is_array($fileInfo)) {
783
                    $line['msg'] = 'MISSING RTE original FILE: ' . $ID;
784
                    $this->addError('MISSING RTE original FILE: ' . $ID);
785
                }
786
                $line['ref'] = 'FILE';
787
                $line['type'] = 'file';
788
                $line['preCode'] = sprintf(
789
                    '%s<span title="%s">%s</span>',
790
                    $this->renderIndent($indent + 4),
791
                    htmlspecialchars($line['ref']),
792
                    $this->iconFactory->getIcon('status-reference-hard', Icon::SIZE_SMALL)->render()
793
                );
794
                $line['title'] = htmlspecialchars($fileInfo['filename']) . ' <em>(Original)</em>';
795
                $line['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIdMap[$ID]);
796
                $lines[] = $line;
797
                unset($this->remainHeader['files'][$ID]);
798
            }
799
800
            // External resources
801
            if (is_array($fileInfo['EXT_RES_ID'] ?? null)) {
802
                foreach ($fileInfo['EXT_RES_ID'] as $extID) {
803
                    $line = [];
804
                    $fileInfo = $this->dat['header']['files'][$extID];
805
                    if (!is_array($fileInfo)) {
806
                        $line['msg'] = 'MISSING External Resource FILE: ' . $extID;
807
                        $this->addError('MISSING External Resource FILE: ' . $extID);
808
                    } else {
809
                        $line['updatePath'] = $fileInfo['parentRelFileName'];
810
                    }
811
                    $line['ref'] = 'FILE';
812
                    $line['type'] = 'file';
813
                    $line['preCode'] = sprintf(
814
                        '%s<span title="%s">%s</span>',
815
                        $this->renderIndent($indent + 4),
816
                        htmlspecialchars($line['ref']),
817
                        $this->iconFactory->getIcon('actions-insert-reference', Icon::SIZE_SMALL)->render()
818
                    );
819
                    $line['title'] = htmlspecialchars($fileInfo['filename']) . ' <em>(Resource)</em>';
820
                    $line['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIdMap[$extID]);
821
                    $lines[] = $line;
822
                    unset($this->remainHeader['files'][$extID]);
823
                }
824
            }
825
        }
826
    }
827
828
    /**
829
     * Add soft references of a record to the preview
830
     *
831
     * @param array $softrefs Soft references
832
     * @param array $lines Output lines array
833
     * @param int $indent Indentation level
834
     *
835
     * @see addRecord()
836
     */
837
    protected function addSoftRefs(array $softrefs, array &$lines, int $indent): void
838
    {
839
        foreach ($softrefs as $softref) {
840
            $line = [];
841
            $line['ref'] = 'SOFTREF';
842
            $line['type'] = 'softref';
843
            $line['msg'] = '';
844
            $line['preCode'] = sprintf(
845
                '%s<span title="%s">%s</span>',
846
                $this->renderIndent($indent + 2),
847
                htmlspecialchars($line['ref']),
848
                $this->iconFactory->getIcon('status-reference-soft', Icon::SIZE_SMALL)->render()
849
            );
850
            $line['title'] = sprintf(
851
                '<em>%s, "%s"</em> : <span title="%s">%s</span>',
852
                $softref['field'],
853
                $softref['spKey'],
854
                htmlspecialchars($softref['matchString']),
855
                htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['matchString'], 60))
856
            );
857
            if ($softref['subst']['type'] ?? false) {
858
                if ($softref['subst']['title'] ?? false) {
859
                    $line['title'] .= sprintf(
860
                        '<br/>%s<strong>%s</strong> %s',
861
                        $this->renderIndent($indent + 4),
862
                        htmlspecialchars($this->lang->getLL('impexpcore_singlereco_title')),
863
                        htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['subst']['title'], 60))
864
                    );
865
                }
866
                if ($softref['subst']['description'] ?? false) {
867
                    $line['title'] .= sprintf(
868
                        '<br/>%s<strong>%s</strong> %s',
869
                        $this->renderIndent($indent + 4),
870
                        htmlspecialchars($this->lang->getLL('impexpcore_singlereco_descr')),
871
                        htmlspecialchars(GeneralUtility::fixed_lgd_cs($softref['subst']['description'], 60))
872
                    );
873
                }
874
                if ($softref['subst']['type'] === 'db') {
875
                    $line['title'] .= sprintf(
876
                        '<br/>%s%s <strong>%s</strong>',
877
                        $this->renderIndent($indent + 4),
878
                        htmlspecialchars($this->lang->getLL('impexpcore_softrefsel_record')),
879
                        $softref['subst']['recordRef']
880
                    );
881
                } elseif ($softref['subst']['type'] === 'file') {
882
                    $line['title'] .= sprintf(
883
                        '<br/>%s%s <strong>%s</strong>',
884
                        $this->renderIndent($indent + 4),
885
                        htmlspecialchars($this->lang->getLL('impexpcore_singlereco_filename')),
886
                        $softref['subst']['relFileName']
887
                    );
888
                } elseif ($softref['subst']['type'] === 'string') {
889
                    $line['title'] .= sprintf(
890
                        '<br/>%s%s <strong>%s</strong>',
891
                        $this->renderIndent($indent + 4),
892
                        htmlspecialchars($this->lang->getLL('impexpcore_singlereco_value')),
893
                        $softref['subst']['tokenValue']
894
                    );
895
                }
896
            }
897
            $line['_softRefInfo'] = $softref;
898
            $mode = $this->softrefCfg[$softref['subst']['tokenID'] ?? null]['mode'] ?? '';
899
            if (isset($softref['error']) && $mode !== Import::SOFTREF_IMPORT_MODE_EDITABLE && $mode !== Import::SOFTREF_IMPORT_MODE_EXCLUDE) {
900
                $line['msg'] .= $softref['error'];
901
            }
902
            $lines[] = $line;
903
904
            // Add database relations
905
            if (($softref['subst']['type'] ?? '') === 'db') {
906
                [$referencedTable, $referencedUid] = explode(':', $softref['subst']['recordRef']);
907
                $relations = [['table' => $referencedTable, 'id' => $referencedUid, 'tokenID' => $softref['subst']['tokenID']]];
908
                $this->addRelations($relations, $lines, $indent + 4);
909
            }
910
            // Add files relations
911
            if (($softref['subst']['type'] ?? '') === 'file') {
912
                $relations = [$softref['file_ID']];
913
                $this->addFiles($relations, $lines, $indent + 4, $softref['subst']['tokenID']);
914
            }
915
        }
916
    }
917
918
    protected function renderIndent(int $indent): string
919
    {
920
        return str_repeat('&nbsp;&nbsp;', $indent);
921
    }
922
923
    /**
924
     * Verifies that a table is allowed on a certain doktype of a page.
925
     *
926
     * @param string $table Table name to check
927
     * @param int $dokType Page doktype
928
     * @return bool TRUE if OK
929
     */
930
    protected function checkDokType(string $table, int $dokType): bool
931
    {
932
        $allowedTableList = $GLOBALS['PAGES_TYPES'][$dokType]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
933
        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
934
        if (str_contains($allowedTableList, '*') || in_array($table, $allowedArray, true)) {
935
            return true;
936
        }
937
        return false;
938
    }
939
940
    /**
941
     * Render input controls for import or export
942
     *
943
     * @param array $line Output line array
944
     * @return string HTML
945
     */
946
    protected function renderControls(array $line): string
947
    {
948
        if ($this->mode === 'export') {
949
            if ($line['type'] === 'record') {
950
                return $this->renderRecordExcludeCheckbox($line['ref']);
951
            }
952
            if ($line['type'] === 'softref') {
953
                return $this->renderSoftRefExportSelector($line['_softRefInfo']);
954
            }
955
        } elseif ($this->mode === 'import') {
956
            if ($line['type'] === 'softref') {
957
                return $this->renderSoftRefImportTextField($line['_softRefInfo']);
958
            }
959
        }
960
        return '';
961
    }
962
963
    /**
964
     * Render check box for exclusion of a record from export.
965
     *
966
     * @param string $recordRef The record ID of the form [table]:[id].
967
     * @return string HTML
968
     */
969
    protected function renderRecordExcludeCheckbox(string $recordRef): string
970
    {
971
        return sprintf(
972
            '
973
            <input type="checkbox" class="t3js-exclude-checkbox" name="tx_impexp[exclude][%1$s]" id="checkExclude%1$s" value="1" />
974
            <label for="checkExclude%1$s">%2$s</label>',
975
            $recordRef,
976
            htmlspecialchars($this->lang->getLL('impexpcore_singlereco_exclude'))
977
        );
978
    }
979
980
    /**
981
     * Render text field when importing a soft reference.
982
     *
983
     * @param array $softref Soft reference
984
     * @return string HTML
985
     */
986
    protected function renderSoftRefImportTextField(array $softref): string
987
    {
988
        if (isset($softref['subst']['tokenID'])) {
989
            $tokenID = $softref['subst']['tokenID'];
990
            $cfg = $this->softrefCfg[$tokenID] ?? [];
991
            if (($cfg['mode'] ?? '') === Import::SOFTREF_IMPORT_MODE_EDITABLE) {
992
                $html = '';
993
                if ($cfg['title'] ?? false) {
994
                    $html .= '<strong>' . htmlspecialchars((string)$cfg['title']) . '</strong><br/>';
995
                }
996
                $html .= htmlspecialchars((string)$cfg['description']) . '<br/>';
997
                $html .= sprintf(
998
                    '<input type="text" name="tx_impexp[softrefInputValues][%s]" value="%s" />',
999
                    $tokenID,
1000
                    htmlspecialchars($this->softrefInputValues[$tokenID] ?? $cfg['defValue'])
1001
                );
1002
                return $html;
1003
            }
1004
        }
1005
1006
        return '';
1007
    }
1008
1009
    /**
1010
     * Render select box with export options for soft references.
1011
     * An export box is shown only if a substitution scheme is found for the soft reference.
1012
     *
1013
     * @param array $softref Soft reference
1014
     * @return string HTML
1015
     */
1016
    protected function renderSoftRefExportSelector(array $softref): string
1017
    {
1018
        $fileInfo = isset($softref['file_ID']) ? $this->dat['header']['files'][$softref['file_ID']] : [];
1019
        // Substitution scheme has to be around and RTE images MUST be exported.
1020
        if (isset($softref['subst']['tokenID']) && !isset($fileInfo['RTE_ORIG_ID'])) {
1021
            $options = [];
1022
            $options[''] = '';
1023
            $options[Import::SOFTREF_IMPORT_MODE_EDITABLE] = $this->lang->getLL('impexpcore_softrefsel_editable');
1024
            $options[Import::SOFTREF_IMPORT_MODE_EXCLUDE] = $this->lang->getLL('impexpcore_softrefsel_exclude');
1025
            $value = $this->softrefCfg[$softref['subst']['tokenID']]['mode'] ?? '';
1026
            $selectHtml = $this->renderSelectBox(
1027
                'tx_impexp[softrefCfg][' . $softref['subst']['tokenID'] . '][mode]',
1028
                $value,
1029
                $options
1030
            ) . '<br/>';
1031
            $textFieldHtml = '';
1032
            if ($value === Import::SOFTREF_IMPORT_MODE_EDITABLE) {
1033
                if ($softref['subst']['title'] ?? false) {
1034
                    $textFieldHtml .= sprintf(
1035
                        '
1036
                        <input type="hidden" name="tx_impexp[softrefCfg][%1$s][title]" value="%2$s" />
1037
                        <strong>%2$s</strong><br/>',
1038
                        $softref['subst']['tokenID'],
1039
                        htmlspecialchars($softref['subst']['title'])
1040
                    );
1041
                }
1042
                if (!($softref['subst']['description'] ?? false)) {
1043
                    $textFieldHtml .= sprintf(
1044
                        '
1045
                        %s<br/>
1046
                        <input type="text" name="tx_impexp[softrefCfg][%s][description]" value="%s" />',
1047
                        htmlspecialchars($this->lang->getLL('impexpcore_printerror_description')),
1048
                        $softref['subst']['tokenID'],
1049
                        htmlspecialchars($this->softrefCfg[$softref['subst']['tokenID']]['description'] ?? '')
1050
                    );
1051
                } else {
1052
                    $textFieldHtml .= sprintf(
1053
                        '
1054
                        <input type="hidden" name="tx_impexp[softrefCfg][%1$s][description]" value="%2$s" />%2$s',
1055
                        $softref['subst']['tokenID'],
1056
                        htmlspecialchars($softref['subst']['description'])
1057
                    );
1058
                }
1059
                $textFieldHtml .= sprintf(
1060
                    '
1061
                    <input type="hidden" name="tx_impexp[softrefCfg][%s][defValue]" value="%s" />',
1062
                    $softref['subst']['tokenID'],
1063
                    htmlspecialchars($softref['subst']['tokenValue'])
1064
                );
1065
            }
1066
            return $selectHtml . $textFieldHtml;
1067
        }
1068
        return '';
1069
    }
1070
1071
    /**
1072
     * Render select box with import options for the record.
1073
     *
1074
     * @param string $table Table name
1075
     * @param int $uid Record UID
1076
     * @param bool $doesRecordExist Is there already a record with this UID in the database?
1077
     * @return string HTML
1078
     */
1079
    protected function renderImportModeSelector(string $table, int $uid, bool $doesRecordExist): string
1080
    {
1081
        $options = [];
1082
        if (!$doesRecordExist) {
1083
            $options[] = $this->lang->getLL('impexpcore_singlereco_insert');
1084
            if ($this->getBackendUser()->isAdmin()) {
1085
                $options[Import::IMPORT_MODE_FORCE_UID] = sprintf($this->lang->getLL('impexpcore_singlereco_forceUidSAdmin'), $uid);
1086
            }
1087
        } else {
1088
            $options[] = $this->lang->getLL('impexpcore_singlereco_update');
1089
            $options[Import::IMPORT_MODE_AS_NEW] = $this->lang->getLL('impexpcore_singlereco_importAsNew');
1090
            if (!$this->globalIgnorePid) {
1091
                $options[Import::IMPORT_MODE_IGNORE_PID] = $this->lang->getLL('impexpcore_singlereco_ignorePid');
1092
            } else {
1093
                $options[Import::IMPORT_MODE_RESPECT_PID] = $this->lang->getLL('impexpcore_singlereco_respectPid');
1094
            }
1095
        }
1096
        $options[Import::IMPORT_MODE_EXCLUDE] = $this->lang->getLL('impexpcore_singlereco_exclude');
1097
        return $this->renderSelectBox(
1098
            'tx_impexp[import_mode][' . $table . ':' . $uid . ']',
1099
            (string)($this->importMode[$table . ':' . $uid] ?? ''),
1100
            $options
1101
        );
1102
    }
1103
1104
    /**
1105
     * Renders a select box from option values.
1106
     *
1107
     * @param string $name Form element name
1108
     * @param string $value Current value
1109
     * @param array $options Options to display (key/value pairs)
1110
     * @return string HTML
1111
     */
1112
    protected function renderSelectBox(string $name, string $value, array $options): string
1113
    {
1114
        $optionsHtml = '';
1115
        $isValueInOptions = false;
1116
1117
        foreach ($options as $k => $v) {
1118
            if ((string)$k === $value) {
1119
                $isValueInOptions = true;
1120
                $selectedHtml = ' selected="selected"';
1121
            } else {
1122
                $selectedHtml = '';
1123
            }
1124
            $optionsHtml .= sprintf(
1125
                '<option value="%s"%s>%s</option>',
1126
                htmlspecialchars((string)$k),
1127
                $selectedHtml,
1128
                htmlspecialchars((string)$v)
1129
            );
1130
        }
1131
1132
        // Append and select the current value as an option of the form "[value]"
1133
        // if it is not available in the options.
1134
        if (!$isValueInOptions && $value !== '') {
1135
            $optionsHtml .= sprintf(
1136
                '<option value="%s" selected="selected">%s</option>',
1137
                htmlspecialchars($value),
1138
                htmlspecialchars('[\'' . $value . '\']')
1139
            );
1140
        }
1141
1142
        return '<select name="' . $name . '">' . $optionsHtml . '</select>';
1143
    }
1144
1145
    /*****************************
1146
     * Helper functions of kinds
1147
     *****************************/
1148
1149
    /**
1150
     * @return string
1151
     */
1152
    public function getFileadminFolderName(): string
1153
    {
1154
        if (empty($this->fileadminFolderName)) {
1155
            if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'])) {
1156
                $this->fileadminFolderName = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/');
1157
            } else {
1158
                $this->fileadminFolderName = 'fileadmin';
1159
            }
1160
        }
1161
        return $this->fileadminFolderName;
1162
    }
1163
1164
    /**
1165
     * @return string
1166
     */
1167
    public function getOrCreateTemporaryFolderName(): string
1168
    {
1169
        if (empty($this->temporaryFolderName)) {
1170
            $this->temporaryFolderName = $this->createTemporaryFolderName();
1171
        }
1172
        return $this->temporaryFolderName;
1173
    }
1174
1175
    protected function createTemporaryFolderName(): string
1176
    {
1177
        $temporaryPath = Environment::getVarPath() . '/transient';
1178
        do {
1179
            $temporaryFolderName = sprintf(
1180
                '%s/impexp_%s_files_%d',
1181
                $temporaryPath,
1182
                $this->mode,
1183
                random_int(1, PHP_INT_MAX)
1184
            );
1185
        } while (is_dir($temporaryFolderName));
1186
        GeneralUtility::mkdir_deep($temporaryFolderName);
1187
        return $temporaryFolderName;
1188
    }
1189
1190
    public function removeTemporaryFolderName(): void
1191
    {
1192
        if (!empty($this->temporaryFolderName)) {
1193
            GeneralUtility::rmdir($this->temporaryFolderName, true);
1194
            $this->temporaryFolderName = null;
1195
        }
1196
    }
1197
1198
    /**
1199
     * Returns a \TYPO3\CMS\Core\Resource\Folder object for saving export files
1200
     * to the server and is also used for uploading import files.
1201
     *
1202
     * @return Folder|null
1203
     */
1204
    public function getOrCreateDefaultImportExportFolder(): ?Folder
1205
    {
1206
        if (empty($this->defaultImportExportFolder)) {
1207
            $this->createDefaultImportExportFolder();
1208
        }
1209
        return $this->defaultImportExportFolder;
1210
    }
1211
1212
    /**
1213
     * Creates a \TYPO3\CMS\Core\Resource\Folder object for saving export files
1214
     * to the server and is also used for uploading import files.
1215
     */
1216
    protected function createDefaultImportExportFolder(): void
1217
    {
1218
        $defaultTemporaryFolder = $this->getBackendUser()->getDefaultUploadTemporaryFolder();
1219
        $defaultImportExportFolder = null;
1220
        $importExportFolderName = 'importexport';
1221
1222
        if ($defaultTemporaryFolder !== null) {
1223
            if ($defaultTemporaryFolder->hasFolder($importExportFolderName) === false) {
1224
                $defaultImportExportFolder = $defaultTemporaryFolder->createFolder($importExportFolderName);
1225
            } else {
1226
                $defaultImportExportFolder = $defaultTemporaryFolder->getSubfolder($importExportFolderName);
1227
            }
1228
        }
1229
        $this->defaultImportExportFolder = $defaultImportExportFolder;
1230
    }
1231
1232
    public function removeDefaultImportExportFolder(): void
1233
    {
1234
        if (!empty($this->defaultImportExportFolder)) {
1235
            $this->defaultImportExportFolder->delete(true);
1236
            $this->defaultImportExportFolder = null;
1237
        }
1238
    }
1239
1240
    /**
1241
     * Checks if the input path relative to the public web path can be found in the file mounts of the backend user.
1242
     * If not, it checks all file mounts of the user for the relative path and returns it if found.
1243
     *
1244
     * @param string $dirPrefix Path relative to public web path.
1245
     * @param bool $checkAlternatives If set to false, do not look for an alternative path.
1246
     * @return string If a path is available, it will be returned, otherwise NULL.
1247
     * @throws \Exception
1248
     */
1249
    protected function resolveStoragePath(string $dirPrefix, bool $checkAlternatives = true): ?string
1250
    {
1251
        try {
1252
            GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($dirPrefix);
1253
            return $dirPrefix;
1254
        } catch (InsufficientFolderAccessPermissionsException $e) {
1255
            if ($checkAlternatives) {
1256
                $storagesByUser = $this->getBackendUser()->getFileStorages();
1257
                foreach ($storagesByUser as $storage) {
1258
                    try {
1259
                        $folder = $storage->getFolder(rtrim($dirPrefix, '/'));
1260
                        return $folder->getPublicUrl();
1261
                    } catch (InsufficientFolderAccessPermissionsException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1262
                    }
1263
                }
1264
            }
1265
        }
1266
        return null;
1267
    }
1268
1269
    /**
1270
     * Recursively flattening the $pageTree array to a one-dimensional array with uid-pid pairs.
1271
     *
1272
     * @param array $pageTree Page tree array
1273
     * @param array $list List with uid-pid pairs
1274
     * @param int $pid PID value (internal, don't set from outside)
1275
     */
1276
    protected function flatInversePageTree(array $pageTree, array &$list, int $pid = -1): void
1277
    {
1278
        // @todo: return $list instead of by-reference?!
1279
        $pageTreeInverse = array_reverse($pageTree);
1280
        foreach ($pageTreeInverse as $page) {
1281
            $list[$page['uid']] = $pid;
1282
            if (is_array($page['subrow'] ?? null)) {
1283
                $this->flatInversePageTree($page['subrow'], $list, (int)$page['uid']);
1284
            }
1285
        }
1286
    }
1287
1288
    /**
1289
     * Returns TRUE if the input table name is to be regarded as a static relation (that is, not exported etc).
1290
     *
1291
     * @param string $table Table name
1292
     * @return bool TRUE, if table is marked static
1293
     */
1294
    protected function isTableStatic(string $table): bool
1295
    {
1296
        if (is_array($GLOBALS['TCA'][$table] ?? null)) {
1297
            return ($GLOBALS['TCA'][$table]['ctrl']['is_static'] ?? false)
1298
                || in_array($table, $this->relStaticTables, true)
1299
                || in_array('_ALL', $this->relStaticTables, true);
1300
        }
1301
        return false;
1302
    }
1303
1304
    /**
1305
     * Returns TRUE if the element should be excluded from import and export.
1306
     *
1307
     * @param string $table Table name
1308
     * @param int $uid Record UID
1309
     * @return bool TRUE, if the record should be excluded
1310
     */
1311
    protected function isRecordExcluded(string $table, int $uid): bool
1312
    {
1313
        return (bool)($this->excludeMap[$table . ':' . $uid] ?? false);
1314
    }
1315
1316
    /**
1317
     * Returns TRUE if the soft reference should be included in export.
1318
     *
1319
     * @param string $tokenID Token ID for soft reference
1320
     * @return bool TRUE, if soft reference should be included
1321
     */
1322
    protected function isSoftRefIncluded(string $tokenID): bool
1323
    {
1324
        $mode = $this->softrefCfg[$tokenID]['mode'] ?? '';
1325
        return $tokenID && $mode !== Import::SOFTREF_IMPORT_MODE_EXCLUDE && $mode !== Import::SOFTREF_IMPORT_MODE_EDITABLE;
1326
    }
1327
1328
    /**
1329
     * Returns given fields of record if it exists.
1330
     *
1331
     * @param string $table Table name
1332
     * @param int $uid UID of record
1333
     * @param string $fields Field list to select. Default is "uid,pid"
1334
     * @return array|null Result of \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord() which means the record if found, otherwise NULL
1335
     */
1336
    protected function getRecordFromDatabase(string $table, int $uid, string $fields = 'uid,pid'): ?array
1337
    {
1338
        return BackendUtility::getRecord($table, $uid, $fields);
1339
    }
1340
1341
    /**
1342
     * Returns the page title path of a PID value. Results are cached internally
1343
     *
1344
     * @param int $pid Record PID to check
1345
     * @return string The path for the input PID
1346
     */
1347
    protected function getRecordPath(int $pid): string
1348
    {
1349
        if (!isset($this->cacheGetRecordPath[$pid])) {
1350
            $this->cacheGetRecordPath[$pid] = (string)BackendUtility::getRecordPath($pid, $this->permsClause, 20);
1351
        }
1352
        return $this->cacheGetRecordPath[$pid];
1353
    }
1354
1355
    /**
1356
     * Compares two records, the current database record and the one from the import memory.
1357
     * Will return HTML code to show any differences between them!
1358
     *
1359
     * @param array $databaseRecord Database record, all fields (old values)
1360
     * @param array $importRecord Import memory record for the same table/uid, all fields (new values)
1361
     * @param string $table The table name of the record
1362
     * @param bool $inverse Inverse the diff view (switch red/green, needed for pre-update difference view)
1363
     * @return string HTML
1364
     */
1365
    protected function compareRecords(array $databaseRecord, array $importRecord, string $table, bool $inverse = false): string
1366
    {
1367
        $diffHtml = '';
1368
1369
        // Updated fields
1370
        foreach ($databaseRecord as $fieldName => $_) {
1371
            if (is_array($GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null)
1372
                && $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] !== 'passthrough'
1373
            ) {
1374
                if (isset($importRecord[$fieldName])) {
1375
                    if (trim((string)$databaseRecord[$fieldName]) !== trim((string)$importRecord[$fieldName])) {
1376
                        $diffFieldHtml = $this->getDiffUtility()->makeDiffDisplay(
1377
                            BackendUtility::getProcessedValue(
1378
                                $table,
1379
                                $fieldName,
1380
                                !$inverse ? $importRecord[$fieldName] : $databaseRecord[$fieldName],
1381
                                0,
1382
                                true,
1383
                                true
1384
                            ),
1385
                            BackendUtility::getProcessedValue(
1386
                                $table,
1387
                                $fieldName,
1388
                                !$inverse ? $databaseRecord[$fieldName] : $importRecord[$fieldName],
1389
                                0,
1390
                                true,
1391
                                true
1392
                            )
1393
                        );
1394
                        $diffHtml .= sprintf(
1395
                            '<tr><td>%s (%s)</td><td>%s</td></tr>' . PHP_EOL,
1396
                            htmlspecialchars($this->lang->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])),
1397
                            htmlspecialchars((string)$fieldName),
1398
                            $diffFieldHtml
1399
                        );
1400
                    }
1401
                    unset($importRecord[$fieldName]);
1402
                }
1403
            }
1404
        }
1405
1406
        // New fields
1407
        foreach ($importRecord as $fieldName => $_) {
1408
            if (is_array($GLOBALS['TCA'][$table]['columns'][$fieldName] ?? null)
1409
                && $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['type'] !== 'passthrough'
1410
            ) {
1411
                $diffFieldHtml = '<strong>Field missing</strong> in database';
1412
                $diffHtml .= sprintf(
1413
                    '<tr><td>%s (%s)</td><td>%s</td></tr>' . PHP_EOL,
1414
                    htmlspecialchars($this->lang->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label'])),
1415
                    htmlspecialchars((string)$fieldName),
1416
                    $diffFieldHtml
1417
                );
1418
            }
1419
        }
1420
1421
        if ($diffHtml !== '') {
1422
            $diffHtml = '<table class="table table-striped table-hover">' . PHP_EOL . $diffHtml . '</table>';
1423
        } else {
1424
            $diffHtml = 'Match';
1425
        }
1426
1427
        return sprintf(
1428
            '<strong class="text-nowrap">[%s]:</strong>' . PHP_EOL . '%s',
1429
            htmlspecialchars($table . ':' . $importRecord['uid'] . ' => ' . $databaseRecord['uid']),
1430
            $diffHtml
1431
        );
1432
    }
1433
1434
    /**
1435
     * Returns string comparing object, initialized only once.
1436
     *
1437
     * @return DiffUtility String comparing object
1438
     */
1439
    protected function getDiffUtility(): DiffUtility
1440
    {
1441
        if ($this->diffUtility === null) {
1442
            $this->diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
1443
        }
1444
        return $this->diffUtility;
1445
    }
1446
1447
    /**
1448
     * Returns file processing object, initialized only once.
1449
     *
1450
     * @return ExtendedFileUtility File processor object
1451
     */
1452
    protected function getFileProcObj(): ExtendedFileUtility
1453
    {
1454
        if ($this->fileProcObj === null) {
1455
            $this->fileProcObj = GeneralUtility::makeInstance(ExtendedFileUtility::class);
1456
            $this->fileProcObj->setActionPermissions();
1457
        }
1458
        return $this->fileProcObj;
1459
    }
1460
1461
    /**
1462
     * Returns storage repository object, initialized only once.
1463
     *
1464
     * @return StorageRepository Storage repository object
1465
     */
1466
    protected function getStorageRepository(): StorageRepository
1467
    {
1468
        if ($this->storageRepository === null) {
1469
            $this->storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1470
        }
1471
        return $this->storageRepository;
1472
    }
1473
1474
    /*****************************
1475
     * Error handling
1476
     *****************************/
1477
1478
    /**
1479
     * Sets error message in the internal error log
1480
     *
1481
     * @param string $message Error message
1482
     */
1483
    protected function addError(string $message): void
1484
    {
1485
        $this->errorLog[] = $message;
1486
    }
1487
1488
    public function hasErrors(): bool
1489
    {
1490
        return empty($this->errorLog) === false;
1491
    }
1492
1493
    /**
1494
     * @return BackendUserAuthentication
1495
     */
1496
    protected function getBackendUser(): BackendUserAuthentication
1497
    {
1498
        return $GLOBALS['BE_USER'];
1499
    }
1500
1501
    /**
1502
     * @return LanguageService
1503
     */
1504
    protected function getLanguageService(): LanguageService
1505
    {
1506
        return $GLOBALS['LANG'];
1507
    }
1508
1509
    /**************************
1510
     * Getters and Setters
1511
     *************************/
1512
1513
    /**
1514
     * @return int
1515
     */
1516
    public function getPid(): int
1517
    {
1518
        return $this->pid;
1519
    }
1520
1521
    /**
1522
     * @param int $pid
1523
     */
1524
    public function setPid(int $pid): void
1525
    {
1526
        $this->pid = $pid;
1527
        $this->pidRecord = null;
1528
    }
1529
1530
    /**
1531
     * Return record of root page of import or of export page tree
1532
     * - or null if access denied to that page.
1533
     *
1534
     * If the page is the root of the page tree,
1535
     * add some basic but missing information.
1536
     *
1537
     * @return array|null
1538
     */
1539
    protected function getPidRecord(): ?array
1540
    {
1541
        if ($this->pidRecord === null && $this->pid >= 0) {
1542
            $pidRecord = BackendUtility::readPageAccess($this->pid, $this->permsClause);
1543
1544
            if (is_array($pidRecord)) {
1545
                if ($this->pid === 0) {
1546
                    $pidRecord += ['title' => '[root-level]', 'uid' => 0, 'pid' => 0];
1547
                }
1548
                $this->pidRecord = $pidRecord;
1549
            }
1550
        }
1551
1552
        return $this->pidRecord;
1553
    }
1554
1555
    /**
1556
     * Set flag to control whether disabled records and their children are excluded (true) or included (false). Defaults
1557
     * to the old behaviour of including everything.
1558
     *
1559
     * @param bool $excludeDisabledRecords Set to true if if all disabled records should be excluded, false otherwise
1560
     */
1561
    public function setExcludeDisabledRecords(bool $excludeDisabledRecords): void
1562
    {
1563
        $this->excludeDisabledRecords = $excludeDisabledRecords;
1564
    }
1565
1566
    /**
1567
     * @return bool
1568
     */
1569
    public function isExcludeDisabledRecords(): bool
1570
    {
1571
        return $this->excludeDisabledRecords;
1572
    }
1573
1574
    /**
1575
     * @return array
1576
     */
1577
    public function getExcludeMap(): array
1578
    {
1579
        return $this->excludeMap;
1580
    }
1581
1582
    /**
1583
     * @param array $excludeMap
1584
     */
1585
    public function setExcludeMap(array $excludeMap): void
1586
    {
1587
        $this->excludeMap = $excludeMap;
1588
    }
1589
1590
    /**
1591
     * @return array
1592
     */
1593
    public function getSoftrefCfg(): array
1594
    {
1595
        return $this->softrefCfg;
1596
    }
1597
1598
    /**
1599
     * @param array $softrefCfg
1600
     */
1601
    public function setSoftrefCfg(array $softrefCfg): void
1602
    {
1603
        $this->softrefCfg = $softrefCfg;
1604
    }
1605
1606
    /**
1607
     * @return array
1608
     */
1609
    public function getExtensionDependencies(): array
1610
    {
1611
        return $this->extensionDependencies;
1612
    }
1613
1614
    /**
1615
     * @param array $extensionDependencies
1616
     */
1617
    public function setExtensionDependencies(array $extensionDependencies): void
1618
    {
1619
        $this->extensionDependencies = $extensionDependencies;
1620
    }
1621
1622
    /**
1623
     * @return bool
1624
     */
1625
    public function isShowStaticRelations(): bool
1626
    {
1627
        return $this->showStaticRelations;
1628
    }
1629
1630
    /**
1631
     * @param bool $showStaticRelations
1632
     */
1633
    public function setShowStaticRelations(bool $showStaticRelations): void
1634
    {
1635
        $this->showStaticRelations = $showStaticRelations;
1636
    }
1637
1638
    /**
1639
     * @return array
1640
     */
1641
    public function getRelStaticTables(): array
1642
    {
1643
        return $this->relStaticTables;
1644
    }
1645
1646
    /**
1647
     * @param array $relStaticTables
1648
     */
1649
    public function setRelStaticTables(array $relStaticTables): void
1650
    {
1651
        $this->relStaticTables = $relStaticTables;
1652
    }
1653
1654
    /**
1655
     * @return array
1656
     */
1657
    public function getErrorLog(): array
1658
    {
1659
        return $this->errorLog;
1660
    }
1661
1662
    /**
1663
     * @param array $errorLog
1664
     */
1665
    public function setErrorLog(array $errorLog): void
1666
    {
1667
        $this->errorLog = $errorLog;
1668
    }
1669
1670
    /**
1671
     * @return bool
1672
     */
1673
    public function isUpdate(): bool
1674
    {
1675
        return $this->update;
1676
    }
1677
1678
    /**
1679
     * @param bool $update
1680
     */
1681
    public function setUpdate(bool $update): void
1682
    {
1683
        $this->update = $update;
1684
    }
1685
1686
    /**
1687
     * @return array
1688
     */
1689
    public function getImportMode(): array
1690
    {
1691
        return $this->importMode;
1692
    }
1693
1694
    /**
1695
     * @param array $importMode
1696
     */
1697
    public function setImportMode(array $importMode): void
1698
    {
1699
        $this->importMode = $importMode;
1700
    }
1701
1702
    /**
1703
     * @return bool
1704
     */
1705
    public function isGlobalIgnorePid(): bool
1706
    {
1707
        return $this->globalIgnorePid;
1708
    }
1709
1710
    /**
1711
     * @param bool $globalIgnorePid
1712
     */
1713
    public function setGlobalIgnorePid(bool $globalIgnorePid): void
1714
    {
1715
        $this->globalIgnorePid = $globalIgnorePid;
1716
    }
1717
1718
    /**
1719
     * @return bool
1720
     */
1721
    public function isForceAllUids(): bool
1722
    {
1723
        return $this->forceAllUids;
1724
    }
1725
1726
    /**
1727
     * @param bool $forceAllUids
1728
     */
1729
    public function setForceAllUids(bool $forceAllUids): void
1730
    {
1731
        $this->forceAllUids = $forceAllUids;
1732
    }
1733
1734
    /**
1735
     * @return bool
1736
     */
1737
    public function isShowDiff(): bool
1738
    {
1739
        return $this->showDiff;
1740
    }
1741
1742
    /**
1743
     * @param bool $showDiff
1744
     */
1745
    public function setShowDiff(bool $showDiff): void
1746
    {
1747
        $this->showDiff = $showDiff;
1748
    }
1749
1750
    /**
1751
     * @return array
1752
     */
1753
    public function getSoftrefInputValues(): array
1754
    {
1755
        return $this->softrefInputValues;
1756
    }
1757
1758
    /**
1759
     * @param array $softrefInputValues
1760
     */
1761
    public function setSoftrefInputValues(array $softrefInputValues): void
1762
    {
1763
        $this->softrefInputValues = $softrefInputValues;
1764
    }
1765
1766
    /**
1767
     * @return string
1768
     */
1769
    public function getMode(): string
1770
    {
1771
        return $this->mode;
1772
    }
1773
1774
    /**
1775
     * @param string $mode
1776
     */
1777
    public function setMode(string $mode): void
1778
    {
1779
        $this->mode = $mode;
1780
    }
1781
1782
    /**
1783
     * @return array
1784
     */
1785
    public function getImportMapId(): array
1786
    {
1787
        return $this->importMapId;
1788
    }
1789
1790
    /**
1791
     * @param array $importMapId
1792
     */
1793
    public function setImportMapId(array $importMapId): void
1794
    {
1795
        $this->importMapId = $importMapId;
1796
    }
1797
1798
    /**
1799
     * @return array
1800
     */
1801
    public function getDat(): array
1802
    {
1803
        return $this->dat;
1804
    }
1805
}
1806