1<?php
2namespace TYPO3\CMS\Impexp;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Backend\Utility\BackendUtility;
18use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19use TYPO3\CMS\Core\Core\Environment;
20use TYPO3\CMS\Core\Imaging\Icon;
21use TYPO3\CMS\Core\Imaging\IconFactory;
22use TYPO3\CMS\Core\Localization\LanguageService;
23use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
24use TYPO3\CMS\Core\Resource\ResourceFactory;
25use TYPO3\CMS\Core\Type\Bitmask\Permission;
26use TYPO3\CMS\Core\Utility\DebugUtility;
27use TYPO3\CMS\Core\Utility\DiffUtility;
28use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
29use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
30use TYPO3\CMS\Core\Utility\GeneralUtility;
31use TYPO3\CMS\Core\Utility\PathUtility;
32
33/**
34 * EXAMPLE for using the impexp-class for exporting stuff:
35 *
36 * Create and initialize:
37 * $this->export = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Impexp\ImportExport::class);
38 * $this->export->init();
39 * Set which tables relations we will allow:
40 * $this->export->relOnlyTables[]="tt_news";	// exclusively includes. See comment in the class
41 *
42 * Adding records:
43 * $this->export->export_addRecord("pages", $this->pageinfo);
44 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 38));
45 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 39));
46 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 12));
47 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 74));
48 * $this->export->export_addRecord("sys_template", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("sys_template", 20));
49 *
50 * Adding all the relations (recursively in 5 levels so relations has THEIR relations registered as well)
51 * for($a=0;$a<5;$a++) {
52 * $addR = $this->export->export_addDBRelations($a);
53 * if (empty($addR)) break;
54 * }
55 *
56 * Finally load all the files.
57 * $this->export->export_addFilesFromRelations();	// MUST be after the DBrelations are set so that file from ALL added records are included!
58 *
59 * Write export
60 * $out = $this->export->compileMemoryToFileContent();
61 * @internal this is not part of TYPO3's Core API.
62 */
63
64/**
65 * T3D file Import/Export library (TYPO3 Record Document)
66 */
67abstract class ImportExport
68{
69    /**
70     * If set, static relations (not exported) will be shown in overview as well
71     *
72     * @var bool
73     */
74    public $showStaticRelations = false;
75
76    /**
77     * Name of the "fileadmin" folder where files for export/import should be located
78     *
79     * @var string
80     */
81    public $fileadminFolderName = '';
82
83    /**
84     * Whether "import" or "export" mode of object. Set through init() function
85     *
86     * @var string
87     */
88    public $mode = '';
89
90    /**
91     * Updates all records that has same UID instead of creating new!
92     *
93     * @var bool
94     */
95    public $update = false;
96
97    /**
98     * Is set by importData() when an import has been done.
99     *
100     * @var bool
101     */
102    public $doesImport = false;
103
104    /**
105     * If set to a page-record, then the preview display of the content will expect this page-record to be the target
106     * for the import and accordingly display validation information. This triggers the visual view of the
107     * import/export memory to validate if import is possible
108     *
109     * @var array
110     */
111    public $display_import_pid_record = [];
112
113    /**
114     * Setting import modes during update state: as_new, exclude, force_uid
115     *
116     * @var array
117     */
118    public $import_mode = [];
119
120    /**
121     * If set, PID correct is ignored globally
122     *
123     * @var bool
124     */
125    public $global_ignore_pid = false;
126
127    /**
128     * If set, all UID values are forced! (update or import)
129     *
130     * @var bool
131     */
132    public $force_all_UIDS = false;
133
134    /**
135     * If set, a diff-view column is added to the overview.
136     *
137     * @var bool
138     */
139    public $showDiff = false;
140
141    /**
142     * If set, and if the user is admin, allow the writing of PHP scripts to fileadmin/ area.
143     *
144     * @var bool
145     */
146    public $allowPHPScripts = false;
147
148    /**
149     * Array of values to substitute in editable softreferences.
150     *
151     * @var array
152     */
153    public $softrefInputValues = [];
154
155    /**
156     * Mapping between the fileID from import memory and the final filenames they are written to.
157     *
158     * @var array
159     */
160    public $fileIDMap = [];
161
162    /**
163     * Add table names here which are THE ONLY ones which will be included
164     * into export if found as relations. '_ALL' will allow all tables.
165     *
166     * @var array
167     */
168    public $relOnlyTables = [];
169
170    /**
171     * Add tables names here which should not be exported with the file.
172     * (Where relations should be mapped to same UIDs in target system).
173     *
174     * @var array
175     */
176    public $relStaticTables = [];
177
178    /**
179     * Exclude map. Keys are table:uid  pairs and if set, records are not added to the export.
180     *
181     * @var array
182     */
183    public $excludeMap = [];
184
185    /**
186     * Soft Reference Token ID modes.
187     *
188     * @var array
189     */
190    public $softrefCfg = [];
191
192    /**
193     * Listing extension dependencies.
194     *
195     * @var array
196     */
197    public $extensionDependencies = [];
198
199    /**
200     * After records are written this array is filled with [table][original_uid] = [new_uid]
201     *
202     * @var array
203     */
204    public $import_mapId = [];
205
206    /**
207     * Error log.
208     *
209     * @var array
210     */
211    public $errorLog = [];
212
213    /**
214     * Cache for record paths
215     *
216     * @var array
217     */
218    public $cache_getRecordPath = [];
219
220    /**
221     * Cache of checkPID values.
222     *
223     * @var array
224     */
225    public $checkPID_cache = [];
226
227    /**
228     * Set internally if the gzcompress function exists
229     * Used by ImportExportController
230     *
231     * @var bool
232     */
233    public $compress = false;
234
235    /**
236     * Internal import/export memory
237     *
238     * @var array
239     */
240    public $dat = [];
241
242    /**
243     * File processing object
244     *
245     * @var ExtendedFileUtility
246     */
247    protected $fileProcObj;
248
249    /**
250     * @var array
251     */
252    protected $remainHeader = [];
253
254    /**
255     * @var IconFactory
256     */
257    protected $iconFactory;
258
259    /**
260     * Flag to control whether all disabled records and their children are excluded (true) or included (false). Defaults
261     * to the old behaviour of including everything.
262     *
263     * @var bool
264     */
265    protected $excludeDisabledRecords = false;
266
267    /**
268     * The constructor
269     */
270    public function __construct()
271    {
272        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
273    }
274
275    /**************************
276     * Initialize
277     *************************/
278
279    /**
280     * Init the object, both import and export
281     */
282    public function init()
283    {
284        $this->compress = function_exists('gzcompress');
285        $this->fileadminFolderName = !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']) ? rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') : 'fileadmin';
286    }
287
288    /********************************************************
289     * Visual rendering of import/export memory, $this->dat
290     ********************************************************/
291
292    /**
293     * Displays an overview of the header-content.
294     *
295     * @return array The view data
296     */
297    public function displayContentOverview()
298    {
299        if (!isset($this->dat['header'])) {
300            return [];
301        }
302        // Check extension dependencies:
303        foreach ($this->dat['header']['extensionDependencies'] as $extKey) {
304            if (!empty($extKey) && !ExtensionManagementUtility::isLoaded($extKey)) {
305                $this->error('DEPENDENCY: The extension with key "' . $extKey . '" must be installed!');
306            }
307        }
308
309        // Probably this is done to save memory space?
310        unset($this->dat['files']);
311
312        $viewData = [];
313        // Traverse header:
314        $this->remainHeader = $this->dat['header'];
315        // If there is a page tree set, show that:
316        if (is_array($this->dat['header']['pagetree'])) {
317            reset($this->dat['header']['pagetree']);
318            $lines = [];
319            $this->traversePageTree($this->dat['header']['pagetree'], $lines);
320
321            $viewData['dat'] = $this->dat;
322            $viewData['update'] = $this->update;
323            $viewData['showDiff'] = $this->showDiff;
324            if (!empty($lines)) {
325                foreach ($lines as &$r) {
326                    $r['controls'] = $this->renderControls($r);
327                    $r['message'] = ($r['msg'] && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($r['msg']) . '</span>' : '');
328                }
329                $viewData['pagetreeLines'] = $lines;
330            } else {
331                $viewData['pagetreeLines'] = [];
332            }
333        }
334        // Print remaining records that were not contained inside the page tree:
335        if (is_array($this->remainHeader['records'])) {
336            $lines = [];
337            if (is_array($this->remainHeader['records']['pages'])) {
338                $this->traversePageRecords($this->remainHeader['records']['pages'], $lines);
339            }
340            $this->traverseAllRecords($this->remainHeader['records'], $lines);
341            if (!empty($lines)) {
342                foreach ($lines as &$r) {
343                    $r['controls'] = $this->renderControls($r);
344                    $r['message'] = ($r['msg'] && !$this->doesImport ? '<span class="text-danger">' . htmlspecialchars($r['msg']) . '</span>' : '');
345                }
346                $viewData['remainingRecords'] = $lines;
347            }
348        }
349
350        return $viewData;
351    }
352
353    /**
354     * Go through page tree for display
355     *
356     * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
357     * @param array $lines Output lines array (is passed by reference and modified)
358     * @param string $preCode Pre-HTML code
359     */
360    public function traversePageTree($pT, &$lines, $preCode = '')
361    {
362        foreach ($pT as $k => $v) {
363            if ($this->excludeDisabledRecords === true && !$this->isActive('pages', $k)) {
364                $this->excludePageAndRecords($k, $v);
365                continue;
366            }
367
368            // Add this page:
369            $this->singleRecordLines('pages', $k, $lines, $preCode);
370            // Subrecords:
371            if (is_array($this->dat['header']['pid_lookup'][$k])) {
372                foreach ($this->dat['header']['pid_lookup'][$k] as $t => $recUidArr) {
373                    if ($t !== 'pages') {
374                        foreach ($recUidArr as $ruid => $value) {
375                            $this->singleRecordLines($t, $ruid, $lines, $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;');
376                        }
377                    }
378                }
379                unset($this->remainHeader['pid_lookup'][$k]);
380            }
381            // Subpages, called recursively:
382            if (is_array($v['subrow'])) {
383                $this->traversePageTree($v['subrow'], $lines, $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;');
384            }
385        }
386    }
387
388    /**
389     * Test whether a record is active (i.e. not hidden)
390     *
391     * @param string $table Name of the records' database table
392     * @param int $uid Database uid of the record
393     * @return bool true if the record is active, false otherwise
394     */
395    protected function isActive($table, $uid)
396    {
397        return
398            !isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])
399            || !(bool)$this->dat['records'][$table . ':' . $uid]['data'][
400                $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled']
401            ];
402    }
403
404    /**
405     * Exclude a page, its sub pages (recursively) and records placed in them from this import/export
406     *
407     * @param int $pageUid Uid of the page to exclude
408     * @param array $pageTree Page tree array with uid/subrow (from ->dat[header][pagetree]
409     */
410    protected function excludePageAndRecords($pageUid, $pageTree)
411    {
412        // Prevent having this page appear in "remaining records" table
413        unset($this->remainHeader['records']['pages'][$pageUid]);
414
415        // Subrecords
416        if (is_array($this->dat['header']['pid_lookup'][$pageUid])) {
417            foreach ($this->dat['header']['pid_lookup'][$pageUid] as $table => $recordData) {
418                if ($table !== 'pages') {
419                    foreach (array_keys($recordData) as $uid) {
420                        unset($this->remainHeader['records'][$table][$uid]);
421                    }
422                }
423            }
424            unset($this->remainHeader['pid_lookup'][$pageUid]);
425        }
426        // Subpages excluded recursively
427        if (is_array($pageTree['subrow'])) {
428            foreach ($pageTree['subrow'] as $subPageUid => $subPageTree) {
429                $this->excludePageAndRecords($subPageUid, $subPageTree);
430            }
431        }
432    }
433
434    /**
435     * Go through remaining pages (not in tree)
436     *
437     * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
438     * @param array $lines Output lines array (is passed by reference and modified)
439     */
440    public function traversePageRecords($pT, &$lines)
441    {
442        foreach ($pT as $k => $rHeader) {
443            $this->singleRecordLines('pages', $k, $lines, '', 1);
444            // Subrecords:
445            if (is_array($this->dat['header']['pid_lookup'][$k])) {
446                foreach ($this->dat['header']['pid_lookup'][$k] as $t => $recUidArr) {
447                    if ($t !== 'pages') {
448                        foreach ($recUidArr as $ruid => $value) {
449                            $this->singleRecordLines($t, $ruid, $lines, '&nbsp;&nbsp;&nbsp;&nbsp;');
450                        }
451                    }
452                }
453                unset($this->remainHeader['pid_lookup'][$k]);
454            }
455        }
456    }
457
458    /**
459     * Go through ALL records (if the pages are displayed first, those will not be among these!)
460     *
461     * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
462     * @param array $lines Output lines array (is passed by reference and modified)
463     */
464    public function traverseAllRecords($pT, &$lines)
465    {
466        foreach ($pT as $t => $recUidArr) {
467            $this->addGeneralErrorsByTable($t);
468            if ($t !== 'pages') {
469                $preCode = '';
470                foreach ($recUidArr as $ruid => $value) {
471                    $this->singleRecordLines($t, $ruid, $lines, $preCode, 1);
472                }
473            }
474        }
475    }
476
477    /**
478     * Log general error message for a given table
479     *
480     * @param string $table database table name
481     */
482    protected function addGeneralErrorsByTable($table)
483    {
484        if ($this->update && $table === 'sys_file') {
485            $this->error('Updating sys_file records is not supported! They will be imported as new records!');
486        }
487        if ($this->force_all_UIDS && $table === 'sys_file') {
488            $this->error('Forcing uids of sys_file records is not supported! They will be imported as new records!');
489        }
490    }
491
492    /**
493     * Add entries for a single record
494     *
495     * @param string $table Table name
496     * @param int $uid Record uid
497     * @param array $lines Output lines array (is passed by reference and modified)
498     * @param string $preCode Pre-HTML code
499     * @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.
500     */
501    public function singleRecordLines($table, $uid, &$lines, $preCode, $checkImportInPidRecord = false)
502    {
503        // Get record:
504        $record = $this->dat['header']['records'][$table][$uid];
505        unset($this->remainHeader['records'][$table][$uid]);
506        if (!is_array($record) && !($table === 'pages' && !$uid)) {
507            $this->error('MISSING RECORD: ' . $table . ':' . $uid);
508        }
509        // Begin to create the line arrays information record, pInfo:
510        $pInfo = [];
511        $pInfo['ref'] = $table . ':' . $uid;
512        // Unknown table name:
513        $lang = $this->getLanguageService();
514        if ($table === '_SOFTREF_') {
515            $pInfo['preCode'] = $preCode;
516            $pInfo['title'] = '<em>' . htmlspecialchars($lang->getLL('impexpcore_singlereco_softReferencesFiles')) . '</em>';
517        } elseif (!isset($GLOBALS['TCA'][$table])) {
518            // Unknown table name:
519            $pInfo['preCode'] = $preCode;
520            $pInfo['msg'] = 'UNKNOWN TABLE \'' . $pInfo['ref'] . '\'';
521            $pInfo['title'] = '<em>' . htmlspecialchars($record['title']) . '</em>';
522        } else {
523            // prepare data attribute telling whether the record is active or hidden, allowing frontend bulk selection
524            $pInfo['active'] = $this->isActive($table, $uid) ? 'active' : 'hidden';
525
526            // Otherwise, set table icon and title.
527            // Import Validation (triggered by $this->display_import_pid_record) will show messages if import is not possible of various items.
528            if (is_array($this->display_import_pid_record) && !empty($this->display_import_pid_record)) {
529                if ($checkImportInPidRecord) {
530                    if (!$this->getBackendUser()->doesUserHaveAccess($this->display_import_pid_record, ($table === 'pages' ? 8 : 16))) {
531                        $pInfo['msg'] .= '\'' . $pInfo['ref'] . '\' cannot be INSERTED on this page! ';
532                    }
533                    if (!$this->checkDokType($table, $this->display_import_pid_record['doktype']) && !$GLOBALS['TCA'][$table]['ctrl']['rootLevel']) {
534                        $pInfo['msg'] .= '\'' . $table . '\' cannot be INSERTED on this page type (change page type to \'Folder\'.) ';
535                    }
536                }
537                if (!$this->getBackendUser()->check('tables_modify', $table)) {
538                    $pInfo['msg'] .= 'You are not allowed to CREATE \'' . $table . '\' tables! ';
539                }
540                if ($GLOBALS['TCA'][$table]['ctrl']['readOnly']) {
541                    $pInfo['msg'] .= 'TABLE \'' . $table . '\' is READ ONLY! ';
542                }
543                if ($GLOBALS['TCA'][$table]['ctrl']['adminOnly'] && !$this->getBackendUser()->isAdmin()) {
544                    $pInfo['msg'] .= 'TABLE \'' . $table . '\' is ADMIN ONLY! ';
545                }
546                if ($GLOBALS['TCA'][$table]['ctrl']['is_static']) {
547                    $pInfo['msg'] .= 'TABLE \'' . $table . '\' is a STATIC TABLE! ';
548                }
549                if ((int)$GLOBALS['TCA'][$table]['ctrl']['rootLevel'] === 1) {
550                    $pInfo['msg'] .= 'TABLE \'' . $table . '\' will be inserted on ROOT LEVEL! ';
551                }
552                $diffInverse = false;
553                $recInf = null;
554                if ($this->update) {
555                    // In case of update-PREVIEW we swap the diff-sources.
556                    $diffInverse = true;
557                    $recInf = $this->doesRecordExist($table, $uid, $this->showDiff ? '*' : '');
558                    $pInfo['updatePath'] = $recInf ? htmlspecialchars($this->getRecordPath($recInf['pid'])) : '<strong>NEW!</strong>';
559                    // Mode selector:
560                    $optValues = [];
561                    $optValues[] = $recInf ? $lang->getLL('impexpcore_singlereco_update') : $lang->getLL('impexpcore_singlereco_insert');
562                    if ($recInf) {
563                        $optValues['as_new'] = $lang->getLL('impexpcore_singlereco_importAsNew');
564                    }
565                    if ($recInf) {
566                        if (!$this->global_ignore_pid) {
567                            $optValues['ignore_pid'] = $lang->getLL('impexpcore_singlereco_ignorePid');
568                        } else {
569                            $optValues['respect_pid'] = $lang->getLL('impexpcore_singlereco_respectPid');
570                        }
571                    }
572                    if (!$recInf && $this->getBackendUser()->isAdmin()) {
573                        $optValues['force_uid'] = sprintf($lang->getLL('impexpcore_singlereco_forceUidSAdmin'), $uid);
574                    }
575                    $optValues['exclude'] = $lang->getLL('impexpcore_singlereco_exclude');
576                    if ($table === 'sys_file') {
577                        $pInfo['updateMode'] = '';
578                    } else {
579                        $pInfo['updateMode'] = $this->renderSelectBox('tx_impexp[import_mode][' . $table . ':' . $uid . ']', $this->import_mode[$table . ':' . $uid], $optValues);
580                    }
581                }
582                // Diff view:
583                if ($this->showDiff) {
584                    // For IMPORTS, get new id:
585                    if ($newUid = $this->import_mapId[$table][$uid]) {
586                        $diffInverse = false;
587                        $recInf = $this->doesRecordExist($table, $newUid, '*');
588                        BackendUtility::workspaceOL($table, $recInf);
589                    }
590                    if (is_array($recInf)) {
591                        $pInfo['showDiffContent'] = $this->compareRecords($recInf, $this->dat['records'][$table . ':' . $uid]['data'], $table, $diffInverse);
592                    }
593                }
594            }
595            $pInfo['preCode'] = $preCode . '<span title="' . htmlspecialchars($table . ':' . $uid) . '">'
596                . $this->iconFactory->getIconForRecord($table, (array)$this->dat['records'][$table . ':' . $uid]['data'], Icon::SIZE_SMALL)->render()
597                . '</span>';
598            $pInfo['title'] = htmlspecialchars($record['title']);
599            // View page:
600            if ($table === 'pages') {
601                $viewID = $this->mode === 'export' ? $uid : ($this->doesImport ? $this->import_mapId['pages'][$uid] : 0);
602                if ($viewID) {
603                    $pInfo['title'] = '<a href="#" onclick="' . htmlspecialchars(BackendUtility::viewOnClick($viewID)) . 'return false;">' . $pInfo['title'] . '</a>';
604                }
605            }
606        }
607        $pInfo['type'] = 'record';
608        $lines[] = $pInfo;
609        // File relations:
610        if (is_array($record['filerefs'])) {
611            $this->addFiles($record['filerefs'], $lines, $preCode);
612        }
613        // DB relations
614        if (is_array($record['rels'])) {
615            $this->addRelations($record['rels'], $lines, $preCode);
616        }
617        // Soft ref
618        if (!empty($record['softrefs'])) {
619            $preCode_A = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;';
620            $preCode_B = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
621            foreach ($record['softrefs'] as $info) {
622                $pInfo = [];
623                $pInfo['preCode'] = $preCode_A . $this->iconFactory->getIcon('status-reference-soft', Icon::SIZE_SMALL)->render();
624                $pInfo['title'] = '<em>' . $info['field'] . ', "' . $info['spKey'] . '" </em>: <span title="' . htmlspecialchars($info['matchString']) . '">' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($info['matchString'], 60)) . '</span>';
625                if ($info['subst']['type']) {
626                    if (strlen($info['subst']['title'])) {
627                        $pInfo['title'] .= '<br/>' . $preCode_B . '<strong>' . htmlspecialchars($lang->getLL('impexpcore_singlereco_title')) . '</strong> ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($info['subst']['title'], 60));
628                    }
629                    if (strlen($info['subst']['description'])) {
630                        $pInfo['title'] .= '<br/>' . $preCode_B . '<strong>' . htmlspecialchars($lang->getLL('impexpcore_singlereco_descr')) . '</strong> ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($info['subst']['description'], 60));
631                    }
632                    $pInfo['title'] .= '<br/>' . $preCode_B . ($info['subst']['type'] === 'file' ? htmlspecialchars($lang->getLL('impexpcore_singlereco_filename')) . ' <strong>' . $info['subst']['relFileName'] . '</strong>' : '') . ($info['subst']['type'] === 'string' ? htmlspecialchars($lang->getLL('impexpcore_singlereco_value')) . ' <strong>' . $info['subst']['tokenValue'] . '</strong>' : '') . ($info['subst']['type'] === 'db' ? htmlspecialchars($lang->getLL('impexpcore_softrefsel_record')) . ' <strong>' . $info['subst']['recordRef'] . '</strong>' : '');
633                }
634                $pInfo['ref'] = 'SOFTREF';
635                $pInfo['type'] = 'softref';
636                $pInfo['_softRefInfo'] = $info;
637                $pInfo['type'] = 'softref';
638                $mode = $this->softrefCfg[$info['subst']['tokenID']]['mode'];
639                if ($info['error'] && $mode !== 'editable' && $mode !== 'exclude') {
640                    $pInfo['msg'] .= $info['error'];
641                }
642                $lines[] = $pInfo;
643                // Add relations:
644                if ($info['subst']['type'] === 'db') {
645                    list($tempTable, $tempUid) = explode(':', $info['subst']['recordRef']);
646                    $this->addRelations([['table' => $tempTable, 'id' => $tempUid, 'tokenID' => $info['subst']['tokenID']]], $lines, $preCode_B, [], '');
647                }
648                // Add files:
649                if ($info['subst']['type'] === 'file') {
650                    $this->addFiles([$info['file_ID']], $lines, $preCode_B, '', $info['subst']['tokenID']);
651                }
652            }
653        }
654    }
655
656    /**
657     * Add DB relations entries for a record's rels-array
658     *
659     * @param array $rels Array of relations
660     * @param array $lines Output lines array (is passed by reference and modified)
661     * @param string $preCode Pre-HTML code
662     * @param array $recurCheck Recursivity check stack
663     * @param string $htmlColorClass Alternative HTML color class to use.
664     * @internal
665     * @see singleRecordLines()
666     */
667    public function addRelations($rels, &$lines, $preCode, $recurCheck = [], $htmlColorClass = '')
668    {
669        foreach ($rels as $dat) {
670            $table = $dat['table'];
671            $uid = $dat['id'];
672            $pInfo = [];
673            $pInfo['ref'] = $table . ':' . $uid;
674            if (in_array($pInfo['ref'], $recurCheck)) {
675                continue;
676            }
677            $iconName = 'status-status-checked';
678            $iconClass = '';
679            $staticFixed = false;
680            $record = null;
681            if ($uid > 0) {
682                $record = $this->dat['header']['records'][$table][$uid];
683                if (!is_array($record)) {
684                    if ($this->isTableStatic($table) || $this->isExcluded($table, $uid) || $dat['tokenID'] && !$this->includeSoftref($dat['tokenID'])) {
685                        $pInfo['title'] = htmlspecialchars('STATIC: ' . $pInfo['ref']);
686                        $iconClass = 'text-info';
687                        $staticFixed = true;
688                    } else {
689                        $doesRE = $this->doesRecordExist($table, $uid);
690                        $lostPath = $this->getRecordPath($table === 'pages' ? $doesRE['uid'] : $doesRE['pid']);
691                        $pInfo['title'] = htmlspecialchars($pInfo['ref']);
692                        $pInfo['title'] = '<span title="' . htmlspecialchars($lostPath) . '">' . $pInfo['title'] . '</span>';
693                        $pInfo['msg'] = 'LOST RELATION' . (!$doesRE ? ' (Record not found!)' : ' (Path: ' . $lostPath . ')');
694                        $iconClass = 'text-danger';
695                        $iconName = 'status-dialog-warning';
696                    }
697                } else {
698                    $pInfo['title'] = htmlspecialchars($record['title']);
699                    $pInfo['title'] = '<span title="' . htmlspecialchars($this->getRecordPath(($table === 'pages' ? $record['uid'] : $record['pid']))) . '">' . $pInfo['title'] . '</span>';
700                }
701            } else {
702                // Negative values in relation fields. This is typically sys_language fields, fe_users fields etc. They are static values. They CAN theoretically be negative pointers to uids in other tables but this is so rarely used that it is not supported
703                $pInfo['title'] = htmlspecialchars('FIXED: ' . $pInfo['ref']);
704                $staticFixed = true;
705            }
706
707            $icon = '<span class="' . $iconClass . '" title="' . htmlspecialchars($pInfo['ref']) . '">' . $this->iconFactory->getIcon($iconName, Icon::SIZE_SMALL)->render() . '</span>';
708
709            $pInfo['preCode'] = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;' . $icon;
710            $pInfo['type'] = 'rel';
711            if (!$staticFixed || $this->showStaticRelations) {
712                $lines[] = $pInfo;
713                if (is_array($record) && is_array($record['rels'])) {
714                    $this->addRelations($record['rels'], $lines, $preCode . '&nbsp;&nbsp;', array_merge($recurCheck, [$pInfo['ref']]));
715                }
716            }
717        }
718    }
719
720    /**
721     * Add file relation entries for a record's rels-array
722     *
723     * @param array $rels Array of file IDs
724     * @param array $lines Output lines array (is passed by reference and modified)
725     * @param string $preCode Pre-HTML code
726     * @param string $htmlColorClass Alternative HTML color class to use.
727     * @param string $tokenID Token ID if this is a softreference (in which case it only makes sense with a single element in the $rels array!)
728     * @internal
729     * @see singleRecordLines()
730     */
731    public function addFiles($rels, &$lines, $preCode, $htmlColorClass = '', $tokenID = '')
732    {
733        foreach ($rels as $ID) {
734            // Process file:
735            $pInfo = [];
736            $fI = $this->dat['header']['files'][$ID];
737            if (!is_array($fI)) {
738                if (!$tokenID || $this->includeSoftref($tokenID)) {
739                    $pInfo['msg'] = 'MISSING FILE: ' . $ID;
740                    $this->error('MISSING FILE: ' . $ID);
741                } else {
742                    return;
743                }
744            }
745            $pInfo['preCode'] = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;' . $this->iconFactory->getIcon('status-reference-hard', Icon::SIZE_SMALL)->render();
746            $pInfo['title'] = htmlspecialchars($fI['filename']);
747            $pInfo['ref'] = 'FILE';
748            $pInfo['type'] = 'file';
749            // If import mode and there is a non-RTE softreference, check the destination directory:
750            if ($this->mode === 'import' && $tokenID && !$fI['RTE_ORIG_ID']) {
751                if (isset($fI['parentRelFileName'])) {
752                    $pInfo['msg'] = 'Seems like this file is already referenced from within an HTML/CSS file. That takes precedence. ';
753                } else {
754                    $testDirPrefix = PathUtility::dirname($fI['relFileName']) . '/';
755                    $testDirPrefix2 = $this->verifyFolderAccess($testDirPrefix);
756                    if (!$testDirPrefix2) {
757                        $pInfo['msg'] = 'ERROR: There are no available filemounts to write file in! ';
758                    } elseif ($testDirPrefix !== $testDirPrefix2) {
759                        $pInfo['msg'] = 'File will be attempted written to "' . $testDirPrefix2 . '". ';
760                    }
761                }
762                // Check if file exists:
763                if (file_exists(Environment::getPublicPath() . '/' . $fI['relFileName'])) {
764                    if ($this->update) {
765                        $pInfo['updatePath'] .= 'File exists.';
766                    } else {
767                        $pInfo['msg'] .= 'File already exists! ';
768                    }
769                }
770                // Check extension:
771                $fileProcObj = $this->getFileProcObj();
772                if ($fileProcObj->actionPerms['addFile']) {
773                    $testFI = GeneralUtility::split_fileref(Environment::getPublicPath() . '/' . $fI['relFileName']);
774                    if (!$this->allowPHPScripts && !$fileProcObj->checkIfAllowed($testFI['fileext'], $testFI['path'], $testFI['file'])) {
775                        $pInfo['msg'] .= 'File extension was not allowed!';
776                    }
777                } else {
778                    $pInfo['msg'] = 'You user profile does not allow you to create files on the server!';
779                }
780            }
781            $pInfo['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIDMap[$ID]);
782            $lines[] = $pInfo;
783            unset($this->remainHeader['files'][$ID]);
784            // RTE originals:
785            if ($fI['RTE_ORIG_ID']) {
786                $ID = $fI['RTE_ORIG_ID'];
787                $pInfo = [];
788                $fI = $this->dat['header']['files'][$ID];
789                if (!is_array($fI)) {
790                    $pInfo['msg'] = 'MISSING RTE original FILE: ' . $ID;
791                    $this->error('MISSING RTE original FILE: ' . $ID);
792                }
793                $pInfo['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIDMap[$ID]);
794                $pInfo['preCode'] = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' . $this->iconFactory->getIcon('status-reference-hard', Icon::SIZE_SMALL)->render();
795                $pInfo['title'] = htmlspecialchars($fI['filename']) . ' <em>(Original)</em>';
796                $pInfo['ref'] = 'FILE';
797                $pInfo['type'] = 'file';
798                $lines[] = $pInfo;
799                unset($this->remainHeader['files'][$ID]);
800            }
801            // External resources:
802            if (is_array($fI['EXT_RES_ID'])) {
803                foreach ($fI['EXT_RES_ID'] as $extID) {
804                    $pInfo = [];
805                    $fI = $this->dat['header']['files'][$extID];
806                    if (!is_array($fI)) {
807                        $pInfo['msg'] = 'MISSING External Resource FILE: ' . $extID;
808                        $this->error('MISSING External Resource FILE: ' . $extID);
809                    } else {
810                        $pInfo['updatePath'] = $fI['parentRelFileName'];
811                    }
812                    $pInfo['showDiffContent'] = PathUtility::stripPathSitePrefix($this->fileIDMap[$extID]);
813                    $pInfo['preCode'] = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' . $this->iconFactory->getIcon('actions-insert-reference', Icon::SIZE_SMALL)->render();
814                    $pInfo['title'] = htmlspecialchars($fI['filename']) . ' <em>(Resource)</em>';
815                    $pInfo['ref'] = 'FILE';
816                    $pInfo['type'] = 'file';
817                    $lines[] = $pInfo;
818                    unset($this->remainHeader['files'][$extID]);
819                }
820            }
821        }
822    }
823
824    /**
825     * Verifies that a table is allowed on a certain doktype of a page
826     *
827     * @param string $checkTable Table name to check
828     * @param int $doktype doktype value.
829     * @return bool TRUE if OK
830     */
831    public function checkDokType($checkTable, $doktype)
832    {
833        $allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'] ?? $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
834        $allowedArray = GeneralUtility::trimExplode(',', $allowedTableList, true);
835        // If all tables or the table is listed as an allowed type, return TRUE
836        if (strstr($allowedTableList, '*') || in_array($checkTable, $allowedArray)) {
837            return true;
838        }
839        return false;
840    }
841
842    /**
843     * Render input controls for import or export
844     *
845     * @param array $r Configuration for element
846     * @return string HTML
847     */
848    public function renderControls($r)
849    {
850        if ($this->mode === 'export') {
851            if ($r['type'] === 'record') {
852                return '<input type="checkbox" class="t3js-exclude-checkbox" name="tx_impexp[exclude][' . $r['ref'] . ']" id="checkExclude' . $r['ref'] . '" value="1" /> <label for="checkExclude' . $r['ref'] . '">' . htmlspecialchars($this->getLanguageService()->getLL('impexpcore_singlereco_exclude')) . '</label>';
853            }
854            return  $r['type'] === 'softref' ? $this->softrefSelector($r['_softRefInfo']) : '';
855        }
856        // During import
857        // For softreferences with editable fields:
858        if ($r['type'] === 'softref' && is_array($r['_softRefInfo']['subst']) && $r['_softRefInfo']['subst']['tokenID']) {
859            $tokenID = $r['_softRefInfo']['subst']['tokenID'];
860            $cfg = $this->softrefCfg[$tokenID];
861            if ($cfg['mode'] === 'editable') {
862                return (strlen($cfg['title']) ? '<strong>' . htmlspecialchars($cfg['title']) . '</strong><br/>' : '') . htmlspecialchars($cfg['description']) . '<br/>
863						<input type="text" name="tx_impexp[softrefInputValues][' . $tokenID . ']" value="' . htmlspecialchars($this->softrefInputValues[$tokenID] ?? $cfg['defValue']) . '" />';
864            }
865        }
866
867        return '';
868    }
869
870    /**
871     * Selectorbox with export options for soft references
872     *
873     * @param array $cfg Softref configuration array. An export box is shown only if a substitution scheme is found for the soft reference.
874     * @return string Selector box HTML
875     */
876    public function softrefSelector($cfg)
877    {
878        // Looking for file ID if any:
879        $fI = $cfg['file_ID'] ? $this->dat['header']['files'][$cfg['file_ID']] : [];
880        // Substitution scheme has to be around and RTE images MUST be exported.
881        if (is_array($cfg['subst']) && $cfg['subst']['tokenID'] && !$fI['RTE_ORIG_ID']) {
882            // Create options:
883            $optValues = [];
884            $optValues[''] = '';
885            $optValues['editable'] = $this->getLanguageService()->getLL('impexpcore_softrefsel_editable');
886            $optValues['exclude'] = $this->getLanguageService()->getLL('impexpcore_softrefsel_exclude');
887            // Get current value:
888            $value = $this->softrefCfg[$cfg['subst']['tokenID']]['mode'];
889            // Render options selector:
890            $selectorbox = $this->renderSelectBox('tx_impexp[softrefCfg][' . $cfg['subst']['tokenID'] . '][mode]', $value, $optValues) . '<br/>';
891            if ($value === 'editable') {
892                $descriptionField = '';
893                // Title:
894                if (strlen($cfg['subst']['title'])) {
895                    $descriptionField .= '
896					<input type="hidden" name="tx_impexp[softrefCfg][' . $cfg['subst']['tokenID'] . '][title]" value="' . htmlspecialchars($cfg['subst']['title']) . '" />
897					<strong>' . htmlspecialchars($cfg['subst']['title']) . '</strong><br/>';
898                }
899                // Description:
900                if (!strlen($cfg['subst']['description'])) {
901                    $descriptionField .= '
902					' . htmlspecialchars($this->getLanguageService()->getLL('impexpcore_printerror_description')) . '<br/>
903					<input type="text" name="tx_impexp[softrefCfg][' . $cfg['subst']['tokenID'] . '][description]" value="' . htmlspecialchars($this->softrefCfg[$cfg['subst']['tokenID']]['description']) . '" />';
904                } else {
905                    $descriptionField .= '
906
907					<input type="hidden" name="tx_impexp[softrefCfg][' . $cfg['subst']['tokenID'] . '][description]" value="' . htmlspecialchars($cfg['subst']['description']) . '" />' . htmlspecialchars($cfg['subst']['description']);
908                }
909                // Default Value:
910                $descriptionField .= '<input type="hidden" name="tx_impexp[softrefCfg][' . $cfg['subst']['tokenID'] . '][defValue]" value="' . htmlspecialchars($cfg['subst']['tokenValue']) . '" />';
911            } else {
912                $descriptionField = '';
913            }
914            return $selectorbox . $descriptionField;
915        }
916        return '';
917    }
918
919    /**
920     * Verifies that the input path relative to public web path is found in the backend users filemounts.
921     * If it doesn't it will try to find another relative filemount for the user and return an alternative path prefix for the file.
922     *
923     * @param string $dirPrefix Path relative to public web path
924     * @param bool $noAlternative If set, Do not look for alternative path! Just return FALSE
925     * @return string|bool If a path is available that will be returned, otherwise FALSE.
926     */
927    public function verifyFolderAccess($dirPrefix, $noAlternative = false)
928    {
929        // Check the absolute path for public web path, if the user has access - no problem
930        try {
931            ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($dirPrefix);
932            return $dirPrefix;
933        } catch (InsufficientFolderAccessPermissionsException $e) {
934            // Check all storages available for the user as alternative
935            if (!$noAlternative) {
936                $fileStorages = $this->getBackendUser()->getFileStorages();
937                foreach ($fileStorages as $fileStorage) {
938                    try {
939                        $folder = $fileStorage->getFolder(rtrim($dirPrefix, '/'));
940                        return $folder->getPublicUrl();
941                    } catch (InsufficientFolderAccessPermissionsException $e) {
942                    }
943                }
944            }
945        }
946        return false;
947    }
948
949    /*****************************
950     * Helper functions of kinds
951     *****************************/
952
953    /**
954     * @return string
955     */
956    protected function getTemporaryFolderName()
957    {
958        $temporaryPath = Environment::getVarPath() . '/transient/';
959        do {
960            $temporaryFolderName = $temporaryPath . 'export_temp_files_' . mt_rand(1, PHP_INT_MAX);
961        } while (is_dir($temporaryFolderName));
962        GeneralUtility::mkdir($temporaryFolderName);
963        return $temporaryFolderName;
964    }
965
966    /**
967     * Recursively flattening the idH array
968     *
969     * @param array $idH Page uid hierarchy
970     * @param array $a Accumulation array of pages (internal, don't set from outside)
971     * @return array Array with uid-uid pairs for all pages in the page tree.
972     * @see Import::flatInversePageTree_pid()
973     */
974    public function flatInversePageTree($idH, $a = [])
975    {
976        if (is_array($idH)) {
977            $idH = array_reverse($idH);
978            foreach ($idH as $k => $v) {
979                $a[$v['uid']] = $v['uid'];
980                if (is_array($v['subrow'])) {
981                    $a = $this->flatInversePageTree($v['subrow'], $a);
982                }
983            }
984        }
985        return $a;
986    }
987
988    /**
989     * Returns TRUE if the input table name is to be regarded as a static relation (that is, not exported etc).
990     *
991     * @param string $table Table name
992     * @return bool TRUE, if table is marked static
993     */
994    public function isTableStatic($table)
995    {
996        if (is_array($GLOBALS['TCA'][$table])) {
997            return $GLOBALS['TCA'][$table]['ctrl']['is_static'] || in_array($table, $this->relStaticTables) || in_array('_ALL', $this->relStaticTables);
998        }
999        return false;
1000    }
1001
1002    /**
1003     * Returns TRUE if the input table name is to be included as relation
1004     *
1005     * @param string $table Table name
1006     * @return bool TRUE, if table is marked static
1007     */
1008    public function inclRelation($table)
1009    {
1010        return is_array($GLOBALS['TCA'][$table])
1011            && (in_array($table, $this->relOnlyTables) || in_array('_ALL', $this->relOnlyTables))
1012            && $this->getBackendUser()->check('tables_select', $table);
1013    }
1014
1015    /**
1016     * Returns TRUE if the element should be excluded as static record.
1017     *
1018     * @param string $table Table name
1019     * @param int $uid UID value
1020     * @return bool TRUE, if table is marked static
1021     */
1022    public function isExcluded($table, $uid)
1023    {
1024        return (bool)$this->excludeMap[$table . ':' . $uid];
1025    }
1026
1027    /**
1028     * Returns TRUE if soft reference should be included in exported file.
1029     *
1030     * @param string $tokenID Token ID for soft reference
1031     * @return bool TRUE if softreference media should be included
1032     */
1033    public function includeSoftref($tokenID)
1034    {
1035        $mode = $this->softrefCfg[$tokenID]['mode'];
1036        return $tokenID && $mode !== 'exclude' && $mode !== 'editable';
1037    }
1038
1039    /**
1040     * Checking if a PID is in the webmounts of the user
1041     *
1042     * @param int $pid Page ID to check
1043     * @return bool TRUE if OK
1044     */
1045    public function checkPID($pid)
1046    {
1047        if (!isset($this->checkPID_cache[$pid])) {
1048            $this->checkPID_cache[$pid] = (bool)$this->getBackendUser()->isInWebMount($pid);
1049        }
1050        return $this->checkPID_cache[$pid];
1051    }
1052
1053    /**
1054     * Checks if the position of an updated record is configured to be corrected. This can be disabled globally and changed for elements individually.
1055     *
1056     * @param string $table Table name
1057     * @param int $uid Uid or record
1058     * @return bool TRUE if the position of the record should be updated to match the one in the import structure
1059     */
1060    public function dontIgnorePid($table, $uid)
1061    {
1062        return $this->import_mode[$table . ':' . $uid] !== 'ignore_pid' && (!$this->global_ignore_pid || $this->import_mode[$table . ':' . $uid] === 'respect_pid');
1063    }
1064
1065    /**
1066     * Checks if the record exists
1067     *
1068     * @param string $table Table name
1069     * @param int $uid UID of record
1070     * @param string $fields Field list to select. Default is "uid,pid
1071     * @return array Result of \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord() which means the record if found, otherwise FALSE
1072     */
1073    public function doesRecordExist($table, $uid, $fields = '')
1074    {
1075        return BackendUtility::getRecord($table, $uid, $fields ? $fields : 'uid,pid');
1076    }
1077
1078    /**
1079     * Returns the page title path of a PID value. Results are cached internally
1080     *
1081     * @param int $pid Record PID to check
1082     * @return string The path for the input PID
1083     */
1084    public function getRecordPath($pid)
1085    {
1086        if (!isset($this->cache_getRecordPath[$pid])) {
1087            $clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
1088            $this->cache_getRecordPath[$pid] = (string)BackendUtility::getRecordPath($pid, $clause, 20);
1089        }
1090        return $this->cache_getRecordPath[$pid];
1091    }
1092
1093    /**
1094     * Makes a selector-box from optValues
1095     *
1096     * @param string $prefix Form element name
1097     * @param string $value Current value
1098     * @param array $optValues Options to display (key/value pairs)
1099     * @return string HTML select element
1100     */
1101    public function renderSelectBox($prefix, $value, $optValues)
1102    {
1103        $opt = [];
1104        $isSelFlag = 0;
1105        foreach ($optValues as $k => $v) {
1106            $sel = (string)$k === (string)$value ? ' selected="selected"' : '';
1107            if ($sel) {
1108                $isSelFlag++;
1109            }
1110            $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1111        }
1112        if (!$isSelFlag && (string)$value !== '') {
1113            $opt[] = '<option value="' . htmlspecialchars($value) . '" selected="selected">' . htmlspecialchars('[\'' . $value . '\']') . '</option>';
1114        }
1115        return '<select name="' . $prefix . '">' . implode('', $opt) . '</select>';
1116    }
1117
1118    /**
1119     * Compares two records, the current database record and the one from the import memory.
1120     * Will return HTML code to show any differences between them!
1121     *
1122     * @param array $databaseRecord Database record, all fields (new values)
1123     * @param array $importRecord Import memorys record for the same table/uid, all fields (old values)
1124     * @param string $table The table name of the record
1125     * @param bool $inverseDiff Inverse the diff view (switch red/green, needed for pre-update difference view)
1126     * @return string HTML
1127     */
1128    public function compareRecords($databaseRecord, $importRecord, $table, $inverseDiff = false)
1129    {
1130        // Initialize:
1131        $output = [];
1132        $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
1133        // Check if both inputs are records:
1134        if (is_array($databaseRecord) && is_array($importRecord)) {
1135            // Traverse based on database record
1136            foreach ($databaseRecord as $fN => $value) {
1137                if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
1138                    if (isset($importRecord[$fN])) {
1139                        if (trim($databaseRecord[$fN]) !== trim($importRecord[$fN])) {
1140                            // Create diff-result:
1141                            $output[$fN] = $diffUtility->makeDiffDisplay(BackendUtility::getProcessedValue($table, $fN, !$inverseDiff ? $importRecord[$fN] : $databaseRecord[$fN], 0, 1, 1), BackendUtility::getProcessedValue($table, $fN, !$inverseDiff ? $databaseRecord[$fN] : $importRecord[$fN], 0, 1, 1));
1142                        }
1143                        unset($importRecord[$fN]);
1144                    }
1145                }
1146            }
1147            // Traverse remaining in import record:
1148            foreach ($importRecord as $fN => $value) {
1149                if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
1150                    $output[$fN] = '<strong>Field missing</strong> in database';
1151                }
1152            }
1153            // Create output:
1154            if (!empty($output)) {
1155                $tRows = [];
1156                foreach ($output as $fN => $state) {
1157                    $tRows[] = '
1158						<tr>
1159							<td>' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$table]['columns'][$fN]['label'])) . ' (' . htmlspecialchars($fN) . ')</td>
1160							<td>' . $state . '</td>
1161						</tr>
1162					';
1163                }
1164                $output = '<table class="table table-striped table-hover">' . implode('', $tRows) . '</table>';
1165            } else {
1166                $output = 'Match';
1167            }
1168            return '<strong class="text-nowrap">[' . htmlspecialchars($table . ':' . $importRecord['uid'] . ' => ' . $databaseRecord['uid']) . ']:</strong> ' . $output;
1169        }
1170        return 'ERROR: One of the inputs were not an array!';
1171    }
1172
1173    /**
1174     * Creates the original file name for a copy-RTE image (magic type)
1175     *
1176     * @param string $string RTE copy filename, eg. "RTEmagicC_user_pm_icon_01.gif.gif
1177     * @return string|null RTE original filename, eg. "RTEmagicP_user_pm_icon_01.gif". If the input filename was NOT prefixed RTEmagicC_ as RTE images would be, NULL is returned!
1178     */
1179    public function getRTEoriginalFilename($string)
1180    {
1181        // If "magic image":
1182        if (GeneralUtility::isFirstPartOfStr($string, 'RTEmagicC_')) {
1183            // Find original file:
1184            $pI = pathinfo(substr($string, strlen('RTEmagicC_')));
1185            $filename = substr($pI['basename'], 0, -strlen('.' . $pI['extension']));
1186            $origFilePath = 'RTEmagicP_' . $filename;
1187            return $origFilePath;
1188        }
1189        return null;
1190    }
1191
1192    /**
1193     * Returns file processing object, initialized only once.
1194     *
1195     * @return ExtendedFileUtility File processor object
1196     */
1197    public function getFileProcObj()
1198    {
1199        if ($this->fileProcObj === null) {
1200            $this->fileProcObj = GeneralUtility::makeInstance(ExtendedFileUtility::class);
1201            $this->fileProcObj->setActionPermissions();
1202        }
1203        return $this->fileProcObj;
1204    }
1205
1206    /**
1207     * Call Hook
1208     *
1209     * @param string $name Name of the hook
1210     * @param array $params Array with params
1211     */
1212    public function callHook($name, $params)
1213    {
1214        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/impexp/class.tx_impexp.php'][$name] ?? [] as $hook) {
1215            GeneralUtility::callUserFunction($hook, $params, $this);
1216        }
1217    }
1218
1219    /**
1220     * Set flag to control whether disabled records and their children are excluded (true) or included (false). Defaults
1221     * to the old behaviour of including everything.
1222     *
1223     * @param bool $excludeDisabledRecords Set to true if if all disabled records should be excluded, false otherwise
1224     * @return \TYPO3\CMS\Impexp\ImportExport $this for fluent calls
1225     */
1226    public function setExcludeDisabledRecords($excludeDisabledRecords = false)
1227    {
1228        $this->excludeDisabledRecords = $excludeDisabledRecords;
1229        return $this;
1230    }
1231
1232    /*****************************
1233     * Error handling
1234     *****************************/
1235
1236    /**
1237     * Sets error message in the internal error log
1238     *
1239     * @param string $msg Error message
1240     */
1241    public function error($msg)
1242    {
1243        $this->errorLog[] = $msg;
1244    }
1245
1246    /**
1247     * Returns a table with the error-messages.
1248     *
1249     * @return string HTML print of error log
1250     */
1251    public function printErrorLog()
1252    {
1253        return !empty($this->errorLog) ? DebugUtility::viewArray($this->errorLog) : '';
1254    }
1255
1256    /**
1257     * @return BackendUserAuthentication
1258     */
1259    protected function getBackendUser()
1260    {
1261        return $GLOBALS['BE_USER'];
1262    }
1263
1264    /**
1265     * @return LanguageService
1266     */
1267    protected function getLanguageService()
1268    {
1269        return $GLOBALS['LANG'];
1270    }
1271}
1272