[BUGFIX] Hardcoded fileadmin directory in Import-Export
[Packages/TYPO3.CMS.git] / typo3 / sysext / impexp / classes / ImportExport.php
1 <?php
2 namespace TYPO3\CMS\Impexp;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 1999-2011 Kasper Skårhøj (kasperYYYY@typo3.com)
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /**
30 * T3D file Import/Export library (TYPO3 Record Document)
31 *
32 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
33 */
34 /**
35 * EXAMPLE for using the impexp-class for exporting stuff:
36 *
37 * Create and initialize:
38 * $this->export = t3lib_div::makeInstance('tx_impexp');
39 * $this->export->init();
40 * Set which tables relations we will allow:
41 * $this->export->relOnlyTables[]="tt_news"; // exclusively includes. See comment in the class
42 *
43 * Adding records:
44 * $this->export->export_addRecord("pages", $this->pageinfo);
45 * $this->export->export_addRecord("pages", t3lib_BEfunc::getRecord("pages", 38));
46 * $this->export->export_addRecord("pages", t3lib_BEfunc::getRecord("pages", 39));
47 * $this->export->export_addRecord("tt_content", t3lib_BEfunc::getRecord("tt_content", 12));
48 * $this->export->export_addRecord("tt_content", t3lib_BEfunc::getRecord("tt_content", 74));
49 * $this->export->export_addRecord("sys_template", t3lib_BEfunc::getRecord("sys_template", 20));
50 *
51 * Adding all the relations (recursively in 5 levels so relations has THEIR relations registered as well)
52 * for($a=0;$a<5;$a++) {
53 * $addR = $this->export->export_addDBRelations($a);
54 * if (!count($addR)) break;
55 * }
56 *
57 * Finally load all the files.
58 * $this->export->export_addFilesFromRelations(); // MUST be after the DBrelations are set so that file from ALL added records are included!
59 *
60 * Write export
61 * $out = $this->export->compileMemoryToFileContent();
62 */
63 /**
64 * T3D file Import/Export library (TYPO3 Record Document)
65 *
66 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
67 * @package TYPO3
68 * @subpackage tx_impexp
69 */
70 class ImportExport {
71
72 // Configuration, general
73 // If set, static relations (not exported) will be shown in overview as well
74 /**
75 * @todo Define visibility
76 */
77 public $showStaticRelations = FALSE;
78
79 // Name of the "fileadmin" folder where files for export/import should be located
80 /**
81 * @todo Define visibility
82 */
83 public $fileadminFolderName = '';
84
85 // Whether "import" or "export" mode of object. Set through init() function
86 /**
87 * @todo Define visibility
88 */
89 public $mode = '';
90
91 // Updates all records that has same UID instead of creating new!
92 /**
93 * @todo Define visibility
94 */
95 public $update = FALSE;
96
97 // Is set by importData() when an import has been done.
98 /**
99 * @todo Define visibility
100 */
101 public $doesImport = FALSE;
102
103 // Configuration, import
104 // If set to a page-record, then the preview display of the content will expect this page-record to be the target
105 // for the import and accordingly display validation information. This triggers the visual view of the
106 // import/export memory to validate if import is possible
107 /**
108 * @todo Define visibility
109 */
110 public $display_import_pid_record = '';
111
112 // Used to register the forged UID values for imported records that we want to create with the same UIDs as in the import file. Admin-only feature.
113 /**
114 * @todo Define visibility
115 */
116 public $suggestedInsertUids = array();
117
118 // Setting import modes during update state: as_new, exclude, force_uid
119 /**
120 * @todo Define visibility
121 */
122 public $import_mode = array();
123
124 // If set, PID correct is ignored globally
125 /**
126 * @todo Define visibility
127 */
128 public $global_ignore_pid = FALSE;
129
130 // If set, all UID values are forced! (update or import)
131 /**
132 * @todo Define visibility
133 */
134 public $force_all_UIDS = FALSE;
135
136 // If set, a diff-view column is added to the overview.
137 /**
138 * @todo Define visibility
139 */
140 public $showDiff = FALSE;
141
142 // If set, and if the user is admin, allow the writing of PHP scripts to fileadmin/ area.
143 /**
144 * @todo Define visibility
145 */
146 public $allowPHPScripts = FALSE;
147
148 // Disable logging when importing
149 /**
150 * @todo Define visibility
151 */
152 public $enableLogging = FALSE;
153
154 // Array of values to substitute in editable softreferences.
155 /**
156 * @todo Define visibility
157 */
158 public $softrefInputValues = array();
159
160 // Mapping between the fileID from import memory and the final filenames they are written to.
161 /**
162 * @todo Define visibility
163 */
164 public $fileIDMap = array();
165
166 // Configuration, export
167 // 1MB max file size
168 /**
169 * @todo Define visibility
170 */
171 public $maxFileSize = 1000000;
172
173 // 1MB max record size
174 /**
175 * @todo Define visibility
176 */
177 public $maxRecordSize = 1000000;
178
179 // 10MB max export size
180 /**
181 * @todo Define visibility
182 */
183 public $maxExportSize = 10000000;
184
185 // add table names here which are THE ONLY ones which will be included into export if found as relations. '_ALL' will allow all tables.
186 /**
187 * @todo Define visibility
188 */
189 public $relOnlyTables = array();
190
191 // add tables names here which should not be exported with the file. (Where relations should be mapped to same UIDs in target system).
192 /**
193 * @todo Define visibility
194 */
195 public $relStaticTables = array();
196
197 // Exclude map. Keys are table:uid pairs and if set, records are not added to the export.
198 /**
199 * @todo Define visibility
200 */
201 public $excludeMap = array();
202
203 // Soft Reference Token ID modes.
204 /**
205 * @todo Define visibility
206 */
207 public $softrefCfg = array();
208
209 // Listing extension dependencies.
210 /**
211 * @todo Define visibility
212 */
213 public $extensionDependencies = array();
214
215 // Set by user: If set, compression in t3d files is disabled
216 /**
217 * @todo Define visibility
218 */
219 public $dontCompress = 0;
220
221 // Boolean, if set, HTML file resources are included.
222 /**
223 * @todo Define visibility
224 */
225 public $includeExtFileResources = 0;
226
227 // Files with external media (HTML/css style references inside)
228 /**
229 * @todo Define visibility
230 */
231 public $extFileResourceExtensions = 'html,htm,css';
232
233 // Internal, dynamic:
234 // After records are written this array is filled with [table][original_uid] = [new_uid]
235 /**
236 * @todo Define visibility
237 */
238 public $import_mapId = array();
239
240 // Keys are [tablename]:[new NEWxxx ids (or when updating it is uids)] while values are arrays with table/uid of the original record it is based on. By the array keys the new ids can be looked up inside tcemain
241 /**
242 * @todo Define visibility
243 */
244 public $import_newId = array();
245
246 // Page id map for page tree (import)
247 /**
248 * @todo Define visibility
249 */
250 public $import_newId_pids = array();
251
252 // Internal data accumulation for writing records during import
253 /**
254 * @todo Define visibility
255 */
256 public $import_data = array();
257
258 // Error log.
259 /**
260 * @todo Define visibility
261 */
262 public $errorLog = array();
263
264 // Cache for record paths
265 /**
266 * @todo Define visibility
267 */
268 public $cache_getRecordPath = array();
269
270 // Cache of checkPID values.
271 /**
272 * @todo Define visibility
273 */
274 public $checkPID_cache = array();
275
276 // Set internally if the gzcompress function exists
277 /**
278 * @todo Define visibility
279 */
280 public $compress = 0;
281
282 // Internal import/export memory
283 /**
284 * @todo Define visibility
285 */
286 public $dat = array();
287
288 /**
289 * File processing object
290 *
291 * @var \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility
292 * @todo Define visibility
293 */
294 public $fileProcObj = '';
295
296 /**************************
297 *
298 * Initialize
299 *
300 *************************/
301 /**
302 * Init the object, both import and export
303 *
304 * @param boolean $dontCompress If set, compression in t3d files is disabled
305 * @param string $mode Mode of usage, either "import" or "export
306 * @return void
307 * @todo Define visibility
308 */
309 public function init($dontCompress = 0, $mode = '') {
310 $this->compress = function_exists('gzcompress');
311 $this->dontCompress = $dontCompress;
312 $this->mode = $mode;
313 $this->fileadminFolderName = !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']) ? $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] : 'fileadmin';
314 }
315
316 /**************************
317 *
318 * Export / Init + Meta Data
319 *
320 *************************/
321 /**
322 * Set header basics
323 *
324 * @return void
325 * @todo Define visibility
326 */
327 public function setHeaderBasics() {
328 // Initializing:
329 if (is_array($this->softrefCfg)) {
330 foreach ($this->softrefCfg as $key => $value) {
331 if (!strlen($value['mode'])) {
332 unset($this->softrefCfg[$key]);
333 }
334 }
335 }
336 // Setting in header memory:
337 // Version of file format
338 $this->dat['header']['XMLversion'] = '1.0';
339 // Initialize meta data array (to put it in top of file)
340 $this->dat['header']['meta'] = array();
341 // Add list of tables to consider static
342 $this->dat['header']['relStaticTables'] = $this->relStaticTables;
343 // The list of excluded records
344 $this->dat['header']['excludeMap'] = $this->excludeMap;
345 // Soft Reference mode for elements
346 $this->dat['header']['softrefCfg'] = $this->softrefCfg;
347 // List of extensions the import depends on.
348 $this->dat['header']['extensionDependencies'] = $this->extensionDependencies;
349 }
350
351 /**
352 * Set charset
353 *
354 * @param string $charset Charset for the content in the export. During import the character set will be converted if the target system uses another charset.
355 * @return void
356 * @todo Define visibility
357 */
358 public function setCharset($charset) {
359 $this->dat['header']['charset'] = $charset;
360 }
361
362 /**
363 * Sets meta data
364 *
365 * @param string $title Title of the export
366 * @param string $description Description of the export
367 * @param string $notes Notes about the contents
368 * @param string $packager_username Backend Username of the packager (the guy making the export)
369 * @param string $packager_name Real name of the packager
370 * @param string $packager_email Email of the packager
371 * @return void
372 * @todo Define visibility
373 */
374 public function setMetaData($title, $description, $notes, $packager_username, $packager_name, $packager_email) {
375 $this->dat['header']['meta'] = array(
376 'title' => $title,
377 'description' => $description,
378 'notes' => $notes,
379 'packager_username' => $packager_username,
380 'packager_name' => $packager_name,
381 'packager_email' => $packager_email,
382 'TYPO3_version' => TYPO3_version,
383 'created' => strftime('%A %e. %B %Y', $GLOBALS['EXEC_TIME'])
384 );
385 }
386
387 /**
388 * Sets a thumbnail image to the exported file
389 *
390 * @param string $imgFilepath Filename reference, gif, jpg, png. Absolute path.
391 * @return void
392 * @todo Define visibility
393 */
394 public function addThumbnail($imgFilepath) {
395 if (@is_file($imgFilepath)) {
396 $imgInfo = @getimagesize($imgFilepath);
397 if (is_array($imgInfo)) {
398 $fileContent = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($imgFilepath);
399 $this->dat['header']['thumbnail'] = array(
400 'imgInfo' => $imgInfo,
401 'content' => $fileContent,
402 'filesize' => strlen($fileContent),
403 'filemtime' => filemtime($imgFilepath),
404 'filename' => basename($imgFilepath)
405 );
406 }
407 }
408 }
409
410 /**************************
411 *
412 * Export / Init Page tree
413 *
414 *************************/
415 /**
416 * Sets the page-tree array in the export header and returns the array in a flattened version
417 *
418 * @param array $idH Hierarchy of ids, the page tree: array([uid] => array("uid" => [uid], "subrow" => array(.....)), [uid] => ....)
419 * @return array The hierarchical page tree converted to a one-dimensional list of pages
420 * @todo Define visibility
421 */
422 public function setPageTree($idH) {
423 $this->dat['header']['pagetree'] = $this->unsetExcludedSections($idH);
424 return $this->flatInversePageTree($this->dat['header']['pagetree']);
425 }
426
427 /**
428 * Removes entries in the page tree which are found in ->excludeMap[]
429 *
430 * @param array $idH Page uid hierarchy
431 * @return array Modified input array
432 * @access private
433 * @see setPageTree()
434 * @todo Define visibility
435 */
436 public function unsetExcludedSections($idH) {
437 if (is_array($idH)) {
438 foreach ($idH as $k => $v) {
439 if ($this->excludeMap['pages:' . $idH[$k]['uid']]) {
440 unset($idH[$k]);
441 } elseif (is_array($idH[$k]['subrow'])) {
442 $idH[$k]['subrow'] = $this->unsetExcludedSections($idH[$k]['subrow']);
443 }
444 }
445 }
446 return $idH;
447 }
448
449 /**
450 * Recursively flattening the idH array (for setPageTree() function)
451 *
452 * @param array $idH Page uid hierarchy
453 * @param array $a Accumulation array of pages (internal, don't set from outside)
454 * @return array Array with uid-uid pairs for all pages in the page tree.
455 * @see flatInversePageTree_pid()
456 * @todo Define visibility
457 */
458 public function flatInversePageTree($idH, $a = array()) {
459 if (is_array($idH)) {
460 $idH = array_reverse($idH);
461 foreach ($idH as $k => $v) {
462 $a[$v['uid']] = $v['uid'];
463 if (is_array($v['subrow'])) {
464 $a = $this->flatInversePageTree($v['subrow'], $a);
465 }
466 }
467 }
468 return $a;
469 }
470
471 /**
472 * Recursively flattening the idH array (for setPageTree() function), setting PIDs as values
473 *
474 * @param array $idH Page uid hierarchy
475 * @param array $a Accumulation array of pages (internal, don't set from outside)
476 * @param integer $pid PID value (internal)
477 * @return array Array with uid-pid pairs for all pages in the page tree.
478 * @see flatInversePageTree()
479 * @todo Define visibility
480 */
481 public function flatInversePageTree_pid($idH, $a = array(), $pid = -1) {
482 if (is_array($idH)) {
483 $idH = array_reverse($idH);
484 foreach ($idH as $k => $v) {
485 $a[$v['uid']] = $pid;
486 if (is_array($v['subrow'])) {
487 $a = $this->flatInversePageTree_pid($v['subrow'], $a, $v['uid']);
488 }
489 }
490 }
491 return $a;
492 }
493
494 /**************************
495 *
496 * Export
497 *
498 *************************/
499 /**
500 * Adds the record $row from $table.
501 * No checking for relations done here. Pure data.
502 *
503 * @param string $table Table name
504 * @param array $row Record row.
505 * @param integer $relationLevel (Internal) if the record is added as a relation, this is set to the "level" it was on.
506 * @return void
507 * @todo Define visibility
508 */
509 public function export_addRecord($table, $row, $relationLevel = 0) {
510 \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL($table, $row);
511 if (strcmp($table, '') && is_array($row) && $row['uid'] > 0 && !$this->excludeMap[($table . ':' . $row['uid'])]) {
512 if ($this->checkPID($table === 'pages' ? $row['uid'] : $row['pid'])) {
513 if (!isset($this->dat['records'][($table . ':' . $row['uid'])])) {
514 // Prepare header info:
515 $headerInfo = array();
516 $headerInfo['uid'] = $row['uid'];
517 $headerInfo['pid'] = $row['pid'];
518 $headerInfo['title'] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixed_lgd_cs(\TYPO3\CMS\Backend\Utility\BackendUtility::getRecordTitle($table, $row), 40);
519 $headerInfo['size'] = strlen(serialize($row));
520 if ($relationLevel) {
521 $headerInfo['relationLevel'] = $relationLevel;
522 }
523 // If record content is not too large in size, set the header content and add the rest:
524 if ($headerInfo['size'] < $this->maxRecordSize) {
525 // Set the header summary:
526 $this->dat['header']['records'][$table][$row['uid']] = $headerInfo;
527 // Create entry in the PID lookup:
528 $this->dat['header']['pid_lookup'][$row['pid']][$table][$row['uid']] = 1;
529 // Initialize reference index object:
530 $refIndexObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
531 // Yes to workspace overlays for exporting....
532 $refIndexObj->WSOL = TRUE;
533 // Data:
534 $this->dat['records'][$table . ':' . $row['uid']] = array();
535 $this->dat['records'][$table . ':' . $row['uid']]['data'] = $row;
536 $this->dat['records'][$table . ':' . $row['uid']]['rels'] = $refIndexObj->getRelations($table, $row);
537 $this->errorLog = array_merge($this->errorLog, $refIndexObj->errorLog);
538 // Merge error logs.
539 // Add information about the relations in the record in the header:
540 $this->dat['header']['records'][$table][$row['uid']]['rels'] = $this->flatDBrels($this->dat['records'][$table . ':' . $row['uid']]['rels']);
541 // Add information about the softrefs to header:
542 $this->dat['header']['records'][$table][$row['uid']]['softrefs'] = $this->flatSoftRefs($this->dat['records'][$table . ':' . $row['uid']]['rels']);
543 } else {
544 $this->error('Record ' . $table . ':' . $row['uid'] . ' was larger than maxRecordSize (' . \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize($this->maxRecordSize) . ')');
545 }
546 } else {
547 $this->error('Record ' . $table . ':' . $row['uid'] . ' already added.');
548 }
549 } else {
550 $this->error('Record ' . $table . ':' . $row['uid'] . ' was outside your DB mounts!');
551 }
552 }
553 }
554
555 /**
556 * This analyses the existing added records, finds all database relations to records and adds these records to the export file.
557 * This function can be called repeatedly until it returns an empty array. In principle it should not allow to infinite recursivity, but you better set a limit...
558 * Call this BEFORE the ext_addFilesFromRelations (so files from added relations are also included of course)
559 *
560 * @param integer $relationLevel Recursion level
561 * @return array overview of relations found and added: Keys [table]:[uid], values array with table and id
562 * @see export_addFilesFromRelations()
563 * @todo Define visibility
564 */
565 public function export_addDBRelations($relationLevel = 0) {
566 // Initialize:
567 $addR = array();
568 // Traverse all "rels" registered for "records"
569 if (is_array($this->dat['records'])) {
570 foreach ($this->dat['records'] as $k => $value) {
571 if (is_array($this->dat['records'][$k])) {
572 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
573 // For all DB types of relations:
574 if ($vR['type'] == 'db') {
575 foreach ($vR['itemArray'] as $fI) {
576 $this->export_addDBRelations_registerRelation($fI, $addR);
577 }
578 }
579 // For all flex/db types of relations:
580 if ($vR['type'] == 'flex') {
581 // DB relations in flex form fields:
582 if (is_array($vR['flexFormRels']['db'])) {
583 foreach ($vR['flexFormRels']['db'] as $subList) {
584 foreach ($subList as $fI) {
585 $this->export_addDBRelations_registerRelation($fI, $addR);
586 }
587 }
588 }
589 // DB oriented soft references in flex form fields:
590 if (is_array($vR['flexFormRels']['softrefs'])) {
591 foreach ($vR['flexFormRels']['softrefs'] as $subList) {
592 foreach ($subList['keys'] as $spKey => $elements) {
593 foreach ($elements as $el) {
594 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
595 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
596 $fI = array(
597 'table' => $tempTable,
598 'id' => $tempUid
599 );
600 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
601 }
602 }
603 }
604 }
605 }
606 }
607 // In any case, if there are soft refs:
608 if (is_array($vR['softrefs']['keys'])) {
609 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
610 foreach ($elements as $el) {
611 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
612 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
613 $fI = array(
614 'table' => $tempTable,
615 'id' => $tempUid
616 );
617 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
618 }
619 }
620 }
621 }
622 }
623 }
624 }
625 } else {
626 $this->error('There were no records available.');
627 }
628 // Now, if there were new records to add, do so:
629 if (count($addR)) {
630 foreach ($addR as $fI) {
631 // Get and set record:
632 $row = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($fI['table'], $fI['id']);
633 if (is_array($row)) {
634 $this->export_addRecord($fI['table'], $row, $relationLevel + 1);
635 }
636 // Set status message
637 // Relation pointers always larger than zero except certain "select" types with negative values pointing to uids - but that is not supported here.
638 if ($fI['id'] > 0) {
639 $rId = $fI['table'] . ':' . $fI['id'];
640 if (!isset($this->dat['records'][$rId])) {
641 $this->dat['records'][$rId] = 'NOT_FOUND';
642 $this->error('Relation record ' . $rId . ' was not found!');
643 }
644 }
645 }
646 }
647 // Return overview of relations found and added
648 return $addR;
649 }
650
651 /**
652 * Helper function for export_addDBRelations()
653 *
654 * @param array $fI Array with table/id keys to add
655 * @param array $addR Add array, passed by reference to be modified
656 * @param string $tokenID Softref Token ID, if applicable.
657 * @return void
658 * @see export_addDBRelations()
659 * @todo Define visibility
660 */
661 public function export_addDBRelations_registerRelation($fI, &$addR, $tokenID = '') {
662 $rId = $fI['table'] . ':' . $fI['id'];
663 if (isset($GLOBALS['TCA'][$fI['table']]) && !$this->isTableStatic($fI['table']) && !$this->isExcluded($fI['table'], $fI['id']) && (!$tokenID || $this->includeSoftref($tokenID)) && $this->inclRelation($fI['table'])) {
664 if (!isset($this->dat['records'][$rId])) {
665 // Set this record to be included since it is not already.
666 $addR[$rId] = $fI;
667 }
668 }
669 }
670
671 /**
672 * This adds all files in relations.
673 * Call this method AFTER adding all records including relations.
674 *
675 * @return void
676 * @see export_addDBRelations()
677 * @todo Define visibility
678 */
679 public function export_addFilesFromRelations() {
680 // Traverse all "rels" registered for "records"
681 if (is_array($this->dat['records'])) {
682 foreach ($this->dat['records'] as $k => $value) {
683 if (is_array($this->dat['records'][$k]['rels'])) {
684 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
685 // For all file type relations:
686 if ($vR['type'] == 'file') {
687 foreach ($vR['newValueFiles'] as $key => $fI) {
688 $this->export_addFile($fI, $k, $fieldname);
689 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
690 unset($this->dat['records'][$k]['rels'][$fieldname]['newValueFiles'][$key]['ID_absFile']);
691 }
692 }
693 // For all flex type relations:
694 if ($vR['type'] == 'flex') {
695 if (is_array($vR['flexFormRels']['file'])) {
696 foreach ($vR['flexFormRels']['file'] as $key => $subList) {
697 foreach ($subList as $subKey => $fI) {
698 $this->export_addFile($fI, $k, $fieldname);
699 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
700 unset($this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['file'][$key][$subKey]['ID_absFile']);
701 }
702 }
703 }
704 // DB oriented soft references in flex form fields:
705 if (is_array($vR['flexFormRels']['softrefs'])) {
706 foreach ($vR['flexFormRels']['softrefs'] as $key => $subList) {
707 foreach ($subList['keys'] as $spKey => $elements) {
708 foreach ($elements as $subKey => $el) {
709 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
710 // Create abs path and ID for file:
711 $ID_absFile = \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName(PATH_site . $el['subst']['relFileName']);
712 $ID = md5($ID_absFile);
713 if ($ID_absFile) {
714 if (!$this->dat['files'][$ID]) {
715 $fI = array(
716 'filename' => basename($ID_absFile),
717 'ID_absFile' => $ID_absFile,
718 'ID' => $ID,
719 'relFileName' => $el['subst']['relFileName']
720 );
721 $this->export_addFile($fI, '_SOFTREF_');
722 }
723 $this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['softrefs'][$key]['keys'][$spKey][$subKey]['file_ID'] = $ID;
724 }
725 }
726 }
727 }
728 }
729 }
730 }
731 // In any case, if there are soft refs:
732 if (is_array($vR['softrefs']['keys'])) {
733 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
734 foreach ($elements as $subKey => $el) {
735 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
736 // Create abs path and ID for file:
737 $ID_absFile = \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName(PATH_site . $el['subst']['relFileName']);
738 $ID = md5($ID_absFile);
739 if ($ID_absFile) {
740 if (!$this->dat['files'][$ID]) {
741 $fI = array(
742 'filename' => basename($ID_absFile),
743 'ID_absFile' => $ID_absFile,
744 'ID' => $ID,
745 'relFileName' => $el['subst']['relFileName']
746 );
747 $this->export_addFile($fI, '_SOFTREF_');
748 }
749 $this->dat['records'][$k]['rels'][$fieldname]['softrefs']['keys'][$spKey][$subKey]['file_ID'] = $ID;
750 }
751 }
752 }
753 }
754 }
755 }
756 }
757 }
758 } else {
759 $this->error('There were no records available.');
760 }
761 }
762
763 /**
764 * Adds a files content to the export memory
765 *
766 * @param array $fI File information with three keys: "filename" = filename without path, "ID_absFile" = absolute filepath to the file (including the filename), "ID" = md5 hash of "ID_absFile". "relFileName" is optional for files attached to records, but mandatory for soft referenced files (since the relFileName determines where such a file should be stored!)
767 * @param string $recordRef If the file is related to a record, this is the id on the form [table]:[id]. Information purposes only.
768 * @param string $fieldname If the file is related to a record, this is the field name it was related to. Information purposes only.
769 * @return void
770 * @todo Define visibility
771 */
772 public function export_addFile($fI, $recordRef = '', $fieldname = '') {
773 if (@is_file($fI['ID_absFile'])) {
774 if (filesize($fI['ID_absFile']) < $this->maxFileSize) {
775 $fileRec = array();
776 $fileRec['filesize'] = filesize($fI['ID_absFile']);
777 $fileRec['filename'] = basename($fI['ID_absFile']);
778 $fileRec['filemtime'] = filemtime($fI['ID_absFile']);
779 //for internal type file_reference
780 $fileRec['relFileRef'] = substr($fI['ID_absFile'], strlen(PATH_site));
781 if ($recordRef) {
782 $fileRec['record_ref'] = $recordRef . '/' . $fieldname;
783 }
784 if ($fI['relFileName']) {
785 $fileRec['relFileName'] = $fI['relFileName'];
786 }
787 // Setting this data in the header
788 $this->dat['header']['files'][$fI['ID']] = $fileRec;
789 // ... and for the recordlisting, why not let us know WHICH relations there was...
790 if ($recordRef && $recordRef !== '_SOFTREF_') {
791 $refParts = explode(':', $recordRef, 2);
792 if (!is_array($this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'])) {
793 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'] = array();
794 }
795 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'][] = $fI['ID'];
796 }
797 // ... and finally add the heavy stuff:
798 $fileRec['content'] = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($fI['ID_absFile']);
799 $fileRec['content_md5'] = md5($fileRec['content']);
800 $this->dat['files'][$fI['ID']] = $fileRec;
801 // For soft references, do further processing:
802 if ($recordRef === '_SOFTREF_') {
803 // RTE files?
804 if ($RTEoriginal = $this->getRTEoriginalFilename(basename($fI['ID_absFile']))) {
805 $RTEoriginal_absPath = dirname($fI['ID_absFile']) . '/' . $RTEoriginal;
806 if (@is_file($RTEoriginal_absPath)) {
807 $RTEoriginal_ID = md5($RTEoriginal_absPath);
808 $fileRec = array();
809 $fileRec['filesize'] = filesize($RTEoriginal_absPath);
810 $fileRec['filename'] = basename($RTEoriginal_absPath);
811 $fileRec['filemtime'] = filemtime($RTEoriginal_absPath);
812 $fileRec['record_ref'] = '_RTE_COPY_ID:' . $fI['ID'];
813 $this->dat['header']['files'][$fI['ID']]['RTE_ORIG_ID'] = $RTEoriginal_ID;
814 // Setting this data in the header
815 $this->dat['header']['files'][$RTEoriginal_ID] = $fileRec;
816 // ... and finally add the heavy stuff:
817 $fileRec['content'] = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($RTEoriginal_absPath);
818 $fileRec['content_md5'] = md5($fileRec['content']);
819 $this->dat['files'][$RTEoriginal_ID] = $fileRec;
820 } else {
821 $this->error('RTE original file "' . substr($RTEoriginal_absPath, strlen(PATH_site)) . '" was not found!');
822 }
823 }
824 // Files with external media?
825 // This is only done with files grabbed by a softreference parser since it is deemed improbable that hard-referenced files should undergo this treatment.
826 $html_fI = pathinfo(basename($fI['ID_absFile']));
827 if ($this->includeExtFileResources && \TYPO3\CMS\Core\Utility\GeneralUtility::inList($this->extFileResourceExtensions, strtolower($html_fI['extension']))) {
828 $uniquePrefix = '###' . md5($GLOBALS['EXEC_TIME']) . '###';
829 if (strtolower($html_fI['extension']) === 'css') {
830 $prefixedMedias = explode($uniquePrefix, preg_replace('/(url[[:space:]]*\\([[:space:]]*["\']?)([^"\')]*)(["\']?[[:space:]]*\\))/i', '\\1' . $uniquePrefix . '\\2' . $uniquePrefix . '\\3', $fileRec['content']));
831 } else {
832 // html, htm:
833 $htmlParser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Html\\HtmlParser');
834 $prefixedMedias = explode($uniquePrefix, $htmlParser->prefixResourcePath($uniquePrefix, $fileRec['content'], array(), $uniquePrefix));
835 }
836 $htmlResourceCaptured = FALSE;
837 foreach ($prefixedMedias as $k => $v) {
838 if ($k % 2) {
839 $EXTres_absPath = \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath(dirname($fI['ID_absFile']) . '/' . $v);
840 $EXTres_absPath = \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($EXTres_absPath);
841 if ($EXTres_absPath && \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($EXTres_absPath, PATH_site . $this->fileadminFolderName . '/') && @is_file($EXTres_absPath)) {
842 $htmlResourceCaptured = TRUE;
843 $EXTres_ID = md5($EXTres_absPath);
844 $this->dat['header']['files'][$fI['ID']]['EXT_RES_ID'][] = $EXTres_ID;
845 $prefixedMedias[$k] = '{EXT_RES_ID:' . $EXTres_ID . '}';
846 // Add file to memory if it is not set already:
847 if (!isset($this->dat['header']['files'][$EXTres_ID])) {
848 $fileRec = array();
849 $fileRec['filesize'] = filesize($EXTres_absPath);
850 $fileRec['filename'] = basename($EXTres_absPath);
851 $fileRec['filemtime'] = filemtime($EXTres_absPath);
852 $fileRec['record_ref'] = '_EXT_PARENT_:' . $fI['ID'];
853 // Media relative to the HTML file.
854 $fileRec['parentRelFileName'] = $v;
855 // Setting this data in the header
856 $this->dat['header']['files'][$EXTres_ID] = $fileRec;
857 // ... and finally add the heavy stuff:
858 $fileRec['content'] = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($EXTres_absPath);
859 $fileRec['content_md5'] = md5($fileRec['content']);
860 $this->dat['files'][$EXTres_ID] = $fileRec;
861 }
862 }
863 }
864 }
865 if ($htmlResourceCaptured) {
866 $this->dat['files'][$fI['ID']]['tokenizedContent'] = implode('', $prefixedMedias);
867 }
868 }
869 }
870 } else {
871 $this->error($fI['ID_absFile'] . ' was larger (' . \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize(filesize($fI['ID_absFile'])) . ') than the maxFileSize (' . \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize($this->maxFileSize) . ')! Skipping.');
872 }
873 } else {
874 $this->error($fI['ID_absFile'] . ' was not a file! Skipping.');
875 }
876 }
877
878 /**
879 * DB relations flattend to 1-dim array.
880 * The list will be unique, no table/uid combination will appear twice.
881 *
882 * @param array $dbrels 2-dim Array of database relations organized by table key
883 * @return array 1-dim array where entries are table:uid and keys are array with table/id
884 * @todo Define visibility
885 */
886 public function flatDBrels($dbrels) {
887 $list = array();
888 foreach ($dbrels as $dat) {
889 if ($dat['type'] == 'db') {
890 foreach ($dat['itemArray'] as $i) {
891 $list[$i['table'] . ':' . $i['id']] = $i;
892 }
893 }
894 if ($dat['type'] == 'flex' && is_array($dat['flexFormRels']['db'])) {
895 foreach ($dat['flexFormRels']['db'] as $subList) {
896 foreach ($subList as $i) {
897 $list[$i['table'] . ':' . $i['id']] = $i;
898 }
899 }
900 }
901 }
902 return $list;
903 }
904
905 /**
906 * Soft References flattend to 1-dim array.
907 *
908 * @param array $dbrels 2-dim Array of database relations organized by table key
909 * @return array 1-dim array where entries are arrays with properties of the soft link found and keys are a unique combination of field, spKey, structure path if applicable and token ID
910 * @todo Define visibility
911 */
912 public function flatSoftRefs($dbrels) {
913 $list = array();
914 foreach ($dbrels as $field => $dat) {
915 if (is_array($dat['softrefs']['keys'])) {
916 foreach ($dat['softrefs']['keys'] as $spKey => $elements) {
917 if (is_array($elements)) {
918 foreach ($elements as $subKey => $el) {
919 $lKey = $field . ':' . $spKey . ':' . $subKey;
920 $list[$lKey] = array_merge(array('field' => $field, 'spKey' => $spKey), $el);
921 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation changes for the same value in $this->records[...] this will not work anymore!
922 if ($el['subst'] && $el['subst']['relFileName']) {
923 $list[$lKey]['file_ID'] = md5(PATH_site . $el['subst']['relFileName']);
924 }
925 }
926 }
927 }
928 }
929 if ($dat['type'] == 'flex' && is_array($dat['flexFormRels']['softrefs'])) {
930 foreach ($dat['flexFormRels']['softrefs'] as $structurePath => $subSoftrefs) {
931 if (is_array($subSoftrefs['keys'])) {
932 foreach ($subSoftrefs['keys'] as $spKey => $elements) {
933 foreach ($elements as $subKey => $el) {
934 $lKey = $field . ':' . $structurePath . ':' . $spKey . ':' . $subKey;
935 $list[$lKey] = array_merge(array('field' => $field, 'spKey' => $spKey, 'structurePath' => $structurePath), $el);
936 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation changes for the same value in $this->records[...] this will not work anymore!
937 if ($el['subst'] && $el['subst']['relFileName']) {
938 $list[$lKey]['file_ID'] = md5(PATH_site . $el['subst']['relFileName']);
939 }
940 }
941 }
942 }
943 }
944 }
945 }
946 return $list;
947 }
948
949 /**************************
950 *
951 * File Output
952 *
953 *************************/
954 /**
955 * This compiles and returns the data content for an exported file
956 *
957 * @param string $type Type of output; "xml" gives xml, otherwise serialized array, possibly compressed.
958 * @return string The output file stream
959 * @todo Define visibility
960 */
961 public function compileMemoryToFileContent($type = '') {
962 if ($type == 'xml') {
963 $out = $this->createXML();
964 } else {
965 $compress = $this->doOutputCompress();
966 $out = '';
967 // adding header:
968 $out .= $this->addFilePart(serialize($this->dat['header']), $compress);
969 // adding records:
970 $out .= $this->addFilePart(serialize($this->dat['records']), $compress);
971 // adding files:
972 $out .= $this->addFilePart(serialize($this->dat['files']), $compress);
973 }
974 return $out;
975 }
976
977 /**
978 * Creates XML string from input array
979 *
980 * @return string XML content
981 * @todo Define visibility
982 */
983 public function createXML() {
984 // Options:
985 $options = array(
986 'alt_options' => array(
987 '/header' => array(
988 'disableTypeAttrib' => TRUE,
989 'clearStackPath' => TRUE,
990 'parentTagMap' => array(
991 'files' => 'file',
992 'records' => 'table',
993 'table' => 'rec',
994 'rec:rels' => 'relations',
995 'relations' => 'element',
996 'filerefs' => 'file',
997 'pid_lookup' => 'page_contents',
998 'header:relStaticTables' => 'static_tables',
999 'static_tables' => 'tablename',
1000 'excludeMap' => 'item',
1001 'softrefCfg' => 'softrefExportMode',
1002 'extensionDependencies' => 'extkey',
1003 'softrefs' => 'softref_element'
1004 ),
1005 'alt_options' => array(
1006 '/pagetree' => array(
1007 'disableTypeAttrib' => TRUE,
1008 'useIndexTagForNum' => 'node',
1009 'parentTagMap' => array(
1010 'node:subrow' => 'node'
1011 )
1012 ),
1013 '/pid_lookup/page_contents' => array(
1014 'disableTypeAttrib' => TRUE,
1015 'parentTagMap' => array(
1016 'page_contents' => 'table'
1017 ),
1018 'grandParentTagMap' => array(
1019 'page_contents/table' => 'item'
1020 )
1021 )
1022 )
1023 ),
1024 '/records' => array(
1025 'disableTypeAttrib' => TRUE,
1026 'parentTagMap' => array(
1027 'records' => 'tablerow',
1028 'tablerow:data' => 'fieldlist',
1029 'tablerow:rels' => 'related',
1030 'related' => 'field',
1031 'field:itemArray' => 'relations',
1032 'field:newValueFiles' => 'filerefs',
1033 'field:flexFormRels' => 'flexform',
1034 'relations' => 'element',
1035 'filerefs' => 'file',
1036 'flexform:db' => 'db_relations',
1037 'flexform:file' => 'file_relations',
1038 'flexform:softrefs' => 'softref_relations',
1039 'softref_relations' => 'structurePath',
1040 'db_relations' => 'path',
1041 'file_relations' => 'path',
1042 'path' => 'element',
1043 'keys' => 'softref_key',
1044 'softref_key' => 'softref_element'
1045 ),
1046 'alt_options' => array(
1047 '/records/tablerow/fieldlist' => array(
1048 'useIndexTagForAssoc' => 'field'
1049 )
1050 )
1051 ),
1052 '/files' => array(
1053 'disableTypeAttrib' => TRUE,
1054 'parentTagMap' => array(
1055 'files' => 'file'
1056 )
1057 )
1058 )
1059 );
1060 // Creating XML file from $outputArray:
1061 $charset = $this->dat['header']['charset'] ? $this->dat['header']['charset'] : 'utf-8';
1062 $XML = '<?xml version="1.0" encoding="' . $charset . '" standalone="yes" ?>' . LF;
1063 $XML .= \TYPO3\CMS\Core\Utility\GeneralUtility::array2xml($this->dat, '', 0, 'T3RecordDocument', 0, $options);
1064 return $XML;
1065 }
1066
1067 /**
1068 * Returns TRUE if the output should be compressed.
1069 *
1070 * @return boolean TRUE if compression is possible AND requested.
1071 * @todo Define visibility
1072 */
1073 public function doOutputCompress() {
1074 return $this->compress && !$this->dontCompress;
1075 }
1076
1077 /**
1078 * Returns a content part for a filename being build.
1079 *
1080 * @param array $data Data to store in part
1081 * @param boolean $compress Compress file?
1082 * @return string Content stream.
1083 * @todo Define visibility
1084 */
1085 public function addFilePart($data, $compress = FALSE) {
1086 if ($compress) {
1087 $data = gzcompress($data);
1088 }
1089 return md5($data) . ':' . ($compress ? '1' : '0') . ':' . str_pad(strlen($data), 10, '0', STR_PAD_LEFT) . ':' . $data . ':';
1090 }
1091
1092 /***********************
1093 *
1094 * Import
1095 *
1096 ***********************/
1097 /**
1098 * Imports the internal data array to $pid.
1099 *
1100 * @param integer $pid Page ID in which to import the content
1101 * @return void
1102 * @todo Define visibility
1103 */
1104 public function importData($pid) {
1105 // Set this flag to indicate that an import is being/has been done.
1106 $this->doesImport = 1;
1107 // Initialize:
1108 // These vars MUST last for the whole section not being cleared. They are used by the method setRelations() which are called at the end of the import session.
1109 $this->import_mapId = array();
1110 $this->import_newId = array();
1111 $this->import_newId_pids = array();
1112 // Temporary files stack initialized:
1113 $this->unlinkFiles = array();
1114 $this->alternativeFileName = array();
1115 $this->alternativeFilePath = array();
1116 // Write records, first pages, then the rest
1117 // Fields with "hard" relations to database, files and flexform fields are kept empty during this run
1118 $this->writeRecords_pages($pid);
1119 $this->writeRecords_records($pid);
1120 // Finally all the file and DB record references must be fixed. This is done after all records have supposedly been written to database:
1121 // $this->import_mapId will indicate two things: 1) that a record WAS written to db and 2) that it has got a new id-number.
1122 $this->setRelations();
1123 // And when all DB relations are in place, we can fix file and DB relations in flexform fields (since data structures often depends on relations to a DS record):
1124 $this->setFlexFormRelations();
1125 // Unlink temporary files:
1126 $this->unlinkTempFiles();
1127 // Finally, traverse all records and process softreferences with substitution attributes.
1128 $this->processSoftReferences();
1129 }
1130
1131 /**
1132 * Writing pagetree/pages to database:
1133 *
1134 * @param integer $pid PID in which to import. If the operation is an update operation, the root of the page tree inside will be moved to this PID unless it is the same as the root page from the import
1135 * @return void
1136 * @see writeRecords_records()
1137 * @todo Define visibility
1138 */
1139 public function writeRecords_pages($pid) {
1140 // First, write page structure if any:
1141 if (is_array($this->dat['header']['records']['pages'])) {
1142 // $pageRecords is a copy of the pages array in the imported file. Records here are unset one by one when the addSingle function is called.
1143 $pageRecords = $this->dat['header']['records']['pages'];
1144 $this->import_data = array();
1145 // First add page tree if any
1146 if (is_array($this->dat['header']['pagetree'])) {
1147 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
1148 foreach ($pagesFromTree as $uid) {
1149 $thisRec = $this->dat['header']['records']['pages'][$uid];
1150 // PID: Set the main $pid, unless a NEW-id is found
1151 $setPid = isset($this->import_newId_pids[$thisRec['pid']]) ? $this->import_newId_pids[$thisRec['pid']] : $pid;
1152 $this->addSingle('pages', $uid, $setPid);
1153 unset($pageRecords[$uid]);
1154 }
1155 }
1156 // Then add all remaining pages not in tree on root level:
1157 if (count($pageRecords)) {
1158 $remainingPageUids = array_keys($pageRecords);
1159 foreach ($remainingPageUids as $pUid) {
1160 $this->addSingle('pages', $pUid, $pid);
1161 }
1162 }
1163 // Now write to database:
1164 $tce = $this->getNewTCE();
1165 $this->callHook('before_writeRecordsPages', array(
1166 'tce' => &$tce,
1167 'data' => &$this->import_data
1168 ));
1169 $tce->suggestedInsertUids = $this->suggestedInsertUids;
1170 $tce->start($this->import_data, array());
1171 $tce->process_datamap();
1172 $this->callHook('after_writeRecordsPages', array(
1173 'tce' => &$tce
1174 ));
1175 // post-processing: Registering new ids (end all tcemain sessions with this)
1176 $this->addToMapId($tce->substNEWwithIDs);
1177 // In case of an update, order pages from the page tree correctly:
1178 if ($this->update && is_array($this->dat['header']['pagetree'])) {
1179 $this->writeRecords_pages_order($pid);
1180 }
1181 }
1182 }
1183
1184 /**
1185 * Organize all updated pages in page tree so they are related like in the import file
1186 * Only used for updates and when $this->dat['header']['pagetree'] is an array.
1187 *
1188 * @param integer $pid Page id in which to import
1189 * @return void
1190 * @access private
1191 * @see writeRecords_pages(), writeRecords_records_order()
1192 * @todo Define visibility
1193 */
1194 public function writeRecords_pages_order($pid) {
1195 $cmd_data = array();
1196 // Get uid-pid relations and traverse them in order to map to possible new IDs
1197 $pidsFromTree = $this->flatInversePageTree_pid($this->dat['header']['pagetree']);
1198 foreach ($pidsFromTree as $origPid => $newPid) {
1199 if ($newPid >= 0 && $this->dontIgnorePid('pages', $origPid)) {
1200 // If the page had a new id (because it was created) use that instead!
1201 if (substr($this->import_newId_pids[$origPid], 0, 3) === 'NEW') {
1202 if ($this->import_mapId['pages'][$origPid]) {
1203 $mappedPid = $this->import_mapId['pages'][$origPid];
1204 $cmd_data['pages'][$mappedPid]['move'] = $newPid;
1205 }
1206 } else {
1207 $cmd_data['pages'][$origPid]['move'] = $newPid;
1208 }
1209 }
1210 }
1211 // Execute the move commands if any:
1212 if (count($cmd_data)) {
1213 $tce = $this->getNewTCE();
1214 $this->callHook('before_writeRecordsPagesOrder', array(
1215 'tce' => &$tce,
1216 'data' => &$cmd_data
1217 ));
1218 $tce->start(array(), $cmd_data);
1219 $tce->process_cmdmap();
1220 $this->callHook('after_writeRecordsPagesOrder', array(
1221 'tce' => &$tce
1222 ));
1223 }
1224 }
1225
1226 /**
1227 * Write all database records except pages (writtein in writeRecords_pages())
1228 *
1229 * @param integer $pid Page id in which to import
1230 * @return void
1231 * @see writeRecords_pages()
1232 * @todo Define visibility
1233 */
1234 public function writeRecords_records($pid) {
1235 // Write the rest of the records
1236 $this->import_data = array();
1237 if (is_array($this->dat['header']['records'])) {
1238 foreach ($this->dat['header']['records'] as $table => $recs) {
1239 if ($table != 'pages') {
1240 foreach ($recs as $uid => $thisRec) {
1241 // PID: Set the main $pid, unless a NEW-id is found
1242 $setPid = isset($this->import_mapId['pages'][$thisRec['pid']]) ? $this->import_mapId['pages'][$thisRec['pid']] : $pid;
1243 if (is_array($GLOBALS['TCA'][$table]) && $GLOBALS['TCA'][$table]['ctrl']['rootLevel']) {
1244 $setPid = 0;
1245 }
1246 // Add record:
1247 $this->addSingle($table, $uid, $setPid);
1248 }
1249 }
1250 }
1251 } else {
1252 $this->error('Error: No records defined in internal data array.');
1253 }
1254 // Now write to database:
1255 $tce = $this->getNewTCE();
1256 $this->callHook('before_writeRecordsRecords', array(
1257 'tce' => &$tce,
1258 'data' => &$this->import_data
1259 ));
1260 $tce->suggestedInsertUids = $this->suggestedInsertUids;
1261 // Because all records are being submitted in their correct order with positive pid numbers - and so we should reverse submission order internally.
1262 $tce->reverseOrder = 1;
1263 $tce->start($this->import_data, array());
1264 $tce->process_datamap();
1265 $this->callHook('after_writeRecordsRecords', array(
1266 'tce' => &$tce
1267 ));
1268 // post-processing: Removing files and registering new ids (end all tcemain sessions with this)
1269 $this->addToMapId($tce->substNEWwithIDs);
1270 // In case of an update, order pages from the page tree correctly:
1271 if ($this->update) {
1272 $this->writeRecords_records_order($pid);
1273 }
1274 }
1275
1276 /**
1277 * Organize all updated record to their new positions.
1278 * Only used for updates
1279 *
1280 * @param integer $mainPid Main PID into which we import.
1281 * @return void
1282 * @access private
1283 * @see writeRecords_records(), writeRecords_pages_order()
1284 * @todo Define visibility
1285 */
1286 public function writeRecords_records_order($mainPid) {
1287 $cmd_data = array();
1288 if (is_array($this->dat['header']['pagetree'])) {
1289 $pagesFromTree = $this->flatInversePageTree($this->dat['header']['pagetree']);
1290 } else {
1291 $pagesFromTree = array();
1292 }
1293 if (is_array($this->dat['header']['pid_lookup'])) {
1294 foreach ($this->dat['header']['pid_lookup'] as $pid => $recList) {
1295 $newPid = isset($this->import_mapId['pages'][$pid]) ? $this->import_mapId['pages'][$pid] : $mainPid;
1296 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($newPid)) {
1297 foreach ($recList as $tableName => $uidList) {
1298 // If $mainPid===$newPid then we are on root level and we can consider to move pages as well!
1299 // (they will not be in the page tree!)
1300 if (($tableName != 'pages' || !$pagesFromTree[$pid]) && is_array($uidList)) {
1301 $uidList = array_reverse(array_keys($uidList));
1302 foreach ($uidList as $uid) {
1303 if ($this->dontIgnorePid($tableName, $uid)) {
1304 $cmd_data[$tableName][$uid]['move'] = $newPid;
1305 } else {
1306
1307 }
1308 }
1309 }
1310 }
1311 }
1312 }
1313 }
1314 // Execute the move commands if any:
1315 if (count($cmd_data)) {
1316 $tce = $this->getNewTCE();
1317 $this->callHook('before_writeRecordsRecordsOrder', array(
1318 'tce' => &$tce,
1319 'data' => &$cmd_data
1320 ));
1321 $tce->start(array(), $cmd_data);
1322 $tce->process_cmdmap();
1323 $this->callHook('after_writeRecordsRecordsOrder', array(
1324 'tce' => &$tce
1325 ));
1326 }
1327 }
1328
1329 /**
1330 * Adds a single record to the $importData array. Also copies files to tempfolder.
1331 * However all File/DB-references and flexform field contents are set to blank for now! That is done with setRelations() later
1332 *
1333 * @param string $table Table name (from import memory)
1334 * @param integer $uid Record UID (from import memory)
1335 * @param integer $pid Page id
1336 * @return void
1337 * @see writeRecords()
1338 * @todo Define visibility
1339 */
1340 public function addSingle($table, $uid, $pid) {
1341 if ($this->import_mode[$table . ':' . $uid] !== 'exclude') {
1342 $record = $this->dat['records'][$table . ':' . $uid]['data'];
1343 if (is_array($record)) {
1344 if ($this->update && $this->doesRecordExist($table, $uid) && $this->import_mode[$table . ':' . $uid] !== 'as_new') {
1345 $ID = $uid;
1346 } else {
1347 $ID = uniqid('NEW');
1348 }
1349 $this->import_newId[$table . ':' . $ID] = array('table' => $table, 'uid' => $uid);
1350 if ($table == 'pages') {
1351 $this->import_newId_pids[$uid] = $ID;
1352 }
1353 // Set main record data:
1354 $this->import_data[$table][$ID] = $record;
1355 $this->import_data[$table][$ID]['tx_impexp_origuid'] = $this->import_data[$table][$ID]['uid'];
1356 // Reset permission data:
1357 if ($table === 'pages') {
1358 // Have to reset the user/group IDs so pages are owned by importing user. Otherwise strange things may happen for non-admins!
1359 unset($this->import_data[$table][$ID]['perms_userid']);
1360 unset($this->import_data[$table][$ID]['perms_groupid']);
1361 }
1362 // PID and UID:
1363 unset($this->import_data[$table][$ID]['uid']);
1364 // Updates:
1365 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($ID)) {
1366 unset($this->import_data[$table][$ID]['pid']);
1367 } else {
1368 // Inserts:
1369 $this->import_data[$table][$ID]['pid'] = $pid;
1370 if (($this->import_mode[$table . ':' . $uid] === 'force_uid' && $this->update || $this->force_all_UIDS) && $GLOBALS['BE_USER']->isAdmin()) {
1371 $this->import_data[$table][$ID]['uid'] = $uid;
1372 $this->suggestedInsertUids[$table . ':' . $uid] = 'DELETE';
1373 }
1374 }
1375 // Setting db/file blank:
1376 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1377 switch ((string) $config['type']) {
1378 case 'db':
1379
1380 case 'file':
1381 // Fixed later in ->setRelations() [because we need to know ALL newly created IDs before we can map relations!]
1382 // In the meantime we set NO values for relations:
1383 $this->import_data[$table][$ID][$field] = '';
1384 break;
1385 case 'flex':
1386 // Fixed later in setFlexFormRelations()
1387 // In the meantime we set NO value for flexforms - this is mainly because file references inside will not be processed properly; In fact references will point to no file or existing files (in which case there will be double-references which is a big problem of course!)
1388 $this->import_data[$table][$ID][$field] = '';
1389 break;
1390 }
1391 }
1392 } elseif ($table . ':' . $uid != 'pages:0') {
1393 // On root level we don't want this error message.
1394 $this->error('Error: no record was found in data array!', 1);
1395 }
1396 }
1397 }
1398
1399 /**
1400 * Registers the substNEWids in memory.
1401 *
1402 * @param array $substNEWwithIDs From tcemain to be merged into internal mapping variable in this object
1403 * @return void
1404 * @see writeRecords()
1405 * @todo Define visibility
1406 */
1407 public function addToMapId($substNEWwithIDs) {
1408 foreach ($this->import_data as $table => $recs) {
1409 foreach ($recs as $id => $value) {
1410 $old_uid = $this->import_newId[$table . ':' . $id]['uid'];
1411 if (isset($substNEWwithIDs[$id])) {
1412 $this->import_mapId[$table][$old_uid] = $substNEWwithIDs[$id];
1413 } elseif ($this->update) {
1414 // Map same ID to same ID....
1415 $this->import_mapId[$table][$old_uid] = $id;
1416 } else {
1417 $this->error('Possible error: ' . $table . ':' . $old_uid . ' had no new id assigned to it. This indicates that the record was not added to database during import. Please check changelog!', 1);
1418 }
1419 }
1420 }
1421 }
1422
1423 /**
1424 * Returns a new $TCE object
1425 *
1426 * @return object $TCE object
1427 * @todo Define visibility
1428 */
1429 public function getNewTCE() {
1430 $tce = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
1431 $tce->stripslashes_values = 0;
1432 $tce->dontProcessTransformations = 1;
1433 $tce->enableLogging = $this->enableLogging;
1434 $tce->alternativeFileName = $this->alternativeFileName;
1435 $tce->alternativeFilePath = $this->alternativeFilePath;
1436 return $tce;
1437 }
1438
1439 /**
1440 * Cleaning up all the temporary files stored in typo3temp/ folder
1441 *
1442 * @return void
1443 * @todo Define visibility
1444 */
1445 public function unlinkTempFiles() {
1446 foreach ($this->unlinkFiles as $fileName) {
1447 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($fileName, PATH_site . 'typo3temp/')) {
1448 \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile($fileName);
1449 clearstatcache();
1450 if (is_file($fileName)) {
1451 $this->error('Error: ' . $fileName . ' was NOT unlinked as it should have been!', 1);
1452 }
1453 } else {
1454 $this->error('Error: ' . $fileName . ' was not in temp-path. Not removed!', 1);
1455 }
1456 }
1457 $this->unlinkFiles = array();
1458 }
1459
1460 /***************************
1461 *
1462 * Import / Relations setting
1463 *
1464 ***************************/
1465 /**
1466 * At the end of the import process all file and DB relations should be set properly (that is relations to imported records are all re-created so imported records are correctly related again)
1467 * Relations in flexform fields are processed in setFlexFormRelations() after this function
1468 *
1469 * @return void
1470 * @see setFlexFormRelations()
1471 * @todo Define visibility
1472 */
1473 public function setRelations() {
1474 $updateData = array();
1475 // import_newId contains a register of all records that was in the import memorys "records" key
1476 foreach ($this->import_newId as $nId => $dat) {
1477 $table = $dat['table'];
1478 $uid = $dat['uid'];
1479 // original UID - NOT the new one!
1480 // If the record has been written and received a new id, then proceed:
1481 if (is_array($this->import_mapId[$table]) && isset($this->import_mapId[$table][$uid])) {
1482 $thisNewUid = \TYPO3\CMS\Backend\Utility\BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1483 if (is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
1484 // Traverse relation fields of each record
1485 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1486 switch ((string) $config['type']) {
1487 case 'db':
1488 if (is_array($config['itemArray']) && count($config['itemArray'])) {
1489 $valArray = $this->setRelations_db($config['itemArray']);
1490 $updateData[$table][$thisNewUid][$field] = implode(',', $valArray);
1491 }
1492 break;
1493 case 'file':
1494 if (is_array($config['newValueFiles']) && count($config['newValueFiles'])) {
1495 $valArr = array();
1496 foreach ($config['newValueFiles'] as $fI) {
1497 $valArr[] = $this->import_addFileNameToBeCopied($fI);
1498 }
1499 $updateData[$table][$thisNewUid][$field] = implode(',', $valArr);
1500 }
1501 break;
1502 }
1503 }
1504 } else {
1505 $this->error('Error: no record was found in data array!', 1);
1506 }
1507 } else {
1508 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')', 1);
1509 }
1510 }
1511 if (count($updateData)) {
1512 $tce = $this->getNewTCE();
1513 $this->callHook('before_setRelation', array(
1514 'tce' => &$tce,
1515 'data' => &$updateData
1516 ));
1517 $tce->start($updateData, array());
1518 $tce->process_datamap();
1519 $this->callHook('after_setRelations', array(
1520 'tce' => &$tce
1521 ));
1522 }
1523 }
1524
1525 /**
1526 * Maps relations for database
1527 *
1528 * @param array $itemArray Array of item sets (table/uid) from a dbAnalysis object
1529 * @return array Array with values [table]_[uid]. These values have the regular tcemain-input group/select type which means they will automatically be processed into a uid-list or MM relations.
1530 * @todo Define visibility
1531 */
1532 public function setRelations_db($itemArray) {
1533 $valArray = array();
1534 foreach ($itemArray as $relDat) {
1535 if (is_array($this->import_mapId[$relDat['table']]) && isset($this->import_mapId[$relDat['table']][$relDat['id']])) {
1536 $valArray[] = $relDat['table'] . '_' . $this->import_mapId[$relDat['table']][$relDat['id']];
1537 } elseif ($this->isTableStatic($relDat['table']) || $this->isExcluded($relDat['table'], $relDat['id']) || $relDat['id'] < 0) {
1538 // Checking for less than zero because some select types could contain negative values, eg. fe_groups (-1, -2) and sys_language (-1 = ALL languages). This must be handled on both export and import.
1539 $valArray[] = $relDat['table'] . '_' . $relDat['id'];
1540 } else {
1541 $this->error('Lost relation: ' . $relDat['table'] . ':' . $relDat['id'], 1);
1542 }
1543 }
1544 return $valArray;
1545 }
1546
1547 /**
1548 * Writes the file from import array to temp dir and returns the filename of it.
1549 *
1550 * @param array $fI File information with three keys: "filename" = filename without path, "ID_absFile" = absolute filepath to the file (including the filename), "ID" = md5 hash of "ID_absFile
1551 * @return string Absolute filename of the temporary filename of the file. In ->alternativeFileName the original name is set.
1552 * @todo Define visibility
1553 */
1554 public function import_addFileNameToBeCopied($fI) {
1555 if (is_array($this->dat['files'][$fI['ID']])) {
1556 $tmpFile = \TYPO3\CMS\Core\Utility\GeneralUtility::tempnam('import_temp_');
1557 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile($tmpFile, $this->dat['files'][$fI['ID']]['content']);
1558 clearstatcache();
1559 if (@is_file($tmpFile)) {
1560 $this->unlinkFiles[] = $tmpFile;
1561 if (filesize($tmpFile) == $this->dat['files'][$fI['ID']]['filesize']) {
1562 $this->alternativeFileName[$tmpFile] = $fI['filename'];
1563 $this->alternativeFilePath[$tmpFile] = $this->dat['files'][$fI['ID']]['relFileRef'];
1564 return $tmpFile;
1565 } else {
1566 $this->error('Error: temporary file ' . $tmpFile . ' had a size (' . filesize($tmpFile) . ') different from the original (' . $this->dat['files'][$fI['ID']]['filesize'] . ')', 1);
1567 }
1568 } else {
1569 $this->error('Error: temporary file ' . $tmpFile . ' was not written as it should have been!', 1);
1570 }
1571 } else {
1572 $this->error('Error: No file found for ID ' . $fI['ID'], 1);
1573 }
1574 }
1575
1576 /**
1577 * After all DB relations has been set in the end of the import (see setRelations()) then it is time to correct all relations inside of FlexForm fields.
1578 * The reason for doing this after is that the setting of relations may affect (quite often!) which data structure is used for the flexforms field!
1579 *
1580 * @return void
1581 * @see setRelations()
1582 * @todo Define visibility
1583 */
1584 public function setFlexFormRelations() {
1585 $updateData = array();
1586 // import_newId contains a register of all records that was in the import memorys "records" key
1587 foreach ($this->import_newId as $nId => $dat) {
1588 $table = $dat['table'];
1589 $uid = $dat['uid'];
1590 // original UID - NOT the new one!
1591 // If the record has been written and received a new id, then proceed:
1592 if (is_array($this->import_mapId[$table]) && isset($this->import_mapId[$table][$uid])) {
1593 $thisNewUid = \TYPO3\CMS\Backend\Utility\BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1594 if (is_array($this->dat['records'][$table . ':' . $uid]['rels'])) {
1595 \TYPO3\CMS\Core\Utility\GeneralUtility::loadTCA($table);
1596 // Traverse relation fields of each record
1597 foreach ($this->dat['records'][$table . ':' . $uid]['rels'] as $field => $config) {
1598 switch ((string) $config['type']) {
1599 case 'flex':
1600 // Get XML content and set as default value (string, non-processed):
1601 $updateData[$table][$thisNewUid][$field] = $this->dat['records'][$table . ':' . $uid]['data'][$field];
1602 // If there has been registered relations inside the flex form field, run processing on the content:
1603 if (count($config['flexFormRels']['db']) || count($config['flexFormRels']['file'])) {
1604 $origRecordRow = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($table, $thisNewUid, '*');
1605 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
1606 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1607 if (is_array($origRecordRow) && is_array($conf) && $conf['type'] === 'flex') {
1608 // Get current data structure and value array:
1609 $dataStructArray = \TYPO3\CMS\Backend\Utility\BackendUtility::getFlexFormDS($conf, $origRecordRow, $table);
1610 $currentValueArray = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($updateData[$table][$thisNewUid][$field]);
1611 // Do recursive processing of the XML data:
1612 $iteratorObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
1613 $iteratorObj->callBackObj = $this;
1614 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $thisNewUid, $field, $config), 'remapListedDBRecords_flexFormCallBack');
1615 // The return value is set as an array which means it will be processed by tcemain for file and DB references!
1616 if (is_array($currentValueArray['data'])) {
1617 $updateData[$table][$thisNewUid][$field] = $currentValueArray;
1618 }
1619 }
1620 }
1621 break;
1622 }
1623 }
1624 } else {
1625 $this->error('Error: no record was found in data array!', 1);
1626 }
1627 } else {
1628 $this->error('Error: this records is NOT created it seems! (' . $table . ':' . $uid . ')', 1);
1629 }
1630 }
1631 if (count($updateData)) {
1632 $tce = $this->getNewTCE();
1633 $this->callHook('before_setFlexFormRelations', array(
1634 'tce' => &$tce,
1635 'data' => &$updateData
1636 ));
1637 $tce->start($updateData, array());
1638 $tce->process_datamap();
1639 $this->callHook('after_setFlexFormRelations', array(
1640 'tce' => &$tce
1641 ));
1642 }
1643 }
1644
1645 /**
1646 * Callback function for traversing the FlexForm structure in relation to remapping database relations
1647 *
1648 * @param array $pParams Set of parameters in numeric array: table, uid, field
1649 * @param array $dsConf TCA config for field (from Data Structure of course)
1650 * @param string $dataValue Field value (from FlexForm XML)
1651 * @param string $dataValue_ext1 Not used
1652 * @param string $dataValue_ext2 Not used
1653 * @param string $path Path of where the data structure of the element is found
1654 * @return array Array where the "value" key carries the value.
1655 * @see setFlexFormRelations()
1656 * @todo Define visibility
1657 */
1658 public function remapListedDBRecords_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path) {
1659 // Extract parameters:
1660 list($table, $uid, $field, $config) = $pParams;
1661 // In case the $path is used as index without a trailing slash we will remove that
1662 if (!is_array($config['flexFormRels']['db'][$path]) && is_array($config['flexFormRels']['db'][rtrim($path, '/')])) {
1663 $path = rtrim($path, '/');
1664 }
1665 if (is_array($config['flexFormRels']['db'][$path])) {
1666 $valArray = $this->setRelations_db($config['flexFormRels']['db'][$path]);
1667 $dataValue = implode(',', $valArray);
1668 }
1669 if (is_array($config['flexFormRels']['file'][$path])) {
1670 foreach ($config['flexFormRels']['file'][$path] as $fI) {
1671 $valArr[] = $this->import_addFileNameToBeCopied($fI);
1672 }
1673 $dataValue = implode(',', $valArr);
1674 }
1675 return array('value' => $dataValue);
1676 }
1677
1678 /**************************
1679 *
1680 * Import / Soft References
1681 *
1682 *************************/
1683 /**
1684 * Processing of soft references
1685 *
1686 * @return void
1687 * @todo Define visibility
1688 */
1689 public function processSoftReferences() {
1690 // Initialize:
1691 $inData = array();
1692 // Traverse records:
1693 if (is_array($this->dat['header']['records'])) {
1694 foreach ($this->dat['header']['records'] as $table => $recs) {
1695 foreach ($recs as $uid => $thisRec) {
1696 // If there are soft references defined, traverse those:
1697 if (isset($GLOBALS['TCA'][$table]) && is_array($thisRec['softrefs'])) {
1698 \TYPO3\CMS\Core\Utility\GeneralUtility::loadTCA($table);
1699 // First traversal is to collect softref configuration and split them up based on fields. This could probably also have been done with the "records" key instead of the header.
1700 $fieldsIndex = array();
1701 foreach ($thisRec['softrefs'] as $softrefDef) {
1702 // If a substitution token is set:
1703 if ($softrefDef['field'] && is_array($softrefDef['subst']) && $softrefDef['subst']['tokenID']) {
1704 $fieldsIndex[$softrefDef['field']][$softrefDef['subst']['tokenID']] = $softrefDef;
1705 }
1706 }
1707 // The new id:
1708 $thisNewUid = \TYPO3\CMS\Backend\Utility\BackendUtility::wsMapId($table, $this->import_mapId[$table][$uid]);
1709 // Now, if there are any fields that require substitution to be done, lets go for that:
1710 foreach ($fieldsIndex as $field => $softRefCfgs) {
1711 if (is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
1712 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
1713 if ($conf['type'] === 'flex') {
1714 // This will fetch the new row for the element (which should be updated with any references to data structures etc.)
1715 $origRecordRow = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($table, $thisNewUid, '*');
1716 if (is_array($origRecordRow)) {
1717 // Get current data structure and value array:
1718 $dataStructArray = \TYPO3\CMS\Backend\Utility\BackendUtility::getFlexFormDS($conf, $origRecordRow, $table);
1719 $currentValueArray = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($origRecordRow[$field]);
1720 // Do recursive processing of the XML data:
1721 /** @var $iteratorObj \TYPO3\CMS\Core\DataHandling\DataHandler */
1722 $iteratorObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
1723 $iteratorObj->callBackObj = $this;
1724 $currentValueArray['data'] = $iteratorObj->checkValue_flex_procInData($currentValueArray['data'], array(), array(), $dataStructArray, array($table, $uid, $field, $softRefCfgs), 'processSoftReferences_flexFormCallBack');
1725 // The return value is set as an array which means it will be processed by tcemain for file and DB references!
1726 if (is_array($currentValueArray['data'])) {
1727 $inData[$table][$thisNewUid][$field] = $currentValueArray;
1728 }
1729 }
1730 } else {
1731 // Get tokenizedContent string and proceed only if that is not blank:
1732 $tokenizedContent = $this->dat['records'][$table . ':' . $uid]['rels'][$field]['softrefs']['tokenizedContent'];
1733 if (strlen($tokenizedContent) && is_array($softRefCfgs)) {
1734 $inData[$table][$thisNewUid][$field] = $this->processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid);
1735 }
1736 }
1737 }
1738 }
1739 }
1740 }
1741 }
1742 }
1743 // Now write to database:
1744 $tce = $this->getNewTCE();
1745 $this->callHook('before_processSoftReferences', array(
1746 'tce' => &$tce,
1747 'data' => &$inData
1748 ));
1749 $tce->enableLogging = TRUE;
1750 $tce->start($inData, array());
1751 $tce->process_datamap();
1752 $this->callHook('after_processSoftReferences', array(
1753 'tce' => &$tce
1754 ));
1755 }
1756
1757 /**
1758 * Callback function for traversing the FlexForm structure in relation to remapping softreference relations
1759 *
1760 * @param array $pParams Set of parameters in numeric array: table, uid, field
1761 * @param array $dsConf TCA config for field (from Data Structure of course)
1762 * @param string $dataValue Field value (from FlexForm XML)
1763 * @param string $dataValue_ext1 Not used
1764 * @param string $dataValue_ext2 Not used
1765 * @param string $path Path of where the data structure where the element is found
1766 * @return array Array where the "value" key carries the value.
1767 * @see setFlexFormRelations()
1768 * @todo Define visibility
1769 */
1770 public function processSoftReferences_flexFormCallBack($pParams, $dsConf, $dataValue, $dataValue_ext1, $dataValue_ext2, $path) {
1771 // Extract parameters:
1772 list($table, $origUid, $field, $softRefCfgs) = $pParams;
1773 if (is_array($softRefCfgs)) {
1774 // First, find all soft reference configurations for this structure path (they are listed flat in the header):
1775 $thisSoftRefCfgList = array();
1776 foreach ($softRefCfgs as $sK => $sV) {
1777 if ($sV['structurePath'] === $path) {
1778 $thisSoftRefCfgList[$sK] = $sV;
1779 }
1780 }
1781 // If any was found, do processing:
1782 if (count($thisSoftRefCfgList)) {
1783 // Get tokenizedContent string and proceed only if that is not blank:
1784 $tokenizedContent = $this->dat['records'][$table . ':' . $origUid]['rels'][$field]['flexFormRels']['softrefs'][$path]['tokenizedContent'];
1785 if (strlen($tokenizedContent)) {
1786 $dataValue = $this->processSoftReferences_substTokens($tokenizedContent, $thisSoftRefCfgList, $table, $origUid);
1787 }
1788 }
1789 }
1790 // Return
1791 return array('value' => $dataValue);
1792 }
1793
1794 /**
1795 * Substition of softreference tokens
1796 *
1797 * @param string $tokenizedContent Content of field with soft reference tokens in.
1798 * @param array $softRefCfgs Soft reference configurations
1799 * @param string $table Table for which the processing occurs
1800 * @param string $uid UID of record from table
1801 * @return string The input content with tokens substituted according to entries in softRefCfgs
1802 * @todo Define visibility
1803 */
1804 public function processSoftReferences_substTokens($tokenizedContent, $softRefCfgs, $table, $uid) {
1805 // traverse each softref type for this field:
1806 foreach ($softRefCfgs as $cfg) {
1807 // Get token ID:
1808 $tokenID = $cfg['subst']['tokenID'];
1809 // Default is current token value:
1810 $insertValue = $cfg['subst']['tokenValue'];
1811 // Based on mode:
1812 switch ((string) $this->softrefCfg[$tokenID]['mode']) {
1813 case 'exclude':
1814 // Exclude is a simple passthrough of the value
1815 break;
1816 case 'editable':
1817 // Editable always picks up the value from this input array:
1818 $insertValue = $this->softrefInputValues[$tokenID];
1819 break;
1820 default:
1821 // Mapping IDs/creating files: Based on type, look up new value:
1822 switch ((string) $cfg['subst']['type']) {
1823 case 'file':
1824 // Create / Overwrite file:
1825 $insertValue = $this->processSoftReferences_saveFile($cfg['subst']['relFileName'], $cfg, $table, $uid);
1826 break;
1827 case 'db':
1828 default:
1829 // Trying to map database element if found in the mapID array:
1830 list($tempTable, $tempUid) = explode(':', $cfg['subst']['recordRef']);
1831 if (isset($this->import_mapId[$tempTable][$tempUid])) {
1832 $insertValue = \TYPO3\CMS\Backend\Utility\BackendUtility::wsMapId($tempTable, $this->import_mapId[$tempTable][$tempUid]);
1833 // Look if reference is to a page and the original token value was NOT an integer - then we assume is was an alias and try to look up the new one!
1834 if ($tempTable === 'pages' && !\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($cfg['subst']['tokenValue'])) {
1835 $recWithUniqueValue = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($tempTable, $insertValue, 'alias');
1836 if ($recWithUniqueValue['alias']) {
1837 $insertValue = $recWithUniqueValue['alias'];
1838 }
1839 }
1840 }
1841 break;
1842 }
1843 break;
1844 }
1845 // Finally, swap the soft reference token in tokenized content with the insert value:
1846 $tokenizedContent = str_replace('{softref:' . $tokenID . '}', $insertValue, $tokenizedContent);
1847 }
1848 return $tokenizedContent;
1849 }
1850
1851 /**
1852 * Process a soft reference file
1853 *
1854 * @param string $relFileName Old Relative filename
1855 * @param array $cfg soft reference configuration array
1856 * @param string $table Table for which the processing occurs
1857 * @param string $uid UID of record from table
1858 * @return string New relative filename (value to insert instead of the softref token)
1859 * @todo Define visibility
1860 */
1861 public function processSoftReferences_saveFile($relFileName, $cfg, $table, $uid) {
1862 if ($fileHeaderInfo = $this->dat['header']['files'][$cfg['file_ID']]) {
1863 // Initialize; Get directory prefix for file and find possible RTE filename
1864 $dirPrefix = dirname($relFileName) . '/';
1865 $rteOrigName = $this->getRTEoriginalFilename(basename($relFileName));
1866 // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
1867 if ($rteOrigName && \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/')) {
1868 // RTE:
1869 // First, find unique RTE file name:
1870 if (@is_dir((PATH_site . $dirPrefix))) {
1871 // From the "original" RTE filename, produce a new "original" destination filename which is unused. Even if updated, the image should be unique. Currently the problem with this is that it leaves a lot of unused RTE images...
1872 $fileProcObj = $this->getFileProcObj();
1873 $origDestName = $fileProcObj->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
1874 // Create copy file name:
1875 $pI = pathinfo($relFileName);
1876 $copyDestName = dirname($origDestName) . '/RTEmagicC_' . substr(basename($origDestName), 10) . '.' . $pI['extension'];
1877 if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($copyDestName)) {
1878 if ($this->dat['header']['files'][$fileHeaderInfo['RTE_ORIG_ID']]) {
1879 // Write the copy and original RTE file to the respective filenames:
1880 $this->writeFileVerify($copyDestName, $cfg['file_ID'], TRUE);
1881 $this->writeFileVerify($origDestName, $fileHeaderInfo['RTE_ORIG_ID'], TRUE);
1882 // Return the relative path of the copy file name:
1883 return substr($copyDestName, strlen(PATH_site));
1884 } else {
1885 $this->error('ERROR: Could not find original file ID');
1886 }
1887 } else {
1888 $this->error('ERROR: The destination filenames "' . $copyDestName . '" and "' . $origDestName . '" either existed or have non-valid names');
1889 }
1890 } else {
1891 $this->error('ERROR: "' . PATH_site . $dirPrefix . '" was not a directory, so could not process file "' . $relFileName . '"');
1892 }
1893 } elseif (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($dirPrefix, $this->fileadminFolderName . '/')) {
1894 // File in fileadmin/ folder:
1895 // Create file (and possible resources)
1896 $newFileName = $this->processSoftReferences_saveFile_createRelFile($dirPrefix, basename($relFileName), $cfg['file_ID'], $table, $uid);
1897 if (strlen($newFileName)) {
1898 $relFileName = $newFileName;
1899 } else {
1900 $this->error('ERROR: No new file created for "' . $relFileName . '"');
1901 }
1902 } else {
1903 $this->error('ERROR: Sorry, cannot operate on non-RTE files which are outside the fileadmin folder.');
1904 }
1905 } else {
1906 $this->error('ERROR: Could not find file ID in header.');
1907 }
1908 // Return (new) filename relative to PATH_site:
1909 return $relFileName;
1910 }
1911
1912 /**
1913 * Create file in directory and return the new (unique) filename
1914 *
1915 * @param string $origDirPrefix Directory prefix, relative, with trailing slash
1916 * @param string $fileName Filename (without path)
1917 * @param string $fileID File ID from import memory
1918 * @param string $table Table for which the processing occurs
1919 * @param string $uid UID of record from table
1920 * @return string New relative filename, if any
1921 * @todo Define visibility
1922 */
1923 public function processSoftReferences_saveFile_createRelFile($origDirPrefix, $fileName, $fileID, $table, $uid) {
1924 // If the fileID map contains an entry for this fileID then just return the relative filename of that entry; we don't want to write another unique filename for this one!
1925 if ($this->fileIDMap[$fileID]) {
1926 return substr($this->fileIDMap[$fileID], strlen(PATH_site));
1927 }
1928 // Verify FileMount access to dir-prefix. Returns the best alternative relative path if any
1929 $dirPrefix = $this->verifyFolderAccess($origDirPrefix);
1930 if ($dirPrefix && (!$this->update || $origDirPrefix === $dirPrefix) && $this->checkOrCreateDir($dirPrefix)) {
1931 $fileHeaderInfo = $this->dat['header']['files'][$fileID];
1932 $updMode = $this->update && $this->import_mapId[$table][$uid] === $uid && $this->import_mode[$table . ':' . $uid] !== 'as_new';
1933 // Create new name for file:
1934 // Must have same ID in map array (just for security, is not really needed) and NOT be set "as_new".
1935 if ($updMode) {
1936 $newName = PATH_site . $dirPrefix . $fileName;
1937 } else {
1938 // Create unique filename:
1939 $fileProcObj = $this->getFileProcObj();
1940 $newName = $fileProcObj->getUniqueName($fileName, PATH_site . $dirPrefix);
1941 }
1942 // Write main file:
1943 if ($this->writeFileVerify($newName, $fileID)) {
1944 // If the resource was an HTML/CSS file with resources attached, we will write those as well!
1945 if (is_array($fileHeaderInfo['EXT_RES_ID'])) {
1946 $tokenizedContent = $this->dat['files'][$fileID]['tokenizedContent'];
1947 $tokenSubstituted = FALSE;
1948 $fileProcObj = $this->getFileProcObj();
1949 if ($updMode) {
1950 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1951 if ($this->dat['files'][$res_fileID]['filename']) {
1952 // Resolve original filename:
1953 $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
1954 $absResourceFileName = \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath(PATH_site . $origDirPrefix . $relResourceFileName);
1955 $absResourceFileName = \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($absResourceFileName);
1956 if ($absResourceFileName && \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($absResourceFileName, PATH_site . $this->fileadminFolderName . '/')) {
1957 $destDir = substr(dirname($absResourceFileName) . '/', strlen(PATH_site));
1958 if ($this->verifyFolderAccess($destDir, TRUE) && $this->checkOrCreateDir($destDir)) {
1959 $this->writeFileVerify($absResourceFileName, $res_fileID);
1960 } else {
1961 $this->error('ERROR: Could not create file in directory "' . $destDir . '"');
1962 }
1963 } else {
1964 $this->error('ERROR: Could not resolve path for "' . $relResourceFileName . '"');
1965 }
1966 $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1967 $tokenSubstituted = TRUE;
1968 }
1969 }
1970 } else {
1971 // Create the resouces directory name (filename without extension, suffixed "_FILES")
1972 $resourceDir = dirname($newName) . '/' . preg_replace('/\\.[^.]*$/', '', basename($newName)) . '_FILES';
1973 if (\TYPO3\CMS\Core\Utility\GeneralUtility::mkdir($resourceDir)) {
1974 foreach ($fileHeaderInfo['EXT_RES_ID'] as $res_fileID) {
1975 if ($this->dat['files'][$res_fileID]['filename']) {
1976 $absResourceFileName = $fileProcObj->getUniqueName($this->dat['files'][$res_fileID]['filename'], $resourceDir);
1977 $relResourceFileName = substr($absResourceFileName, strlen(dirname($resourceDir)) + 1);
1978 $this->writeFileVerify($absResourceFileName, $res_fileID);
1979 $tokenizedContent = str_replace('{EXT_RES_ID:' . $res_fileID . '}', $relResourceFileName, $tokenizedContent);
1980 $tokenSubstituted = TRUE;
1981 }
1982 }
1983 }
1984 }
1985 // If substitutions has been made, write the content to the file again:
1986 if ($tokenSubstituted) {
1987 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile($newName, $tokenizedContent);
1988 }
1989 }
1990 return substr($newName, strlen(PATH_site));
1991 }
1992 }
1993 }
1994
1995 /**
1996 * Writes a file from the import memory having $fileID to file name $fileName which must be an absolute path inside PATH_site
1997 *
1998 * @param string $fileName Absolute filename inside PATH_site to write to
1999 * @param string $fileID File ID from import memory
2000 * @param boolean $bypassMountCheck Bypasses the checking against filemounts - only for RTE files!
2001 * @return boolean Returns TRUE if it went well. Notice that the content of the file is read again, and md5 from import memory is validated.
2002 * @todo Define visibility
2003 */
2004 public function writeFileVerify($fileName, $fileID, $bypassMountCheck = FALSE) {
2005 $fileProcObj = $this->getFileProcObj();
2006 if ($fileProcObj->actionPerms['newFile']) {
2007 // Just for security, check again. Should actually not be necessary.
2008 if ($fileProcObj->checkPathAgainstMounts($fileName) || $bypassMountCheck) {
2009 $fI = \TYPO3\CMS\Core\Utility\GeneralUtility::split_fileref($fileName);
2010 if ($fileProcObj->checkIfAllowed($fI['fileext'], $fI['path'], $fI['file']) || $this->allowPHPScripts && $GLOBALS['BE_USER']->isAdmin()) {
2011 if (\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($fileName)) {
2012 if ($this->dat['files'][$fileID]) {
2013 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile($fileName, $this->dat['files'][$fileID]['content']);
2014 $this->fileIDMap[$fileID] = $fileName;
2015 if (md5(\TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($fileName)) == $this->dat['files'][$fileID]['content_md5']) {
2016 return TRUE;
2017 } else {
2018 $this->error('ERROR: File content "' . $fileName . '" was corrupted');
2019 }
2020 } else {
2021 $this->error('ERROR: File ID "' . $fileID . '" could not be found');
2022 }
2023 } else {
2024 $this->error('ERROR: Filename "' . $fileName . '" was not a valid relative file path!');
2025 }
2026 } else {
2027 $this->error('ERROR: Filename "' . $fileName . '" failed against extension check or deny-pattern!');
2028 }
2029 } else {
2030 $this->error('ERROR: Filename "' . $fileName . '" was not allowed in destination path!');
2031 }
2032 } else {
2033 $this->error('ERROR: You did not have sufficient permissions to write the file "' . $fileName . '"');
2034 }
2035 }
2036
2037 /**
2038 * Returns TRUE if directory exists and if it doesn't it will create directory and return TRUE if that succeeded.
2039 *
2040 * @param string $dirPrefix Directory to create. Having a trailing slash. Must be in fileadmin/. Relative to PATH_site
2041 * @return boolean TRUE, if directory exists (was created)
2042 * @todo Define visibility
2043 */
2044 public function checkOrCreateDir($dirPrefix) {
2045 // Split dir path and remove first directory (which should be "fileadmin")
2046 $filePathParts = explode('/', $dirPrefix);
2047 $firstDir = array_shift($filePathParts);
2048 if ($firstDir === $this->fileadminFolderName && \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($dirPrefix)) {
2049 $pathAcc = '';
2050 foreach ($filePathParts as $dirname) {
2051 $pathAcc .= '/' . $dirname;
2052 if (strlen($dirname)) {
2053 if (!@is_dir((PATH_site . $this->fileadminFolderName . $pathAcc))) {
2054 if (!\TYPO3\CMS\Core\Utility\GeneralUtility::mkdir((PATH_site . $this->fileadminFolderName . $pathAcc))) {
2055 $this->error('ERROR: Directory could not be created....B');
2056 return FALSE;
2057 }
2058 }
2059 } elseif ($dirPrefix === $this->fileadminFolderName . $pathAcc) {
2060 return TRUE;
2061 } else {
2062 $this->error('ERROR: Directory could not be created....A');
2063 }
2064 }
2065 }
2066 }
2067
2068 /**
2069 * Verifies that the input path (relative to PATH_site) is found in the backend users filemounts.
2070 * If it doesn't it will try to find another relative filemount for the user and return an alternative path prefix for the file.
2071 *
2072 * @param string $dirPrefix Path relative to PATH_site
2073 * @param boolean $noAlternative If set, Do not look for alternative path! Just return FALSE
2074 * @return string If a path is available that will be returned, otherwise FALSE.
2075 * @todo Define visibility
2076 */
2077 public function verifyFolderAccess($dirPrefix, $noAlternative = FALSE) {
2078 $fileProcObj = $this->getFileProcObj();
2079 // Check, if dirPrefix is inside a valid Filemount for user:
2080 $result = $fileProcObj->checkPathAgainstMounts(PATH_site . $dirPrefix);
2081 // If not, try to find another relative filemount and use that instead:
2082 if (!$result) {
2083 if ($noAlternative) {
2084 return FALSE;
2085 }
2086 // Find first web folder:
2087 $result = $fileProcObj->findFirstWebFolder();
2088 // If that succeeded, return the path to it:
2089 if ($result) {
2090 // Remove the "fileadmin/" prefix of input path - and append the rest to the return value:
2091 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($dirPrefix, $this->fileadminFolderName . '/')) {
2092 $dirPrefix = substr($dirPrefix, strlen($this->fileadminFolderName . '/'));
2093 }
2094 return substr($fileProcObj->mounts[$result]['path'] . $dirPrefix, strlen(PATH_site));
2095 }
2096 } else {
2097 return $dirPrefix;
2098 }
2099 }
2100
2101 /**************************
2102 *
2103 * File Input
2104 *
2105 *************************/
2106 /**
2107 * Loads the header section/all of the $filename into memory
2108 *
2109 * @param string $filename Filename, absolute
2110 * @param boolean $all If set, all information is loaded (header, records and files). Otherwise the default is to read only the header information
2111 * @return boolean TRUE if the operation went well
2112 * @todo Define visibility
2113 */
2114 public function loadFile($filename, $all = 0) {
2115 if (@is_file($filename)) {
2116 $fI = pathinfo($filename);
2117 if (strtolower($fI['extension']) == 'xml') {
2118 // XML:
2119 $xmlContent = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($filename);
2120 if (strlen($xmlContent)) {
2121 $this->dat = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($xmlContent, '', TRUE);
2122 if (is_array($this->dat)) {
2123 if ($this->dat['_DOCUMENT_TAG'] === 'T3RecordDocument' && is_array($this->dat['header']) && is_array($this->dat['records'])) {
2124 $this->loadInit();
2125 return TRUE;
2126 } else {
2127 $this->error('XML file did not contain proper XML for TYPO3 Import');
2128 }
2129 } else {
2130 $this->error('XML could not be parsed: ' . $this->dat);
2131 }
2132 } else {
2133 $this->error('Error opening file: ' . $filename);
2134 }
2135 } else {
2136 // T3D
2137 if ($fd = fopen($filename, 'rb')) {
2138 $this->dat['header'] = $this->getNextFilePart($fd, 1, 'header');
2139 if ($all) {
2140 $this->dat['records'] = $this->getNextFilePart($fd, 1, 'records');
2141 $this->dat['files'] = $this->getNextFilePart($fd, 1, 'files');
2142 }
2143 $this->loadInit();
2144 return TRUE;
2145 } else {
2146 $this->error('Error opening file: ' . $filename);
2147 }
2148 fclose($fd);
2149 }
2150 } else {
2151 $this->error('Filename not found: ' . $filename);
2152 }
2153 return FALSE;
2154 }
2155
2156 /**
2157 * Returns the next content part form the fileresource (t3d), $fd
2158 *
2159 * @param pointer $fd File pointer
2160 * @param boolean $unserialize If set, the returned content is unserialized into an array, otherwise you get the raw string
2161 * @param string $name For error messages this indicates the section of the problem.
2162 * @return string Data string
2163 * @access private
2164 * @see loadFile()
2165 * @todo Define visibility
2166 */
2167 public function getNextFilePart($fd, $unserialize = 0, $name = '') {
2168 $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
2169 // Getting header data
2170 $initStr = fread($fd, $initStrLen);
2171 $initStrDat = explode(':', $initStr);
2172 if (strstr($initStrDat[0], 'Warning') == FALSE) {
2173 if (!strcmp($initStrDat[3], '')) {
2174 $datString = fread($fd, intval($initStrDat[2]));
2175 fread($fd, 1);
2176 if (!strcmp(md5($datString), $initStrDat[0])) {
2177 if ($initStrDat[1]) {
2178 if ($this->compress) {
2179 $datString = gzuncompress($datString);
2180 } else {
2181 $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.', 1);
2182 }
2183 }
2184 return $unserialize ? unserialize($datString) : $datString;
2185 } else {
2186 $this->error('MD5 check failed (' . $name . ')');
2187 }
2188 } else {
2189 $this->error('File read error: InitString had a wrong length. (' . $name . ')');
2190 }
2191 } else {
2192 $this->error('File read error: Warning message in file. (' . $initStr . fgets($fd) . ')');
2193 }
2194 }
2195
2196 /**
2197 * Loads T3D file content into the $this->dat array
2198 * (This function can be used to test the output strings from ->compileMemoryToFileContent())
2199 *
2200 * @param string $filecontent File content
2201 * @return void
2202 * @todo Define visibility
2203 */
2204 public function loadContent($filecontent) {
2205 $pointer = 0;
2206 $this->dat['header'] = $this->getNextContentPart($filecontent, $pointer, 1, 'header');
2207 $this->dat['records'] = $this->getNextContentPart($filecontent, $pointer, 1, 'records');
2208 $this->dat['files'] = $this->getNextContentPart($filecontent, $pointer, 1, 'files');
2209 $this->loadInit();
2210 }
2211
2212 /**
2213 * Returns the next content part from the $filecontent
2214 *
2215 * @param string $filecontent File content string
2216 * @param integer $pointer File pointer (where to read from)
2217 * @param boolean $unserialize If set, the returned content is unserialized into an array, otherwise you get the raw string
2218 * @param string $name For error messages this indicates the section of the problem.
2219 * @return string Data string
2220 * @todo Define visibility
2221 */
2222 public function getNextContentPart($filecontent, &$pointer, $unserialize = 0, $name = '') {
2223 $initStrLen = 32 + 1 + 1 + 1 + 10 + 1;
2224 // getting header data
2225 $initStr = substr($filecontent, $pointer, $initStrLen);
2226 $pointer += $initStrLen;
2227 $initStrDat = explode(':', $initStr);
2228 if (!strcmp($initStrDat[3], '')) {
2229 $datString = substr($filecontent, $pointer, intval($initStrDat[2]));
2230 $pointer += intval($initStrDat[2]) + 1;
2231 if (!strcmp(md5($datString), $initStrDat[0])) {
2232 if ($initStrDat[1]) {
2233 if ($this->compress) {
2234 $datString = gzuncompress($datString);
2235 } else {
2236 $this->error('Content read error: This file requires decompression, but this server does not offer gzcompress()/gzuncompress() functions.', 1);
2237 }
2238 }
2239 return $unserialize ? unserialize($datString) : $datString;
2240 } else {
2241 $this->error('MD5 check failed (' . $name . ')');
2242 }
2243 } else {
2244 $this->error('Content read error: InitString had a wrong length. (' . $name . ')');
2245 }
2246 }
2247
2248 /**
2249 * Setting up the object based on the recently loaded ->dat array
2250 *
2251 * @return void
2252 * @todo Define visibility
2253 */
2254 public function loadInit() {
2255 $this->relStaticTables = (array) $this->dat['header']['relStaticTables'];
2256 $this->excludeMap = (array) $this->dat['header']['excludeMap'];
2257 $this->softrefCfg = (array) $this->dat['header']['softrefCfg'];
2258 $this->extensionDependencies = (array) $this->dat['header']['extensionDependencies'];
2259 $this->fixCharsets();
2260 }
2261
2262 /**
2263 * Fix charset of import memory if different from system charset
2264 *
2265 * @return void
2266 * @see loadInit()
2267 * @todo Define visibility
2268 */
2269 public function fixCharsets() {
2270 global $LANG;
2271 $importCharset = $this->dat['header']['charset'];
2272 if ($importCharset) {
2273 if ($importCharset !== $LANG->charSet) {
2274 $this->error('CHARSET: Converting charset of input file (' . $importCharset . ') to the system charset (' . $LANG->charSet . ')');
2275 // Convert meta data:
2276 if (is_array($this->dat['header']['meta'])) {
2277 $LANG->csConvObj->convArray($this->dat['header']['meta'], $importCharset, $LANG->charSet);
2278 }
2279 // Convert record headers:
2280 if (is_array($this->dat['header']['records'])) {
2281 $LANG->csConvObj->convArray($this->dat['header']['records'], $importCharset, $LANG->charSet);
2282 }
2283 // Convert records themselves:
2284 if (is_array($this->dat['records'])) {
2285 $LANG->csConvObj->convArray($this->dat['records'], $importCharset, $LANG->charSet);
2286 }
2287 }
2288 } else {
2289 $this->error('CHARSET: No charset found in import file!');
2290 }
2291 }
2292
2293 /********************************************************
2294 *
2295 * Visual rendering of import/export memory, $this->dat
2296 *
2297 ********************************************************/
2298 /**
2299 * Displays an overview of the header-content.
2300 *
2301 * @return string HTML content
2302 * @todo Define visibility
2303 */
2304 public function displayContentOverview() {
2305 global $LANG;
2306 // Check extension dependencies:
2307 if (is_array($this->dat['header']['extensionDependencies'])) {
2308 foreach ($this->dat['header']['extensionDependencies'] as $extKey) {
2309 if (!\TYPO3\CMS\Core\Extension\ExtensionManager::isLoaded($extKey)) {
2310 $this->error('DEPENDENCY: The extension with key "' . $extKey . '" must be installed!');
2311 }
2312 }
2313 }
2314 // Probably this is done to save memory space?
2315 unset($this->dat['files']);
2316 // Traverse header:
2317 $this->remainHeader = $this->dat['header'];
2318 if (is_array($this->remainHeader)) {
2319 // If there is a page tree set, show that:
2320 if (is_array($this->dat['header']['pagetree'])) {
2321 reset($this->dat['header']['pagetree']);
2322 $lines = array();
2323 $this->traversePageTree($this->dat['header']['pagetree'], $lines);
2324 $rows = array();
2325 $rows[] = '
2326 <tr class="bgColor5 tableheader">
2327 <td>' . $LANG->getLL('impexpcore_displaycon_controls', 1) . '</td>
2328 <td>' . $LANG->getLL('impexpcore_displaycon_title', 1) . '</td>
2329 <td>' . $LANG->getLL('impexpcore_displaycon_size', 1) . '</td>
2330 <td>' . $LANG->getLL('impexpcore_displaycon_message', 1) . '</td>
2331 ' . ($this->update ? '<td>' . $LANG->getLL('impexpcore_displaycon_updateMode', 1) . '</td>' : '') . '
2332 ' . ($this->update ? '<td>' . $LANG->getLL('impexpcore_displaycon_currentPath', 1) . '</td>' : '') . '
2333 ' . ($this->showDiff ? '<td>' . $LANG->getLL('impexpcore_displaycon_result', 1) . '</td>' : '') . '
2334 </tr>';
2335 foreach ($lines as $r) {
2336 $rows[] = '
2337 <tr class="' . $r['class'] . '">
2338 <td>' . $this->renderControls($r) . '</td>
2339 <td nowrap="nowrap">' . $r['preCode'] . $r['title'] . '</td>
2340 <td nowrap="nowrap">' . \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize($r['size']) . '</td>
2341 <td nowrap="nowrap">' . ($r['msg'] && !$this->doesImport ? '<span class="typo3-red">' . htmlspecialchars($r['msg']) . '</span>' : '') . '</td>
2342 ' . ($this->update ? '<td nowrap="nowrap">' . $r['updateMode'] . '</td>' : '') . '
2343 ' . ($this->update ? '<td nowrap="nowrap">' . $r['updatePath'] . '</td>' : '') . '
2344 ' . ($this->showDiff ? '<td>' . $r['showDiffContent'] . '</td>' : '') . '
2345 </tr>';
2346 }
2347 $out = '
2348 <strong>' . $LANG->getLL('impexpcore_displaycon_insidePagetree', 1) . '</strong>
2349 <br /><br />
2350 <table border="0" cellpadding="0" cellspacing="1">' . implode('', $rows) . '</table>
2351 <br /><br />';
2352 }
2353 // Print remaining records that were not contained inside the page tree:
2354 $lines = array();
2355 if (is_array($this->remainHeader['records'])) {
2356 if (is_array($this->remainHeader['records']['pages'])) {
2357 $this->traversePageRecords($this->remainHeader['records']['pages'], $lines);
2358 }
2359 $this->traverseAllRecords($this->remainHeader['records'], $lines);
2360 if (count($lines)) {
2361 $rows = array();
2362 $rows[] = '
2363 <tr class="bgColor5 tableheader">
2364 <td>' . $LANG->getLL('impexpcore_displaycon_controls', 1) . '</td>
2365 <td>' . $LANG->getLL('impexpcore_displaycon_title', 1) . '</td>
2366 <td>' . $LANG->getLL('impexpcore_displaycon_size', 1) . '</td>
2367 <td>' . $LANG->getLL('impexpcore_displaycon_message', 1) . '</td>
2368 ' . ($this->update ? '<td>' . $LANG->getLL('impexpcore_displaycon_updateMode', 1) . '</td>' : '') . '
2369 ' . ($this->update ? '<td>' . $LANG->getLL('impexpcore_displaycon_currentPath', 1) . '</td>' : '') . '
2370 ' . ($this->showDiff ? '<td>' . $LANG->getLL('impexpcore_displaycon_result', 1) . '</td>' : '') . '
2371 </tr>';
2372 foreach ($lines as $r) {
2373 $rows[] = '<tr class="' . $r['class'] . '">
2374 <td>' . $this->renderControls($r) . '</td>
2375 <td nowrap="nowrap">' . $r['preCode'] . $r['title'] . '</td>
2376 <td nowrap="nowrap">' . \TYPO3\CMS\Core\Utility\GeneralUtility::formatSize($r['size']) . '</td>
2377 <td nowrap="nowrap">' . ($r['msg'] && !$this->doesImport ? '<span class="typo3-red">' . htmlspecialchars($r['msg']) . '</span>' : '') . '</td>
2378 ' . ($this->update ? '<td nowrap="nowrap">' . $r['updateMode'] . '</td>' : '') . '
2379 ' . ($this->update ? '<td nowrap="nowrap">' . $r['updatePath'] . '</td>' : '') . '
2380 ' . ($this->showDiff ? '<td>' . $r['showDiffContent'] . '</td>' : '') . '
2381 </tr>';
2382 }
2383 $out .= '
2384 <strong>' . $LANG->getLL('impexpcore_singlereco_outsidePagetree', 1) . '</strong>
2385 <br /><br />
2386 <table border="0" cellpadding="0" cellspacing="1">' . implode('', $rows) . '</table>';
2387 }
2388 }
2389 }
2390 return $out;
2391 }
2392
2393 /**
2394 * Go through page tree for display
2395 *
2396 * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
2397 * @param array $lines Output lines array (is passed by reference and modified)
2398 * @param string $preCode Pre-HTML code
2399 * @return void
2400 * @todo Define visibility
2401 */
2402 public function traversePageTree($pT, &$lines, $preCode = '') {
2403 foreach ($pT as $k => $v) {
2404 // Add this page:
2405 $this->singleRecordLines('pages', $k, $lines, $preCode);
2406 // Subrecords:
2407 if (is_array($this->dat['header']['pid_lookup'][$k])) {
2408 foreach ($this->dat['header']['pid_lookup'][$k] as $t => $recUidArr) {
2409 if ($t != 'pages') {
2410 foreach ($recUidArr as $ruid => $value) {
2411 $this->singleRecordLines($t, $ruid, $lines, $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;');
2412 }
2413 }
2414 }
2415 unset($this->remainHeader['pid_lookup'][$k]);
2416 }
2417 // Subpages, called recursively:
2418 if (is_array($v['subrow'])) {
2419 $this->traversePageTree($v['subrow'], $lines, $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;');
2420 }
2421 }
2422 }
2423
2424 /**
2425 * Go through remaining pages (not in tree)
2426 *
2427 * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
2428 * @param array $lines Output lines array (is passed by reference and modified)
2429 * @return void
2430 * @todo Define visibility
2431 */
2432 public function traversePageRecords($pT, &$lines) {
2433 foreach ($pT as $k => $rHeader) {
2434 $this->singleRecordLines('pages', $k, $lines, '', 1);
2435 // Subrecords:
2436 if (is_array($this->dat['header']['pid_lookup'][$k])) {
2437 foreach ($this->dat['header']['pid_lookup'][$k] as $t => $recUidArr) {
2438 if ($t != 'pages') {
2439 foreach ($recUidArr as $ruid => $value) {
2440 $this->singleRecordLines($t, $ruid, $lines, '&nbsp;&nbsp;&nbsp;&nbsp;');
2441 }
2442 }
2443 }
2444 unset($this->remainHeader['pid_lookup'][$k]);
2445 }
2446 }
2447 }
2448
2449 /**
2450 * Go through ALL records (if the pages are displayed first, those will not be amoung these!)
2451 *
2452 * @param array $pT Page tree array with uid/subrow (from ->dat[header][pagetree]
2453 * @param array $lines Output lines array (is passed by reference and modified)
2454 * @return void
2455 * @todo Define visibility
2456 */
2457 public function traverseAllRecords($pT, &$lines) {
2458 foreach ($pT as $t => $recUidArr) {
2459 if ($t != 'pages') {
2460 $preCode = '';
2461 foreach ($recUidArr as $ruid => $value) {
2462 $this->singleRecordLines($t, $ruid, $lines, $preCode, 1);
2463 }
2464 }
2465 }
2466 }
2467
2468 /**
2469 * Add entries for a single record
2470 *
2471 * @param string $table Table name
2472 * @param integer $uid Record uid
2473 * @param array $lines Output lines array (is passed by reference and modified)
2474 * @param string $preCode Pre-HTML code
2475 * @param boolean $checkImportInPidRecord If you want import validation, you can set this so it checks if the import can take place on the specified page.
2476 * @return void
2477 * @todo Define visibility
2478 */
2479 public function singleRecordLines($table, $uid, &$lines, $preCode, $checkImportInPidRecord = 0) {
2480 global $LANG;
2481 // Get record:
2482 $record = $this->dat['header']['records'][$table][$uid];
2483 unset($this->remainHeader['records'][$table][$uid]);
2484 if (!is_array($record) && !($table === 'pages' && !$uid)) {
2485 $this->error('MISSING RECORD: ' . $table . ':' . $uid, 1);
2486 }
2487 // Begin to create the line arrays information record, pInfo:
2488 $pInfo = array();
2489 $pInfo['ref'] = $table . ':' . $uid;
2490 // Unknown table name:
2491 if ($table === '_SOFTREF_') {
2492 $pInfo['preCode'] = $preCode;
2493 $pInfo['title'] = '<em>' . $GLOBALS['LANG']->getLL('impexpcore_singlereco_softReferencesFiles', 1) . '</em>';
2494 } elseif (!isset($GLOBALS['TCA'][$table])) {
2495 // Unknown table name:
2496 $pInfo['preCode'] = $preCode;
2497 $pInfo['msg'] = 'UNKNOWN TABLE \'' . $pInfo['ref'] . '\'';
2498 $pInfo['title'] = '<em>' . htmlspecialchars($record['title']) . '</em>';
2499 } else {
2500 // Otherwise, set table icon and title.
2501 // Import Validation (triggered by $this->display_import_pid_record) will show messages if import is not possible of various items.
2502 if (is_array($this->display_import_pid_record)) {
2503 if ($checkImportInPidRecord) {
2504 if (!$GLOBALS['BE_USER']->doesUserHaveAccess($this->display_import_pid_record, ($table === 'pages' ? 8 : 16))) {
2505 $pInfo['msg'] .= '\'' . $pInfo['ref'] . '\' cannot be INSERTED on this page! ';
2506 }
2507 if (!$this->checkDokType($table, $this->display_import_pid_record['doktype']) && !$GLOBALS['TCA'][$table]['ctrl']['rootLevel']) {
2508 $pInfo['msg'] .= '\'' . $table . '\' cannot be INSERTED on this page type (change page type to \'Folder\'.) ';
2509 }
2510 }
2511 if (!$GLOBALS['BE_USER']->check('tables_modify', $table)) {
2512 $pInfo['msg'] .= 'You are not allowed to CREATE \'' . $table . '\' tables! ';
2513 }
2514 if ($GLOBALS['TCA'][$table]['ctrl']['readOnly']) {
2515 $pInfo['msg'] .= 'TABLE \'' . $table . '\' is READ ONLY! ';
2516 }
2517 if ($GLOBALS['TCA'][$table]['ctrl']['adminOnly'] && !$GLOBALS['BE_USER']->isAdmin()) {
2518 $pInfo['msg'] .= 'TABLE \'' . $table . '\' is ADMIN ONLY! ';
2519 }
2520 if ($GLOBALS['TCA'][$table]['ctrl']['is_static']) {
2521 $pInfo['msg'] .= 'TABLE \'' . $table . '\' is a STATIC TABLE! ';
2522 }
2523 if ($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) {
2524 $pInfo['msg'] .= 'TABLE \'' . $table . '\' will be inserted on ROOT LEVEL! ';
2525 }
2526 $diffInverse = FALSE;
2527 if ($this->update) {
2528 // In case of update-PREVIEW we swap the diff-sources.
2529 $diffInverse = TRUE;
2530 $recInf = $this->doesRecordExist($table, $uid, $this->showDiff ? '*' : '');
2531 $pInfo['updatePath'] = $recInf ? htmlspecialchars($this->getRecordPath($recInf['pid'])) : '<strong>NEW!</strong>';
2532 // Mode selector:
2533 $optValues = array();
2534 $optValues[] = $recInf ? $LANG->getLL('impexpcore_singlereco_update') : $LANG->getLL('impexpcore_singlereco_insert');
2535 if ($recInf) {
2536 $optValues['as_new'] = $LANG->getLL('impexpcore_singlereco_importAsNew');
2537 }
2538 if ($recInf) {
2539 if (!$this->global_ignore_pid) {
2540 $optValues['ignore_pid'] = $LANG->getLL('impexpcore_singlereco_ignorePid');
2541 } else {
2542 $optValues['respect_pid'] = $LANG->getLL('impexpcore_singlereco_respectPid');
2543 }
2544 }
2545 if (!$recInf && $GLOBALS['BE_USER']->isAdmin()) {
2546 $optValues['force_uid'] = sprintf($LANG->getLL('impexpcore_singlereco_forceUidSAdmin'), $uid);
2547 }
2548 $optValues['exclude'] = $LANG->getLL('impexpcore_singlereco_exclude');
2549 $pInfo['updateMode'] = $this->renderSelectBox('tx_impexp[import_mode][' . $table . ':' . $uid . ']', $this->import_mode[$table . ':' . $uid], $optValues);
2550 }
2551 // Diff vieiw:
2552 if ($this->showDiff) {
2553 // For IMPORTS, get new id:
2554 if ($newUid = $this->import_mapId[$table][$uid]) {
2555 $diffInverse = FALSE;
2556 $recInf = $this->doesRecordExist($table, $newUid, '*');
2557 \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL($table, $recInf);
2558 }
2559 if (is_array($recInf)) {
2560 $pInfo['showDiffContent'] = $this->compareRecords($recInf, $this->dat['records'][$table . ':' . $uid]['data'], $table, $diffInverse);
2561 }
2562 }
2563 }
2564 $pInfo['preCode'] = $preCode . \t3lib_iconworks::getSpriteIconForRecord($table, (array) $this->dat['records'][($table . ':' . $uid)]['data'], array('title' => htmlspecialchars(($table . ':' . $uid))));
2565 $pInfo['title'] = htmlspecialchars($record['title']);
2566 // View page:
2567 if ($table === 'pages') {
2568 $viewID = $this->mode === 'export' ? $uid : ($this->doesImport ? $this->import_mapId['pages'][$uid] : 0);
2569 if ($viewID) {
2570 $pInfo['title'] = '<a href="#" onclick="' . htmlspecialchars(\TYPO3\CMS\Backend\Utility\BackendUtility::viewOnClick($viewID, $GLOBALS['BACK_PATH'])) . 'return false;">' . $pInfo['title'] . '</a>';
2571 }
2572 }
2573 }
2574 $pInfo['class'] = $table == 'pages' ? 'bgColor4-20' : 'bgColor4';
2575 $pInfo['type'] = 'record';
2576 $pInfo['size'] = $record['size'];
2577 $lines[] = $pInfo;
2578 // File relations:
2579 if (is_array($record['filerefs'])) {
2580 $this->addFiles($record['filerefs'], $lines, $preCode);
2581 }
2582 // DB relations
2583 if (is_array($record['rels'])) {
2584 $this->addRelations($record['rels'], $lines, $preCode);
2585 }
2586 // Soft ref
2587 if (count($record['softrefs'])) {
2588 $preCode_A = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;';
2589 $preCode_B = $preCode . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
2590 foreach ($record['softrefs'] as $info) {
2591 $pInfo = array();
2592 $pInfo['preCode'] = $preCode_A . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('status-status-reference-soft');
2593 $pInfo['title'] = '<em>' . $info['field'] . ', "' . $info['spKey'] . '" </em>: <span title="' . htmlspecialchars($info['matchString']) . '">' . htmlspecialchars(\TYPO3\CMS\Core\Utility\GeneralUtility::fixed_lgd_cs($info['matchString'], 60)) . '</span>';
2594 if ($info['subst']['type']) {
2595 if (strlen($info['subst']['title'])) {
2596 $pInfo['title'] .= '<br/>' . $preCode_B . '<strong>' . $LANG->getLL('impexpcore_singlereco_title', 1) . '</strong> ' . htmlspecialchars(\TYPO3\CMS\Core\Utility\GeneralUtility::fixed_lgd_cs($info['subst']['title'], 60));
2597 }
2598 if (strlen($info['subst']['description'])) {
2599 $pInfo['title'] .= '<br/>' . $preCode_B . '<strong>' . $LANG->getLL('impexpcore_singlereco_descr', 1) . '</strong> ' . htmlspecialchars(\TYPO3\CMS\Core\Utility\GeneralUtility::fixed_lgd_cs($info['subst']['description'], 60));
2600 }
2601 $pInfo['title'] .= '<br/>' . $preCode_B . ($info['subst']['type'] == 'file' ? $LANG->getLL('impexpcore_singlereco_filename', 1) . ' <strong>' . $info['subst']['relFileName'] . '</strong>' : '') . ($info['subst']['type'] == 'string' ? $LANG->getLL('impexpcore_singlereco_value', 1) . ' <strong>' . $info['subst']['tokenValue'] . '</strong>' : '') . ($info['subst']['type'] == 'db' ? $LANG->getLL('impexpcore_softrefsel_record', 1) . ' <strong>' . $info['subst']['recordRef'] . '</strong>' : '');
2602 }
2603 $pInfo['ref'] = 'SOFTREF';
2604 $pInfo['size'] = '';
2605 $pInfo['class'] = 'bgColor3';
2606 $pInfo['type'] = 'softref';
2607 $pInfo['_softRefInfo'] = $info;
2608 $pInfo['type'] = 'softref';
2609 if ($info['error'] && !\TYPO3\CMS\Core\Utility\GeneralUtility::inList('editable,exclude', $this->softrefCfg[$info['subst']['tokenID']]['mode'])) {
2610 $pInfo['msg'] .= $info['error'];
2611 }
2612 $lines[] = $pInfo;
2613 // Add relations:
2614 if ($info['subst']['type'] == 'db') {
2615 list($tempTable, $tempUid) = explode(':', $info['subst']['recordRef']);
2616 $this->addRelations(array(array('table' => $tempTable, 'id' => $tempUid, 'tokenID' => $info['subst']['tokenID'])), $lines, $preCode_B, array(), '');
2617 }
2618 // Add files:
2619 if ($info['subst']['type'] == 'file') {
2620 $this->addFiles(array($info['file_ID']), $lines, $preCode_B, '', $info['subst']['tokenID']);
2621 }
2622 }
2623 }
2624 }
2625
2626 /**
2627 * Add DB relations entries for a record's rels-array
2628 *
2629 * @param array $rels Array of relations
2630 * @param array $lines Output lines array (is passed by reference and modified)
2631 * @param string $preCode Pre-HTML code
2632 * @param array $recurCheck Recursivity check stack
2633 * @param string $htmlColorClass Alternative HTML color class to use.
2634 * @return void
2635 * @access private
2636 * @see singleRecordLines()
2637 * @todo Define visibility
2638 */
2639 public function addRelations($rels, &$lines, $preCode, $recurCheck = array(), $htmlColorClass = '') {
2640 foreach ($rels as $dat) {
2641 $table = $dat['table'];
2642 $uid = $dat['id'];
2643 $pInfo = array();
2644 $Iprepend = '';
2645 $staticFixed = FALSE;
2646 $pInfo['ref'] = $table . ':' . $uid;
2647 if (!in_array($pInfo['ref'], $recurCheck)) {
2648 if ($uid > 0) {
2649 $record = $this->dat['header']['records'][$table][$uid];
2650 if (!is_array($record)) {
2651 if ($this->isTableStatic($table) || $this->isExcluded($table, $uid) || $dat['tokenID'] && !$this->includeSoftref($dat['tokenID'])) {
2652 $pInfo['title'] = htmlspecialchars('STATIC: ' . $pInfo['ref']);
2653 $Iprepend = '_static';
2654 $staticFixed = TRUE;
2655 } else {
2656 $doesRE = $this->doesRecordExist($table, $uid);
2657 $lostPath = $this->getRecordPath($table === 'pages' ? $doesRE['uid'] : $doesRE['pid']);
2658 $pInfo['title'] = htmlspecialchars($pInfo['ref']);
2659 $pInfo['title'] = '<span title="' . htmlspecialchars($lostPath) . '">' . $pInfo['title'] . '</span>';
2660 $pInfo['msg'] = 'LOST RELATION' . (!$doesRE ? ' (Record not found!)' : ' (Path: ' . $lostPath . ')');
2661 $Iprepend = '_lost';
2662 }
2663 } else {
2664 $pInfo['title'] = htmlspecialchars($record['title']);
2665 $pInfo['title'] = '<span title="' . htmlspecialchars($this->getRecordPath(($table === 'pages' ? $record['uid'] : $record['pid']))) . '">' . $pInfo['title'] . '</span>';
2666 }
2667 } else {
2668 // Negative values in relation fields. This is typically sys_language fields, fe_users fields etc. They ar