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