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