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