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