[FEATURE] Introduce MM_oppositeUsage property
[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 * This array will be filled by getFromDB().
223 *
224 * @var array
225 */
226 public $results = array();
227
228 /**
229 * Initialization of the class.
230 *
231 * @param string $itemlist List of group/select items
232 * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list.
233 * @param string $MMtable Name of a MM table.
234 * @param integer $MMuid Local UID for MM lookup
235 * @param string $currentTable Current table name
236 * @param array $conf TCA configuration for current field
237 * @return void
238 * @todo Define visibility
239 */
240 public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = array()) {
241 // SECTION: MM reverse relations
242 $this->MM_is_foreign = (boolean)$conf['MM_opposite_field'];
243 $this->MM_oppositeField = $conf['MM_opposite_field'];
244 $this->MM_table_where = $conf['MM_table_where'];
245 $this->MM_hasUidField = $conf['MM_hasUidField'];
246 $this->MM_match_fields = is_array($conf['MM_match_fields']) ? $conf['MM_match_fields'] : array();
247 $this->MM_insert_fields = is_array($conf['MM_insert_fields']) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
248 $this->currentTable = $currentTable;
249 if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
250 $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
251 }
252 if ($this->MM_is_foreign) {
253 $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
254 // Normally, $conf['allowed'] can contain a list of tables,
255 // but as we are looking at a MM relation from the foreign side,
256 // it only makes sense to allow one one table in $conf['allowed']
257 $tmp = GeneralUtility::trimExplode(',', $tmp);
258 $this->MM_oppositeTable = $tmp[0];
259 unset($tmp);
260 // Only add the current table name if there is more than one allowed field
261 // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
262 $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
263 if ($this->MM_oppositeFieldConf['allowed']) {
264 $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
265 if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
266 $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
267 }
268 }
269 }
270 // SECTION: normal MM relations
271 // If the table list is "*" then all tables are used in the list:
272 if (trim($tablelist) === '*') {
273 $tablelist = implode(',', array_keys($GLOBALS['TCA']));
274 }
275 // The tables are traversed and internal arrays are initialized:
276 $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, TRUE);
277 foreach ($tempTableArray as $val) {
278 $tName = trim($val);
279 $this->tableArray[$tName] = array();
280 if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
281 $fieldN = $tName . '.' . $GLOBALS['TCA'][$tName]['ctrl']['delete'];
282 $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
283 }
284 }
285 if (is_array($this->tableArray)) {
286 reset($this->tableArray);
287 } else {
288 // No tables
289 return;
290 }
291 // Set first and second tables:
292 // Is the first table
293 $this->firstTable = key($this->tableArray);
294 next($this->tableArray);
295 // If the second table is set and the ID number is less than zero (later)
296 // then the record is regarded to come from the second table...
297 $this->secondTable = key($this->tableArray);
298 // Now, populate the internal itemArray and tableArray arrays:
299 // If MM, then call this function to do that:
300 if ($MMtable) {
301 if ($MMuid) {
302 $this->readMM($MMtable, $MMuid);
303 } else {
304 // Revert to readList() for new records in order to load possible default values from $itemlist
305 $this->readList($itemlist);
306 }
307 } elseif ($MMuid && $conf['foreign_field']) {
308 // If not MM but foreign_field, the read the records by the foreign_field
309 $this->readForeignField($MMuid, $conf);
310 } else {
311 // If not MM, then explode the itemlist by "," and traverse the list:
312 $this->readList($itemlist);
313 // Do automatic default_sortby, if any
314 if ($conf['foreign_default_sortby']) {
315 $this->sortList($conf['foreign_default_sortby']);
316 }
317 }
318 }
319
320 /**
321 * Magic setter method.
322 * Used for compatibility with changed attribute visibility
323 *
324 * @param string $name name of the attribute
325 * @param mixed $value value to set the attribute to
326 * @deprecated since 6.1, only required as compatibility layer for renamed attribute $fromTC
327 */
328 public function __set($name, $value) {
329 if($name === 'fromTC') {
330 GeneralUtility::deprecationLog(
331 '$fromTC is protected since TYPO3 6.1. Use setFetchAllFields() instead!'
332 );
333 $this->setFetchAllFields(!$value);
334 }
335 }
336
337 /**
338 * Sets $fetchAllFields
339 *
340 * @param boolean $allFields enables fetching of all fields in getFromDB()
341 */
342 public function setFetchAllFields($allFields) {
343 $this->fetchAllFields = (boolean)$allFields;
344 }
345
346 /**
347 * Sets whether the reference index shall be updated.
348 *
349 * @param boolean $updateReferenceIndex Whether the reference index shall be updated
350 * @return void
351 */
352 public function setUpdateReferenceIndex($updateReferenceIndex) {
353 $this->updateReferenceIndex = (boolean)$updateReferenceIndex;
354 }
355
356 /**
357 * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
358 *
359 * @param string $itemlist Item list
360 * @return void
361 * @todo Define visibility
362 */
363 public function readList($itemlist) {
364 if ((string) trim($itemlist) != '') {
365 $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
366 // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
367 // if there were spaces in the list... I suppose this is better overall...
368 foreach ($tempItemArray as $key => $val) {
369 // Will be set to "1" if the entry was a real table/id:
370 $isSet = 0;
371 // Extract table name and id. This is un the formular [tablename]_[id]
372 // where table name MIGHT contain "_", hence the reversion of the string!
373 $val = strrev($val);
374 $parts = explode('_', $val, 2);
375 $theID = strrev($parts[0]);
376 // Check that the id IS an integer:
377 if (MathUtility::canBeInterpretedAsInteger($theID)) {
378 // Get the table name: If a part of the exploded string, use that.
379 // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
380 $theTable = trim($parts[1])
381 ? strrev(trim($parts[1]))
382 : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
383 // If the ID is not blank and the table name is among the names in the inputted tableList
384 if (((string) $theID != '' && $theID) && $theTable && isset($this->tableArray[$theTable])) {
385 // Get ID as the right value:
386 $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
387 // Register ID/table name in internal arrays:
388 $this->itemArray[$key]['id'] = $theID;
389 $this->itemArray[$key]['table'] = $theTable;
390 $this->tableArray[$theTable][] = $theID;
391 // Set update-flag:
392 $isSet = 1;
393 }
394 }
395 // If it turns out that the value from the list was NOT a valid reference to a table-record,
396 // then we might still set it as a NO_TABLE value:
397 if (!$isSet && $this->registerNonTableValues) {
398 $this->itemArray[$key]['id'] = $tempItemArray[$key];
399 $this->itemArray[$key]['table'] = '_NO_TABLE';
400 $this->nonTableArray[] = $tempItemArray[$key];
401 }
402 }
403 }
404 }
405
406 /**
407 * Does a sorting on $this->itemArray depending on a default sortby field.
408 * This is only used for automatic sorting of comma separated lists.
409 * This function is only relevant for data that is stored in comma separated lists!
410 *
411 * @param string $sortby The default_sortby field/command (e.g. 'price DESC')
412 * @return void
413 * @todo Define visibility
414 */
415 public function sortList($sortby) {
416 // Sort directly without fetching addional data
417 if ($sortby == 'uid') {
418 usort(
419 $this->itemArray,
420 function ($a, $b) {
421 return $a['id'] < $b['id'] ? -1 : 1;
422 }
423 );
424 } elseif (count($this->tableArray) == 1) {
425 reset($this->tableArray);
426 $table = key($this->tableArray);
427 $uidList = implode(',', current($this->tableArray));
428 if ($uidList) {
429 $this->itemArray = array();
430 $this->tableArray = array();
431 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'uid IN (' . $uidList . ')', '', $sortby);
432 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
433 $this->itemArray[] = array('id' => $row['uid'], 'table' => $table);
434 $this->tableArray[$table][] = $row['uid'];
435 }
436 $GLOBALS['TYPO3_DB']->sql_free_result($res);
437 }
438 }
439 }
440
441 /**
442 * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
443 * You can call this function after start if you supply no list to start()
444 *
445 * @param string $tableName MM Tablename
446 * @param integer $uid Local UID
447 * @return void
448 * @todo Define visibility
449 */
450 public function readMM($tableName, $uid) {
451 $key = 0;
452 $additionalWhere = '';
453 $theTable = NULL;
454 // In case of a reverse relation
455 if ($this->MM_is_foreign) {
456 $uidLocal_field = 'uid_foreign';
457 $uidForeign_field = 'uid_local';
458 $sorting_field = 'sorting_foreign';
459 if ($this->MM_isMultiTableRelationship) {
460 $additionalWhere .= ' AND ( tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $tableName);
461 // Be backwards compatible! When allowing more than one table after
462 // having previously allowed only one table, this case applies.
463 if ($this->currentTable == $this->MM_isMultiTableRelationship) {
464 $additionalWhere .= ' OR tablenames=\'\'';
465 }
466 $additionalWhere .= ' ) ';
467 }
468 $theTable = $this->MM_oppositeTable;
469 } else {
470 // Default
471 $uidLocal_field = 'uid_local';
472 $uidForeign_field = 'uid_foreign';
473 $sorting_field = 'sorting';
474 }
475 if ($this->MM_table_where) {
476 $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
477 }
478 foreach ($this->MM_match_fields as $field => $value) {
479 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $tableName);
480 }
481 // Select all MM relations:
482 $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere;
483 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tableName, $where, '', $sorting_field);
484 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
485 // Default
486 if (!$this->MM_is_foreign) {
487 // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
488 $theTable = $row['tablenames'] ?: $this->firstTable;
489 }
490 if (($row[$uidForeign_field] || $theTable == 'pages') && $theTable && isset($this->tableArray[$theTable])) {
491 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
492 $this->itemArray[$key]['table'] = $theTable;
493 $this->tableArray[$theTable][] = $row[$uidForeign_field];
494 } elseif ($this->registerNonTableValues) {
495 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
496 $this->itemArray[$key]['table'] = '_NO_TABLE';
497 $this->nonTableArray[] = $row[$uidForeign_field];
498 }
499 $key++;
500 }
501 $GLOBALS['TYPO3_DB']->sql_free_result($res);
502 }
503
504 /**
505 * Writes the internal itemArray to MM table:
506 *
507 * @param string $MM_tableName MM table name
508 * @param integer $uid Local UID
509 * @param boolean $prependTableName If set, then table names will always be written.
510 * @return void
511 * @todo Define visibility
512 */
513 public function writeMM($MM_tableName, $uid, $prependTableName = FALSE) {
514 // In case of a reverse relation
515 if ($this->MM_is_foreign) {
516 $uidLocal_field = 'uid_foreign';
517 $uidForeign_field = 'uid_local';
518 $sorting_field = 'sorting_foreign';
519 } else {
520 // default
521 $uidLocal_field = 'uid_local';
522 $uidForeign_field = 'uid_foreign';
523 $sorting_field = 'sorting';
524 }
525 // If there are tables...
526 $tableC = count($this->tableArray);
527 if ($tableC) {
528 // Boolean: does the field "tablename" need to be filled?
529 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
530 $c = 0;
531 $additionalWhere_tablenames = '';
532 if ($this->MM_is_foreign && $prep) {
533 $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
534 }
535 $additionalWhere = '';
536 // Add WHERE clause if configured
537 if ($this->MM_table_where) {
538 $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
539 }
540 // Select, update or delete only those relations that match the configured fields
541 foreach ($this->MM_match_fields as $field => $value) {
542 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
543 }
544 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
545 $uidForeign_field . ($prep ? ', tablenames' : '') . ($this->MM_hasUidField ? ', uid' : ''),
546 $MM_tableName,
547 $uidLocal_field . '=' . $uid . $additionalWhere_tablenames . $additionalWhere,
548 '',
549 $sorting_field
550 );
551 $oldMMs = array();
552 // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
553 // If the UID is present it will be used to update sorting and delete MM-records.
554 // This is necessary if the "multiple" feature is used for the MM relations.
555 // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
556 $oldMMs_inclUid = array();
557 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
558 if (!$this->MM_is_foreign && $prep) {
559 $oldMMs[] = array($row['tablenames'], $row[$uidForeign_field]);
560 } else {
561 $oldMMs[] = $row[$uidForeign_field];
562 }
563 $oldMMs_inclUid[] = array($row['tablenames'], $row[$uidForeign_field], $row['uid']);
564 }
565 $GLOBALS['TYPO3_DB']->sql_free_result($res);
566 // For each item, insert it:
567 foreach ($this->itemArray as $val) {
568 $c++;
569 if ($prep || $val['table'] == '_NO_TABLE') {
570 // Insert current table if needed
571 if ($this->MM_is_foreign) {
572 $tablename = $this->currentTable;
573 } else {
574 $tablename = $val['table'];
575 }
576 } else {
577 $tablename = '';
578 }
579 if (!$this->MM_is_foreign && $prep) {
580 $item = array($val['table'], $val['id']);
581 } else {
582 $item = $val['id'];
583 }
584 if (in_array($item, $oldMMs)) {
585 $oldMMs_index = array_search($item, $oldMMs);
586 // In principle, selecting on the UID is all we need to do
587 // if a uid field is available since that is unique!
588 // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
589 $whereClause = $uidLocal_field . '=' . $uid . ' AND ' . $uidForeign_field . '=' . $val['id']
590 . ($this->MM_hasUidField ? ' AND uid=' . (int)$oldMMs_inclUid[$oldMMs_index][2] : '');
591 if ($tablename) {
592 $whereClause .= ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tablename, $MM_tableName);
593 }
594 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $whereClause . $additionalWhere, array($sorting_field => $c));
595 // Remove the item from the $oldMMs array so after this
596 // foreach loop only the ones that need to be deleted are in there.
597 unset($oldMMs[$oldMMs_index]);
598 // Remove the item from the $oldMMs_inclUid array so after this
599 // foreach loop only the ones that need to be deleted are in there.
600 unset($oldMMs_inclUid[$oldMMs_index]);
601 } else {
602 $insertFields = $this->MM_insert_fields;
603 $insertFields[$uidLocal_field] = $uid;
604 $insertFields[$uidForeign_field] = $val['id'];
605 $insertFields[$sorting_field] = $c;
606 if ($tablename) {
607 $insertFields['tablenames'] = $tablename;
608 $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
609 }
610 $GLOBALS['TYPO3_DB']->exec_INSERTquery($MM_tableName, $insertFields);
611 if ($this->MM_is_foreign) {
612 $this->updateRefIndex($val['table'], $val['id']);
613 }
614 }
615 }
616 // Delete all not-used relations:
617 if (is_array($oldMMs) && count($oldMMs) > 0) {
618 $removeClauses = array();
619 $updateRefIndex_records = array();
620 foreach ($oldMMs as $oldMM_key => $mmItem) {
621 // If UID field is present, of course we need only use that for deleting.
622 if ($this->MM_hasUidField) {
623 $removeClauses[] = 'uid=' . (int)$oldMMs_inclUid[$oldMM_key][2];
624 } else {
625 if (is_array($mmItem)) {
626 $removeClauses[] = 'tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mmItem[0], $MM_tableName)
627 . ' AND ' . $uidForeign_field . '=' . $mmItem[1];
628 } else {
629 $removeClauses[] = $uidForeign_field . '=' . $mmItem;
630 }
631 }
632 if ($this->MM_is_foreign) {
633 if (is_array($mmItem)) {
634 $updateRefIndex_records[] = array($mmItem[0], $mmItem[1]);
635 } else {
636 $updateRefIndex_records[] = array($this->firstTable, $mmItem);
637 }
638 }
639 }
640 $deleteAddWhere = ' AND (' . implode(' OR ', $removeClauses) . ')';
641 $where = $uidLocal_field . '=' . (int)$uid . $deleteAddWhere . $additionalWhere_tablenames . $additionalWhere;
642 $GLOBALS['TYPO3_DB']->exec_DELETEquery($MM_tableName, $where);
643 // Update ref index:
644 foreach ($updateRefIndex_records as $pair) {
645 $this->updateRefIndex($pair[0], $pair[1]);
646 }
647 }
648 // Update ref index; In tcemain it is not certain that this will happen because
649 // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
650 // This could also have been fixed in updateDB in tcemain, however I decided to do it here ...
651 $this->updateRefIndex($this->currentTable, $uid);
652 }
653 }
654
655 /**
656 * Remaps MM table elements from one local uid to another
657 * Does NOT update the reference index for you, must be called subsequently to do that!
658 *
659 * @param string $MM_tableName MM table name
660 * @param integer $uid Local, current UID
661 * @param integer $newUid Local, new UID
662 * @param boolean $prependTableName If set, then table names will always be written.
663 * @return void
664 * @todo Define visibility
665 */
666 public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = FALSE) {
667 // In case of a reverse relation
668 if ($this->MM_is_foreign) {
669 $uidLocal_field = 'uid_foreign';
670 } else {
671 // default
672 $uidLocal_field = 'uid_local';
673 }
674 // If there are tables...
675 $tableC = count($this->tableArray);
676 if ($tableC) {
677 // Boolean: does the field "tablename" need to be filled?
678 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
679 $additionalWhere_tablenames = '';
680 if ($this->MM_is_foreign && $prep) {
681 $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
682 }
683 $additionalWhere = '';
684 // Add WHERE clause if configured
685 if ($this->MM_table_where) {
686 $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
687 }
688 // Select, update or delete only those relations that match the configured fields
689 foreach ($this->MM_match_fields as $field => $value) {
690 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
691 }
692 $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere_tablenames . $additionalWhere;
693 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $where, array($uidLocal_field => $newUid));
694 }
695 }
696
697 /**
698 * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and
699 * stores the parts in the internal array itemArray and tableArray.
700 *
701 * @param integer $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field)
702 * @param array $conf TCA configuration for current field
703 * @return void
704 * @todo Define visibility
705 */
706 public function readForeignField($uid, $conf) {
707 $key = 0;
708 $uid = (int)$uid;
709 $foreign_table = $conf['foreign_table'];
710 $foreign_table_field = $conf['foreign_table_field'];
711 $useDeleteClause = !$this->undeleteRecord;
712 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
713 // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
714 if ($conf['symmetric_field']) {
715 $whereClause = '(' . $conf['foreign_field'] . '=' . $uid . ' OR ' . $conf['symmetric_field'] . '=' . $uid . ')';
716 } else {
717 $whereClause = $conf['foreign_field'] . '=' . $uid;
718 }
719 // Use the deleteClause (e.g. "deleted=0") on this table
720 if ($useDeleteClause) {
721 $whereClause .= BackendUtility::deleteClause($foreign_table);
722 }
723 // If it's requested to look for the parent uid AND the parent table,
724 // add an additional SQL-WHERE clause
725 if ($foreign_table_field && $this->currentTable) {
726 $whereClause .= ' AND ' . $foreign_table_field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $foreign_table);
727 }
728 // Add additional where clause if foreign_match_fields are defined
729 foreach ($foreign_match_fields as $field => $value) {
730 $whereClause .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $foreign_table);
731 }
732 // Select children in the same workspace:
733 if (BackendUtility::isTableWorkspaceEnabled($this->currentTable) && BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
734 $currentRecord = BackendUtility::getRecord($this->currentTable, $uid, 't3ver_wsid', '', $useDeleteClause);
735 $whereClause .= BackendUtility::getWorkspaceWhereClause($foreign_table, $currentRecord['t3ver_wsid']);
736 }
737 // Get the correct sorting field
738 // Specific manual sortby for data handled by this field
739 $sortby = '';
740 if ($conf['foreign_sortby']) {
741 if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
742 // Sorting depends on, from which side of the relation we're looking at it
743 $sortby = '
744 CASE
745 WHEN ' . $conf['foreign_field'] . '=' . $uid . '
746 THEN ' . $conf['foreign_sortby'] . '
747 ELSE ' . $conf['symmetric_sortby'] . '
748 END';
749 } else {
750 // Regular single-side behaviour
751 $sortby = $conf['foreign_sortby'];
752 }
753 } elseif ($conf['foreign_default_sortby']) {
754 // Specific default sortby for data handled by this field
755 $sortby = $conf['foreign_default_sortby'];
756 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
757 // Manual sortby for all table records
758 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
759 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
760 // Default sortby for all table records
761 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
762 }
763 // Strip a possible "ORDER BY" in front of the $sortby value
764 $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
765 // Get the rows from storage
766 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', $foreign_table, $whereClause, '', $sortby);
767 if (count($rows)) {
768 foreach ($rows as $row) {
769 $this->itemArray[$key]['id'] = $row['uid'];
770 $this->itemArray[$key]['table'] = $foreign_table;
771 $this->tableArray[$foreign_table][] = $row['uid'];
772 $key++;
773 }
774 }
775 }
776
777 /**
778 * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record)
779 *
780 * @param array $conf TCA configuration for current field
781 * @param integer $parentUid The uid of the parent record
782 * @param integer $updateToUid If this is larger than zero it will be used as foreign UID instead of the given $parentUid (on Copy)
783 * @param boolean $skipSorting Do not update the sorting columns, this could happen for imported values
784 * @return void
785 * @todo Define visibility
786 */
787 public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = FALSE) {
788 $c = 0;
789 $foreign_table = $conf['foreign_table'];
790 $foreign_field = $conf['foreign_field'];
791 $symmetric_field = $conf['symmetric_field'];
792 $foreign_table_field = $conf['foreign_table_field'];
793 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
794 // If there are table items and we have a proper $parentUid
795 if (MathUtility::canBeInterpretedAsInteger($parentUid) && count($this->tableArray)) {
796 // If updateToUid is not a positive integer, set it to '0', so it will be ignored
797 if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
798 $updateToUid = 0;
799 }
800 $considerWorkspaces = $GLOBALS['BE_USER']->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreign_table);
801 $fields = 'uid,' . $foreign_field;
802 // Consider the symmetric field if defined:
803 if ($symmetric_field) {
804 $fields .= ',' . $symmetric_field;
805 }
806 // Consider workspaces if defined and currently used:
807 if ($considerWorkspaces) {
808 $fields .= ',' . 't3ver_state,t3ver_oid';
809 }
810 // Update all items
811 foreach ($this->itemArray as $val) {
812 $uid = $val['id'];
813 $table = $val['table'];
814 $row = array();
815 // Fetch the current (not overwritten) relation record if we should handle symmetric relations
816 if ($symmetric_field || $considerWorkspaces) {
817 $row = BackendUtility::getRecord($table, $uid, $fields, '', FALSE);
818 }
819 $isOnSymmetricSide = FALSE;
820 if ($symmetric_field) {
821 $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
822 }
823 $updateValues = $foreign_match_fields;
824 $workspaceValues = array();
825 // No update to the uid is requested, so this is the normal behaviour
826 // just update the fields and care about sorting
827 if (!$updateToUid) {
828 // Always add the pointer to the parent uid
829 if ($isOnSymmetricSide) {
830 $updateValues[$symmetric_field] = $parentUid;
831 } else {
832 $updateValues[$foreign_field] = $parentUid;
833 }
834 // If it is configured in TCA also to store the parent table in the child record, just do it
835 if ($foreign_table_field && $this->currentTable) {
836 $updateValues[$foreign_table_field] = $this->currentTable;
837 }
838 // Update sorting columns if not to be skipped
839 if (!$skipSorting) {
840 // Get the correct sorting field
841 // Specific manual sortby for data handled by this field
842 $sortby = '';
843 if ($conf['foreign_sortby']) {
844 $sortby = $conf['foreign_sortby'];
845 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
846 // manual sortby for all table records
847 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
848 }
849 // Apply sorting on the symmetric side
850 // (it depends on who created the relation, so what uid is in the symmetric_field):
851 if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
852 $sortby = $conf['symmetric_sortby'];
853 } else {
854 $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
855 }
856 if ($sortby) {
857 $updateValues[$sortby] = ($workspaceValues[$sortby] = ++$c);
858 }
859 }
860 } else {
861 if ($isOnSymmetricSide) {
862 $updateValues[$symmetric_field] = $updateToUid;
863 } else {
864 $updateValues[$foreign_field] = $updateToUid;
865 }
866 }
867 // Update accordant fields in the database:
868 if (count($updateValues)) {
869 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateValues);
870 $this->updateRefIndex($table, $uid);
871 }
872 // Update accordant fields in the database for workspaces overlays/placeholders:
873 if (count($workspaceValues) && $considerWorkspaces) {
874 if (
875 isset($row['t3ver_oid'])
876 && $row['t3ver_oid']
877 && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)
878 ) {
879 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$row['t3ver_oid'], $workspaceValues);
880 }
881 }
882 }
883 }
884 }
885
886 /**
887 * After initialization you can extract an array of the elements from the object. Use this function for that.
888 *
889 * @param boolean $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
890 * @return array A numeric array.
891 * @todo Define visibility
892 */
893 public function getValueArray($prependTableName = FALSE) {
894 // INIT:
895 $valueArray = array();
896 $tableC = count($this->tableArray);
897 // If there are tables in the table array:
898 if ($tableC) {
899 // If there are more than ONE table in the table array, then always prepend table names:
900 $prep = $tableC > 1 || $prependTableName;
901 // Traverse the array of items:
902 foreach ($this->itemArray as $val) {
903 $valueArray[] = ($prep && $val['table'] != '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
904 }
905 }
906 // Return the array
907 return $valueArray;
908 }
909
910 /**
911 * Converts id numbers from negative to positive.
912 *
913 * @param array $valueArray Array of [table]_[id] pairs.
914 * @param string $fTable Foreign table (the one used for positive numbers)
915 * @param string $nfTable Negative foreign table
916 * @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.
917 * @todo Define visibility
918 */
919 public function convertPosNeg($valueArray, $fTable, $nfTable) {
920 if (is_array($valueArray) && $fTable) {
921 foreach ($valueArray as $key => $val) {
922 $val = strrev($val);
923 $parts = explode('_', $val, 2);
924 $theID = strrev($parts[0]);
925 $theTable = strrev($parts[1]);
926 if (MathUtility::canBeInterpretedAsInteger($theID)
927 && (!$theTable || $theTable === (string)$fTable || $theTable === (string)$nfTable)
928 ) {
929 $valueArray[$key] = $theTable && $theTable !== (string)$fTable ? $theID * -1 : $theID;
930 }
931 }
932 }
933 return $valueArray;
934 }
935
936 /**
937 * Reads all records from internal tableArray into the internal ->results array
938 * where keys are table names and for each table, records are stored with uids as their keys.
939 * If $this->fetchAllFields is false you can save a little memory
940 * since only uid,pid and a few other fields are selected.
941 *
942 * @return array
943 * @todo Define visibility
944 */
945 public function getFromDB() {
946 // Traverses the tables listed:
947 foreach ($this->tableArray as $key => $val) {
948 if (is_array($val)) {
949 $itemList = implode(',', $val);
950 if ($itemList) {
951 if ($this->fetchAllFields) {
952 $from = '*';
953 } else {
954 $from = 'uid,pid';
955 if ($GLOBALS['TCA'][$key]['ctrl']['label']) {
956 // Titel
957 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label'];
958 }
959 if ($GLOBALS['TCA'][$key]['ctrl']['label_alt']) {
960 // Alternative Title-Fields
961 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label_alt'];
962 }
963 if ($GLOBALS['TCA'][$key]['ctrl']['thumbnail']) {
964 // Thumbnail
965 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['thumbnail'];
966 }
967 }
968 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($from, $key, 'uid IN (' . $itemList . ')' . $this->additionalWhere[$key]);
969 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
970 $this->results[$key][$row['uid']] = $row;
971 }
972 $GLOBALS['TYPO3_DB']->sql_free_result($res);
973 }
974 }
975 }
976 return $this->results;
977 }
978
979 /**
980 * Prepare items from itemArray to be transferred to the TCEforms interface (as a comma list)
981 *
982 * @return string
983 * @todo Define visibility
984 */
985 public function readyForInterface() {
986 if (!is_array($this->itemArray)) {
987 return FALSE;
988 }
989 $output = array();
990 $titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
991 foreach ($this->itemArray as $val) {
992 $theRow = $this->results[$val['table']][$val['id']];
993 if ($theRow && is_array($GLOBALS['TCA'][$val['table']])) {
994 $label = GeneralUtility::fixed_lgd_cs(strip_tags(
995 BackendUtility::getRecordTitle($val['table'], $theRow)), $titleLen);
996 $label = $label ? $label : '[...]';
997 $output[] = str_replace(',', '', $val['table'] . '_' . $val['id'] . '|' . rawurlencode($label));
998 }
999 }
1000 return implode(',', $output);
1001 }
1002
1003 /**
1004 * Counts the items in $this->itemArray and puts this value in an array by default.
1005 *
1006 * @param boolean $returnAsArray Whether to put the count value in an array
1007 * @return mixed The plain count as integer or the same inside an array
1008 * @todo Define visibility
1009 */
1010 public function countItems($returnAsArray = TRUE) {
1011 $count = count($this->itemArray);
1012 if ($returnAsArray) {
1013 $count = array($count);
1014 }
1015 return $count;
1016 }
1017
1018 /**
1019 * Update Reference Index (sys_refindex) for a record
1020 * Should be called any almost any update to a record which could affect references inside the record.
1021 * (copied from TCEmain)
1022 *
1023 * @param string $table Table name
1024 * @param integer $id Record UID
1025 * @return array Information concerning modifications delivered by \TYPO3\CMS\Core\Database\ReferenceIndex::updateRefIndexTable()
1026 * @todo Define visibility
1027 */
1028 public function updateRefIndex($table, $id) {
1029 $statisticsArray = array();
1030 if ($this->updateReferenceIndex) {
1031 /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
1032 $refIndexObj = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
1033 $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1034 }
1035 return $statisticsArray;
1036 }
1037
1038 /**
1039 * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation.
1040 *
1041 * @param string $parentUid The uid of the parent record
1042 * @param array $parentConf The TCA configuration of the parent field embedding the child records
1043 * @param array $childRec The record row of the child record
1044 * @return boolean Returns TRUE if looking from the symmetric ("other") side to the relation.
1045 * @todo Define visibility
1046 */
1047 public function isOnSymmetricSide($parentUid, $parentConf, $childRec) {
1048 return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1049 && $parentConf['symmetric_field']
1050 && $parentUid == $childRec[$parentConf['symmetric_field']];
1051 }
1052
1053 /**
1054 * Completes MM values to be written by values from the opposite relation.
1055 * This method used MM insert field or MM match fields if defined.
1056 *
1057 * @param string $tableName Name of the opposite table
1058 * @param array $referenceValues Values to be written
1059 * @return array Values to be written, possibly modified
1060 */
1061 protected function completeOppositeUsageValues($tableName, array $referenceValues) {
1062 if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1063 return $referenceValues;
1064 }
1065
1066 $fieldName = $this->MM_oppositeUsage[$tableName][0];
1067 if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1068 return $referenceValues;
1069 }
1070
1071 $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1072 if (!empty($configuration['MM_insert_fields'])) {
1073 $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1074 } elseif (!empty($configuration['MM_match_fields'])) {
1075 $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1076 }
1077
1078 return $referenceValues;
1079 }
1080
1081 }