[!!!][TASK] Flex form data structure refactoring
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / ReferenceIndex.php
1 <?php
2 namespace TYPO3\CMS\Core\Database;
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 Doctrine\DBAL\DBALException;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Cache\CacheManager;
20 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
21 use TYPO3\CMS\Core\DataHandling\DataHandler;
22 use TYPO3\CMS\Core\Messaging\FlashMessage;
23 use TYPO3\CMS\Core\Messaging\FlashMessageService;
24 use TYPO3\CMS\Core\Registry;
25 use TYPO3\CMS\Core\Resource\File;
26 use TYPO3\CMS\Core\Resource\Folder;
27 use TYPO3\CMS\Core\Resource\ResourceFactory;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Core\Utility\PathUtility;
30
31 /**
32 * Reference index processing and relation extraction
33 *
34 * NOTICE: When the reference index is updated for an offline version the results may not be correct.
35 * First, lets assumed that the reference update happens in LIVE workspace (ALWAYS update from Live workspace if you analyse whole database!)
36 * Secondly, lets assume that in a Draft workspace you have changed the data structure of a parent page record - this is (in TemplaVoila) inherited by subpages.
37 * When in the LIVE workspace the data structure for the records/pages in the offline workspace will not be evaluated to the right one simply because the data
38 * structure is taken from a rootline traversal and in the Live workspace that will NOT include the changed DataStructure! Thus the evaluation will be based
39 * on the Data Structure set in the Live workspace!
40 * Somehow this scenario is rarely going to happen. Yet, it is an inconsistency and I see now practical way to handle it - other than simply ignoring
41 * maintaining the index for workspace records. Or we can say that the index is precise for all Live elements while glitches might happen in an offline workspace?
42 * Anyway, I just wanted to document this finding - I don't think we can find a solution for it. And its very TemplaVoila specific.
43 */
44 class ReferenceIndex
45 {
46 /**
47 * Definition of tables to exclude from searching for relations
48 *
49 * Only tables which do not contain any relations and never did so far since references also won't be deleted for
50 * these. Since only tables with an entry in $GLOBALS['TCA] are handled by ReferenceIndex there is no need to add
51 * *_mm-tables.
52 *
53 * This is implemented as an array with fields as keys and booleans as values to be able to fast isset() instead of
54 * slow in_array() lookup.
55 *
56 * @var array
57 * @see updateRefIndexTable()
58 * @todo #65461 Create configuration for tables to exclude from ReferenceIndex
59 */
60 protected static $nonRelationTables = [
61 'sys_log' => true,
62 'sys_history' => true,
63 'tx_extensionmanager_domain_model_extension' => true
64 ];
65
66 /**
67 * Definition of fields to exclude from searching for relations
68 *
69 * This is implemented as an array with fields as keys and booleans as values to be able to fast isset() instead of
70 * slow in_array() lookup.
71 *
72 * @var array
73 * @see getRelations()
74 * @see fetchTableRelationFields()
75 * @todo #65460 Create configuration for fields to exclude from ReferenceIndex
76 */
77 protected static $nonRelationFields = [
78 'uid' => true,
79 'perms_userid' => true,
80 'perms_groupid' => true,
81 'perms_user' => true,
82 'perms_group' => true,
83 'perms_everybody' => true,
84 'pid' => true
85 ];
86
87 /**
88 * Fields of tables that could contain relations are cached per table. This is the prefix for the cache entries since
89 * the runtimeCache has a global scope.
90 *
91 * @var string
92 */
93 protected static $cachePrefixTableRelationFields = 'core-refidx-tblRelFields-';
94
95 /**
96 * This array holds the FlexForm references of a record
97 *
98 * @var array
99 * @see getRelations(),FlexFormTools::traverseFlexFormXMLData(),getRelations_flexFormCallBack()
100 */
101 public $temp_flexRelations = [];
102
103 /**
104 * This variable used to indicate whether referencing should take workspace overlays into account
105 * It is not used since commit 0c34dac08605ba from 10.04.2006, the bug is investigated in https://forge.typo3.org/issues/65725
106 *
107 * @var bool
108 * @see getRelations()
109 */
110 public $WSOL = false;
111
112 /**
113 * An index of all found references of a single record created in createEntryData() and accumulated in generateRefIndexData()
114 *
115 * @var array
116 * @see createEntryData(),generateRefIndexData()
117 */
118 public $relations = [];
119
120 /**
121 * Number which we can increase if a change in the code means we will have to force a re-generation of the index.
122 *
123 * @var int
124 * @see updateRefIndexTable()
125 */
126 public $hashVersion = 1;
127
128 /**
129 * Current workspace id
130 *
131 * @var int
132 */
133 protected $workspaceId = 0;
134
135 /**
136 * Runtime Cache to store and retrieve data computed for a single request
137 *
138 * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
139 */
140 protected $runtimeCache = null;
141
142 /**
143 * Constructor
144 */
145 public function __construct()
146 {
147 $this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
148 }
149
150 /**
151 * Sets the current workspace id
152 *
153 * @param int $workspaceId
154 * @see updateIndex()
155 */
156 public function setWorkspaceId($workspaceId)
157 {
158 $this->workspaceId = (int)$workspaceId;
159 }
160
161 /**
162 * Gets the current workspace id
163 *
164 * @return int
165 * @see updateRefIndexTable(),createEntryData()
166 */
167 public function getWorkspaceId()
168 {
169 return $this->workspaceId;
170 }
171
172 /**
173 * Call this function to update the sys_refindex table for a record (even one just deleted)
174 * NOTICE: Currently, references updated for a deleted-flagged record will not include those from within FlexForm
175 * fields in some cases where the data structure is defined by another record since the resolving process ignores
176 * deleted records! This will also result in bad cleaning up in DataHandler I think... Anyway, that's the story of
177 * FlexForms; as long as the DS can change, lots of references can get lost in no time.
178 *
179 * @param string $tableName Table name
180 * @param int $uid UID of record
181 * @param bool $testOnly If set, nothing will be written to the index but the result value will still report statistics on what is added, deleted and kept. Can be used for mere analysis.
182 * @return array Array with statistics about how many index records were added, deleted and not altered plus the complete reference set for the record.
183 */
184 public function updateRefIndexTable($tableName, $uid, $testOnly = false)
185 {
186
187 // First, secure that the index table is not updated with workspace tainted relations:
188 $this->WSOL = false;
189
190 // Init:
191 $result = [
192 'keptNodes' => 0,
193 'deletedNodes' => 0,
194 'addedNodes' => 0
195 ];
196
197 // If this table cannot contain relations, skip it
198 if (isset(static::$nonRelationTables[$tableName])) {
199 return $result;
200 }
201
202 // Fetch tableRelationFields and save them in cache if not there yet
203 $cacheId = static::$cachePrefixTableRelationFields . $tableName;
204 if (!$this->runtimeCache->has($cacheId)) {
205 $tableRelationFields = $this->fetchTableRelationFields($tableName);
206 $this->runtimeCache->set($cacheId, $tableRelationFields);
207 } else {
208 $tableRelationFields = $this->runtimeCache->get($cacheId);
209 }
210
211 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_refindex');
212
213 // Get current index from Database with hash as index using $uidIndexField
214 // no restrictions are needed, since sys_refindex is not a TCA table
215 $queryBuilder = $connection->createQueryBuilder();
216 $queryBuilder->getRestrictions()->removeAll();
217 $queryResult = $queryBuilder->select('*')->from('sys_refindex')->where(
218 $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)),
219 $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
220 $queryBuilder->expr()->eq(
221 'workspace',
222 $queryBuilder->createNamedParameter($this->getWorkspaceId(), \PDO::PARAM_INT)
223 )
224 )->execute();
225 $currentRelations = [];
226 while ($relation = $queryResult->fetch()) {
227 $currentRelations[$relation['hash']] = $currentRelations;
228 }
229
230 // If the table has fields which could contain relations and the record does exist (including deleted-flagged)
231 if ($tableRelationFields !== '' && BackendUtility::getRecordRaw($tableName, 'uid=' . (int)$uid, 'uid')) {
232 // Then, get relations:
233 $relations = $this->generateRefIndexData($tableName, $uid);
234 if (is_array($relations)) {
235 // Traverse the generated index:
236 foreach ($relations as &$relation) {
237 if (!is_array($relation)) {
238 continue;
239 }
240 $relation['hash'] = md5(implode('///', $relation) . '///' . $this->hashVersion);
241 // First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!)
242 if (isset($currentRelations[$relation['hash']])) {
243 unset($currentRelations[$relation['hash']]);
244 $result['keptNodes']++;
245 $relation['_ACTION'] = 'KEPT';
246 } else {
247 // If new, add it:
248 if (!$testOnly) {
249 $connection->insert('sys_refindex', $relation);
250 }
251 $result['addedNodes']++;
252 $relation['_ACTION'] = 'ADDED';
253 }
254 }
255 $result['relations'] = $relations;
256 } else {
257 return $result;
258 }
259 }
260
261 // If any old are left, remove them:
262 if (!empty($currentRelations)) {
263 $hashList = array_keys($currentRelations);
264 if (!empty($hashList)) {
265 $result['deletedNodes'] = count($hashList);
266 $result['deletedNodes_hashList'] = implode(',', $hashList);
267 if (!$testOnly) {
268 $queryBuilder = $connection->createQueryBuilder();
269 $queryBuilder
270 ->delete('sys_refindex')
271 ->where(
272 $queryBuilder->expr()->in(
273 'hash',
274 $queryBuilder->createNamedParameter($hashList, Connection::PARAM_STR_ARRAY)
275 )
276 )
277 ->execute();
278 }
279 }
280 }
281
282 return $result;
283 }
284
285 /**
286 * Returns array of arrays with an index of all references found in record from table/uid
287 * If the result is used to update the sys_refindex table then ->WSOL must NOT be TRUE (no workspace overlay anywhere!)
288 *
289 * @param string $tableName Table name from $GLOBALS['TCA']
290 * @param int $uid Record UID
291 * @return array|NULL Index Rows
292 */
293 public function generateRefIndexData($tableName, $uid)
294 {
295 if (!isset($GLOBALS['TCA'][$tableName])) {
296 return null;
297 }
298
299 $this->relations = [];
300
301 // Fetch tableRelationFields and save them in cache if not there yet
302 $cacheId = static::$cachePrefixTableRelationFields . $tableName;
303 if (!$this->runtimeCache->has($cacheId)) {
304 $tableRelationFields = $this->fetchTableRelationFields($tableName);
305 $this->runtimeCache->set($cacheId, $tableRelationFields);
306 } else {
307 $tableRelationFields = $this->runtimeCache->get($cacheId);
308 }
309
310 // Return if there are no fields which could contain relations
311 if ($tableRelationFields === '') {
312 return $this->relations;
313 }
314
315 $deleteField = $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
316
317 if ($tableRelationFields === '*') {
318 // If one field of a record is of type flex, all fields have to be fetched
319 // to be passed to FlexFormTools->getDataStructureIdentifier()
320 $selectFields = '*';
321 } else {
322 // otherwise only fields that might contain relations are fetched
323 $selectFields = 'uid,' . $tableRelationFields . ($deleteField ? ',' . $deleteField : '');
324 }
325
326 // Get raw record from DB
327 $record = BackendUtility::getRecordRaw($tableName, 'uid=' . (int)$uid, $selectFields);
328 if (!is_array($record)) {
329 return null;
330 }
331
332 // Deleted:
333 $deleted = $deleteField && $record[$deleteField] ? 1 : 0;
334
335 // Get all relations from record:
336 $recordRelations = $this->getRelations($tableName, $record);
337 // Traverse those relations, compile records to insert in table:
338 foreach ($recordRelations as $fieldName => $fieldRelations) {
339 // Based on type
340 switch ((string)$fieldRelations['type']) {
341 case 'db':
342 $this->createEntryData_dbRels($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['itemArray']);
343 break;
344 case 'file_reference':
345 // not used (see getRelations()), but fallback to file
346 case 'file':
347 $this->createEntryData_fileRels($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['newValueFiles']);
348 break;
349 case 'flex':
350 // DB references in FlexForms
351 if (is_array($fieldRelations['flexFormRels']['db'])) {
352 foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) {
353 $this->createEntryData_dbRels($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList);
354 }
355 }
356 // File references in FlexForms
357 // @todo #65463 Test correct handling of file references in FlexForms
358 if (is_array($fieldRelations['flexFormRels']['file'])) {
359 foreach ($fieldRelations['flexFormRels']['file'] as $flexPointer => $subList) {
360 $this->createEntryData_fileRels($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList);
361 }
362 }
363 // Soft references in FlexForms
364 // @todo #65464 Test correct handling of soft references in FlexForms
365 if (is_array($fieldRelations['flexFormRels']['softrefs'])) {
366 foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) {
367 $this->createEntryData_softreferences($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList['keys']);
368 }
369 }
370 break;
371 }
372 // Soft references in the field
373 if (is_array($fieldRelations['softrefs'])) {
374 $this->createEntryData_softreferences($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['softrefs']['keys']);
375 }
376 }
377
378 return $this->relations;
379 }
380
381 /**
382 * Create array with field/value pairs ready to insert in database.
383 * The "hash" field is a fingerprint value across this table.
384 *
385 * @param string $table Tablename of source record (where reference is located)
386 * @param int $uid UID of source record (where reference is located)
387 * @param string $field Fieldname of source record (where reference is located)
388 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
389 * @param int $deleted Whether record is deleted-flagged or not
390 * @param string $ref_table For database references; the tablename the reference points to. Special keyword "_FILE" indicates that "ref_string" is a file reference either absolute or relative to PATH_site. Special keyword "_STRING" indicates some special usage (typ. softreference) where "ref_string" is used for the value.
391 * @param int $ref_uid For database references; The UID of the record (zero "ref_table" is "_FILE" or "_STRING")
392 * @param string $ref_string For "_FILE" or "_STRING" references: The filepath (relative to PATH_site or absolute) or other string.
393 * @param int $sort The sorting order of references if many (the "group" or "select" TCA types). -1 if no sorting order is specified.
394 * @param string $softref_key If the reference is a soft reference, this is the soft reference parser key. Otherwise empty.
395 * @param string $softref_id Soft reference ID for key. Might be useful for replace operations.
396 * @return array Array record to insert into table.
397 */
398 public function createEntryData($table, $uid, $field, $flexPointer, $deleted, $ref_table, $ref_uid, $ref_string = '', $sort = -1, $softref_key = '', $softref_id = '')
399 {
400 if (BackendUtility::isTableWorkspaceEnabled($table)) {
401 $element = BackendUtility::getRecord($table, $uid, 't3ver_wsid');
402 if ($element !== null && isset($element['t3ver_wsid']) && (int)$element['t3ver_wsid'] !== $this->getWorkspaceId()) {
403 //The given Element is ws-enabled but doesn't live in the selected workspace
404 // => don't add index as it's not actually there
405 return false;
406 }
407 }
408 return [
409 'tablename' => $table,
410 'recuid' => $uid,
411 'field' => $field,
412 'flexpointer' => $flexPointer,
413 'softref_key' => $softref_key,
414 'softref_id' => $softref_id,
415 'sorting' => $sort,
416 'deleted' => $deleted,
417 'workspace' => $this->getWorkspaceId(),
418 'ref_table' => $ref_table,
419 'ref_uid' => $ref_uid,
420 'ref_string' => $ref_string
421 ];
422 }
423
424 /**
425 * Enter database references to ->relations array
426 *
427 * @param string $table Tablename of source record (where reference is located)
428 * @param int $uid UID of source record (where reference is located)
429 * @param string $fieldName Fieldname of source record (where reference is located)
430 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
431 * @param int $deleted Whether record is deleted-flagged or not
432 * @param array $items Data array with database relations (table/id)
433 * @return void
434 */
435 public function createEntryData_dbRels($table, $uid, $fieldName, $flexPointer, $deleted, $items)
436 {
437 foreach ($items as $sort => $i) {
438 $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, $i['table'], $i['id'], '', $sort);
439 }
440 }
441
442 /**
443 * Enter file references to ->relations array
444 *
445 * @param string $table Tablename of source record (where reference is located)
446 * @param int $uid UID of source record (where reference is located)
447 * @param string $fieldName Fieldname of source record (where reference is located)
448 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
449 * @param int $deleted Whether record is deleted-flagged or not
450 * @param array $items Data array with file relations
451 * @return void
452 */
453 public function createEntryData_fileRels($table, $uid, $fieldName, $flexPointer, $deleted, $items)
454 {
455 foreach ($items as $sort => $i) {
456 $filePath = $i['ID_absFile'];
457 if (GeneralUtility::isFirstPartOfStr($filePath, PATH_site)) {
458 $filePath = PathUtility::stripPathSitePrefix($filePath);
459 }
460 $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_FILE', 0, $filePath, $sort);
461 }
462 }
463
464 /**
465 * Enter softref references to ->relations array
466 *
467 * @param string $table Tablename of source record (where reference is located)
468 * @param int $uid UID of source record (where reference is located)
469 * @param string $fieldName Fieldname of source record (where reference is located)
470 * @param string $flexPointer Pointer to location inside FlexForm structure
471 * @param int $deleted
472 * @param array $keys Data array with soft reference keys
473 * @return void
474 */
475 public function createEntryData_softreferences($table, $uid, $fieldName, $flexPointer, $deleted, $keys)
476 {
477 if (is_array($keys)) {
478 foreach ($keys as $spKey => $elements) {
479 if (is_array($elements)) {
480 foreach ($elements as $subKey => $el) {
481 if (is_array($el['subst'])) {
482 switch ((string)$el['subst']['type']) {
483 case 'db':
484 list($tableName, $recordId) = explode(':', $el['subst']['recordRef']);
485 $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, $tableName, $recordId, '', -1, $spKey, $subKey);
486 break;
487 case 'file_reference':
488 // not used (see getRelations()), but fallback to file
489 case 'file':
490 $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_FILE', 0, $el['subst']['relFileName'], -1, $spKey, $subKey);
491 break;
492 case 'string':
493 $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_STRING', 0, $el['subst']['tokenValue'], -1, $spKey, $subKey);
494 break;
495 }
496 }
497 }
498 }
499 }
500 }
501 }
502
503 /*******************************
504 *
505 * Get relations from table row
506 *
507 *******************************/
508
509 /**
510 * Returns relation information for a $table/$row-array
511 * Traverses all fields in input row which are configured in TCA/columns
512 * It looks for hard relations to files and records in the TCA types "select" and "group"
513 *
514 * @param string $table Table name
515 * @param array $row Row from table
516 * @param string $onlyField Specific field to fetch for.
517 * @return array Array with information about relations
518 * @see export_addRecord()
519 */
520 public function getRelations($table, $row, $onlyField = '')
521 {
522 // Initialize:
523 $uid = $row['uid'];
524 $outRow = [];
525 foreach ($row as $field => $value) {
526 if (!isset(static::$nonRelationFields[$field]) && is_array($GLOBALS['TCA'][$table]['columns'][$field]) && (!$onlyField || $onlyField === $field)) {
527 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
528 // Add files
529 $resultsFromFiles = $this->getRelations_procFiles($value, $conf, $uid);
530 if (!empty($resultsFromFiles)) {
531 // We have to fill different arrays here depending on the result.
532 // internal_type file is still a relation of type file and
533 // since http://forge.typo3.org/issues/49538 internal_type file_reference
534 // is a database relation to a sys_file record
535 $fileResultsFromFiles = [];
536 $dbResultsFromFiles = [];
537 foreach ($resultsFromFiles as $resultFromFiles) {
538 if (isset($resultFromFiles['table']) && $resultFromFiles['table'] === 'sys_file') {
539 $dbResultsFromFiles[] = $resultFromFiles;
540 } else {
541 // Creates an entry for the field with all the files:
542 $fileResultsFromFiles[] = $resultFromFiles;
543 }
544 }
545 if (!empty($fileResultsFromFiles)) {
546 $outRow[$field] = [
547 'type' => 'file',
548 'newValueFiles' => $fileResultsFromFiles
549 ];
550 }
551 if (!empty($dbResultsFromFiles)) {
552 $outRow[$field] = [
553 'type' => 'db',
554 'itemArray' => $dbResultsFromFiles
555 ];
556 }
557 }
558 // Add a softref definition for link fields if the TCA does not specify one already
559 if ($conf['type'] === 'input' && isset($conf['wizards']['link']) && empty($conf['softref'])) {
560 $conf['softref'] = 'typolink';
561 }
562 // Add DB:
563 $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table, $field);
564 if (!empty($resultsFromDatabase)) {
565 // Create an entry for the field with all DB relations:
566 $outRow[$field] = [
567 'type' => 'db',
568 'itemArray' => $resultsFromDatabase
569 ];
570 }
571 // For "flex" fieldtypes we need to traverse the structure looking for file and db references of course!
572 if ($conf['type'] === 'flex') {
573 // Get current value array:
574 // NOTICE: failure to resolve Data Structures can lead to integrity problems with the reference index. Please look up
575 // the note in the JavaDoc documentation for the function FlexFormTools->getDataStructureIdentifier()
576 $currentValueArray = GeneralUtility::xml2array($value);
577 // Traversing the XML structure, processing files:
578 if (is_array($currentValueArray)) {
579 $this->temp_flexRelations = [
580 'db' => [],
581 'file' => [],
582 'softrefs' => []
583 ];
584 // Create and call iterator object:
585 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
586 $flexFormTools->traverseFlexFormXMLData($table, $field, $row, $this, 'getRelations_flexFormCallBack');
587 // Create an entry for the field:
588 $outRow[$field] = [
589 'type' => 'flex',
590 'flexFormRels' => $this->temp_flexRelations
591 ];
592 }
593 }
594 // Soft References:
595 if ((string)$value !== '') {
596 $softRefValue = $value;
597 $softRefs = BackendUtility::explodeSoftRefParserList($conf['softref']);
598 if ($softRefs !== false) {
599 foreach ($softRefs as $spKey => $spParams) {
600 $softRefObj = BackendUtility::softRefParserObj($spKey);
601 if (is_object($softRefObj)) {
602 $resultArray = $softRefObj->findRef($table, $field, $uid, $softRefValue, $spKey, $spParams);
603 if (is_array($resultArray)) {
604 $outRow[$field]['softrefs']['keys'][$spKey] = $resultArray['elements'];
605 if ((string)$resultArray['content'] !== '') {
606 $softRefValue = $resultArray['content'];
607 }
608 }
609 }
610 }
611 }
612 if (!empty($outRow[$field]['softrefs']) && (string)$value !== (string)$softRefValue && strpos($softRefValue, '{softref:') !== false) {
613 $outRow[$field]['softrefs']['tokenizedContent'] = $softRefValue;
614 }
615 }
616 }
617 }
618 return $outRow;
619 }
620
621 /**
622 * Callback function for traversing the FlexForm structure in relation to finding file and DB references!
623 *
624 * @param array $dsArr Data structure for the current value
625 * @param mixed $dataValue Current value
626 * @param array $PA Additional configuration used in calling function
627 * @param string $structurePath Path of value in DS structure
628 * @param object $parentObject Object reference to caller (unused)
629 * @return void
630 * @see DataHandler::checkValue_flex_procInData_travDS(),FlexFormTools::traverseFlexFormXMLData()
631 */
632 public function getRelations_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $parentObject)
633 {
634 // Removing "data/" in the beginning of path (which points to location in data array)
635 $structurePath = substr($structurePath, 5) . '/';
636 $dsConf = $dsArr['TCEforms']['config'];
637 // Implode parameter values:
638 list($table, $uid, $field) = [
639 $PA['table'],
640 $PA['uid'],
641 $PA['field']
642 ];
643 // Add files
644 $resultsFromFiles = $this->getRelations_procFiles($dataValue, $dsConf, $uid);
645 if (!empty($resultsFromFiles)) {
646 // We have to fill different arrays here depending on the result.
647 // internal_type file is still a relation of type file and
648 // since http://forge.typo3.org/issues/49538 internal_type file_reference
649 // is a database relation to a sys_file record
650 $fileResultsFromFiles = [];
651 $dbResultsFromFiles = [];
652 foreach ($resultsFromFiles as $resultFromFiles) {
653 if (isset($resultFromFiles['table']) && $resultFromFiles['table'] === 'sys_file') {
654 $dbResultsFromFiles[] = $resultFromFiles;
655 } else {
656 $fileResultsFromFiles[] = $resultFromFiles;
657 }
658 }
659 if (!empty($fileResultsFromFiles)) {
660 $this->temp_flexRelations['file'][$structurePath] = $fileResultsFromFiles;
661 }
662 if (!empty($dbResultsFromFiles)) {
663 $this->temp_flexRelations['db'][$structurePath] = $dbResultsFromFiles;
664 }
665 }
666 // Add a softref definition for link fields if the TCA does not specify one already
667 if ($dsConf['type'] === 'input' && isset($dsConf['wizards']['link']) && empty($dsConf['softref'])) {
668 $dsConf['softref'] = 'typolink';
669 }
670 // Add DB:
671 $resultsFromDatabase = $this->getRelations_procDB($dataValue, $dsConf, $uid, $table, $field);
672 if (!empty($resultsFromDatabase)) {
673 // Create an entry for the field with all DB relations:
674 $this->temp_flexRelations['db'][$structurePath] = $resultsFromDatabase;
675 }
676 // Soft References:
677 if (is_array($dataValue) || (string)$dataValue !== '') {
678 $softRefValue = $dataValue;
679 $softRefs = BackendUtility::explodeSoftRefParserList($dsConf['softref']);
680 if ($softRefs !== false) {
681 foreach ($softRefs as $spKey => $spParams) {
682 $softRefObj = BackendUtility::softRefParserObj($spKey);
683 if (is_object($softRefObj)) {
684 $resultArray = $softRefObj->findRef($table, $field, $uid, $softRefValue, $spKey, $spParams, $structurePath);
685 if (is_array($resultArray) && is_array($resultArray['elements'])) {
686 $this->temp_flexRelations['softrefs'][$structurePath]['keys'][$spKey] = $resultArray['elements'];
687 if ((string)$resultArray['content'] !== '') {
688 $softRefValue = $resultArray['content'];
689 }
690 }
691 }
692 }
693 }
694 if (!empty($this->temp_flexRelations['softrefs']) && (string)$dataValue !== (string)$softRefValue) {
695 $this->temp_flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $softRefValue;
696 }
697 }
698 }
699
700 /**
701 * Check field configuration if it is a file relation field and extract file relations if any
702 *
703 * @param string $value Field value
704 * @param array $conf Field configuration array of type "TCA/columns
705 * @param int $uid Field uid
706 * @return bool|array If field type is OK it will return an array with the files inside. Else FALSE
707 */
708 public function getRelations_procFiles($value, $conf, $uid)
709 {
710 if ($conf['type'] !== 'group' || ($conf['internal_type'] !== 'file' && $conf['internal_type'] !== 'file_reference')) {
711 return false;
712 }
713
714 // Collect file values in array:
715 if ($conf['MM']) {
716 $theFileValues = [];
717 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
718 $dbAnalysis->start('', 'files', $conf['MM'], $uid);
719 foreach ($dbAnalysis->itemArray as $someval) {
720 if ($someval['id']) {
721 $theFileValues[] = $someval['id'];
722 }
723 }
724 } else {
725 $theFileValues = explode(',', $value);
726 }
727 // Traverse the files and add them:
728 $uploadFolder = $conf['internal_type'] === 'file' ? $conf['uploadfolder'] : '';
729 $destinationFolder = $this->destPathFromUploadFolder($uploadFolder);
730 $newValueFiles = [];
731 foreach ($theFileValues as $file) {
732 if (trim($file)) {
733 $realFile = $destinationFolder . '/' . trim($file);
734 $newValueFile = [
735 'filename' => basename($file),
736 'ID' => md5($realFile),
737 'ID_absFile' => $realFile
738 ];
739 // Set sys_file and id for referenced files
740 if ($conf['internal_type'] === 'file_reference') {
741 try {
742 $file = ResourceFactory::getInstance()->retrieveFileOrFolderObject($file);
743 if ($file instanceof File || $file instanceof Folder) {
744 // For setting this as sys_file relation later, the keys filename, ID and ID_absFile
745 // have not to be included, because the are not evaluated for db relations.
746 $newValueFile = [
747 'table' => 'sys_file',
748 'id' => $file->getUid()
749 ];
750 }
751 } catch (\Exception $e) {
752 }
753 }
754 $newValueFiles[] = $newValueFile;
755 }
756 }
757 return $newValueFiles;
758 }
759
760 /**
761 * Check field configuration if it is a DB relation field and extract DB relations if any
762 *
763 * @param string $value Field value
764 * @param array $conf Field configuration array of type "TCA/columns
765 * @param int $uid Field uid
766 * @param string $table Table name
767 * @param string $field Field name
768 * @return array If field type is OK it will return an array with the database relations. Else FALSE
769 */
770 public function getRelations_procDB($value, $conf, $uid, $table = '', $field = '')
771 {
772 // Get IRRE relations
773 if (empty($conf)) {
774 return false;
775 } elseif ($conf['type'] === 'inline' && !empty($conf['foreign_table']) && empty($conf['MM'])) {
776 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
777 $dbAnalysis->setUseLiveReferenceIds(false);
778 $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
779 return $dbAnalysis->itemArray;
780 // DB record lists:
781 } elseif ($this->isDbReferenceField($conf)) {
782 $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
783 if ($conf['MM_opposite_field']) {
784 return [];
785 }
786 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
787 $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
788 return $dbAnalysis->itemArray;
789 }
790 return false;
791 }
792
793 /*******************************
794 *
795 * Setting values
796 *
797 *******************************/
798
799 /**
800 * Setting the value of a reference or removing it completely.
801 * Usage: For lowlevel clean up operations!
802 * WARNING: With this you can set values that are not allowed in the database since it will bypass all checks for validity!
803 * Hence it is targeted at clean-up operations. Please use DataHandler in the usual ways if you wish to manipulate references.
804 * Since this interface allows updates to soft reference values (which DataHandler does not directly) you may like to use it
805 * for that as an exception to the warning above.
806 * Notice; If you want to remove multiple references from the same field, you MUST start with the one having the highest
807 * sorting number. If you don't the removal of a reference with a lower number will recreate an index in which the remaining
808 * references in that field has new hash-keys due to new sorting numbers - and you will get errors for the remaining operations
809 * which cannot find the hash you feed it!
810 * To ensure proper working only admin-BE_USERS in live workspace should use this function
811 *
812 * @param string $hash 32-byte hash string identifying the record from sys_refindex which you wish to change the value for
813 * @param mixed $newValue Value you wish to set for reference. If NULL, the reference is removed (unless a soft-reference in which case it can only be set to a blank string). If you wish to set a database reference, use the format "[table]:[uid]". Any other case, the input value is set as-is
814 * @param bool $returnDataArray Return $dataArray only, do not submit it to database.
815 * @param bool $bypassWorkspaceAdminCheck If set, it will bypass check for workspace-zero and admin user
816 * @return string|bool|array FALSE (=OK), error message string or array (if $returnDataArray is set!)
817 */
818 public function setReferenceValue($hash, $newValue, $returnDataArray = false, $bypassWorkspaceAdminCheck = false)
819 {
820 $backendUser = $this->getBackendUser();
821 if ($backendUser->workspace === 0 && $backendUser->isAdmin() || $bypassWorkspaceAdminCheck) {
822 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
823 $queryBuilder->getRestrictions()->removeAll();
824
825 // Get current index from Database
826 $referenceRecord = $queryBuilder
827 ->select('*')
828 ->from('sys_refindex')
829 ->where(
830 $queryBuilder->expr()->eq('hash', $queryBuilder->createNamedParameter($hash, \PDO::PARAM_STR))
831 )
832 ->setMaxResults(1)
833 ->execute()
834 ->fetch();
835
836 // Check if reference existed.
837 if (!is_array($referenceRecord)) {
838 return 'ERROR: No reference record with hash="' . $hash . '" was found!';
839 }
840
841 if (empty($GLOBALS['TCA'][$referenceRecord['tablename']])) {
842 return 'ERROR: Table "' . $referenceRecord['tablename'] . '" was not in TCA!';
843 }
844
845 // Get that record from database
846 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
847 ->getQueryBuilderForTable($referenceRecord['tablename']);
848 $queryBuilder->getRestrictions()->removeAll();
849 $record = $queryBuilder
850 ->select('*')
851 ->from($referenceRecord['tablename'])
852 ->where(
853 $queryBuilder->expr()->eq(
854 'uid',
855 $queryBuilder->createNamedParameter($referenceRecord['recuid'], \PDO::PARAM_INT)
856 )
857 )
858 ->setMaxResults(1)
859 ->execute()
860 ->fetch();
861
862 if (is_array($record)) {
863 // Get relation for single field from record
864 $recordRelations = $this->getRelations($referenceRecord['tablename'], $record, $referenceRecord['field']);
865 if ($fieldRelation = $recordRelations[$referenceRecord['field']]) {
866 // Initialize data array that is to be sent to DataHandler afterwards:
867 $dataArray = [];
868 // Based on type
869 switch ((string)$fieldRelation['type']) {
870 case 'db':
871 $error = $this->setReferenceValue_dbRels($referenceRecord, $fieldRelation['itemArray'], $newValue, $dataArray);
872 if ($error) {
873 return $error;
874 }
875 break;
876 case 'file_reference':
877 // not used (see getRelations()), but fallback to file
878 case 'file':
879 $error = $this->setReferenceValue_fileRels($referenceRecord, $fieldRelation['newValueFiles'], $newValue, $dataArray);
880 if ($error) {
881 return $error;
882 }
883 break;
884 case 'flex':
885 // DB references in FlexForms
886 if (is_array($fieldRelation['flexFormRels']['db'][$referenceRecord['flexpointer']])) {
887 $error = $this->setReferenceValue_dbRels($referenceRecord, $fieldRelation['flexFormRels']['db'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']);
888 if ($error) {
889 return $error;
890 }
891 }
892 // File references in FlexForms
893 if (is_array($fieldRelation['flexFormRels']['file'][$referenceRecord['flexpointer']])) {
894 $error = $this->setReferenceValue_fileRels($referenceRecord, $fieldRelation['flexFormRels']['file'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']);
895 if ($error) {
896 return $error;
897 }
898 }
899 // Soft references in FlexForms
900 if ($referenceRecord['softref_key'] && is_array($fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']]['keys'][$referenceRecord['softref_key']])) {
901 $error = $this->setReferenceValue_softreferences($referenceRecord, $fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']);
902 if ($error) {
903 return $error;
904 }
905 }
906 break;
907 }
908 // Soft references in the field:
909 if ($referenceRecord['softref_key'] && is_array($fieldRelation['softrefs']['keys'][$referenceRecord['softref_key']])) {
910 $error = $this->setReferenceValue_softreferences($referenceRecord, $fieldRelation['softrefs'], $newValue, $dataArray);
911 if ($error) {
912 return $error;
913 }
914 }
915 // Data Array, now ready to be sent to DataHandler
916 if ($returnDataArray) {
917 return $dataArray;
918 } else {
919 // Execute CMD array:
920 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
921 $dataHandler->dontProcessTransformations = true;
922 $dataHandler->bypassWorkspaceRestrictions = true;
923 $dataHandler->bypassFileHandling = true;
924 // Otherwise this cannot update things in deleted records...
925 $dataHandler->bypassAccessCheckForRecords = true;
926 // Check has been done previously that there is a backend user which is Admin and also in live workspace
927 $dataHandler->start($dataArray, []);
928 $dataHandler->process_datamap();
929 // Return errors if any:
930 if (!empty($dataHandler->errorLog)) {
931 return LF . 'DataHandler:' . implode((LF . 'DataHandler:'), $dataHandler->errorLog);
932 }
933 }
934 }
935 }
936 } else {
937 return 'ERROR: BE_USER object is not admin OR not in workspace 0 (Live)';
938 }
939
940 return false;
941 }
942
943 /**
944 * Setting a value for a reference for a DB field:
945 *
946 * @param array $refRec sys_refindex record
947 * @param array $itemArray Array of references from that field
948 * @param string $newValue Value to substitute current value with (or NULL to unset it)
949 * @param array $dataArray Data array in which the new value is set (passed by reference)
950 * @param string $flexPointer Flexform pointer, if in a flex form field.
951 * @return string Error message if any, otherwise FALSE = OK
952 */
953 public function setReferenceValue_dbRels($refRec, $itemArray, $newValue, &$dataArray, $flexPointer = '')
954 {
955 if ((int)$itemArray[$refRec['sorting']]['id'] === (int)$refRec['ref_uid'] && (string)$itemArray[$refRec['sorting']]['table'] === (string)$refRec['ref_table']) {
956 // Setting or removing value:
957 // Remove value:
958 if ($newValue === null) {
959 unset($itemArray[$refRec['sorting']]);
960 } else {
961 list($itemArray[$refRec['sorting']]['table'], $itemArray[$refRec['sorting']]['id']) = explode(':', $newValue);
962 }
963 // Traverse and compile new list of records:
964 $saveValue = [];
965 foreach ($itemArray as $pair) {
966 $saveValue[] = $pair['table'] . '_' . $pair['id'];
967 }
968 // Set in data array:
969 if ($flexPointer) {
970 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
971 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = [];
972 $flexFormTools->setArrayValueByPath(substr($flexPointer, 0, -1), $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'], implode(',', $saveValue));
973 } else {
974 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = implode(',', $saveValue);
975 }
976 } else {
977 return 'ERROR: table:id pair "' . $refRec['ref_table'] . ':' . $refRec['ref_uid'] . '" did not match that of the record ("' . $itemArray[$refRec['sorting']]['table'] . ':' . $itemArray[$refRec['sorting']]['id'] . '") in sorting index "' . $refRec['sorting'] . '"';
978 }
979
980 return false;
981 }
982
983 /**
984 * Setting a value for a reference for a FILE field:
985 *
986 * @param array $refRec sys_refindex record
987 * @param array $itemArray Array of references from that field
988 * @param string $newValue Value to substitute current value with (or NULL to unset it)
989 * @param array $dataArray Data array in which the new value is set (passed by reference)
990 * @param string $flexPointer Flexform pointer, if in a flex form field.
991 * @return string Error message if any, otherwise FALSE = OK
992 */
993 public function setReferenceValue_fileRels($refRec, $itemArray, $newValue, &$dataArray, $flexPointer = '')
994 {
995 $ID_absFile = PathUtility::stripPathSitePrefix($itemArray[$refRec['sorting']]['ID_absFile']);
996 if ($ID_absFile === (string)$refRec['ref_string'] && $refRec['ref_table'] === '_FILE') {
997 // Setting or removing value:
998 // Remove value:
999 if ($newValue === null) {
1000 unset($itemArray[$refRec['sorting']]);
1001 } else {
1002 $itemArray[$refRec['sorting']]['filename'] = $newValue;
1003 }
1004 // Traverse and compile new list of records:
1005 $saveValue = [];
1006 foreach ($itemArray as $fileInfo) {
1007 $saveValue[] = $fileInfo['filename'];
1008 }
1009 // Set in data array:
1010 if ($flexPointer) {
1011 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1012 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = [];
1013 $flexFormTools->setArrayValueByPath(substr($flexPointer, 0, -1), $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'], implode(',', $saveValue));
1014 } else {
1015 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = implode(',', $saveValue);
1016 }
1017 } else {
1018 return 'ERROR: either "' . $refRec['ref_table'] . '" was not "_FILE" or file PATH_site+"' . $refRec['ref_string'] . '" did not match that of the record ("' . $itemArray[$refRec['sorting']]['ID_absFile'] . '") in sorting index "' . $refRec['sorting'] . '"';
1019 }
1020
1021 return false;
1022 }
1023
1024 /**
1025 * Setting a value for a soft reference token
1026 *
1027 * @param array $refRec sys_refindex record
1028 * @param array $softref Array of soft reference occurencies
1029 * @param string $newValue Value to substitute current value with
1030 * @param array $dataArray Data array in which the new value is set (passed by reference)
1031 * @param string $flexPointer Flexform pointer, if in a flex form field.
1032 * @return string Error message if any, otherwise FALSE = OK
1033 */
1034 public function setReferenceValue_softreferences($refRec, $softref, $newValue, &$dataArray, $flexPointer = '')
1035 {
1036 if (!is_array($softref['keys'][$refRec['softref_key']][$refRec['softref_id']])) {
1037 return 'ERROR: Soft reference parser key "' . $refRec['softref_key'] . '" or the index "' . $refRec['softref_id'] . '" was not found.';
1038 }
1039
1040 // Set new value:
1041 $softref['keys'][$refRec['softref_key']][$refRec['softref_id']]['subst']['tokenValue'] = '' . $newValue;
1042 // Traverse softreferences and replace in tokenized content to rebuild it with new value inside:
1043 foreach ($softref['keys'] as $sfIndexes) {
1044 foreach ($sfIndexes as $data) {
1045 $softref['tokenizedContent'] = str_replace('{softref:' . $data['subst']['tokenID'] . '}', $data['subst']['tokenValue'], $softref['tokenizedContent']);
1046 }
1047 }
1048 // Set in data array:
1049 if (!strstr($softref['tokenizedContent'], '{softref:')) {
1050 if ($flexPointer) {
1051 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
1052 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = [];
1053 $flexFormTools->setArrayValueByPath(substr($flexPointer, 0, -1), $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'], $softref['tokenizedContent']);
1054 } else {
1055 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = $softref['tokenizedContent'];
1056 }
1057 } else {
1058 return 'ERROR: After substituting all found soft references there were still soft reference tokens in the text. (theoretically this does not have to be an error if the string "{softref:" happens to be in the field for another reason.)';
1059 }
1060
1061 return false;
1062 }
1063
1064 /*******************************
1065 *
1066 * Helper functions
1067 *
1068 *******************************/
1069
1070 /**
1071 * Returns TRUE if the TCA/columns field type is a DB reference field
1072 *
1073 * @param array $configuration Config array for TCA/columns field
1074 * @return bool TRUE if DB reference field (group/db or select with foreign-table)
1075 */
1076 protected function isDbReferenceField(array $configuration)
1077 {
1078 return
1079 ($configuration['type'] === 'group' && $configuration['internal_type'] === 'db')
1080 || (
1081 ($configuration['type'] === 'select' || $configuration['type'] === 'inline')
1082 && !empty($configuration['foreign_table'])
1083 )
1084 ;
1085 }
1086
1087 /**
1088 * Returns TRUE if the TCA/columns field type is a reference field
1089 *
1090 * @param array $configuration Config array for TCA/columns field
1091 * @return bool TRUE if reference field
1092 */
1093 public function isReferenceField(array $configuration)
1094 {
1095 return
1096 $this->isDbReferenceField($configuration)
1097 ||
1098 ($configuration['type'] === 'group' && ($configuration['internal_type'] === 'file' || $configuration['internal_type'] === 'file_reference')) // getRelations_procFiles
1099 ||
1100 ($configuration['type'] === 'input' && isset($configuration['wizards']['link'])) // getRelations_procDB
1101 ||
1102 $configuration['type'] === 'flex'
1103 ||
1104 isset($configuration['softref'])
1105 ;
1106 }
1107
1108 /**
1109 * Returns all fields of a table which could contain a relation
1110 *
1111 * @param string $tableName Name of the table
1112 * @return string Fields which could contain a relation
1113 */
1114 protected function fetchTableRelationFields($tableName)
1115 {
1116 if (!isset($GLOBALS['TCA'][$tableName]['columns'])) {
1117 return '';
1118 }
1119
1120 $fields = [];
1121
1122 foreach ($GLOBALS['TCA'][$tableName]['columns'] as $field => $fieldDefinition) {
1123 if (is_array($fieldDefinition['config'])) {
1124 // Check for flex field
1125 if (isset($fieldDefinition['config']['type']) && $fieldDefinition['config']['type'] === 'flex') {
1126 // Fetch all fields if the is a field of type flex in the table definition because the complete row is passed to
1127 // FlexFormTools->getDataStructureIdentifier() in the end and might be needed in ds_pointerField or a hook
1128 return '*';
1129 }
1130 // Only fetch this field if it can contain a reference
1131 if ($this->isReferenceField($fieldDefinition['config'])) {
1132 $fields[] = $field;
1133 }
1134 }
1135 }
1136
1137 return implode(',', $fields);
1138 }
1139
1140 /**
1141 * Returns destination path to an upload folder given by $folder
1142 *
1143 * @param string $folder Folder relative to PATH_site
1144 * @return string Input folder prefixed with PATH_site. No checking for existence is done. Output must be a folder without trailing slash.
1145 */
1146 public function destPathFromUploadFolder($folder)
1147 {
1148 if (!$folder) {
1149 return substr(PATH_site, 0, -1);
1150 }
1151 return PATH_site . $folder;
1152 }
1153
1154 /**
1155 * Updating Index (External API)
1156 *
1157 * @param bool $testOnly If set, only a test
1158 * @param bool $cli_echo If set, output CLI status
1159 * @return array Header and body status content
1160 */
1161 public function updateIndex($testOnly, $cli_echo = false)
1162 {
1163 $errors = [];
1164 $tableNames = [];
1165 $recCount = 0;
1166 $tableCount = 0;
1167 $headerContent = $testOnly ? 'Reference Index being TESTED (nothing written, remove the "--check" argument)' : 'Reference Index being Updated';
1168 if ($cli_echo) {
1169 echo '*******************************************' . LF . $headerContent . LF . '*******************************************' . LF;
1170 }
1171 // Traverse all tables:
1172 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1173 foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
1174 if (isset(static::$nonRelationTables[$tableName])) {
1175 continue;
1176 }
1177 $fields = ['uid'];
1178 if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
1179 $fields[] = 't3ver_wsid';
1180 }
1181 // Traverse all records in tables, including deleted records
1182 $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
1183 $queryBuilder->getRestrictions()->removeAll();
1184 try {
1185 $queryResult = $queryBuilder
1186 ->select(...$fields)
1187 ->from($tableName)
1188 ->execute();
1189 } catch (DBALException $e) {
1190 // Table exists in $TCA but does not exist in the database
1191 // @todo: improve / change message and add actual sql error?
1192 GeneralUtility::sysLog(sprintf('Table "%s" exists in $TCA but does not exist in the database. You should run the Database Analyzer in the Install Tool to fix this.', $tableName), 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
1193 continue;
1194 }
1195
1196 $tableNames[] = $tableName;
1197 $tableCount++;
1198 $uidList = [0];
1199 while ($record = $queryResult->fetch()) {
1200 $refIndexObj = GeneralUtility::makeInstance(self::class);
1201 if (isset($record['t3ver_wsid'])) {
1202 $refIndexObj->setWorkspaceId($record['t3ver_wsid']);
1203 }
1204 $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly);
1205 $uidList[] = $record['uid'];
1206 $recCount++;
1207 if ($result['addedNodes'] || $result['deletedNodes']) {
1208 $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
1209 $errors[] = $error;
1210 if ($cli_echo) {
1211 echo $error . LF;
1212 }
1213 }
1214 }
1215
1216 // Searching for lost indexes for this table
1217 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex');
1218 $queryBuilder->getRestrictions()->removeAll();
1219 $lostIndexes = $queryBuilder
1220 ->count('hash')
1221 ->from('sys_refindex')
1222 ->where(
1223 $queryBuilder->expr()->eq(
1224 'tablename',
1225 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
1226 ),
1227 $queryBuilder->expr()->notIn(
1228 'recuid',
1229 $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
1230 )
1231 )
1232 ->execute()
1233 ->fetchColumn(0);
1234
1235 if ($lostIndexes > 0) {
1236 $error = 'Table ' . $tableName . ' has ' . $lostIndexes . ' lost indexes which are now deleted';
1237 $errors[] = $error;
1238 if ($cli_echo) {
1239 echo $error . LF;
1240 }
1241 if (!$testOnly) {
1242 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex');
1243 $queryBuilder->delete('sys_refindex')
1244 ->where(
1245 $queryBuilder->expr()->eq(
1246 'tablename',
1247 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
1248 ),
1249 $queryBuilder->expr()->notIn(
1250 'recuid',
1251 $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
1252 )
1253 )->execute();
1254 }
1255 }
1256 }
1257
1258 // Searching lost indexes for non-existing tables
1259 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex');
1260 $queryBuilder->getRestrictions()->removeAll();
1261 $lostTables = $queryBuilder
1262 ->count('hash')
1263 ->from('sys_refindex')
1264 ->where(
1265 $queryBuilder->expr()->notIn(
1266 'tablename',
1267 $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY)
1268 )
1269 )->execute()
1270 ->fetchColumn(0);
1271
1272 if ($lostTables > 0) {
1273 $error = 'Index table hosted ' . $lostTables . ' indexes for non-existing tables, now removed';
1274 $errors[] = $error;
1275 if ($cli_echo) {
1276 echo $error . LF;
1277 }
1278 if (!$testOnly) {
1279 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex');
1280 $queryBuilder->delete('sys_refindex')
1281 ->where(
1282 $queryBuilder->expr()->notIn(
1283 'tablename',
1284 $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY)
1285 )
1286 )->execute();
1287 }
1288 }
1289 $errorCount = count($errors);
1290 $recordsCheckedString = $recCount . ' records from ' . $tableCount . ' tables were checked/updated.' . LF;
1291 $flashMessage = GeneralUtility::makeInstance(
1292 FlashMessage::class,
1293 $errorCount ? implode('##LF##', $errors) : 'Index Integrity was perfect!',
1294 $recordsCheckedString,
1295 $errorCount ? FlashMessage::ERROR : FlashMessage::OK
1296 );
1297 /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
1298 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1299 /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
1300 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1301 $defaultFlashMessageQueue->enqueue($flashMessage);
1302 $bodyContent = $defaultFlashMessageQueue->renderFlashMessages();
1303 if ($cli_echo) {
1304 echo $recordsCheckedString . ($errorCount ? 'Updates: ' . $errorCount : 'Index Integrity was perfect!') . LF;
1305 }
1306 if (!$testOnly) {
1307 $registry = GeneralUtility::makeInstance(Registry::class);
1308 $registry->set('core', 'sys_refindex_lastUpdate', $GLOBALS['EXEC_TIME']);
1309 }
1310 return [$headerContent, $bodyContent, $errorCount];
1311 }
1312
1313 /**
1314 * Returns the current BE user.
1315 *
1316 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1317 */
1318 protected function getBackendUser()
1319 {
1320 return $GLOBALS['BE_USER'];
1321 }
1322 }