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