Fixed bug with bidirectional mm-relations: "group" fields with more than one table...
[Packages/TYPO3.CMS.git] / t3lib / class.t3lib_loaddbgroup.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 1999-2006 Kasper Skaarhoj (kasperYYYY@typo3.com)
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 * A copy is found in the textfile GPL.txt and important notices to the license
17 * from the author is found in LICENSE.txt distributed with these scripts.
18 *
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27 /**
28 * Contains class for loading database groups
29 *
30 * $Id$
31 * Revised for TYPO3 3.6 September/2003 by Kasper Skaarhoj
32 *
33 * @author Kasper Skaarhoj <kasperYYYY@typo3.com>
34 */
35 /**
36 * [CLASS/FUNCTION INDEX of SCRIPT]
37 *
38 *
39 *
40 * 72: class t3lib_loadDBGroup
41 * 99: function start($itemlist,$tablelist, $MMtable='',$MMuid=0)
42 * 140: function readList($itemlist)
43 * 186: function readMM($tableName,$uid)
44 * 215: function writeMM($tableName,$uid,$prependTableName=0)
45 * 251: function getValueArray($prependTableName='')
46 * 279: function convertPosNeg($valueArray,$fTable,$nfTable)
47 * 301: function getFromDB()
48 * 333: function readyForInterface()
49 *
50 * TOTAL FUNCTIONS: 8
51 * (This index is automatically created/updated by the extension "extdeveval")
52 *
53 */
54
55
56
57
58
59
60
61
62
63
64 /**
65 * Load database groups (relations)
66 * Used to process the relations created by the TCA element types "group" and "select" for database records. Manages MM-relations as well.
67 *
68 * @author Kasper Skaarhoj <kasperYYYY@typo3.com>
69 * @package TYPO3
70 * @subpackage t3lib
71 */
72 class t3lib_loadDBGroup {
73 // External, static:
74 var $fromTC = 1; // Means that only uid and the label-field is returned
75 var $registerNonTableValues=0; // If set, values that are not ids in tables are normally discarded. By this options they will be preserved.
76
77 // Internal, dynamic:
78 var $tableArray=Array(); // Contains the table names as keys. The values are the id-values for each table. Should ONLY contain proper table names.
79 var $itemArray=Array(); // Contains items in an numeric array (table/id for each). Tablenames here might be "_NO_TABLE"
80 var $nonTableArray=array(); // Array for NON-table elements
81 var $additionalWhere=array();
82 var $checkIfDeleted = 1; // deleted-column is added to additionalWhere... if this is set...
83 var $dbPaths=Array();
84 var $firstTable = ''; // Will contain the first table name in the $tablelist (for positive ids)
85 var $secondTable = ''; // Will contain the second table name in the $tablelist (for negative ids)
86 // private
87 var $MM_is_foreign = 0; // 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"
88 var $MM_oppositeField = ''; // field name at the "local" side of the MM relation
89 var $MM_oppositeTable = ''; // only set if MM_is_foreign is set
90 var $MM_oppositeFieldConf = ''; // only set if MM_is_foreign is set
91 var $MM_isMultiTableRelationship = 0; // 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)
92 var $currentTable; // current table => Only needed for reverse relations
93
94
95 /**
96 * Initialization of the class.
97 *
98 * @param string List of group/select items
99 * @param string Comma list of tables, first table takes priority if no table is set for an entry in the list.
100 * @param string Name of a MM table.
101 * @param integer Local UID for MM lookup
102 * @param string current table name
103 * @param integer TCA configuration for current field
104 * @return void
105 */
106 function start($itemlist, $tablelist, $MMtable='', $MMuid=0, $currentTable='', $conf=array()) {
107 // SECTION: MM reverse relations
108 $this->MM_is_foreign = ($conf['MM_opposite_field']?1:0);
109 $this->MM_oppositeField = $conf['MM_opposite_field'];
110 $this->currentTable = $currentTable;
111 if ($this->MM_is_foreign) {
112 $tmp = ($conf['type']==='group'?$conf['allowed']:$conf['foreign_table']);
113 // 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']
114 $tmp = t3lib_div::trimExplode(',', $tmp);
115 $this->MM_oppositeTable = $tmp[0];
116 unset($tmp);
117
118 // only add the current table name if there is more than one allowed field
119 $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
120
121 if ($this->MM_oppositeFieldConf['allowed']) {
122 $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
123 if (count($oppositeFieldConf_allowed) > 1) {
124 $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
125 }
126 }
127 }
128
129 // SECTION: normal MM relations
130
131 // If the table list is "*" then all tables are used in the list:
132 if (!strcmp(trim($tablelist),'*')) {
133 $tablelist = implode(',',array_keys($GLOBALS['TCA']));
134 }
135
136 // The tables are traversed and internal arrays are initialized:
137 $tempTableArray = t3lib_div::trimExplode(',',$tablelist,1);
138 foreach($tempTableArray as $key => $val) {
139 $tName = trim($val);
140 $this->tableArray[$tName] = Array();
141 if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
142 $fieldN = $tName.'.'.$GLOBALS['TCA'][$tName]['ctrl']['delete'];
143 $this->additionalWhere[$tName].=' AND '.$fieldN.'=0';
144 }
145 }
146
147 if (is_array($this->tableArray)) {
148 reset($this->tableArray);
149 } else {return 'No tables!';}
150
151 // Set first and second tables:
152 $this->firstTable = key($this->tableArray); // Is the first table
153 next($this->tableArray);
154 $this->secondTable = key($this->tableArray); // 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...
155
156 // Now, populate the internal itemArray and tableArray arrays:
157 if ($MMtable) { // If MM, then call this function to do that:
158 $this->readMM($MMtable,$MMuid);
159 } else {
160 // If not MM, then explode the itemlist by "," and traverse the list:
161 $this->readList($itemlist);
162 }
163 }
164
165 /**
166 * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
167 *
168 * @param string Item list
169 * @return void
170 */
171 function readList($itemlist) {
172 if ((string)trim($itemlist)!='') {
173 $tempItemArray = t3lib_div::trimExplode(',', $itemlist); // 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...
174 foreach($tempItemArray as $key => $val) {
175 $isSet = 0; // Will be set to "1" if the entry was a real table/id:
176
177 // Extract table name and id. This is un the formular [tablename]_[id] where table name MIGHT contain "_", hence the reversion of the string!
178 $val = strrev($val);
179 $parts = explode('_',$val,2);
180 $theID = strrev($parts[0]);
181
182 // Check that the id IS an integer:
183 if (t3lib_div::testInt($theID)) {
184 // 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
185 $theTable = trim($parts[1]) ? strrev(trim($parts[1])) : ($this->secondTable && $theID<0 ? $this->secondTable : $this->firstTable);
186 // If the ID is not blank and the table name is among the names in the inputted tableList, then proceed:
187 if ((string)$theID!='' && $theID && $theTable && isset($this->tableArray[$theTable])) {
188 // Get ID as the right value:
189 $theID = $this->secondTable ? abs(intval($theID)) : intval($theID);
190 // Register ID/table name in internal arrays:
191 $this->itemArray[$key]['id'] = $theID;
192 $this->itemArray[$key]['table'] = $theTable;
193 $this->tableArray[$theTable][] = $theID;
194 // Set update-flag:
195 $isSet=1;
196 }
197 }
198
199 // 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:
200 if (!$isSet && $this->registerNonTableValues) {
201 $this->itemArray[$key]['id'] = $tempItemArray[$key];
202 $this->itemArray[$key]['table'] = '_NO_TABLE';
203 $this->nonTableArray[] = $tempItemArray[$key];
204 }
205 }
206 }
207 }
208
209 /**
210 * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
211 * You can call this function after start if you supply no list to start()
212 *
213 * @param string MM Tablename
214 * @param integer Local UID
215 * @return void
216 */
217 function readMM($tableName,$uid) {
218 $key=0;
219 if ($this->MM_is_foreign) { // in case of a reverse relation
220 $uidLocal_field = 'uid_foreign';
221 $uidForeign_field = 'uid_local';
222 $sorting_field = 'sorting_foreign';
223
224 if ($this->MM_isMultiTableRelationship) {
225 $additionalWhere = ' AND ( tablenames="'.$this->currentTable.'"';
226 if ($this->currentTable == $this->MM_isMultiTableRelationship) { // be backwards compatible! When allowing more than one table after having previously allowed only one table, this case applies.
227 $additionalWhere .= ' OR tablenames=""';
228 }
229 $additionalWhere .= ' ) ';
230 }
231 $theTable = $this->MM_oppositeTable;
232 } else { // default
233 $uidLocal_field = 'uid_local';
234 $uidForeign_field = 'uid_foreign';
235 $additionalWhere = '';
236 $sorting_field = 'sorting';
237 }
238
239 // Select all MM relations:
240 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tableName, $uidLocal_field.'='.intval($uid).$additionalWhere, '', $sorting_field);
241 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
242 if (!$this->MM_is_foreign) { // default
243 $theTable = $row['tablenames'] ? $row['tablenames'] : $this->firstTable; // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
244 }
245 if (($row[$uidForeign_field] || $theTable=='pages') && $theTable && isset($this->tableArray[$theTable])) {
246
247 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
248 $this->itemArray[$key]['table'] = $theTable;
249 $this->tableArray[$theTable][]= $row[$uidForeign_field];
250 } elseif ($this->registerNonTableValues) {
251 $this->itemArray[$key]['id'] = $row[$uidForeign_field];
252 $this->itemArray[$key]['table'] = '_NO_TABLE';
253 $this->nonTableArray[] = $row[$uidForeign_field];
254 }
255 $key++;
256 }
257 $GLOBALS['TYPO3_DB']->sql_free_result($res);
258 }
259
260 /**
261 * Writes the internal itemArray to MM table:
262 *
263 * @param string MM table name
264 * @param integer Local UID
265 * @param boolean If set, then table names will always be written.
266 * @return void
267 */
268 function writeMM($tableName,$uid,$prependTableName=0) {
269
270 if ($this->MM_is_foreign) { // in case of a reverse relation
271 $uidLocal_field = 'uid_foreign';
272 $uidForeign_field = 'uid_local';
273 $sorting_field = 'sorting_foreign';
274 } else { // default
275 $uidLocal_field = 'uid_local';
276 $uidForeign_field = 'uid_foreign';
277 $sorting_field = 'sorting';
278 }
279
280 // If there are tables...
281 $tableC = count($this->tableArray);
282 if ($tableC) {
283 $prep = ($tableC>1||$prependTableName||$this->MM_isMultiTableRelationship) ? 1 : 0; // boolean: does the field "tablename" need to be filled?
284 $c=0;
285
286 $additionalWhere_tablenames = '';
287 if ($this->MM_is_foreign && $prep) {
288 $additionalWhere_tablenames = ' AND tablenames="'.$this->currentTable.'"';
289 }
290 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($uidForeign_field.($prep?', tablenames':''), $tableName, $uidLocal_field.'='.$uid.$additionalWhere_tablenames);
291
292 $oldMMs = array();
293 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
294 if (!$this->MM_is_foreign && $prep) {
295 $oldMMs[] = array($row['tablenames'], $row[$uidForeign_field]);
296 } else {
297 $oldMMs[] = $row[$uidForeign_field];
298 }
299 }
300
301 // For each item, insert it:
302 foreach($this->itemArray as $val) {
303 $c++;
304
305 if ($prep || $val['table']=='_NO_TABLE') {
306 if ($this->MM_is_foreign) { // insert current table if needed
307 $tablename = $this->currentTable;
308 } else {
309 $tablename = $val['table'];
310 }
311 } else {
312 $tablename = '';
313 }
314
315 if(!$this->MM_is_foreign && $prep) {
316 $item = array($val['table'], $val['id']);
317 } else {
318 $item = $val['id'];
319 }
320
321 if (in_array($item, $oldMMs)) {
322 unset($oldMMs[array_search($item, $oldMMs)]); // remove the item from the $oldMMs array so after this foreach loop only the ones that need to be deleted are in there.
323
324 $whereClause = $uidLocal_field.'='.$uid.' AND '.$uidForeign_field.'='.$val['id'];
325 if ($tablename) {
326 $whereClause .= ' AND tablenames="'.$tablename.'"';
327 }
328 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tableName, $whereClause, array($sorting_field => $c));
329 } else {
330 $GLOBALS['TYPO3_DB']->exec_INSERTquery($tableName, array(
331 $uidLocal_field => $uid,
332 $uidForeign_field => $val['id'],
333 $sorting_field => $c,
334 'tablenames' => $tablename
335 ));
336 }
337 }
338
339 // Delete all not-used relations:
340 if(is_array($oldMMs) && count($oldMMs) > 0) {
341 $removeClauses = array();
342 foreach($oldMMs as $mmItem) {
343 if(is_array($mmItem)) {
344 $removeClauses[] = 'tablenames="'.$mmItem[0].'" AND '.$uidForeign_field.'='.$mmItem[1];
345 } else {
346 $removeClauses[] = $uidForeign_field.'='.$mmItem;
347 }
348 }
349 $additionalWhere = ' AND ('.implode(' OR ', $removeClauses).')';
350 $GLOBALS['TYPO3_DB']->exec_DELETEquery($tableName, $uidLocal_field.'='.intval($uid).$additionalWhere.$additionalWhere_tablenames);
351 }
352 }
353 }
354
355 /**
356 * After initialization you can extract an array of the elements from the object. Use this function for that.
357 *
358 * @param boolean If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
359 * @return array A numeric array.
360 */
361 function getValueArray($prependTableName='') {
362 // INIT:
363 $valueArray=Array();
364 $tableC = count($this->tableArray);
365
366 // If there are tables in the table array:
367 if ($tableC) {
368 // If there are more than ONE table in the table array, then always prepend table names:
369 $prep = ($tableC>1||$prependTableName) ? 1 : 0;
370
371 // Traverse the array of items:
372 foreach($this->itemArray as $val) {
373 $valueArray[]=(($prep && $val['table']!='_NO_TABLE') ? $val['table'].'_' : '').
374 $val['id'];
375 }
376 }
377 // Return the array
378 return $valueArray;
379 }
380
381 /**
382 * Converts id numbers from negative to positive.
383 *
384 * @param array Array of [table]_[id] pairs.
385 * @param string Foreign table (the one used for positive numbers)
386 * @param string NEGative foreign table
387 * @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.
388 */
389 function convertPosNeg($valueArray,$fTable,$nfTable) {
390 if (is_array($valueArray) && $fTable) {
391 foreach($valueArray as $key => $val) {
392 $val = strrev($val);
393 $parts = explode('_',$val,2);
394 $theID = strrev($parts[0]);
395 $theTable = strrev($parts[1]);
396
397 if ( t3lib_div::testInt($theID) && (!$theTable || !strcmp($theTable,$fTable) || !strcmp($theTable,$nfTable)) ) {
398 $valueArray[$key]= $theTable && strcmp($theTable,$fTable) ? $theID*-1 : $theID;
399 }
400 }
401 }
402 return $valueArray;
403 }
404
405 /**
406 * 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.
407 * If $this->fromTC is set you can save a little memory since only uid,pid and a few other fields are selected.
408 *
409 * @return void
410 */
411 function getFromDB() {
412 // Traverses the tables listed:
413 foreach($this->tableArray as $key => $val) {
414 if (is_array($val)) {
415 $itemList = implode(',',$val);
416 if ($itemList) {
417 $from = '*';
418 if ($this->fromTC) {
419 $from = 'uid,pid';
420 if ($GLOBALS['TCA'][$key]['ctrl']['label']) {
421 $from.= ','.$GLOBALS['TCA'][$key]['ctrl']['label']; // Titel
422 }
423 if ($GLOBALS['TCA'][$key]['ctrl']['label_alt']) {
424 $from.= ','.$GLOBALS['TCA'][$key]['ctrl']['label_alt']; // Alternative Title-Fields
425 }
426 if ($GLOBALS['TCA'][$key]['ctrl']['thumbnail']) {
427 $from.= ','.$GLOBALS['TCA'][$key]['ctrl']['thumbnail']; // Thumbnail
428 }
429 }
430 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($from, $key, 'uid IN ('.$itemList.')'.$this->additionalWhere[$key]);
431 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
432 $this->results[$key][$row['uid']]=$row;
433 }
434 }
435 }
436 }
437 return $this->results;
438 }
439
440 /**
441 * Prepare items from itemArray to be transferred to the TCEforms interface (as a comma list)
442 *
443 * @return string
444 * @see t3lib_transferdata::renderRecord()
445 */
446 function readyForInterface() {
447 global $TCA;
448
449 if (!is_array($this->itemArray)) {return false;}
450
451 $output=array();
452 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1); // For use when getting the paths....
453 $titleLen=intval($GLOBALS['BE_USER']->uc['titleLen']);
454
455 foreach($this->itemArray as $key => $val) {
456 $theRow = $this->results[$val['table']][$val['id']];
457 if ($theRow && is_array($TCA[$val['table']])) {
458 $label = t3lib_div::fixed_lgd_cs(strip_tags(t3lib_BEfunc::getRecordTitle($val['table'], $theRow)),$titleLen);
459 $label = ($label)?$label:'[...]';
460 $output[]=str_replace(',','',$val['table'].'_'.$val['id'].'|'.rawurlencode($label));
461 }
462 }
463 return implode(',',$output);
464 }
465 }
466
467
468 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_loaddbgroup.php']) {
469 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_loaddbgroup.php']);
470 }
471 ?>