[BUGFIX] Fix return value in command site:list
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / RelationHandler.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 TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
19 use TYPO3\CMS\Core\Database\Query\QueryHelper;
20 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
21 use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
22 use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\MathUtility;
25 use TYPO3\CMS\Core\Versioning\VersionState;
26
27 /**
28 * Load database groups (relations)
29 * Used to process the relations created by the TCA element types "group" and "select" for database records.
30 * Manages MM-relations as well.
31 */
32 class RelationHandler
33 {
34 /**
35 * $fetchAllFields if false getFromDB() fetches only uid, pid, thumbnail and label fields (as defined in TCA)
36 *
37 * @var bool
38 */
39 protected $fetchAllFields = false;
40
41 /**
42 * If set, values that are not ids in tables are normally discarded. By this options they will be preserved.
43 *
44 * @var bool
45 */
46 public $registerNonTableValues = false;
47
48 /**
49 * Contains the table names as keys. The values are the id-values for each table.
50 * Should ONLY contain proper table names.
51 *
52 * @var array
53 */
54 public $tableArray = [];
55
56 /**
57 * Contains items in a numeric array (table/id for each). Tablenames here might be "_NO_TABLE"
58 *
59 * @var array
60 */
61 public $itemArray = [];
62
63 /**
64 * Array for NON-table elements
65 *
66 * @var array
67 */
68 public $nonTableArray = [];
69
70 /**
71 * @var array
72 */
73 public $additionalWhere = [];
74
75 /**
76 * Deleted-column is added to additionalWhere... if this is set...
77 *
78 * @var bool
79 */
80 public $checkIfDeleted = true;
81
82 /**
83 * @var array
84 */
85 public $dbPaths = [];
86
87 /**
88 * Will contain the first table name in the $tablelist (for positive ids)
89 *
90 * @var string
91 */
92 public $firstTable = '';
93
94 /**
95 * Will contain the second table name in the $tablelist (for negative ids)
96 *
97 * @var string
98 */
99 public $secondTable = '';
100
101 /**
102 * If TRUE, uid_local and uid_foreign are switched, and the current table
103 * is inserted as tablename - this means you display a foreign relation "from the opposite side"
104 *
105 * @var bool
106 */
107 public $MM_is_foreign = false;
108
109 /**
110 * Field name at the "local" side of the MM relation
111 *
112 * @var string
113 */
114 public $MM_oppositeField = '';
115
116 /**
117 * Only set if MM_is_foreign is set
118 *
119 * @var string
120 */
121 public $MM_oppositeTable = '';
122
123 /**
124 * Only set if MM_is_foreign is set
125 *
126 * @var string
127 */
128 public $MM_oppositeFieldConf = '';
129
130 /**
131 * Is empty by default; if MM_is_foreign is set and there is more than one table
132 * allowed (on the "local" side), then it contains the first table (as a fallback)
133 * @var string
134 */
135 public $MM_isMultiTableRelationship = '';
136
137 /**
138 * Current table => Only needed for reverse relations
139 *
140 * @var string
141 */
142 public $currentTable;
143
144 /**
145 * If a record should be undeleted
146 * (so do not use the $useDeleteClause on \TYPO3\CMS\Backend\Utility\BackendUtility)
147 *
148 * @var bool
149 */
150 public $undeleteRecord;
151
152 /**
153 * Array of fields value pairs that should match while SELECT
154 * and will be written into MM table if $MM_insert_fields is not set
155 *
156 * @var array
157 */
158 public $MM_match_fields = [];
159
160 /**
161 * This is set to TRUE if the MM table has a UID field.
162 *
163 * @var bool
164 */
165 public $MM_hasUidField;
166
167 /**
168 * Array of fields and value pairs used for insert in MM table
169 *
170 * @var array
171 */
172 public $MM_insert_fields = [];
173
174 /**
175 * Extra MM table where
176 *
177 * @var string
178 */
179 public $MM_table_where = '';
180
181 /**
182 * Usage of a MM field on the opposite relation.
183 *
184 * @var array
185 */
186 protected $MM_oppositeUsage;
187
188 /**
189 * @var bool
190 */
191 protected $updateReferenceIndex = true;
192
193 /**
194 * @var bool
195 */
196 protected $useLiveParentIds = true;
197
198 /**
199 * @var bool
200 */
201 protected $useLiveReferenceIds = true;
202
203 /**
204 * @var int
205 */
206 protected $workspaceId;
207
208 /**
209 * @var bool
210 */
211 protected $purged = false;
212
213 /**
214 * This array will be filled by getFromDB().
215 *
216 * @var array
217 */
218 public $results = [];
219
220 /**
221 * Gets the current workspace id.
222 *
223 * @return int
224 */
225 public function getWorkspaceId()
226 {
227 if (!isset($this->workspaceId)) {
228 $this->workspaceId = (int)$GLOBALS['BE_USER']->workspace;
229 }
230 return $this->workspaceId;
231 }
232
233 /**
234 * Sets the current workspace id.
235 *
236 * @param int $workspaceId
237 */
238 public function setWorkspaceId($workspaceId)
239 {
240 $this->workspaceId = (int)$workspaceId;
241 }
242
243 /**
244 * Whether item array has been purged in this instance.
245 *
246 * @return bool
247 */
248 public function isPurged()
249 {
250 return $this->purged;
251 }
252
253 /**
254 * Initialization of the class.
255 *
256 * @param string $itemlist List of group/select items
257 * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list.
258 * @param string $MMtable Name of a MM table.
259 * @param int $MMuid Local UID for MM lookup
260 * @param string $currentTable Current table name
261 * @param array $conf TCA configuration for current field
262 */
263 public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
264 {
265 $conf = (array)$conf;
266 // SECTION: MM reverse relations
267 $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false);
268 $this->MM_oppositeField = $conf['MM_opposite_field'] ?? null;
269 $this->MM_table_where = $conf['MM_table_where'] ?? null;
270 $this->MM_hasUidField = $conf['MM_hasUidField'] ?? null;
271 $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : [];
272 $this->MM_insert_fields = (isset($conf['MM_insert_fields']) && is_array($conf['MM_insert_fields'])) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
273 $this->currentTable = $currentTable;
274 if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
275 $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
276 }
277 if ($this->MM_is_foreign) {
278 $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
279 // Normally, $conf['allowed'] can contain a list of tables,
280 // but as we are looking at a MM relation from the foreign side,
281 // it only makes sense to allow one one table in $conf['allowed']
282 $tmp = GeneralUtility::trimExplode(',', $tmp);
283 $this->MM_oppositeTable = $tmp[0];
284 unset($tmp);
285 // Only add the current table name if there is more than one allowed field
286 // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
287 $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
288 if ($this->MM_oppositeFieldConf['allowed']) {
289 $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
290 if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
291 $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
292 }
293 }
294 }
295 // SECTION: normal MM relations
296 // If the table list is "*" then all tables are used in the list:
297 if (trim($tablelist) === '*') {
298 $tablelist = implode(',', array_keys($GLOBALS['TCA']));
299 }
300 // The tables are traversed and internal arrays are initialized:
301 $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
302 foreach ($tempTableArray as $val) {
303 $tName = trim($val);
304 $this->tableArray[$tName] = [];
305 $deleteField = $GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false;
306 if ($this->checkIfDeleted && $deleteField) {
307 $fieldN = $tName . '.' . $deleteField;
308 $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
309 }
310 }
311 if (is_array($this->tableArray)) {
312 reset($this->tableArray);
313 } else {
314 // No tables
315 return;
316 }
317 // Set first and second tables:
318 // Is the first table
319 $this->firstTable = key($this->tableArray);
320 next($this->tableArray);
321 // If the second table is set and the ID number is less than zero (later)
322 // then the record is regarded to come from the second table...
323 $this->secondTable = key($this->tableArray);
324 // Now, populate the internal itemArray and tableArray arrays:
325 // If MM, then call this function to do that:
326 if ($MMtable) {
327 if ($MMuid) {
328 $this->readMM($MMtable, $MMuid);
329 $this->purgeItemArray();
330 } else {
331 // Revert to readList() for new records in order to load possible default values from $itemlist
332 $this->readList($itemlist, $conf);
333 $this->purgeItemArray();
334 }
335 } elseif ($MMuid && isset($conf['foreign_field']) && (bool)$conf['foreign_field']) {
336 // If not MM but foreign_field, the read the records by the foreign_field
337 $this->readForeignField($MMuid, $conf);
338 } else {
339 // If not MM, then explode the itemlist by "," and traverse the list:
340 $this->readList($itemlist, $conf);
341 // Do automatic default_sortby, if any
342 if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
343 $this->sortList($conf['foreign_default_sortby']);
344 }
345 }
346 }
347
348 /**
349 * Sets $fetchAllFields
350 *
351 * @param bool $allFields enables fetching of all fields in getFromDB()
352 */
353 public function setFetchAllFields($allFields)
354 {
355 $this->fetchAllFields = (bool)$allFields;
356 }
357
358 /**
359 * Sets whether the reference index shall be updated.
360 *
361 * @param bool $updateReferenceIndex Whether the reference index shall be updated
362 */
363 public function setUpdateReferenceIndex($updateReferenceIndex)
364 {
365 $this->updateReferenceIndex = (bool)$updateReferenceIndex;
366 }
367
368 /**
369 * @param bool $useLiveParentIds
370 */
371 public function setUseLiveParentIds($useLiveParentIds)
372 {
373 $this->useLiveParentIds = (bool)$useLiveParentIds;
374 }
375
376 /**
377 * @param bool $useLiveReferenceIds
378 */
379 public function setUseLiveReferenceIds($useLiveReferenceIds)
380 {
381 $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
382 }
383
384 /**
385 * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
386 *
387 * @param string $itemlist Item list
388 * @param array $configuration Parent field configuration
389 */
390 public function readList($itemlist, array $configuration)
391 {
392 if ((string)trim($itemlist) != '') {
393 $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
394 // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
395 // if there were spaces in the list... I suppose this is better overall...
396 foreach ($tempItemArray as $key => $val) {
397 // Will be set to "1" if the entry was a real table/id:
398 $isSet = 0;
399 // Extract table name and id. This is un the formular [tablename]_[id]
400 // where table name MIGHT contain "_", hence the reversion of the string!
401 $val = strrev($val);
402 $parts = explode('_', $val, 2);
403 $theID = strrev($parts[0]);
404 // Check that the id IS an integer:
405 if (MathUtility::canBeInterpretedAsInteger($theID)) {
406 // Get the table name: If a part of the exploded string, use that.
407 // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
408 $theTable = trim($parts[1] ?? '')
409 ? strrev(trim($parts[1] ?? ''))
410 : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
411 // If the ID is not blank and the table name is among the names in the inputted tableList
412 if (
413 (string)$theID != ''
414 // allow the default language '0' for the special languages configuration
415 && ($theID || ($configuration['special'] ?? null) === 'languages')
416 && $theTable && isset($this->tableArray[$theTable])
417 ) {
418 // Get ID as the right value:
419 $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
420 // Register ID/table name in internal arrays:
421 $this->itemArray[$key]['id'] = $theID;
422 $this->itemArray[$key]['table'] = $theTable;
423 $this->tableArray[$theTable][] = $theID;
424 // Set update-flag:
425 $isSet = 1;
426 }
427 }
428 // If it turns out that the value from the list was NOT a valid reference to a table-record,
429 // then we might still set it as a NO_TABLE value:
430 if (!$isSet && $this->registerNonTableValues) {
431 $this->itemArray[$key]['id'] = $tempItemArray[$key];
432 $this->itemArray[$key]['table'] = '_NO_TABLE';
433 $this->nonTableArray[] = $tempItemArray[$key];
434 }
435 }
436
437 // Skip if not dealing with IRRE in a CSV list on a workspace
438 if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
439 || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
440 || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
441 return;
442 }
443
444 // Fetch live record data
445 if ($this->useLiveReferenceIds) {
446 foreach ($this->itemArray as &$item) {
447 $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
448 }
449 } else {
450 // Directly overlay workspace data
451 $this->itemArray = [];
452 $foreignTable = $configuration['foreign_table'];
453 $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
454 foreach ($ids as $id) {
455 $this->itemArray[] = [
456 'id' => $id,
457 'table' => $foreignTable,
458 ];
459 }
460 }
461 }
462 }
463
464 /**
465 * Does a sorting on $this->itemArray depending on a default sortby field.
466 * This is only used for automatic sorting of comma separated lists.
467 * This function is only relevant for data that is stored in comma separated lists!
468 *
469 * @param string $sortby The default_sortby field/command (e.g. 'price DESC')
470 */
471 public function sortList($sortby)
472 {
473 // Sort directly without fetching additional data
474 if ($sortby === 'uid') {
475 usort(
476 $this->itemArray,
477 function ($a, $b) {
478 return $a['id'] < $b['id'] ? -1 : 1;
479 }
480 );
481 } elseif (count($this->tableArray) === 1) {
482 reset($this->tableArray);
483 $table = key($this->tableArray);
484 $connection = $this->getConnectionForTableName($table);
485 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
486
487 foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
488 if (empty($chunk)) {
489 continue;
490 }
491 $this->itemArray = [];
492 $this->tableArray = [];
493 $queryBuilder = $connection->createQueryBuilder();
494 $queryBuilder->getRestrictions()->removeAll();
495 $queryBuilder->select('uid')
496 ->from($table)
497 ->where(
498 $queryBuilder->expr()->in(
499 'uid',
500 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
501 )
502 );
503 foreach (QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
504 list($fieldName, $order) = $orderPair;
505 $queryBuilder->addOrderBy($fieldName, $order);
506 }
507 $statement = $queryBuilder->execute();
508 while ($row = $statement->fetch()) {
509 $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
510 $this->tableArray[$table][] = $row['uid'];
511 }
512 }
513 }
514 }
515
516 /**
517 * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
518 * You can call this function after start if you supply no list to start()
519 *
520 * @param string $tableName MM Tablename
521 * @param int $uid Local UID
522 */
523 public function readMM($tableName, $uid)
524 {
525 $key = 0;
526 $theTable = null;
527 $queryBuilder = $this->getConnectionForTableName($tableName)
528 ->createQueryBuilder();
529 $queryBuilder->getRestrictions()->removeAll();
530 $queryBuilder->select('*')->from($tableName);
531 // In case of a reverse relation
532 if ($this->MM_is_foreign) {
533 $uidLocal_field = 'uid_foreign';
534 $uidForeign_field = 'uid_local';
535 $sorting_field = 'sorting_foreign';
536 if ($this->MM_isMultiTableRelationship) {
537 // Be backwards compatible! When allowing more than one table after
538 // having previously allowed only one table, this case applies.
539 if ($this->currentTable == $this->MM_isMultiTableRelationship) {
540 $expression = $queryBuilder->expr()->orX(
541 $queryBuilder->expr()->eq(
542 'tablenames',
543 $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
544 ),
545 $queryBuilder->expr()->eq(
546 'tablenames',
547 $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
548 )
549 );
550 } else {
551 $expression = $queryBuilder->expr()->eq(
552 'tablenames',
553 $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
554 );
555 }
556 $queryBuilder->andWhere($expression);
557 }
558 $theTable = $this->MM_oppositeTable;
559 } else {
560 // Default
561 $uidLocal_field = 'uid_local';
562 $uidForeign_field = 'uid_foreign';
563 $sorting_field = 'sorting';
564 }
565 if ($this->MM_table_where) {
566 $queryBuilder->andWhere(
567 QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
568 );
569 }
570 foreach ($this->MM_match_fields as $field => $value) {
571 $queryBuilder->andWhere(
572 $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
573 );
574 }
575 $queryBuilder->andWhere(
576 $queryBuilder->expr()->eq(
577 $uidLocal_field,
578 $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)
579 )
580 );
581 $queryBuilder->orderBy($sorting_field);
582 $statement = $queryBuilder->execute();
583 while ($row = $statement->fetch()) {
584 // Default
585 if (!$this->MM_is_foreign) {
586 // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
587 $theTable = $row['tablenames'] ?: $this->firstTable;
588 }
589 if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
590 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
591 $this->itemArray[$key]['table'] = $theTable;
592 $this->tableArray[$theTable][] = $row[$uidForeign_field];
593 } elseif ($this->registerNonTableValues) {
594 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
595 $this->itemArray[$key]['table'] = '_NO_TABLE';
596 $this->nonTableArray[] = $row[$uidForeign_field];
597 }
598 $key++;
599 }
600 }
601
602 /**
603 * Writes the internal itemArray to MM table:
604 *
605 * @param string $MM_tableName MM table name
606 * @param int $uid Local UID
607 * @param bool $prependTableName If set, then table names will always be written.
608 */
609 public function writeMM($MM_tableName, $uid, $prependTableName = false)
610 {
611 $connection = $this->getConnectionForTableName($MM_tableName);
612 $expressionBuilder = $connection->createQueryBuilder()->expr();
613
614 // In case of a reverse relation
615 if ($this->MM_is_foreign) {
616 $uidLocal_field = 'uid_foreign';
617 $uidForeign_field = 'uid_local';
618 $sorting_field = 'sorting_foreign';
619 } else {
620 // default
621 $uidLocal_field = 'uid_local';
622 $uidForeign_field = 'uid_foreign';
623 $sorting_field = 'sorting';
624 }
625 // If there are tables...
626 $tableC = count($this->tableArray);
627 if ($tableC) {
628 // Boolean: does the field "tablename" need to be filled?
629 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
630 $c = 0;
631 $additionalWhere_tablenames = '';
632 if ($this->MM_is_foreign && $prep) {
633 $additionalWhere_tablenames = $expressionBuilder->eq(
634 'tablenames',
635 $expressionBuilder->literal($this->currentTable)
636 );
637 }
638 $additionalWhere = $expressionBuilder->andX();
639 // Add WHERE clause if configured
640 if ($this->MM_table_where) {
641 $additionalWhere->add(
642 QueryHelper::stripLogicalOperatorPrefix(
643 str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where)
644 )
645 );
646 }
647 // Select, update or delete only those relations that match the configured fields
648 foreach ($this->MM_match_fields as $field => $value) {
649 $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
650 }
651
652 $queryBuilder = $connection->createQueryBuilder();
653 $queryBuilder->getRestrictions()->removeAll();
654 $queryBuilder->select($uidForeign_field)
655 ->from($MM_tableName)
656 ->where($queryBuilder->expr()->eq(
657 $uidLocal_field,
658 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
659 ))
660 ->orderBy($sorting_field);
661
662 if ($prep) {
663 $queryBuilder->addSelect('tablenames');
664 }
665 if ($this->MM_hasUidField) {
666 $queryBuilder->addSelect('uid');
667 }
668 if ($additionalWhere_tablenames) {
669 $queryBuilder->andWhere($additionalWhere_tablenames);
670 }
671 if ($additionalWhere->count()) {
672 $queryBuilder->andWhere($additionalWhere);
673 }
674
675 $result = $queryBuilder->execute();
676 $oldMMs = [];
677 // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
678 // If the UID is present it will be used to update sorting and delete MM-records.
679 // This is necessary if the "multiple" feature is used for the MM relations.
680 // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
681 $oldMMs_inclUid = [];
682 while ($row = $result->fetch()) {
683 if (!$this->MM_is_foreign && $prep) {
684 $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
685 } else {
686 $oldMMs[] = $row[$uidForeign_field];
687 }
688 $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid']];
689 }
690 // For each item, insert it:
691 foreach ($this->itemArray as $val) {
692 $c++;
693 if ($prep || $val['table'] === '_NO_TABLE') {
694 // Insert current table if needed
695 if ($this->MM_is_foreign) {
696 $tablename = $this->currentTable;
697 } else {
698 $tablename = $val['table'];
699 }
700 } else {
701 $tablename = '';
702 }
703 if (!$this->MM_is_foreign && $prep) {
704 $item = [$val['table'], $val['id']];
705 } else {
706 $item = $val['id'];
707 }
708 if (in_array($item, $oldMMs)) {
709 $oldMMs_index = array_search($item, $oldMMs);
710 // In principle, selecting on the UID is all we need to do
711 // if a uid field is available since that is unique!
712 // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
713 $queryBuilder = $connection->createQueryBuilder();
714 $queryBuilder->update($MM_tableName)
715 ->set($sorting_field, $c)
716 ->where(
717 $expressionBuilder->eq(
718 $uidLocal_field,
719 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
720 ),
721 $expressionBuilder->eq(
722 $uidForeign_field,
723 $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT)
724 )
725 );
726
727 if ($additionalWhere->count()) {
728 $queryBuilder->andWhere($additionalWhere);
729 }
730 if ($this->MM_hasUidField) {
731 $queryBuilder->andWhere(
732 $expressionBuilder->eq(
733 'uid',
734 $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index][2], \PDO::PARAM_INT)
735 )
736 );
737 }
738 if ($tablename) {
739 $queryBuilder->andWhere(
740 $expressionBuilder->eq(
741 'tablenames',
742 $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR)
743 )
744 );
745 }
746
747 $queryBuilder->execute();
748 // Remove the item from the $oldMMs array so after this
749 // foreach loop only the ones that need to be deleted are in there.
750 unset($oldMMs[$oldMMs_index]);
751 // Remove the item from the $oldMMs_inclUid array so after this
752 // foreach loop only the ones that need to be deleted are in there.
753 unset($oldMMs_inclUid[$oldMMs_index]);
754 } else {
755 $insertFields = $this->MM_insert_fields;
756 $insertFields[$uidLocal_field] = $uid;
757 $insertFields[$uidForeign_field] = $val['id'];
758 $insertFields[$sorting_field] = $c;
759 if ($tablename) {
760 $insertFields['tablenames'] = $tablename;
761 $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
762 }
763 $connection->insert($MM_tableName, $insertFields);
764 if ($this->MM_is_foreign) {
765 $this->updateRefIndex($val['table'], $val['id']);
766 }
767 }
768 }
769 // Delete all not-used relations:
770 if (is_array($oldMMs) && !empty($oldMMs)) {
771 $queryBuilder = $connection->createQueryBuilder();
772 $removeClauses = $queryBuilder->expr()->orX();
773 $updateRefIndex_records = [];
774 foreach ($oldMMs as $oldMM_key => $mmItem) {
775 // If UID field is present, of course we need only use that for deleting.
776 if ($this->MM_hasUidField) {
777 $removeClauses->add($queryBuilder->expr()->eq(
778 'uid',
779 $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key][2], \PDO::PARAM_INT)
780 ));
781 } else {
782 if (is_array($mmItem)) {
783 $removeClauses->add(
784 $queryBuilder->expr()->andX(
785 $queryBuilder->expr()->eq(
786 'tablenames',
787 $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR)
788 ),
789 $queryBuilder->expr()->eq(
790 $uidForeign_field,
791 $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT)
792 )
793 )
794 );
795 } else {
796 $removeClauses->add(
797 $queryBuilder->expr()->eq(
798 $uidForeign_field,
799 $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT)
800 )
801 );
802 }
803 }
804 if ($this->MM_is_foreign) {
805 if (is_array($mmItem)) {
806 $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
807 } else {
808 $updateRefIndex_records[] = [$this->firstTable, $mmItem];
809 }
810 }
811 }
812
813 $queryBuilder->delete($MM_tableName)
814 ->where(
815 $queryBuilder->expr()->eq(
816 $uidLocal_field,
817 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
818 ),
819 $removeClauses
820 );
821
822 if ($additionalWhere_tablenames) {
823 $queryBuilder->andWhere($additionalWhere_tablenames);
824 }
825 if ($additionalWhere->count()) {
826 $queryBuilder->andWhere($additionalWhere);
827 }
828
829 $queryBuilder->execute();
830
831 // Update ref index:
832 foreach ($updateRefIndex_records as $pair) {
833 $this->updateRefIndex($pair[0], $pair[1]);
834 }
835 }
836 // Update ref index; In DataHandler it is not certain that this will happen because
837 // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
838 // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
839 $this->updateRefIndex($this->currentTable, $uid);
840 }
841 }
842
843 /**
844 * Remaps MM table elements from one local uid to another
845 * Does NOT update the reference index for you, must be called subsequently to do that!
846 *
847 * @param string $MM_tableName MM table name
848 * @param int $uid Local, current UID
849 * @param int $newUid Local, new UID
850 * @param bool $prependTableName If set, then table names will always be written.
851 */
852 public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
853 {
854 // In case of a reverse relation
855 if ($this->MM_is_foreign) {
856 $uidLocal_field = 'uid_foreign';
857 } else {
858 // default
859 $uidLocal_field = 'uid_local';
860 }
861 // If there are tables...
862 $tableC = count($this->tableArray);
863 if ($tableC) {
864 $queryBuilder = $this->getConnectionForTableName($MM_tableName)
865 ->createQueryBuilder();
866 $queryBuilder->update($MM_tableName)
867 ->set($uidLocal_field, (int)$newUid)
868 ->where($queryBuilder->expr()->eq(
869 $uidLocal_field,
870 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
871 ));
872 // Boolean: does the field "tablename" need to be filled?
873 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
874 if ($this->MM_is_foreign && $prep) {
875 $queryBuilder->andWhere(
876 $queryBuilder->expr()->eq(
877 'tablenames',
878 $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
879 )
880 );
881 }
882 // Add WHERE clause if configured
883 if ($this->MM_table_where) {
884 $queryBuilder->andWhere(
885 QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
886 );
887 }
888 // Select, update or delete only those relations that match the configured fields
889 foreach ($this->MM_match_fields as $field => $value) {
890 $queryBuilder->andWhere(
891 $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
892 );
893 }
894 $queryBuilder->execute();
895 }
896 }
897
898 /**
899 * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and
900 * stores the parts in the internal array itemArray and tableArray.
901 *
902 * @param int $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field)
903 * @param array $conf TCA configuration for current field
904 */
905 public function readForeignField($uid, $conf)
906 {
907 if ($this->useLiveParentIds) {
908 $uid = $this->getLiveDefaultId($this->currentTable, $uid);
909 }
910
911 $key = 0;
912 $uid = (int)$uid;
913 // skip further processing if $uid does not
914 // point to a valid parent record
915 if ($uid === 0) {
916 return;
917 }
918
919 $foreign_table = $conf['foreign_table'];
920 $foreign_table_field = $conf['foreign_table_field'];
921 $useDeleteClause = !$this->undeleteRecord;
922 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
923 $queryBuilder = $this->getConnectionForTableName($foreign_table)
924 ->createQueryBuilder();
925 $queryBuilder->getRestrictions()
926 ->removeAll();
927 // Use the deleteClause (e.g. "deleted=0") on this table
928 if ($useDeleteClause) {
929 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
930 }
931
932 $queryBuilder->select('uid')
933 ->from($foreign_table);
934
935 // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
936 if ($conf['symmetric_field']) {
937 $queryBuilder->where(
938 $queryBuilder->expr()->orX(
939 $queryBuilder->expr()->eq(
940 $conf['foreign_field'],
941 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
942 ),
943 $queryBuilder->expr()->eq(
944 $conf['symmetric_field'],
945 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
946 )
947 )
948 );
949 } else {
950 $queryBuilder->where($queryBuilder->expr()->eq(
951 $conf['foreign_field'],
952 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
953 ));
954 }
955 // If it's requested to look for the parent uid AND the parent table,
956 // add an additional SQL-WHERE clause
957 if ($foreign_table_field && $this->currentTable) {
958 $queryBuilder->andWhere(
959 $queryBuilder->expr()->eq(
960 $foreign_table_field,
961 $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
962 )
963 );
964 }
965 // Add additional where clause if foreign_match_fields are defined
966 foreach ($foreign_match_fields as $field => $value) {
967 $queryBuilder->andWhere(
968 $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
969 );
970 }
971 // Select children from the live(!) workspace only
972 if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
973 $queryBuilder->getRestrictions()->add(
974 GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
975 );
976 }
977 // Get the correct sorting field
978 // Specific manual sortby for data handled by this field
979 $sortby = '';
980 if ($conf['foreign_sortby']) {
981 if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
982 // Sorting depends on, from which side of the relation we're looking at it
983 // This requires bypassing automatic quoting and setting of the default sort direction
984 // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
985 $queryBuilder->add(
986 'orderBy',
987 'CASE
988 WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
989 THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
990 ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
991 END'
992 );
993 } else {
994 // Regular single-side behaviour
995 $sortby = $conf['foreign_sortby'];
996 }
997 } elseif ($conf['foreign_default_sortby']) {
998 // Specific default sortby for data handled by this field
999 $sortby = $conf['foreign_default_sortby'];
1000 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1001 // Manual sortby for all table records
1002 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1003 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
1004 // Default sortby for all table records
1005 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1006 }
1007
1008 if (!empty($sortby)) {
1009 foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1010 list($fieldName, $sorting) = $orderPair;
1011 $queryBuilder->addOrderBy($fieldName, $sorting);
1012 }
1013 }
1014
1015 // Get the rows from storage
1016 $rows = [];
1017 $result = $queryBuilder->execute();
1018 while ($row = $result->fetch()) {
1019 $rows[$row['uid']] = $row;
1020 }
1021 if (!empty($rows)) {
1022 // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1023 $sortby = $queryBuilder->getQueryPart('orderBy');
1024 $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
1025 foreach ($ids as $id) {
1026 $this->itemArray[$key]['id'] = $id;
1027 $this->itemArray[$key]['table'] = $foreign_table;
1028 $this->tableArray[$foreign_table][] = $id;
1029 $key++;
1030 }
1031 }
1032 }
1033
1034 /**
1035 * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record)
1036 *
1037 * @param array $conf TCA configuration for current field
1038 * @param int $parentUid The uid of the parent record
1039 * @param int $updateToUid If this is larger than zero it will be used as foreign UID instead of the given $parentUid (on Copy)
1040 * @param bool $skipSorting Do not update the sorting columns, this could happen for imported values
1041 */
1042 public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
1043 {
1044 if ($this->useLiveParentIds) {
1045 $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
1046 if (!empty($updateToUid)) {
1047 $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
1048 }
1049 }
1050
1051 $c = 0;
1052 $foreign_table = $conf['foreign_table'];
1053 $foreign_field = $conf['foreign_field'];
1054 $symmetric_field = $conf['symmetric_field'];
1055 $foreign_table_field = $conf['foreign_table_field'];
1056 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
1057 // If there are table items and we have a proper $parentUid
1058 if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1059 // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1060 if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1061 $updateToUid = 0;
1062 }
1063 $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
1064 $fields = 'uid,pid,' . $foreign_field;
1065 // Consider the symmetric field if defined:
1066 if ($symmetric_field) {
1067 $fields .= ',' . $symmetric_field;
1068 }
1069 // Consider workspaces if defined and currently used:
1070 if ($considerWorkspaces) {
1071 $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1072 }
1073 // Update all items
1074 foreach ($this->itemArray as $val) {
1075 $uid = $val['id'];
1076 $table = $val['table'];
1077 $row = [];
1078 // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1079 if ($symmetric_field || $considerWorkspaces) {
1080 $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
1081 if (empty($row)) {
1082 continue;
1083 }
1084 }
1085 $isOnSymmetricSide = false;
1086 if ($symmetric_field) {
1087 $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
1088 }
1089 $updateValues = $foreign_match_fields;
1090 // No update to the uid is requested, so this is the normal behaviour
1091 // just update the fields and care about sorting
1092 if (!$updateToUid) {
1093 // Always add the pointer to the parent uid
1094 if ($isOnSymmetricSide) {
1095 $updateValues[$symmetric_field] = $parentUid;
1096 } else {
1097 $updateValues[$foreign_field] = $parentUid;
1098 }
1099 // If it is configured in TCA also to store the parent table in the child record, just do it
1100 if ($foreign_table_field && $this->currentTable) {
1101 $updateValues[$foreign_table_field] = $this->currentTable;
1102 }
1103 // Update sorting columns if not to be skipped
1104 if (!$skipSorting) {
1105 // Get the correct sorting field
1106 // Specific manual sortby for data handled by this field
1107 $sortby = '';
1108 if ($conf['foreign_sortby']) {
1109 $sortby = $conf['foreign_sortby'];
1110 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1111 // manual sortby for all table records
1112 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1113 }
1114 // Apply sorting on the symmetric side
1115 // (it depends on who created the relation, so what uid is in the symmetric_field):
1116 if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1117 $sortby = $conf['symmetric_sortby'];
1118 } else {
1119 $tempSortBy = [];
1120 foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1121 list($fieldName, $order) = $orderPair;
1122 if ($order !== null) {
1123 $tempSortBy[] = implode(' ', $orderPair);
1124 } else {
1125 $tempSortBy[] = $fieldName;
1126 }
1127 }
1128 $sortby = implode(',', $tempSortBy);
1129 }
1130 if ($sortby) {
1131 $updateValues[$sortby] = ++$c;
1132 }
1133 }
1134 } else {
1135 if ($isOnSymmetricSide) {
1136 $updateValues[$symmetric_field] = $updateToUid;
1137 } else {
1138 $updateValues[$foreign_field] = $updateToUid;
1139 }
1140 }
1141 // Update accordant fields in the database:
1142 if (!empty($updateValues)) {
1143 // Update tstamp if any foreign field value has changed
1144 if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1145 $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1146 }
1147 $this->getConnectionForTableName($table)
1148 ->update(
1149 $table,
1150 $updateValues,
1151 ['uid' => (int)$uid]
1152 );
1153 $this->updateRefIndex($table, $uid);
1154 }
1155 // Update accordant fields in the database for workspaces overlays/placeholders:
1156 if ($considerWorkspaces) {
1157 // It's the specific versioned record -> update placeholder (if any)
1158 if (!empty($row['t3ver_oid']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
1159 $this->getConnectionForTableName($table)
1160 ->update(
1161 $table,
1162 $updateValues,
1163 ['uid' => (int)$row['t3ver_oid']]
1164 );
1165 }
1166 }
1167 }
1168 }
1169 }
1170
1171 /**
1172 * After initialization you can extract an array of the elements from the object. Use this function for that.
1173 *
1174 * @param bool $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
1175 * @return array A numeric array.
1176 */
1177 public function getValueArray($prependTableName = false)
1178 {
1179 // INIT:
1180 $valueArray = [];
1181 $tableC = count($this->tableArray);
1182 // If there are tables in the table array:
1183 if ($tableC) {
1184 // If there are more than ONE table in the table array, then always prepend table names:
1185 $prep = $tableC > 1 || $prependTableName;
1186 // Traverse the array of items:
1187 foreach ($this->itemArray as $val) {
1188 $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1189 }
1190 }
1191 // Return the array
1192 return $valueArray;
1193 }
1194
1195 /**
1196 * Reads all records from internal tableArray into the internal ->results array
1197 * where keys are table names and for each table, records are stored with uids as their keys.
1198 * If $this->fetchAllFields is false you can save a little memory
1199 * since only uid,pid and a few other fields are selected.
1200 *
1201 * @return array
1202 */
1203 public function getFromDB()
1204 {
1205 // Traverses the tables listed:
1206 foreach ($this->tableArray as $table => $ids) {
1207 if (is_array($ids) && !empty($ids)) {
1208 $connection = $this->getConnectionForTableName($table);
1209 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1210
1211 foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1212 if ($this->fetchAllFields) {
1213 $fields = '*';
1214 } else {
1215 $fields = 'uid,pid';
1216 if ($GLOBALS['TCA'][$table]['ctrl']['label']) {
1217 // Title
1218 $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1219 }
1220 if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1221 // Alternative Title-Fields
1222 $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label_alt'];
1223 }
1224 if ($GLOBALS['TCA'][$table]['ctrl']['thumbnail']) {
1225 // Thumbnail
1226 $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
1227 }
1228 }
1229 $queryBuilder = $connection->createQueryBuilder();
1230 $queryBuilder->getRestrictions()->removeAll();
1231 $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
1232 ->from($table)
1233 ->where($queryBuilder->expr()->in(
1234 'uid',
1235 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1236 ));
1237 if ($this->additionalWhere[$table]) {
1238 $queryBuilder->andWhere(
1239 QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1240 );
1241 }
1242 $statement = $queryBuilder->execute();
1243 while ($row = $statement->fetch()) {
1244 $this->results[$table][$row['uid']] = $row;
1245 }
1246 }
1247 }
1248 }
1249 return $this->results;
1250 }
1251
1252 /**
1253 * This method is typically called after getFromDB().
1254 * $this->results holds a list of resolved and valid relations,
1255 * $this->itemArray hold a list of "selected" relations from the incoming selection array.
1256 * The difference is that "itemArray" may hold a single table/uid combination multiple times,
1257 * for instance in a type=group relation having multiple=true, while "results" hold each
1258 * resolved relation only once.
1259 * The methods creates a sanitized "itemArray" from resolved "results" list, normalized
1260 * the return array to always contain both table name and uid, and keep incoming
1261 * "itemArray" sort order and keeps "multiple" selections.
1262 *
1263 * @return array
1264 */
1265 public function getResolvedItemArray(): array
1266 {
1267 $itemArray = [];
1268 foreach ($this->itemArray as $item) {
1269 if (isset($this->results[$item['table']][$item['id']])) {
1270 $itemArray[] = [
1271 'table' => $item['table'],
1272 'uid' => $item['id'],
1273 ];
1274 }
1275 }
1276 return $itemArray;
1277 }
1278
1279 /**
1280 * Counts the items in $this->itemArray and puts this value in an array by default.
1281 *
1282 * @param bool $returnAsArray Whether to put the count value in an array
1283 * @return mixed The plain count as integer or the same inside an array
1284 */
1285 public function countItems($returnAsArray = true)
1286 {
1287 $count = count($this->itemArray);
1288 if ($returnAsArray) {
1289 $count = [$count];
1290 }
1291 return $count;
1292 }
1293
1294 /**
1295 * Update Reference Index (sys_refindex) for a record
1296 * Should be called any almost any update to a record which could affect references inside the record.
1297 * (copied from DataHandler)
1298 *
1299 * @param string $table Table name
1300 * @param int $id Record UID
1301 * @return array Information concerning modifications delivered by \TYPO3\CMS\Core\Database\ReferenceIndex::updateRefIndexTable()
1302 */
1303 public function updateRefIndex($table, $id)
1304 {
1305 $statisticsArray = [];
1306 if ($this->updateReferenceIndex) {
1307 /** @var \TYPO3\CMS\Core\Database\ReferenceIndex $refIndexObj */
1308 $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
1309 if (BackendUtility::isTableWorkspaceEnabled($table)) {
1310 $refIndexObj->setWorkspaceId($this->getWorkspaceId());
1311 }
1312 $refIndexObj->enableRuntimeCache();
1313 $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1314 }
1315 return $statisticsArray;
1316 }
1317
1318 /**
1319 * Converts elements in the local item array to use version ids instead of
1320 * live ids, if possible. The most common use case is, to call that prior
1321 * to processing with MM relations in a workspace context. For tha special
1322 * case, ids on both side of the MM relation must use version ids if
1323 * available.
1324 *
1325 * @return bool Whether items have been converted
1326 */
1327 public function convertItemArray()
1328 {
1329 $hasBeenConverted = false;
1330
1331 // conversion is only required in a workspace context
1332 // (the case that version ids are submitted in a live context are rare)
1333 if ($this->getWorkspaceId() === 0) {
1334 return $hasBeenConverted;
1335 }
1336
1337 foreach ($this->tableArray as $tableName => $ids) {
1338 if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1339 continue;
1340 }
1341
1342 // convert live ids to version ids if available
1343 $convertedIds = $this->getResolver($tableName, $ids)
1344 ->setKeepDeletePlaceholder(false)
1345 ->setKeepMovePlaceholder(false)
1346 ->processVersionOverlays($ids);
1347 foreach ($this->itemArray as $index => $item) {
1348 if ($item['table'] !== $tableName) {
1349 continue;
1350 }
1351 $currentItemId = $item['id'];
1352 if (
1353 !isset($convertedIds[$currentItemId])
1354 || $currentItemId === $convertedIds[$currentItemId]
1355 ) {
1356 continue;
1357 }
1358 // adjust local item to use resolved version id
1359 $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1360 $hasBeenConverted = true;
1361 }
1362 // update per-table reference for ids
1363 if ($hasBeenConverted) {
1364 $this->tableArray[$tableName] = array_values($convertedIds);
1365 }
1366 }
1367
1368 return $hasBeenConverted;
1369 }
1370
1371 /**
1372 * @param int|null $workspaceId
1373 * @return bool Whether items have been purged
1374 */
1375 public function purgeItemArray($workspaceId = null)
1376 {
1377 if ($workspaceId === null) {
1378 $workspaceId = $this->getWorkspaceId();
1379 } else {
1380 $workspaceId = (int)$workspaceId;
1381 }
1382
1383 // Ensure, only live relations are in the items Array
1384 if ($workspaceId === 0) {
1385 $purgeCallback = 'purgeVersionedIds';
1386 } else {
1387 // Otherwise, ensure that live relations are purged if version exists
1388 $purgeCallback = 'purgeLiveVersionedIds';
1389 }
1390
1391 $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1392 $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1393 return $itemArrayHasBeenPurged;
1394 }
1395
1396 /**
1397 * Removes items having a delete placeholder from $this->itemArray
1398 *
1399 * @return bool Whether items have been purged
1400 */
1401 public function processDeletePlaceholder()
1402 {
1403 if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1404 return false;
1405 }
1406
1407 return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1408 }
1409
1410 /**
1411 * Handles a purge callback on $this->itemArray
1412 *
1413 * @param callable $purgeCallback
1414 * @return bool Whether items have been purged
1415 */
1416 protected function purgeItemArrayHandler($purgeCallback)
1417 {
1418 $itemArrayHasBeenPurged = false;
1419
1420 foreach ($this->tableArray as $itemTableName => $itemIds) {
1421 if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1422 continue;
1423 }
1424
1425 $purgedItemIds = call_user_func([$this, $purgeCallback], $itemTableName, $itemIds);
1426 $removedItemIds = array_diff($itemIds, $purgedItemIds);
1427 foreach ($removedItemIds as $removedItemId) {
1428 $this->removeFromItemArray($itemTableName, $removedItemId);
1429 }
1430 $this->tableArray[$itemTableName] = $purgedItemIds;
1431 if (!empty($removedItemIds)) {
1432 $itemArrayHasBeenPurged = true;
1433 }
1434 }
1435
1436 return $itemArrayHasBeenPurged;
1437 }
1438
1439 /**
1440 * Purges ids that are versioned.
1441 *
1442 * @param string $tableName
1443 * @param array $ids
1444 * @return array
1445 */
1446 protected function purgeVersionedIds($tableName, array $ids)
1447 {
1448 $ids = $this->sanitizeIds($ids);
1449 $ids = array_combine($ids, $ids);
1450 $connection = $this->getConnectionForTableName($tableName);
1451 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1452
1453 foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1454 $queryBuilder = $connection->createQueryBuilder();
1455 $queryBuilder->getRestrictions()->removeAll();
1456 $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1457 ->from($tableName)
1458 ->where(
1459 $queryBuilder->expr()->in(
1460 't3ver_oid',
1461 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1462 ),
1463 $queryBuilder->expr()->neq(
1464 't3ver_wsid',
1465 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1466 )
1467 )
1468 ->orderBy('t3ver_state', 'DESC')
1469 ->execute();
1470
1471 while ($version = $result->fetch()) {
1472 $versionId = $version['uid'];
1473 if (isset($ids[$versionId])) {
1474 unset($ids[$versionId]);
1475 }
1476 }
1477 }
1478
1479 return array_values($ids);
1480 }
1481
1482 /**
1483 * Purges ids that are live but have an accordant version.
1484 *
1485 * @param string $tableName
1486 * @param array $ids
1487 * @return array
1488 */
1489 protected function purgeLiveVersionedIds($tableName, array $ids)
1490 {
1491 $ids = $this->sanitizeIds($ids);
1492 $ids = array_combine($ids, $ids);
1493 $connection = $this->getConnectionForTableName($tableName);
1494 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1495
1496 foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1497 $queryBuilder = $connection->createQueryBuilder();
1498 $queryBuilder->getRestrictions()->removeAll();
1499 $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1500 ->from($tableName)
1501 ->where(
1502 $queryBuilder->expr()->in(
1503 't3ver_oid',
1504 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1505 ),
1506 $queryBuilder->expr()->neq(
1507 't3ver_wsid',
1508 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1509 )
1510 )
1511 ->orderBy('t3ver_state', 'DESC')
1512 ->execute();
1513
1514 while ($version = $result->fetch()) {
1515 $versionId = $version['uid'];
1516 $liveId = $version['t3ver_oid'];
1517 if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1518 unset($ids[$liveId]);
1519 }
1520 }
1521 }
1522
1523 return array_values($ids);
1524 }
1525
1526 /**
1527 * Purges ids that have a delete placeholder
1528 *
1529 * @param string $tableName
1530 * @param array $ids
1531 * @return array
1532 */
1533 protected function purgeDeletePlaceholder($tableName, array $ids)
1534 {
1535 $ids = $this->sanitizeIds($ids);
1536 $ids = array_combine($ids, $ids);
1537 $connection = $this->getConnectionForTableName($tableName);
1538 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1539
1540 foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1541 $queryBuilder = $connection->createQueryBuilder();
1542 $queryBuilder->getRestrictions()->removeAll();
1543 $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1544 ->from($tableName)
1545 ->where(
1546 $queryBuilder->expr()->in(
1547 't3ver_oid',
1548 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1549 ),
1550 $queryBuilder->expr()->eq(
1551 't3ver_wsid',
1552 $queryBuilder->createNamedParameter(
1553 $this->getWorkspaceId(),
1554 \PDO::PARAM_INT
1555 )
1556 ),
1557 $queryBuilder->expr()->eq(
1558 't3ver_state',
1559 $queryBuilder->createNamedParameter(
1560 (string)VersionState::cast(VersionState::DELETE_PLACEHOLDER),
1561 \PDO::PARAM_INT
1562 )
1563 )
1564 )
1565 ->execute();
1566
1567 while ($version = $result->fetch()) {
1568 $liveId = $version['t3ver_oid'];
1569 if (isset($ids[$liveId])) {
1570 unset($ids[$liveId]);
1571 }
1572 }
1573 }
1574
1575 return array_values($ids);
1576 }
1577
1578 protected function removeFromItemArray($tableName, $id)
1579 {
1580 foreach ($this->itemArray as $index => $item) {
1581 if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1582 unset($this->itemArray[$index]);
1583 return true;
1584 }
1585 }
1586 return false;
1587 }
1588
1589 /**
1590 * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation.
1591 *
1592 * @param string $parentUid The uid of the parent record
1593 * @param array $parentConf The TCA configuration of the parent field embedding the child records
1594 * @param array $childRec The record row of the child record
1595 * @return bool Returns TRUE if looking from the symmetric ("other") side to the relation.
1596 */
1597 public static function isOnSymmetricSide($parentUid, $parentConf, $childRec)
1598 {
1599 return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1600 && $parentConf['symmetric_field']
1601 && $parentUid == $childRec[$parentConf['symmetric_field']];
1602 }
1603
1604 /**
1605 * Completes MM values to be written by values from the opposite relation.
1606 * This method used MM insert field or MM match fields if defined.
1607 *
1608 * @param string $tableName Name of the opposite table
1609 * @param array $referenceValues Values to be written
1610 * @return array Values to be written, possibly modified
1611 */
1612 protected function completeOppositeUsageValues($tableName, array $referenceValues)
1613 {
1614 if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1615 return $referenceValues;
1616 }
1617
1618 $fieldName = $this->MM_oppositeUsage[$tableName][0];
1619 if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1620 return $referenceValues;
1621 }
1622
1623 $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1624 if (!empty($configuration['MM_insert_fields'])) {
1625 $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1626 } elseif (!empty($configuration['MM_match_fields'])) {
1627 $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1628 }
1629
1630 return $referenceValues;
1631 }
1632
1633 /**
1634 * Gets the record uid of the live default record. If already
1635 * pointing to the live record, the submitted record uid is returned.
1636 *
1637 * @param string $tableName
1638 * @param int $id
1639 * @return int
1640 */
1641 protected function getLiveDefaultId($tableName, $id)
1642 {
1643 $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1644 if ($liveDefaultId === null) {
1645 $liveDefaultId = $id;
1646 }
1647 return (int)$liveDefaultId;
1648 }
1649
1650 /**
1651 * Removes empty values (null, '0', 0, false).
1652 *
1653 * @param int[] $ids
1654 * @return array
1655 */
1656 protected function sanitizeIds(array $ids): array
1657 {
1658 return array_filter($ids);
1659 }
1660
1661 /**
1662 * @param string $tableName
1663 * @param int[] $ids
1664 * @param array $sortingStatement
1665 * @return PlainDataResolver
1666 */
1667 protected function getResolver($tableName, array $ids, array $sortingStatement = null)
1668 {
1669 /** @var PlainDataResolver $resolver */
1670 $resolver = GeneralUtility::makeInstance(
1671 PlainDataResolver::class,
1672 $tableName,
1673 $ids,
1674 $sortingStatement
1675 );
1676 $resolver->setWorkspaceId($this->getWorkspaceId());
1677 $resolver->setKeepDeletePlaceholder(true);
1678 $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1679 return $resolver;
1680 }
1681
1682 /**
1683 * @param string $tableName
1684 * @return Connection
1685 */
1686 protected function getConnectionForTableName(string $tableName)
1687 {
1688 return GeneralUtility::makeInstance(ConnectionPool::class)
1689 ->getConnectionForTable($tableName);
1690 }
1691 }