[BUGFIX] SqlParser: trim all kinds of whitespaces
[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-2011 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 textfile 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 * Contains class for loading database groups
31 *
32 * Revised for TYPO3 3.6 September/2003 by Kasper Skårhøj
33 *
34 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
35 */
36 /**
37 * Load database groups (relations)
38 * Used to process the relations created by the TCA element types "group" and "select" for database records. Manages MM-relations as well.
39 *
40 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
41 */
42 class RelationHandler {
43
44 // External, static
45 // Means that only uid and the label-field is returned
46 /**
47 * @todo Define visibility
48 */
49 public $fromTC = 1;
50
51 // If set, values that are not ids in tables are normally discarded. By this options they will be preserved.
52 /**
53 * @todo Define visibility
54 */
55 public $registerNonTableValues = 0;
56
57 // Internal, dynamic:
58 // Contains the table names as keys. The values are the id-values for each table. Should ONLY contain proper table names.
59 /**
60 * @todo Define visibility
61 */
62 public $tableArray = array();
63
64 // Contains items in an numeric array (table/id for each). Tablenames here might be "_NO_TABLE"
65 /**
66 * @todo Define visibility
67 */
68 public $itemArray = array();
69
70 // Array for NON-table elements
71 /**
72 * @todo Define visibility
73 */
74 public $nonTableArray = array();
75
76 /**
77 * @todo Define visibility
78 */
79 public $additionalWhere = array();
80
81 // Deleted-column is added to additionalWhere... if this is set...
82 /**
83 * @todo Define visibility
84 */
85 public $checkIfDeleted = 1;
86
87 /**
88 * @todo Define visibility
89 */
90 public $dbPaths = array();
91
92 // Will contain the first table name in the $tablelist (for positive ids)
93 /**
94 * @todo Define visibility
95 */
96 public $firstTable = '';
97
98 // Will contain the second table name in the $tablelist (for negative ids)
99 /**
100 * @todo Define visibility
101 */
102 public $secondTable = '';
103
104 // private
105 // Boolean - if 1, uid_local and uid_foreign are switched, and the current table is inserted as tablename - this means you display a foreign relation "from the opposite side"
106 /**
107 * @todo Define visibility
108 */
109 public $MM_is_foreign = 0;
110
111 // Field name at the "local" side of the MM relation
112 /**
113 * @todo Define visibility
114 */
115 public $MM_oppositeField = '';
116
117 // Only set if MM_is_foreign is set
118 /**
119 * @todo Define visibility
120 */
121 public $MM_oppositeTable = '';
122
123 // Only set if MM_is_foreign is set
124 /**
125 * @todo Define visibility
126 */
127 public $MM_oppositeFieldConf = '';
128
129 // Is empty by default; if MM_is_foreign is set and there is more than one table allowed (on the "local" side), then it contains the first table (as a fallback)
130 /**
131 * @todo Define visibility
132 */
133 public $MM_isMultiTableRelationship = 0;
134
135 // Current table => Only needed for reverse relations
136 /**
137 * @todo Define visibility
138 */
139 public $currentTable;
140
141 // If a record should be undeleted (so do not use the $useDeleteClause on t3lib_BEfunc)
142 /**
143 * @todo Define visibility
144 */
145 public $undeleteRecord;
146
147 // Array of fields value pairs that should match while SELECT and will be written into MM table if $MM_insert_fields is not set
148 /**
149 * @todo Define visibility
150 */
151 public $MM_match_fields = array();
152
153 // Array of fields and value pairs used for insert in MM table
154 /**
155 * @todo Define visibility
156 */
157 public $MM_insert_fields = array();
158
159 // Extra MM table where
160 /**
161 * @todo Define visibility
162 */
163 public $MM_table_where = '';
164
165 /**
166 * @var boolean
167 */
168 protected $updateReferenceIndex = TRUE;
169
170 /**
171 * Initialization of the class.
172 *
173 * @param string $itemlist List of group/select items
174 * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list.
175 * @param string $MMtable Name of a MM table.
176 * @param integer $MMuid Local UID for MM lookup
177 * @param string $currentTable Current table name
178 * @param integer $conf TCA configuration for current field
179 * @return void
180 * @todo Define visibility
181 */
182 public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = array()) {
183 // SECTION: MM reverse relations
184 $this->MM_is_foreign = $conf['MM_opposite_field'] ? 1 : 0;
185 $this->MM_oppositeField = $conf['MM_opposite_field'];
186 $this->MM_table_where = $conf['MM_table_where'];
187 $this->MM_hasUidField = $conf['MM_hasUidField'];
188 $this->MM_match_fields = is_array($conf['MM_match_fields']) ? $conf['MM_match_fields'] : array();
189 $this->MM_insert_fields = is_array($conf['MM_insert_fields']) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
190 $this->currentTable = $currentTable;
191 if ($this->MM_is_foreign) {
192 $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
193 // Normally, $conf['allowed'] can contain a list of tables, but as we are looking at a MM relation from the foreign side, it only makes sense to allow one one table in $conf['allowed']
194 $tmp = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $tmp);
195 $this->MM_oppositeTable = $tmp[0];
196 unset($tmp);
197 // Only add the current table name if there is more than one allowed field
198 \TYPO3\CMS\Core\Utility\GeneralUtility::loadTCA($this->MM_oppositeTable);
199 // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
200 $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
201 if ($this->MM_oppositeFieldConf['allowed']) {
202 $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
203 if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
204 $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
205 }
206 }
207 }
208 // SECTION: normal MM relations
209 // If the table list is "*" then all tables are used in the list:
210 if (!strcmp(trim($tablelist), '*')) {
211 $tablelist = implode(',', array_keys($GLOBALS['TCA']));
212 }
213 // The tables are traversed and internal arrays are initialized:
214 $tempTableArray = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $tablelist, 1);
215 foreach ($tempTableArray as $key => $val) {
216 $tName = trim($val);
217 $this->tableArray[$tName] = array();
218 if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
219 $fieldN = $tName . '.' . $GLOBALS['TCA'][$tName]['ctrl']['delete'];
220 $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
221 }
222 }
223 if (is_array($this->tableArray)) {
224 reset($this->tableArray);
225 } else {
226 return 'No tables!';
227 }
228 // Set first and second tables:
229 // Is the first table
230 $this->firstTable = key($this->tableArray);
231 next($this->tableArray);
232 // If the second table is set and the ID number is less than zero (later) then the record is regarded to come from the second table...
233 $this->secondTable = key($this->tableArray);
234 // Now, populate the internal itemArray and tableArray arrays:
235 // If MM, then call this function to do that:
236 if ($MMtable) {
237 if ($MMuid) {
238 $this->readMM($MMtable, $MMuid);
239 } else {
240 // Revert to readList() for new records in order to load possible default values from $itemlist
241 $this->readList($itemlist);
242 }
243 } elseif ($MMuid && $conf['foreign_field']) {
244 // If not MM but foreign_field, the read the records by the foreign_field
245 $this->readForeignField($MMuid, $conf);
246 } else {
247 // If not MM, then explode the itemlist by "," and traverse the list:
248 $this->readList($itemlist);
249 // Do automatic default_sortby, if any
250 if ($conf['foreign_default_sortby']) {
251 $this->sortList($conf['foreign_default_sortby']);
252 }
253 }
254 }
255
256 /**
257 * Sets whether the reference index shall be updated.
258 *
259 * @param boolean $updateReferenceIndex Whether the reference index shall be updated
260 * @return void
261 */
262 public function setUpdateReferenceIndex($updateReferenceIndex) {
263 $this->updateReferenceIndex = (bool) $updateReferenceIndex;
264 }
265
266 /**
267 * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
268 *
269 * @param string $itemlist Item list
270 * @return void
271 * @todo Define visibility
272 */
273 public function readList($itemlist) {
274 if ((string) trim($itemlist) != '') {
275 $tempItemArray = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $itemlist);
276 // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work if there were spaces in the list... I suppose this is better overall...
277 foreach ($tempItemArray as $key => $val) {
278 // Will be set to "1" if the entry was a real table/id:
279 $isSet = 0;
280 // Extract table name and id. This is un the formular [tablename]_[id] where table name MIGHT contain "_", hence the reversion of the string!
281 $val = strrev($val);
282 $parts = explode('_', $val, 2);
283 $theID = strrev($parts[0]);
284 // Check that the id IS an integer:
285 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($theID)) {
286 // Get the table name: If a part of the exploded string, use that. Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
287 $theTable = trim($parts[1]) ? strrev(trim($parts[1])) : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
288 // If the ID is not blank and the table name is among the names in the inputted tableList, then proceed:
289 if (((string) $theID != '' && $theID) && $theTable && isset($this->tableArray[$theTable])) {
290 // Get ID as the right value:
291 $theID = $this->secondTable ? abs(intval($theID)) : intval($theID);
292 // Register ID/table name in internal arrays:
293 $this->itemArray[$key]['id'] = $theID;
294 $this->itemArray[$key]['table'] = $theTable;
295 $this->tableArray[$theTable][] = $theID;
296 // Set update-flag:
297 $isSet = 1;
298 }
299 }
300 // If it turns out that the value from the list was NOT a valid reference to a table-record, then we might still set it as a NO_TABLE value:
301 if (!$isSet && $this->registerNonTableValues) {
302 $this->itemArray[$key]['id'] = $tempItemArray[$key];
303 $this->itemArray[$key]['table'] = '_NO_TABLE';
304 $this->nonTableArray[] = $tempItemArray[$key];
305 }
306 }
307 }
308 }
309
310 /**
311 * Does a sorting on $this->itemArray depending on a default sortby field.
312 * This is only used for automatic sorting of comma separated lists.
313 * This function is only relevant for data that is stored in comma separated lists!
314 *
315 * @param string $sortby The default_sortby field/command (e.g. 'price DESC')
316 * @return void
317 * @todo Define visibility
318 */
319 public function sortList($sortby) {
320 // Sort directly without fetching addional data
321 if ($sortby == 'uid') {
322 usort($this->itemArray, create_function('$a,$b', 'return $a["id"] < $b["id"] ? -1 : 1;'));
323 } elseif (count($this->tableArray) == 1) {
324 reset($this->tableArray);
325 $table = key($this->tableArray);
326 $uidList = implode(',', current($this->tableArray));
327 if ($uidList) {
328 $this->itemArray = array();
329 $this->tableArray = array();
330 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'uid IN (' . $uidList . ')', '', $sortby);
331 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
332 $this->itemArray[] = array('id' => $row['uid'], 'table' => $table);
333 $this->tableArray[$table][] = $row['uid'];
334 }
335 $GLOBALS['TYPO3_DB']->sql_free_result($res);
336 }
337 }
338 }
339
340 /**
341 * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
342 * You can call this function after start if you supply no list to start()
343 *
344 * @param string $tableName MM Tablename
345 * @param integer $uid Local UID
346 * @return void
347 * @todo Define visibility
348 */
349 public function readMM($tableName, $uid) {
350 $key = 0;
351 $additionalWhere = '';
352 // In case of a reverse relation
353 if ($this->MM_is_foreign) {
354 $uidLocal_field = 'uid_foreign';
355 $uidForeign_field = 'uid_local';
356 $sorting_field = 'sorting_foreign';
357 if ($this->MM_isMultiTableRelationship) {
358 $additionalWhere .= ' AND ( tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $tableName);
359 // Be backwards compatible! When allowing more than one table after having previously allowed only one table, this case applies.
360 if ($this->currentTable == $this->MM_isMultiTableRelationship) {
361 $additionalWhere .= ' OR tablenames=\'\'';
362 }
363 $additionalWhere .= ' ) ';
364 }
365 $theTable = $this->MM_oppositeTable;
366 } else {
367 // Default
368 $uidLocal_field = 'uid_local';
369 $uidForeign_field = 'uid_foreign';
370 $sorting_field = 'sorting';
371 }
372 if ($this->MM_table_where) {
373 $additionalWhere .= LF . str_replace('###THIS_UID###', intval($uid), $this->MM_table_where);
374 }
375 foreach ($this->MM_match_fields as $field => $value) {
376 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $tableName);
377 }
378 // Select all MM relations:
379 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tableName, $uidLocal_field . '=' . intval($uid) . $additionalWhere, '', $sorting_field);
380 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
381 // Default
382 if (!$this->MM_is_foreign) {
383 // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
384 $theTable = $row['tablenames'] ? $row['tablenames'] : $this->firstTable;
385 }
386 if (($row[$uidForeign_field] || $theTable == 'pages') && $theTable && isset($this->tableArray[$theTable])) {
387 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
388 $this->itemArray[$key]['table'] = $theTable;
389 $this->tableArray[$theTable][] = $row[$uidForeign_field];
390 } elseif ($this->registerNonTableValues) {
391 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
392 $this->itemArray[$key]['table'] = '_NO_TABLE';
393 $this->nonTableArray[] = $row[$uidForeign_field];
394 }
395 $key++;
396 }
397 $GLOBALS['TYPO3_DB']->sql_free_result($res);
398 }
399
400 /**
401 * Writes the internal itemArray to MM table:
402 *
403 * @param string $MM_tableName MM table name
404 * @param integer $uid Local UID
405 * @param boolean $prependTableName If set, then table names will always be written.
406 * @return void
407 * @todo Define visibility
408 */
409 public function writeMM($MM_tableName, $uid, $prependTableName = 0) {
410 // In case of a reverse relation
411 if ($this->MM_is_foreign) {
412 $uidLocal_field = 'uid_foreign';
413 $uidForeign_field = 'uid_local';
414 $sorting_field = 'sorting_foreign';
415 } else {
416 // default
417 $uidLocal_field = 'uid_local';
418 $uidForeign_field = 'uid_foreign';
419 $sorting_field = 'sorting';
420 }
421 // If there are tables...
422 $tableC = count($this->tableArray);
423 if ($tableC) {
424 // Boolean: does the field "tablename" need to be filled?
425 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
426 $c = 0;
427 $additionalWhere_tablenames = '';
428 if ($this->MM_is_foreign && $prep) {
429 $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
430 }
431 $additionalWhere = '';
432 // Add WHERE clause if configured
433 if ($this->MM_table_where) {
434 $additionalWhere .= LF . str_replace('###THIS_UID###', intval($uid), $this->MM_table_where);
435 }
436 // Select, update or delete only those relations that match the configured fields
437 foreach ($this->MM_match_fields as $field => $value) {
438 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
439 }
440 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($uidForeign_field . ($prep ? ', tablenames' : '') . ($this->MM_hasUidField ? ', uid' : ''), $MM_tableName, $uidLocal_field . '=' . $uid . $additionalWhere_tablenames . $additionalWhere, '', $sorting_field);
441 $oldMMs = array();
442 // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
443 // If the UID is present it will be used to update sorting and delete MM-records.
444 // This is necessary if the "multiple" feature is used for the MM relations.
445 // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
446 $oldMMs_inclUid = array();
447 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
448 if (!$this->MM_is_foreign && $prep) {
449 $oldMMs[] = array($row['tablenames'], $row[$uidForeign_field]);
450 } else {
451 $oldMMs[] = $row[$uidForeign_field];
452 }
453 $oldMMs_inclUid[] = array($row['tablenames'], $row[$uidForeign_field], $row['uid']);
454 }
455 $GLOBALS['TYPO3_DB']->sql_free_result($res);
456 // For each item, insert it:
457 foreach ($this->itemArray as $val) {
458 $c++;
459 if ($prep || $val['table'] == '_NO_TABLE') {
460 // Insert current table if needed
461 if ($this->MM_is_foreign) {
462 $tablename = $this->currentTable;
463 } else {
464 $tablename = $val['table'];
465 }
466 } else {
467 $tablename = '';
468 }
469 if (!$this->MM_is_foreign && $prep) {
470 $item = array($val['table'], $val['id']);
471 } else {
472 $item = $val['id'];
473 }
474 if (in_array($item, $oldMMs)) {
475 $oldMMs_index = array_search($item, $oldMMs);
476 // In principle, selecting on the UID is all we need to do if a uid field is available since that is unique! But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
477 $whereClause = $uidLocal_field . '=' . $uid . ' AND ' . $uidForeign_field . '=' . $val['id'] . ($this->MM_hasUidField ? ' AND uid=' . intval($oldMMs_inclUid[$oldMMs_index][2]) : '');
478 if ($tablename) {
479 $whereClause .= ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tablename, $MM_tableName);
480 }
481 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $whereClause . $additionalWhere, array($sorting_field => $c));
482 // Remove the item from the $oldMMs array so after this foreach loop only the ones that need to be deleted are in there.
483 unset($oldMMs[$oldMMs_index]);
484 // Remove the item from the $oldMMs array so after this foreach loop only the ones that need to be deleted are in there.
485 unset($oldMMs_inclUid[$oldMMs_index]);
486 } else {
487 $insertFields = $this->MM_insert_fields;
488 $insertFields[$uidLocal_field] = $uid;
489 $insertFields[$uidForeign_field] = $val['id'];
490 $insertFields[$sorting_field] = $c;
491 if ($tablename) {
492 $insertFields['tablenames'] = $tablename;
493 }
494 $GLOBALS['TYPO3_DB']->exec_INSERTquery($MM_tableName, $insertFields);
495 if ($this->MM_is_foreign) {
496 $this->updateRefIndex($val['table'], $val['id']);
497 }
498 }
499 }
500 // Delete all not-used relations:
501 if (is_array($oldMMs) && count($oldMMs) > 0) {
502 $removeClauses = array();
503 $updateRefIndex_records = array();
504 foreach ($oldMMs as $oldMM_key => $mmItem) {
505 // If UID field is present, of course we need only use that for deleting.
506 if ($this->MM_hasUidField) {
507 $removeClauses[] = 'uid=' . intval($oldMMs_inclUid[$oldMM_key][2]);
508 $elDelete = $oldMMs_inclUid[$oldMM_key];
509 } else {
510 if (is_array($mmItem)) {
511 $removeClauses[] = 'tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mmItem[0], $MM_tableName) . ' AND ' . $uidForeign_field . '=' . $mmItem[1];
512 } else {
513 $removeClauses[] = $uidForeign_field . '=' . $mmItem;
514 }
515 }
516 if ($this->MM_is_foreign) {
517 if (is_array($mmItem)) {
518 $updateRefIndex_records[] = array($mmItem[0], $mmItem[1]);
519 } else {
520 $updateRefIndex_records[] = array($this->firstTable, $mmItem);
521 }
522 }
523 }
524 $deleteAddWhere = ' AND (' . implode(' OR ', $removeClauses) . ')';
525 $GLOBALS['TYPO3_DB']->exec_DELETEquery($MM_tableName, $uidLocal_field . '=' . intval($uid) . $deleteAddWhere . $additionalWhere_tablenames . $additionalWhere);
526 // Update ref index:
527 foreach ($updateRefIndex_records as $pair) {
528 $this->updateRefIndex($pair[0], $pair[1]);
529 }
530 }
531 // Update ref index; In tcemain it is not certain that this will happen because if only the MM field is changed the record itself is not updated and so the ref-index is not either.
532 // This could also have been fixed in updateDB in tcemain, however I decided to do it here ...
533 $this->updateRefIndex($this->currentTable, $uid);
534 }
535 }
536
537 /**
538 * Remaps MM table elements from one local uid to another
539 * Does NOT update the reference index for you, must be called subsequently to do that!
540 *
541 * @param string $MM_tableName MM table name
542 * @param integer $uid Local, current UID
543 * @param integer $newUid Local, new UID
544 * @param boolean $prependTableName If set, then table names will always be written.
545 * @return void
546 * @todo Define visibility
547 */
548 public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = 0) {
549 // In case of a reverse relation
550 if ($this->MM_is_foreign) {
551 $uidLocal_field = 'uid_foreign';
552 } else {
553 // default
554 $uidLocal_field = 'uid_local';
555 }
556 // If there are tables...
557 $tableC = count($this->tableArray);
558 if ($tableC) {
559 // Boolean: does the field "tablename" need to be filled?
560 $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
561 $c = 0;
562 $additionalWhere_tablenames = '';
563 if ($this->MM_is_foreign && $prep) {
564 $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
565 }
566 $additionalWhere = '';
567 // Add WHERE clause if configured
568 if ($this->MM_table_where) {
569 $additionalWhere .= LF . str_replace('###THIS_UID###', intval($uid), $this->MM_table_where);
570 }
571 // Select, update or delete only those relations that match the configured fields
572 foreach ($this->MM_match_fields as $field => $value) {
573 $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
574 }
575 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $uidLocal_field . '=' . intval($uid) . $additionalWhere_tablenames . $additionalWhere, array($uidLocal_field => $newUid));
576 }
577 }
578
579 /**
580 * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and
581 * stores the parts in the internal array itemArray and tableArray.
582 *
583 * @param integer $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field)
584 * @param array $conf TCA configuration for current field
585 * @return void
586 * @todo Define visibility
587 */
588 public function readForeignField($uid, $conf) {
589 $key = 0;
590 $uid = intval($uid);
591 $foreign_table = $conf['foreign_table'];
592 $foreign_table_field = $conf['foreign_table_field'];
593 $useDeleteClause = $this->undeleteRecord ? FALSE : TRUE;
594 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
595 // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
596 if ($conf['symmetric_field']) {
597 $whereClause = '(' . $conf['foreign_field'] . '=' . $uid . ' OR ' . $conf['symmetric_field'] . '=' . $uid . ')';
598 } else {
599 $whereClause = $conf['foreign_field'] . '=' . $uid;
600 }
601 // Use the deleteClause (e.g. "deleted=0") on this table
602 if ($useDeleteClause) {
603 $whereClause .= \TYPO3\CMS\Backend\Utility\BackendUtility::deleteClause($foreign_table);
604 }
605 // If it's requested to look for the parent uid AND the parent table,
606 // add an additional SQL-WHERE clause
607 if ($foreign_table_field && $this->currentTable) {
608 $whereClause .= ' AND ' . $foreign_table_field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $foreign_table);
609 }
610 // Add additional where clause if foreign_match_fields are defined
611 foreach ($foreign_match_fields as $field => $value) {
612 $whereClause .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $foreign_table);
613 }
614 // Select children in the same workspace:
615 if (\TYPO3\CMS\Backend\Utility\BackendUtility::isTableWorkspaceEnabled($this->currentTable) && \TYPO3\CMS\Backend\Utility\BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
616 $currentRecord = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($this->currentTable, $uid, 't3ver_wsid', '', $useDeleteClause);
617 $whereClause .= \TYPO3\CMS\Backend\Utility\BackendUtility::getWorkspaceWhereClause($foreign_table, $currentRecord['t3ver_wsid']);
618 }
619 // Get the correct sorting field
620 // Specific manual sortby for data handled by this field
621 if ($conf['foreign_sortby']) {
622 if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
623 // Sorting depends on, from which side of the relation we're looking at it
624 $sortby = '
625 CASE
626 WHEN ' . $conf['foreign_field'] . '=' . $uid . '
627 THEN ' . $conf['foreign_sortby'] . '
628 ELSE ' . $conf['symmetric_sortby'] . '
629 END';
630 } else {
631 // Regular single-side behaviour
632 $sortby = $conf['foreign_sortby'];
633 }
634 } elseif ($conf['foreign_default_sortby']) {
635 // Specific default sortby for data handled by this field
636 $sortby = $conf['foreign_default_sortby'];
637 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
638 // Manual sortby for all table records
639 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
640 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
641 // Default sortby for all table records
642 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
643 }
644 // Strip a possible "ORDER BY" in front of the $sortby value
645 $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
646 // Get the rows from storage
647 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', $foreign_table, $whereClause, '', $sortby);
648 if (count($rows)) {
649 foreach ($rows as $row) {
650 $this->itemArray[$key]['id'] = $row['uid'];
651 $this->itemArray[$key]['table'] = $foreign_table;
652 $this->tableArray[$foreign_table][] = $row['uid'];
653 $key++;
654 }
655 }
656 }
657
658 /**
659 * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record)
660 *
661 * @param array $conf TCA configuration for current field
662 * @param integer $parentUid The uid of the parent record
663 * @param boolean $updateToUid Whether to update the foreign field with the $parentUid (on Copy)
664 * @param boolean $skipSorting Do not update the sorting columns, this could happen for imported values
665 * @return void
666 * @todo Define visibility
667 */
668 public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = FALSE) {
669 $c = 0;
670 $foreign_table = $conf['foreign_table'];
671 $foreign_field = $conf['foreign_field'];
672 $symmetric_field = $conf['symmetric_field'];
673 $foreign_table_field = $conf['foreign_table_field'];
674 $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
675 // If there are table items and we have a proper $parentUid
676 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($parentUid) && count($this->tableArray)) {
677 // If updateToUid is not a positive integer, set it to '0', so it will be ignored
678 if (!(\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
679 $updateToUid = 0;
680 }
681 $considerWorkspaces = $GLOBALS['BE_USER']->workspace !== 0 && \TYPO3\CMS\Backend\Utility\BackendUtility::isTableWorkspaceEnabled($foreign_table);
682 $fields = 'uid,' . $foreign_field;
683 // Consider the symmetric field if defined:
684 if ($symmetric_field) {
685 $fields .= ',' . $symmetric_field;
686 }
687 // Consider workspaces if defined and currently used:
688 if ($considerWorkspaces) {
689 $fields .= ',' . 't3ver_state,t3ver_oid';
690 }
691 // Update all items
692 foreach ($this->itemArray as $val) {
693 $uid = $val['id'];
694 $table = $val['table'];
695 // Fetch the current (not overwritten) relation record if we should handle symmetric relations
696 if ($symmetric_field || $considerWorkspaces) {
697 $row = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($table, $uid, $fields, '', FALSE);
698 }
699 if ($symmetric_field) {
700 $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
701 }
702 $updateValues = $foreign_match_fields;
703 $workspaceValues = array();
704 // No update to the uid is requested, so this is the normal behaviour
705 // just update the fields and care about sorting
706 if (!$updateToUid) {
707 // Always add the pointer to the parent uid
708 if ($isOnSymmetricSide) {
709 $updateValues[$symmetric_field] = $parentUid;
710 } else {
711 $updateValues[$foreign_field] = $parentUid;
712 }
713 // If it is configured in TCA also to store the parent table in the child record, just do it
714 if ($foreign_table_field && $this->currentTable) {
715 $updateValues[$foreign_table_field] = $this->currentTable;
716 }
717 // Update sorting columns if not to be skipped
718 if (!$skipSorting) {
719 // Get the correct sorting field
720 // Specific manual sortby for data handled by this field
721 if ($conf['foreign_sortby']) {
722 $sortby = $conf['foreign_sortby'];
723 } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
724 // manual sortby for all table records
725 $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
726 }
727 // Apply sorting on the symmetric side (it depends on who created the relation, so what uid is in the symmetric_field):
728 if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
729 $sortby = $conf['symmetric_sortby'];
730 } else {
731 $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
732 }
733 if ($sortby) {
734 $updateValues[$sortby] = ($workspaceValues[$sortby] = ++$c);
735 }
736 }
737 } else {
738 if ($isOnSymmetricSide) {
739 $updateValues[$symmetric_field] = $updateToUid;
740 } else {
741 $updateValues[$foreign_field] = $updateToUid;
742 }
743 }
744 // Update accordant fields in the database:
745 if (count($updateValues)) {
746 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($uid), $updateValues);
747 $this->updateRefIndex($table, $uid);
748 }
749 // Update accordant fields in the database for workspaces overlays/placeholders:
750 if (count($workspaceValues) && $considerWorkspaces) {
751 if (isset($row['t3ver_oid']) && $row['t3ver_oid'] && $row['t3ver_state'] == -1) {
752 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($row['t3ver_oid']), $workspaceValues);
753 }
754 }
755 }
756 }
757 }
758
759 /**
760 * After initialization you can extract an array of the elements from the object. Use this function for that.
761 *
762 * @param boolean $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
763 * @return array A numeric array.
764 * @todo Define visibility
765 */
766 public function getValueArray($prependTableName = '') {
767 // INIT:
768 $valueArray = array();
769 $tableC = count($this->tableArray);
770 // If there are tables in the table array:
771 if ($tableC) {
772 // If there are more than ONE table in the table array, then always prepend table names:
773 $prep = $tableC > 1 || $prependTableName ? 1 : 0;
774 // Traverse the array of items:
775 foreach ($this->itemArray as $val) {
776 $valueArray[] = ($prep && $val['table'] != '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
777 }
778 }
779 // Return the array
780 return $valueArray;
781 }
782
783 /**
784 * Converts id numbers from negative to positive.
785 *
786 * @param array $valueArray Array of [table]_[id] pairs.
787 * @param string $fTable Foreign table (the one used for positive numbers)
788 * @param string $nfTable Negative foreign table
789 * @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.
790 * @todo Define visibility
791 */
792 public function convertPosNeg($valueArray, $fTable, $nfTable) {
793 if (is_array($valueArray) && $fTable) {
794 foreach ($valueArray as $key => $val) {
795 $val = strrev($val);
796 $parts = explode('_', $val, 2);
797 $theID = strrev($parts[0]);
798 $theTable = strrev($parts[1]);
799 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($theID) && (!$theTable || !strcmp($theTable, $fTable) || !strcmp($theTable, $nfTable))) {
800 $valueArray[$key] = $theTable && strcmp($theTable, $fTable) ? $theID * -1 : $theID;
801 }
802 }
803 }
804 return $valueArray;
805 }
806
807 /**
808 * Reads all records from internal tableArray into the internal ->results array where keys are table names and for each table, records are stored with uids as their keys.
809 * If $this->fromTC is set you can save a little memory since only uid,pid and a few other fields are selected.
810 *
811 * @return void
812 * @todo Define visibility
813 */
814 public function getFromDB() {
815 // Traverses the tables listed:
816 foreach ($this->tableArray as $key => $val) {
817 if (is_array($val)) {
818 $itemList = implode(',', $val);
819 if ($itemList) {
820 $from = '*';
821 if ($this->fromTC) {
822 $from = 'uid,pid';
823 if ($GLOBALS['TCA'][$key]['ctrl']['label']) {
824 // Titel
825 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label'];
826 }
827 if ($GLOBALS['TCA'][$key]['ctrl']['label_alt']) {
828 // Alternative Title-Fields
829 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label_alt'];
830 }
831 if ($GLOBALS['TCA'][$key]['ctrl']['thumbnail']) {
832 // Thumbnail
833 $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['thumbnail'];
834 }
835 }
836 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($from, $key, 'uid IN (' . $itemList . ')' . $this->additionalWhere[$key]);
837 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
838 $this->results[$key][$row['uid']] = $row;
839 }
840 $GLOBALS['TYPO3_DB']->sql_free_result($res);
841 }
842 }
843 }
844 return $this->results;
845 }
846
847 /**
848 * Prepare items from itemArray to be transferred to the TCEforms interface (as a comma list)
849 *
850 * @return string
851 * @see t3lib_transferdata::renderRecord()
852 * @todo Define visibility
853 */
854 public function readyForInterface() {
855 if (!is_array($this->itemArray)) {
856 return FALSE;
857 }
858 $output = array();
859 // For use when getting the paths
860 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
861 $titleLen = intval($GLOBALS['BE_USER']->uc['titleLen']);
862 foreach ($this->itemArray as $key => $val) {
863 $theRow = $this->results[$val['table']][$val['id']];
864 if ($theRow && is_array($GLOBALS['TCA'][$val['table']])) {
865 $label = \TYPO3\CMS\Core\Utility\GeneralUtility::fixed_lgd_cs(strip_tags(\TYPO3\CMS\Backend\Utility\BackendUtility::getRecordTitle($val['table'], $theRow)), $titleLen);
866 $label = $label ? $label : '[...]';
867 $output[] = str_replace(',', '', $val['table'] . '_' . $val['id'] . '|' . rawurlencode($label));
868 }
869 }
870 return implode(',', $output);
871 }
872
873 /**
874 * Counts the items in $this->itemArray and puts this value in an array by default.
875 *
876 * @param boolean $returnAsArray Whether to put the count value in an array
877 * @return mixed The plain count as integer or the same inside an array
878 * @todo Define visibility
879 */
880 public function countItems($returnAsArray = TRUE) {
881 $count = count($this->itemArray);
882 if ($returnAsArray) {
883 $count = array($count);
884 }
885 return $count;
886 }
887
888 /**
889 * Update Reference Index (sys_refindex) for a record
890 * Should be called any almost any update to a record which could affect references inside the record.
891 * (copied from TCEmain)
892 *
893 * @param string $table Table name
894 * @param integer $id Record UID
895 * @return array Information concerning modifications delivered by t3lib_refindex::updateRefIndexTable()
896 * @todo Define visibility
897 */
898 public function updateRefIndex($table, $id) {
899 if ($this->updateReferenceIndex === TRUE) {
900 /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
901 $refIndexObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
902 return $refIndexObj->updateRefIndexTable($table, $id);
903 }
904 }
905
906 /**
907 * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation.
908 *
909 * @param string $parentUid The uid of the parent record
910 * @param array $parentConf The TCA configuration of the parent field embedding the child records
911 * @param array $childRec The record row of the child record
912 * @return boolean Returns TRUE if looking from the symmetric ("other") side to the relation.
913 * @todo Define visibility
914 */
915 public function isOnSymmetricSide($parentUid, $parentConf, $childRec) {
916 return \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($childRec['uid']) && $parentConf['symmetric_field'] && $parentUid == $childRec[$parentConf['symmetric_field']] ? TRUE : FALSE;
917 }
918
919 }
920
921
922 ?>