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