[BUGFIX] Fix several typos in php comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / impexp / Classes / Export.php
1 <?php
2 namespace TYPO3\CMS\Impexp;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Core\Environment;
19 use TYPO3\CMS\Core\Database\ReferenceIndex;
20 use TYPO3\CMS\Core\Exception;
21 use TYPO3\CMS\Core\Html\HtmlParser;
22 use TYPO3\CMS\Core\Resource\File;
23 use TYPO3\CMS\Core\Resource\ResourceFactory;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Core\Utility\PathUtility;
26
27 /**
28 * EXAMPLE for using the impexp-class for exporting stuff:
29 *
30 * Create and initialize:
31 * $this->export = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Impexp\ImportExport::class);
32 * $this->export->init();
33 * Set which tables relations we will allow:
34 * $this->export->relOnlyTables[]="tt_news"; // exclusively includes. See comment in the class
35 *
36 * Adding records:
37 * $this->export->export_addRecord("pages", $this->pageinfo);
38 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 38));
39 * $this->export->export_addRecord("pages", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("pages", 39));
40 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 12));
41 * $this->export->export_addRecord("tt_content", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("tt_content", 74));
42 * $this->export->export_addRecord("sys_template", \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord("sys_template", 20));
43 *
44 * Adding all the relations (recursively in 5 levels so relations has THEIR relations registered as well)
45 * for($a=0;$a<5;$a++) {
46 * $addR = $this->export->export_addDBRelations($a);
47 * if (empty($addR)) break;
48 * }
49 *
50 * Finally load all the files.
51 * $this->export->export_addFilesFromRelations(); // MUST be after the DBrelations are set so that file from ALL added records are included!
52 *
53 * Write export
54 * $out = $this->export->compileMemoryToFileContent();
55 * @internal this is not part of TYPO3's Core API.
56 */
57
58 /**
59 * T3D file Export library (TYPO3 Record Document)
60 */
61 class Export extends ImportExport
62 {
63 /**
64 * Set by user: If set, compression in t3d files is disabled
65 *
66 * @var bool
67 */
68 public $dontCompress = false;
69
70 /**
71 * If set, HTML file resources are included.
72 *
73 * @var bool
74 */
75 public $includeExtFileResources = false;
76
77 /**
78 * Files with external media (HTML/css style references inside)
79 *
80 * @var string
81 */
82 public $extFileResourceExtensions = 'html,htm,css';
83
84 /**
85 * Keys are [recordname], values are an array of fields to be included
86 * in the export
87 *
88 * @var array
89 */
90 protected $recordTypesIncludeFields = [];
91
92 /**
93 * Default array of fields to be included in the export
94 *
95 * @var array
96 */
97 protected $defaultRecordIncludeFields = ['uid', 'pid'];
98
99 /**
100 * @var bool
101 */
102 protected $saveFilesOutsideExportFile = false;
103
104 /**
105 * @var string|null
106 */
107 protected $temporaryFilesPathForExport;
108
109 /**************************
110 * Initialize
111 *************************/
112
113 /**
114 * Init the object
115 *
116 * @param bool $dontCompress If set, compression of t3d files is disabled
117 */
118 public function init($dontCompress = false)
119 {
120 parent::init();
121 $this->dontCompress = $dontCompress;
122 $this->mode = 'export';
123 }
124
125 /**************************
126 * Export / Init + Meta Data
127 *************************/
128
129 /**
130 * Set header basics
131 */
132 public function setHeaderBasics()
133 {
134 // Initializing:
135 if (is_array($this->softrefCfg)) {
136 foreach ($this->softrefCfg as $key => $value) {
137 if (!strlen($value['mode'])) {
138 unset($this->softrefCfg[$key]);
139 }
140 }
141 }
142 // Setting in header memory:
143 // Version of file format
144 $this->dat['header']['XMLversion'] = '1.0';
145 // Initialize meta data array (to put it in top of file)
146 $this->dat['header']['meta'] = [];
147 // Add list of tables to consider static
148 $this->dat['header']['relStaticTables'] = $this->relStaticTables;
149 // The list of excluded records
150 $this->dat['header']['excludeMap'] = $this->excludeMap;
151 // Soft Reference mode for elements
152 $this->dat['header']['softrefCfg'] = $this->softrefCfg;
153 // List of extensions the import depends on.
154 $this->dat['header']['extensionDependencies'] = $this->extensionDependencies;
155 $this->dat['header']['charset'] = 'utf-8';
156 }
157
158 /**
159 * Set charset
160 *
161 * @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.
162 */
163 public function setCharset($charset)
164 {
165 $this->dat['header']['charset'] = $charset;
166 }
167
168 /**
169 * Sets meta data
170 *
171 * @param string $title Title of the export
172 * @param string $description Description of the export
173 * @param string $notes Notes about the contents
174 * @param string $packager_username Backend Username of the packager (the guy making the export)
175 * @param string $packager_name Real name of the packager
176 * @param string $packager_email Email of the packager
177 */
178 public function setMetaData($title, $description, $notes, $packager_username, $packager_name, $packager_email)
179 {
180 $this->dat['header']['meta'] = [
181 'title' => $title,
182 'description' => $description,
183 'notes' => $notes,
184 'packager_username' => $packager_username,
185 'packager_name' => $packager_name,
186 'packager_email' => $packager_email,
187 'TYPO3_version' => TYPO3_version,
188 'created' => strftime('%A %e. %B %Y', $GLOBALS['EXEC_TIME'])
189 ];
190 }
191
192 /**
193 * Option to enable having the files not included in the export file.
194 * The files are saved to a temporary folder instead.
195 *
196 * @param bool $saveFilesOutsideExportFile
197 * @see getTemporaryFilesPathForExport()
198 */
199 public function setSaveFilesOutsideExportFile($saveFilesOutsideExportFile)
200 {
201 $this->saveFilesOutsideExportFile = $saveFilesOutsideExportFile;
202 }
203
204 /**************************
205 * Export / Init Page tree
206 *************************/
207
208 /**
209 * Sets the page-tree array in the export header and returns the array in a flattened version
210 *
211 * @param array $idH Hierarchy of ids, the page tree: array([uid] => array("uid" => [uid], "subrow" => array(.....)), [uid] => ....)
212 * @return array The hierarchical page tree converted to a one-dimensional list of pages
213 */
214 public function setPageTree($idH)
215 {
216 $this->dat['header']['pagetree'] = $this->unsetExcludedSections($idH);
217 return $this->flatInversePageTree($this->dat['header']['pagetree']);
218 }
219
220 /**
221 * Removes entries in the page tree which are found in ->excludeMap[]
222 *
223 * @param array $idH Page uid hierarchy
224 * @return array Modified input array
225 * @internal
226 * @see setPageTree()
227 */
228 public function unsetExcludedSections($idH)
229 {
230 if (is_array($idH)) {
231 foreach ($idH as $k => $v) {
232 if ($this->excludeMap['pages:' . $idH[$k]['uid']]) {
233 unset($idH[$k]);
234 } elseif (is_array($idH[$k]['subrow'])) {
235 $idH[$k]['subrow'] = $this->unsetExcludedSections($idH[$k]['subrow']);
236 }
237 }
238 }
239 return $idH;
240 }
241
242 /**************************
243 * Export
244 *************************/
245
246 /**
247 * Sets the fields of record types to be included in the export
248 *
249 * @param array $recordTypesIncludeFields Keys are [recordname], values are an array of fields to be included in the export
250 * @throws Exception if an array value is not type of array
251 */
252 public function setRecordTypesIncludeFields(array $recordTypesIncludeFields)
253 {
254 foreach ($recordTypesIncludeFields as $table => $fields) {
255 if (!is_array($fields)) {
256 throw new Exception('The include fields for record type ' . htmlspecialchars($table) . ' are not defined by an array.', 1391440658);
257 }
258 $this->setRecordTypeIncludeFields($table, $fields);
259 }
260 }
261
262 /**
263 * Sets the fields of a record type to be included in the export
264 *
265 * @param string $table The record type
266 * @param array $fields The fields to be included
267 */
268 public function setRecordTypeIncludeFields($table, array $fields)
269 {
270 $this->recordTypesIncludeFields[$table] = $fields;
271 }
272
273 /**
274 * Adds the record $row from $table.
275 * No checking for relations done here. Pure data.
276 *
277 * @param string $table Table name
278 * @param array $row Record row.
279 * @param int $relationLevel (Internal) if the record is added as a relation, this is set to the "level" it was on.
280 */
281 public function export_addRecord($table, $row, $relationLevel = 0)
282 {
283 BackendUtility::workspaceOL($table, $row);
284 if ($this->excludeDisabledRecords && !$this->isActive($table, $row['uid'])) {
285 return;
286 }
287 if ((string)$table !== '' && is_array($row) && $row['uid'] > 0 && !$this->excludeMap[$table . ':' . $row['uid']]) {
288 if ($this->checkPID($table === 'pages' ? $row['uid'] : $row['pid'])) {
289 if (!isset($this->dat['records'][$table . ':' . $row['uid']])) {
290 // Prepare header info:
291 $row = $this->filterRecordFields($table, $row);
292 $headerInfo = [];
293 $headerInfo['uid'] = $row['uid'];
294 $headerInfo['pid'] = $row['pid'];
295 $headerInfo['title'] = GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $row), 40);
296 if ($relationLevel) {
297 $headerInfo['relationLevel'] = $relationLevel;
298 }
299 // Set the header summary:
300 $this->dat['header']['records'][$table][$row['uid']] = $headerInfo;
301 // Create entry in the PID lookup:
302 $this->dat['header']['pid_lookup'][$row['pid']][$table][$row['uid']] = 1;
303 // Initialize reference index object:
304 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
305 $refIndexObj->enableRuntimeCache();
306 // Yes to workspace overlays for exporting....
307 $refIndexObj->WSOL = true;
308 $relations = $refIndexObj->getRelations($table, $row);
309 $relations = $this->fixFileIDsInRelations($relations);
310 $relations = $this->removeSoftrefsHavingTheSameDatabaseRelation($relations);
311 // Data:
312 $this->dat['records'][$table . ':' . $row['uid']] = [];
313 $this->dat['records'][$table . ':' . $row['uid']]['data'] = $row;
314 $this->dat['records'][$table . ':' . $row['uid']]['rels'] = $relations;
315 // Add information about the relations in the record in the header:
316 $this->dat['header']['records'][$table][$row['uid']]['rels'] = $this->flatDBrels($this->dat['records'][$table . ':' . $row['uid']]['rels']);
317 // Add information about the softrefs to header:
318 $this->dat['header']['records'][$table][$row['uid']]['softrefs'] = $this->flatSoftRefs($this->dat['records'][$table . ':' . $row['uid']]['rels']);
319 } else {
320 $this->error('Record ' . $table . ':' . $row['uid'] . ' already added.');
321 }
322 } else {
323 $this->error('Record ' . $table . ':' . $row['uid'] . ' was outside your DB mounts!');
324 }
325 }
326 }
327
328 /**
329 * This changes the file reference ID from a hash based on the absolute file path
330 * (coming from ReferenceIndex) to a hash based on the relative file path.
331 *
332 * @param array $relations
333 * @return array
334 */
335 protected function fixFileIDsInRelations(array $relations)
336 {
337 foreach ($relations as $field => $relation) {
338 if (isset($relation['type']) && $relation['type'] === 'file') {
339 foreach ($relation['newValueFiles'] as $key => $fileRelationData) {
340 $absoluteFilePath = $fileRelationData['ID_absFile'];
341 if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, Environment::getPublicPath())) {
342 $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
343 $relations[$field]['newValueFiles'][$key]['ID'] = md5($relatedFilePath);
344 }
345 }
346 }
347 if ($relation['type'] === 'flex') {
348 if (is_array($relation['flexFormRels']['file'])) {
349 foreach ($relation['flexFormRels']['file'] as $key => $subList) {
350 foreach ($subList as $subKey => $fileRelationData) {
351 $absoluteFilePath = $fileRelationData['ID_absFile'];
352 if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, Environment::getPublicPath())) {
353 $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
354 $relations[$field]['flexFormRels']['file'][$key][$subKey]['ID'] = md5($relatedFilePath);
355 }
356 }
357 }
358 }
359 }
360 }
361 return $relations;
362 }
363
364 /**
365 * Relations could contain db relations to sys_file records. Some configuration combinations of TCA and
366 * SoftReferenceIndex create also softref relation entries for the identical file. This results
367 * in double included files, one in array "files" and one in array "file_fal".
368 * This function checks the relations for this double inclusions and removes the redundant softref relation.
369 *
370 * @param array $relations
371 * @return array
372 */
373 protected function removeSoftrefsHavingTheSameDatabaseRelation($relations)
374 {
375 $fixedRelations = [];
376 foreach ($relations as $field => $relation) {
377 $newRelation = $relation;
378 if (isset($newRelation['type']) && $newRelation['type'] === 'db') {
379 foreach ($newRelation['itemArray'] as $key => $dbRelationData) {
380 if ($dbRelationData['table'] === 'sys_file') {
381 if (isset($newRelation['softrefs']['keys']['typolink'])) {
382 foreach ($newRelation['softrefs']['keys']['typolink'] as $softrefKey => $softRefData) {
383 if ($softRefData['subst']['type'] === 'file') {
384 $file = ResourceFactory::getInstance()->retrieveFileOrFolderObject($softRefData['subst']['relFileName']);
385 if ($file instanceof File) {
386 if ($file->getUid() == $dbRelationData['id']) {
387 unset($newRelation['softrefs']['keys']['typolink'][$softrefKey]);
388 }
389 }
390 }
391 }
392 if (empty($newRelation['softrefs']['keys']['typolink'])) {
393 unset($newRelation['softrefs']);
394 }
395 }
396 }
397 }
398 }
399 $fixedRelations[$field] = $newRelation;
400 }
401 return $fixedRelations;
402 }
403
404 /**
405 * This analyzes the existing added records, finds all database relations to records and adds these records to the export file.
406 * This function can be called repeatedly until it returns an empty array.
407 * In principle it should not allow to infinite recursivity, but you better set a limit...
408 * Call this BEFORE the ext_addFilesFromRelations (so files from added relations are also included of course)
409 *
410 * @param int $relationLevel Recursion level
411 * @return array overview of relations found and added: Keys [table]:[uid], values array with table and id
412 * @see export_addFilesFromRelations()
413 */
414 public function export_addDBRelations($relationLevel = 0)
415 {
416 // Traverse all "rels" registered for "records"
417 if (!is_array($this->dat['records'])) {
418 $this->error('There were no records available.');
419 return [];
420 }
421 $addR = [];
422 foreach ($this->dat['records'] as $k => $value) {
423 if (!is_array($this->dat['records'][$k])) {
424 continue;
425 }
426 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
427 // For all DB types of relations:
428 if ($vR['type'] === 'db') {
429 foreach ($vR['itemArray'] as $fI) {
430 $this->export_addDBRelations_registerRelation($fI, $addR);
431 }
432 }
433 // For all flex/db types of relations:
434 if ($vR['type'] === 'flex') {
435 // DB relations in flex form fields:
436 if (is_array($vR['flexFormRels']['db'])) {
437 foreach ($vR['flexFormRels']['db'] as $subList) {
438 foreach ($subList as $fI) {
439 $this->export_addDBRelations_registerRelation($fI, $addR);
440 }
441 }
442 }
443 // DB oriented soft references in flex form fields:
444 if (is_array($vR['flexFormRels']['softrefs'])) {
445 foreach ($vR['flexFormRels']['softrefs'] as $subList) {
446 foreach ($subList['keys'] as $spKey => $elements) {
447 foreach ($elements as $el) {
448 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
449 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
450 $fI = [
451 'table' => $tempTable,
452 'id' => $tempUid
453 ];
454 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
455 }
456 }
457 }
458 }
459 }
460 }
461 // In any case, if there are soft refs:
462 if (is_array($vR['softrefs']['keys'])) {
463 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
464 foreach ($elements as $el) {
465 if ($el['subst']['type'] === 'db' && $this->includeSoftref($el['subst']['tokenID'])) {
466 list($tempTable, $tempUid) = explode(':', $el['subst']['recordRef']);
467 $fI = [
468 'table' => $tempTable,
469 'id' => $tempUid
470 ];
471 $this->export_addDBRelations_registerRelation($fI, $addR, $el['subst']['tokenID']);
472 }
473 }
474 }
475 }
476 }
477 }
478
479 // Now, if there were new records to add, do so:
480 if (!empty($addR)) {
481 foreach ($addR as $fI) {
482 // Get and set record:
483 $row = BackendUtility::getRecord($fI['table'], $fI['id']);
484 // Depending on db driver, int fields may or may not be returned as integer or as string. The
485 // loop aligns that detail and forces strings for everything to have exports more db agnostic.
486 foreach ($row as $fieldName => $value) {
487 // Keep null but force everything else to string
488 $row[$fieldName] = $value === null ? $value : (string)$value;
489 }
490
491 if (is_array($row)) {
492 $this->export_addRecord($fI['table'], $row, $relationLevel + 1);
493 }
494 // Set status message
495 // Relation pointers always larger than zero except certain "select" types with
496 // negative values pointing to uids - but that is not supported here.
497 if ($fI['id'] > 0) {
498 $rId = $fI['table'] . ':' . $fI['id'];
499 if (!isset($this->dat['records'][$rId])) {
500 $this->dat['records'][$rId] = 'NOT_FOUND';
501 $this->error('Relation record ' . $rId . ' was not found!');
502 }
503 }
504 }
505 }
506 // Return overview of relations found and added
507 return $addR;
508 }
509
510 /**
511 * Helper function for export_addDBRelations()
512 *
513 * @param array $fI Array with table/id keys to add
514 * @param array $addR Add array, passed by reference to be modified
515 * @param string $tokenID Softref Token ID, if applicable.
516 * @see export_addDBRelations()
517 */
518 public function export_addDBRelations_registerRelation($fI, &$addR, $tokenID = '')
519 {
520 $rId = $fI['table'] . ':' . $fI['id'];
521 if (
522 isset($GLOBALS['TCA'][$fI['table']]) && !$this->isTableStatic($fI['table']) && !$this->isExcluded($fI['table'], $fI['id'])
523 && (!$tokenID || $this->includeSoftref($tokenID)) && $this->inclRelation($fI['table'])
524 ) {
525 if (!isset($this->dat['records'][$rId])) {
526 // Set this record to be included since it is not already.
527 $addR[$rId] = $fI;
528 }
529 }
530 }
531
532 /**
533 * This adds all files in relations.
534 * Call this method AFTER adding all records including relations.
535 *
536 * @see export_addDBRelations()
537 */
538 public function export_addFilesFromRelations()
539 {
540 // Traverse all "rels" registered for "records"
541 if (!is_array($this->dat['records'])) {
542 $this->error('There were no records available.');
543 return;
544 }
545 foreach ($this->dat['records'] as $k => $value) {
546 if (!isset($this->dat['records'][$k]['rels']) || !is_array($this->dat['records'][$k]['rels'])) {
547 continue;
548 }
549 foreach ($this->dat['records'][$k]['rels'] as $fieldname => $vR) {
550 // For all file type relations:
551 if ($vR['type'] === 'file') {
552 foreach ($vR['newValueFiles'] as $key => $fI) {
553 $this->export_addFile($fI, $k, $fieldname);
554 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
555 unset($this->dat['records'][$k]['rels'][$fieldname]['newValueFiles'][$key]['ID_absFile']);
556 }
557 }
558 // For all flex type relations:
559 if ($vR['type'] === 'flex') {
560 if (is_array($vR['flexFormRels']['file'])) {
561 foreach ($vR['flexFormRels']['file'] as $key => $subList) {
562 foreach ($subList as $subKey => $fI) {
563 $this->export_addFile($fI, $k, $fieldname);
564 // Remove the absolute reference to the file so it doesn't expose absolute paths from source server:
565 unset($this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['file'][$key][$subKey]['ID_absFile']);
566 }
567 }
568 }
569 // DB oriented soft references in flex form fields:
570 if (is_array($vR['flexFormRels']['softrefs'])) {
571 foreach ($vR['flexFormRels']['softrefs'] as $key => $subList) {
572 foreach ($subList['keys'] as $spKey => $elements) {
573 foreach ($elements as $subKey => $el) {
574 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
575 // Create abs path and ID for file:
576 $ID_absFile = GeneralUtility::getFileAbsFileName(Environment::getPublicPath() . '/' . $el['subst']['relFileName']);
577 $ID = md5($el['subst']['relFileName']);
578 if ($ID_absFile) {
579 if (!$this->dat['files'][$ID]) {
580 $fI = [
581 'filename' => PathUtility::basename($ID_absFile),
582 'ID_absFile' => $ID_absFile,
583 'ID' => $ID,
584 'relFileName' => $el['subst']['relFileName']
585 ];
586 $this->export_addFile($fI, '_SOFTREF_');
587 }
588 $this->dat['records'][$k]['rels'][$fieldname]['flexFormRels']['softrefs'][$key]['keys'][$spKey][$subKey]['file_ID'] = $ID;
589 }
590 }
591 }
592 }
593 }
594 }
595 }
596 // In any case, if there are soft refs:
597 if (is_array($vR['softrefs']['keys'])) {
598 foreach ($vR['softrefs']['keys'] as $spKey => $elements) {
599 foreach ($elements as $subKey => $el) {
600 if ($el['subst']['type'] === 'file' && $this->includeSoftref($el['subst']['tokenID'])) {
601 // Create abs path and ID for file:
602 $ID_absFile = GeneralUtility::getFileAbsFileName(Environment::getPublicPath() . '/' . $el['subst']['relFileName']);
603 $ID = md5($el['subst']['relFileName']);
604 if ($ID_absFile) {
605 if (!$this->dat['files'][$ID]) {
606 $fI = [
607 'filename' => PathUtility::basename($ID_absFile),
608 'ID_absFile' => $ID_absFile,
609 'ID' => $ID,
610 'relFileName' => $el['subst']['relFileName']
611 ];
612 $this->export_addFile($fI, '_SOFTREF_');
613 }
614 $this->dat['records'][$k]['rels'][$fieldname]['softrefs']['keys'][$spKey][$subKey]['file_ID'] = $ID;
615 }
616 }
617 }
618 }
619 }
620 }
621 }
622 }
623
624 /**
625 * This adds all files from sys_file records
626 */
627 public function export_addFilesFromSysFilesRecords()
628 {
629 if (!isset($this->dat['header']['records']['sys_file']) || !is_array($this->dat['header']['records']['sys_file'])) {
630 return;
631 }
632 foreach ($this->dat['header']['records']['sys_file'] as $sysFileUid => $_) {
633 $recordData = $this->dat['records']['sys_file:' . $sysFileUid]['data'];
634 $file = ResourceFactory::getInstance()->createFileObject($recordData);
635 $this->export_addSysFile($file);
636 }
637 }
638
639 /**
640 * Adds a files content from a sys file record to the export memory
641 *
642 * @param File $file
643 */
644 public function export_addSysFile(File $file)
645 {
646 $fileContent = '';
647 try {
648 if (!$this->saveFilesOutsideExportFile) {
649 $fileContent = $file->getContents();
650 } else {
651 $file->checkActionPermission('read');
652 }
653 } catch (\Exception $e) {
654 $this->error('Error when trying to add file ' . $file->getCombinedIdentifier() . ': ' . $e->getMessage());
655 return;
656 }
657 $fileUid = $file->getUid();
658 $fileSha1 = $file->getStorage()->hashFile($file, 'sha1');
659 if ($fileSha1 !== $file->getProperty('sha1')) {
660 $this->error('File sha1 hash of ' . $file->getCombinedIdentifier() . ' is not up-to-date in index! File added on current sha1.');
661 $this->dat['records']['sys_file:' . $fileUid]['data']['sha1'] = $fileSha1;
662 }
663
664 $fileRec = [];
665 $fileRec['filename'] = $file->getProperty('name');
666 $fileRec['filemtime'] = $file->getProperty('modification_date');
667
668 // build unique id based on the storage and the file identifier
669 $fileId = md5($file->getStorage()->getUid() . ':' . $file->getProperty('identifier_hash'));
670
671 // Setting this data in the header
672 $this->dat['header']['files_fal'][$fileId] = $fileRec;
673
674 if (!$this->saveFilesOutsideExportFile) {
675 // ... and finally add the heavy stuff:
676 $fileRec['content'] = $fileContent;
677 } else {
678 GeneralUtility::upload_copy_move($file->getForLocalProcessing(false), $this->getTemporaryFilesPathForExport() . $file->getProperty('sha1'));
679 }
680 $fileRec['content_sha1'] = $fileSha1;
681
682 $this->dat['files_fal'][$fileId] = $fileRec;
683 }
684
685 /**
686 * Adds a files content to the export memory
687 *
688 * @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!)
689 * @param string $recordRef If the file is related to a record, this is the id on the form [table]:[id]. Information purposes only.
690 * @param string $fieldname If the file is related to a record, this is the field name it was related to. Information purposes only.
691 */
692 public function export_addFile($fI, $recordRef = '', $fieldname = '')
693 {
694 if (!@is_file($fI['ID_absFile'])) {
695 $this->error($fI['ID_absFile'] . ' was not a file! Skipping.');
696 return;
697 }
698 $fileInfo = stat($fI['ID_absFile']);
699 $fileRec = [];
700 $fileRec['filename'] = PathUtility::basename($fI['ID_absFile']);
701 $fileRec['filemtime'] = $fileInfo['mtime'];
702 //for internal type file_reference
703 $fileRec['relFileRef'] = PathUtility::stripPathSitePrefix($fI['ID_absFile']);
704 if ($recordRef) {
705 $fileRec['record_ref'] = $recordRef . '/' . $fieldname;
706 }
707 if ($fI['relFileName']) {
708 $fileRec['relFileName'] = $fI['relFileName'];
709 }
710 // Setting this data in the header
711 $this->dat['header']['files'][$fI['ID']] = $fileRec;
712 // ... and for the recordlisting, why not let us know WHICH relations there was...
713 if ($recordRef && $recordRef !== '_SOFTREF_') {
714 $refParts = explode(':', $recordRef, 2);
715 if (!is_array($this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'])) {
716 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'] = [];
717 }
718 $this->dat['header']['records'][$refParts[0]][$refParts[1]]['filerefs'][] = $fI['ID'];
719 }
720 $fileMd5 = md5_file($fI['ID_absFile']);
721 if (!$this->saveFilesOutsideExportFile) {
722 // ... and finally add the heavy stuff:
723 $fileRec['content'] = file_get_contents($fI['ID_absFile']);
724 } else {
725 GeneralUtility::upload_copy_move($fI['ID_absFile'], $this->getTemporaryFilesPathForExport() . $fileMd5);
726 }
727 $fileRec['content_md5'] = $fileMd5;
728 $this->dat['files'][$fI['ID']] = $fileRec;
729 // For soft references, do further processing:
730 if ($recordRef === '_SOFTREF_') {
731 // Files with external media?
732 // This is only done with files grabbed by a softreference parser since it is deemed improbable that hard-referenced files should undergo this treatment.
733 $html_fI = pathinfo(PathUtility::basename($fI['ID_absFile']));
734 if ($this->includeExtFileResources && GeneralUtility::inList($this->extFileResourceExtensions, strtolower($html_fI['extension']))) {
735 $uniquePrefix = '###' . md5($GLOBALS['EXEC_TIME']) . '###';
736 if (strtolower($html_fI['extension']) === 'css') {
737 $prefixedMedias = explode($uniquePrefix, preg_replace('/(url[[:space:]]*\\([[:space:]]*["\']?)([^"\')]*)(["\']?[[:space:]]*\\))/i', '\\1' . $uniquePrefix . '\\2' . $uniquePrefix . '\\3', $fileRec['content']));
738 } else {
739 // html, htm:
740 $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
741 $prefixedMedias = explode($uniquePrefix, $htmlParser->prefixResourcePath($uniquePrefix, $fileRec['content'], [], $uniquePrefix));
742 }
743 $htmlResourceCaptured = false;
744 foreach ($prefixedMedias as $k => $v) {
745 if ($k % 2) {
746 $EXTres_absPath = GeneralUtility::resolveBackPath(PathUtility::dirname($fI['ID_absFile']) . '/' . $v);
747 $EXTres_absPath = GeneralUtility::getFileAbsFileName($EXTres_absPath);
748 if ($EXTres_absPath && GeneralUtility::isFirstPartOfStr($EXTres_absPath, Environment::getPublicPath() . '/' . $this->fileadminFolderName . '/') && @is_file($EXTres_absPath)) {
749 $htmlResourceCaptured = true;
750 $EXTres_ID = md5($EXTres_absPath);
751 $this->dat['header']['files'][$fI['ID']]['EXT_RES_ID'][] = $EXTres_ID;
752 $prefixedMedias[$k] = '{EXT_RES_ID:' . $EXTres_ID . '}';
753 // Add file to memory if it is not set already:
754 if (!isset($this->dat['header']['files'][$EXTres_ID])) {
755 $fileInfo = stat($EXTres_absPath);
756 $fileRec = [];
757 $fileRec['filename'] = PathUtility::basename($EXTres_absPath);
758 $fileRec['filemtime'] = $fileInfo['mtime'];
759 $fileRec['record_ref'] = '_EXT_PARENT_:' . $fI['ID'];
760 // Media relative to the HTML file.
761 $fileRec['parentRelFileName'] = $v;
762 // Setting this data in the header
763 $this->dat['header']['files'][$EXTres_ID] = $fileRec;
764 // ... and finally add the heavy stuff:
765 $fileRec['content'] = file_get_contents($EXTres_absPath);
766 $fileRec['content_md5'] = md5($fileRec['content']);
767 $this->dat['files'][$EXTres_ID] = $fileRec;
768 }
769 }
770 }
771 }
772 if ($htmlResourceCaptured) {
773 $this->dat['files'][$fI['ID']]['tokenizedContent'] = implode('', $prefixedMedias);
774 }
775 }
776 }
777 }
778
779 /**
780 * If saveFilesOutsideExportFile is enabled, this function returns the path
781 * where the files referenced in the export are copied to.
782 *
783 * @return string
784 * @throws \RuntimeException
785 * @see setSaveFilesOutsideExportFile()
786 */
787 public function getTemporaryFilesPathForExport()
788 {
789 if (!$this->saveFilesOutsideExportFile) {
790 throw new \RuntimeException('You need to set saveFilesOutsideExportFile to TRUE before you want to get the temporary files path for export.', 1401205213);
791 }
792 if ($this->temporaryFilesPathForExport === null) {
793 $temporaryFolderName = $this->getTemporaryFolderName();
794 $this->temporaryFilesPathForExport = $temporaryFolderName . '/';
795 }
796 return $this->temporaryFilesPathForExport;
797 }
798
799 /**
800 * DB relations flattened to 1-dim array.
801 * The list will be unique, no table/uid combination will appear twice.
802 *
803 * @param array $dbrels 2-dim Array of database relations organized by table key
804 * @return array 1-dim array where entries are table:uid and keys are array with table/id
805 */
806 public function flatDBrels($dbrels)
807 {
808 $list = [];
809 foreach ($dbrels as $dat) {
810 if ($dat['type'] === 'db') {
811 foreach ($dat['itemArray'] as $i) {
812 $list[$i['table'] . ':' . $i['id']] = $i;
813 }
814 }
815 if ($dat['type'] === 'flex' && is_array($dat['flexFormRels']['db'])) {
816 foreach ($dat['flexFormRels']['db'] as $subList) {
817 foreach ($subList as $i) {
818 $list[$i['table'] . ':' . $i['id']] = $i;
819 }
820 }
821 }
822 }
823 return $list;
824 }
825
826 /**
827 * Soft References flattened to 1-dim array.
828 *
829 * @param array $dbrels 2-dim Array of database relations organized by table key
830 * @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
831 */
832 public function flatSoftRefs($dbrels)
833 {
834 $list = [];
835 foreach ($dbrels as $field => $dat) {
836 if (is_array($dat['softrefs']['keys'])) {
837 foreach ($dat['softrefs']['keys'] as $spKey => $elements) {
838 if (is_array($elements)) {
839 foreach ($elements as $subKey => $el) {
840 $lKey = $field . ':' . $spKey . ':' . $subKey;
841 $list[$lKey] = array_merge(['field' => $field, 'spKey' => $spKey], $el);
842 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation
843 // changes for the same value in $this->records[...] this will not work anymore!
844 if ($el['subst'] && $el['subst']['relFileName']) {
845 $list[$lKey]['file_ID'] = md5(Environment::getPublicPath() . '/' . $el['subst']['relFileName']);
846 }
847 }
848 }
849 }
850 }
851 if ($dat['type'] === 'flex' && is_array($dat['flexFormRels']['softrefs'])) {
852 foreach ($dat['flexFormRels']['softrefs'] as $structurePath => $subSoftrefs) {
853 if (is_array($subSoftrefs['keys'])) {
854 foreach ($subSoftrefs['keys'] as $spKey => $elements) {
855 foreach ($elements as $subKey => $el) {
856 $lKey = $field . ':' . $structurePath . ':' . $spKey . ':' . $subKey;
857 $list[$lKey] = array_merge(['field' => $field, 'spKey' => $spKey, 'structurePath' => $structurePath], $el);
858 // Add file_ID key to header - slightly "risky" way of doing this because if the calculation
859 // changes for the same value in $this->records[...] this will not work anymore!
860 if ($el['subst'] && $el['subst']['relFileName']) {
861 $list[$lKey]['file_ID'] = md5(Environment::getPublicPath() . '/' . $el['subst']['relFileName']);
862 }
863 }
864 }
865 }
866 }
867 }
868 }
869 return $list;
870 }
871
872 /**
873 * If include fields for a specific record type are set, the data
874 * are filtered out with fields are not included in the fields.
875 *
876 * @param string $table The record type to be filtered
877 * @param array $row The data to be filtered
878 * @return array The filtered record row
879 */
880 protected function filterRecordFields($table, array $row)
881 {
882 if (isset($this->recordTypesIncludeFields[$table])) {
883 $includeFields = array_unique(array_merge(
884 $this->recordTypesIncludeFields[$table],
885 $this->defaultRecordIncludeFields
886 ));
887 $newRow = [];
888 foreach ($row as $key => $value) {
889 if (in_array($key, $includeFields)) {
890 $newRow[$key] = $value;
891 }
892 }
893 } else {
894 $newRow = $row;
895 }
896 return $newRow;
897 }
898
899 /**************************
900 * File Output
901 *************************/
902
903 /**
904 * This compiles and returns the data content for an exported file
905 *
906 * @param string $type Type of output; "xml" gives xml, otherwise serialized array, possibly compressed.
907 * @return string The output file stream
908 */
909 public function compileMemoryToFileContent($type = '')
910 {
911 if ($type === 'xml') {
912 $out = $this->createXML();
913 } else {
914 $compress = $this->doOutputCompress();
915 $out = '';
916 // adding header:
917 $out .= $this->addFilePart(serialize($this->dat['header']), $compress);
918 // adding records:
919 $out .= $this->addFilePart(serialize($this->dat['records']), $compress);
920 // adding files:
921 $out .= $this->addFilePart(serialize($this->dat['files']), $compress);
922 // adding files_fal:
923 $out .= $this->addFilePart(serialize($this->dat['files_fal']), $compress);
924 }
925 return $out;
926 }
927
928 /**
929 * Creates XML string from input array
930 *
931 * @return string XML content
932 */
933 public function createXML()
934 {
935 // Options:
936 $options = [
937 'alt_options' => [
938 '/header' => [
939 'disableTypeAttrib' => true,
940 'clearStackPath' => true,
941 'parentTagMap' => [
942 'files' => 'file',
943 'files_fal' => 'file',
944 'records' => 'table',
945 'table' => 'rec',
946 'rec:rels' => 'relations',
947 'relations' => 'element',
948 'filerefs' => 'file',
949 'pid_lookup' => 'page_contents',
950 'header:relStaticTables' => 'static_tables',
951 'static_tables' => 'tablename',
952 'excludeMap' => 'item',
953 'softrefCfg' => 'softrefExportMode',
954 'extensionDependencies' => 'extkey',
955 'softrefs' => 'softref_element'
956 ],
957 'alt_options' => [
958 '/pagetree' => [
959 'disableTypeAttrib' => true,
960 'useIndexTagForNum' => 'node',
961 'parentTagMap' => [
962 'node:subrow' => 'node'
963 ]
964 ],
965 '/pid_lookup/page_contents' => [
966 'disableTypeAttrib' => true,
967 'parentTagMap' => [
968 'page_contents' => 'table'
969 ],
970 'grandParentTagMap' => [
971 'page_contents/table' => 'item'
972 ]
973 ]
974 ]
975 ],
976 '/records' => [
977 'disableTypeAttrib' => true,
978 'parentTagMap' => [
979 'records' => 'tablerow',
980 'tablerow:data' => 'fieldlist',
981 'tablerow:rels' => 'related',
982 'related' => 'field',
983 'field:itemArray' => 'relations',
984 'field:newValueFiles' => 'filerefs',
985 'field:flexFormRels' => 'flexform',
986 'relations' => 'element',
987 'filerefs' => 'file',
988 'flexform:db' => 'db_relations',
989 'flexform:softrefs' => 'softref_relations',
990 'softref_relations' => 'structurePath',
991 'db_relations' => 'path',
992 'path' => 'element',
993 'keys' => 'softref_key',
994 'softref_key' => 'softref_element'
995 ],
996 'alt_options' => [
997 '/records/tablerow/fieldlist' => [
998 'useIndexTagForAssoc' => 'field'
999 ]
1000 ]
1001 ],
1002 '/files' => [
1003 'disableTypeAttrib' => true,
1004 'parentTagMap' => [
1005 'files' => 'file'
1006 ]
1007 ],
1008 '/files_fal' => [
1009 'disableTypeAttrib' => true,
1010 'parentTagMap' => [
1011 'files_fal' => 'file'
1012 ]
1013 ]
1014 ]
1015 ];
1016 // Creating XML file from $outputArray:
1017 $charset = $this->dat['header']['charset'] ?: 'utf-8';
1018 $XML = '<?xml version="1.0" encoding="' . $charset . '" standalone="yes" ?>' . LF;
1019 $XML .= GeneralUtility::array2xml($this->dat, '', 0, 'T3RecordDocument', 0, $options);
1020 return $XML;
1021 }
1022
1023 /**
1024 * Returns TRUE if the output should be compressed.
1025 *
1026 * @return bool TRUE if compression is possible AND requested.
1027 */
1028 public function doOutputCompress()
1029 {
1030 return $this->compress && !$this->dontCompress;
1031 }
1032
1033 /**
1034 * Returns a content part for a filename being build.
1035 *
1036 * @param array $data Data to store in part
1037 * @param bool $compress Compress file?
1038 * @return string Content stream.
1039 */
1040 public function addFilePart($data, $compress = false)
1041 {
1042 if ($compress) {
1043 $data = gzcompress($data);
1044 }
1045 return md5($data) . ':' . ($compress ? '1' : '0') . ':' . str_pad(strlen($data), 10, '0', STR_PAD_LEFT) . ':' . $data . ':';
1046 }
1047 }