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