TYPO3  7.6
RelationHandler.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Database;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
22 
29 {
35  protected $fetchAllFields = false;
36 
42  public $registerNonTableValues = false;
43 
50  public $tableArray = array();
51 
57  public $itemArray = array();
58 
64  public $nonTableArray = array();
65 
69  public $additionalWhere = array();
70 
76  public $checkIfDeleted = true;
77 
81  public $dbPaths = array();
82 
88  public $firstTable = '';
89 
95  public $secondTable = '';
96 
103  public $MM_is_foreign = false;
104 
110  public $MM_oppositeField = '';
111 
117  public $MM_oppositeTable = '';
118 
125 
132 
139 
147 
154  public $MM_match_fields = array();
155 
162 
168  public $MM_insert_fields = array();
169 
175  public $MM_table_where = '';
176 
182  protected $MM_oppositeUsage;
183 
187  protected $updateReferenceIndex = true;
188 
192  protected $useLiveParentIds = true;
193 
197  protected $useLiveReferenceIds = true;
198 
202  protected $workspaceId;
203 
207  protected $purged = false;
208 
214  public $results = array();
215 
221  public function getWorkspaceId()
222  {
223  if (!isset($this->workspaceId)) {
224  $this->workspaceId = (int)$GLOBALS['BE_USER']->workspace;
225  }
226  return $this->workspaceId;
227  }
228 
234  public function setWorkspaceId($workspaceId)
235  {
236  $this->workspaceId = (int)$workspaceId;
237  }
238 
244  public function isPurged()
245  {
246  return $this->purged;
247  }
248 
260  public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = array())
261  {
262  $conf = (array)$conf;
263  // SECTION: MM reverse relations
264  $this->MM_is_foreign = (bool)$conf['MM_opposite_field'];
265  $this->MM_oppositeField = $conf['MM_opposite_field'];
266  $this->MM_table_where = $conf['MM_table_where'];
267  $this->MM_hasUidField = $conf['MM_hasUidField'];
268  $this->MM_match_fields = is_array($conf['MM_match_fields']) ? $conf['MM_match_fields'] : array();
269  $this->MM_insert_fields = is_array($conf['MM_insert_fields']) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
270  $this->currentTable = $currentTable;
271  if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
272  $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
273  }
274  if ($this->MM_is_foreign) {
275  $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
276  // Normally, $conf['allowed'] can contain a list of tables,
277  // but as we are looking at a MM relation from the foreign side,
278  // it only makes sense to allow one one table in $conf['allowed']
279  $tmp = GeneralUtility::trimExplode(',', $tmp);
280  $this->MM_oppositeTable = $tmp[0];
281  unset($tmp);
282  // Only add the current table name if there is more than one allowed field
283  // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
284  $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
285  if ($this->MM_oppositeFieldConf['allowed']) {
286  $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
287  if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
288  $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
289  }
290  }
291  }
292  // SECTION: normal MM relations
293  // If the table list is "*" then all tables are used in the list:
294  if (trim($tablelist) === '*') {
295  $tablelist = implode(',', array_keys($GLOBALS['TCA']));
296  }
297  // The tables are traversed and internal arrays are initialized:
298  $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
299  foreach ($tempTableArray as $val) {
300  $tName = trim($val);
301  $this->tableArray[$tName] = array();
302  if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
303  $fieldN = $tName . '.' . $GLOBALS['TCA'][$tName]['ctrl']['delete'];
304  $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
305  }
306  }
307  if (is_array($this->tableArray)) {
308  reset($this->tableArray);
309  } else {
310  // No tables
311  return;
312  }
313  // Set first and second tables:
314  // Is the first table
315  $this->firstTable = key($this->tableArray);
316  next($this->tableArray);
317  // If the second table is set and the ID number is less than zero (later)
318  // then the record is regarded to come from the second table...
319  $this->secondTable = key($this->tableArray);
320  // Now, populate the internal itemArray and tableArray arrays:
321  // If MM, then call this function to do that:
322  if ($MMtable) {
323  if ($MMuid) {
324  $this->readMM($MMtable, $MMuid);
325  $this->purgeItemArray();
326  } else {
327  // Revert to readList() for new records in order to load possible default values from $itemlist
328  $this->readList($itemlist, $conf);
329  $this->purgeItemArray();
330  }
331  } elseif ($MMuid && $conf['foreign_field']) {
332  // If not MM but foreign_field, the read the records by the foreign_field
333  $this->readForeignField($MMuid, $conf);
334  } else {
335  // If not MM, then explode the itemlist by "," and traverse the list:
336  $this->readList($itemlist, $conf);
337  // Do automatic default_sortby, if any
338  if ($conf['foreign_default_sortby']) {
339  $this->sortList($conf['foreign_default_sortby']);
340  }
341  }
342  }
343 
349  public function setFetchAllFields($allFields)
350  {
351  $this->fetchAllFields = (bool)$allFields;
352  }
353 
361  {
362  $this->updateReferenceIndex = (bool)$updateReferenceIndex;
363  }
364 
369  {
370  $this->useLiveParentIds = (bool)$useLiveParentIds;
371  }
372 
377  {
378  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
379  }
380 
388  public function readList($itemlist, array $configuration)
389  {
390  if ((string)trim($itemlist) != '') {
391  $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
392  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
393  // if there were spaces in the list... I suppose this is better overall...
394  foreach ($tempItemArray as $key => $val) {
395  // Will be set to "1" if the entry was a real table/id:
396  $isSet = 0;
397  // Extract table name and id. This is un the formular [tablename]_[id]
398  // where table name MIGHT contain "_", hence the reversion of the string!
399  $val = strrev($val);
400  $parts = explode('_', $val, 2);
401  $theID = strrev($parts[0]);
402  // Check that the id IS an integer:
404  // Get the table name: If a part of the exploded string, use that.
405  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
406  $theTable = trim($parts[1])
407  ? strrev(trim($parts[1]))
408  : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
409  // If the ID is not blank and the table name is among the names in the inputted tableList
410  if (((string)$theID != '' && $theID) && $theTable && isset($this->tableArray[$theTable])) {
411  // Get ID as the right value:
412  $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
413  // Register ID/table name in internal arrays:
414  $this->itemArray[$key]['id'] = $theID;
415  $this->itemArray[$key]['table'] = $theTable;
416  $this->tableArray[$theTable][] = $theID;
417  // Set update-flag:
418  $isSet = 1;
419  }
420  }
421  // If it turns out that the value from the list was NOT a valid reference to a table-record,
422  // then we might still set it as a NO_TABLE value:
423  if (!$isSet && $this->registerNonTableValues) {
424  $this->itemArray[$key]['id'] = $tempItemArray[$key];
425  $this->itemArray[$key]['table'] = '_NO_TABLE';
426  $this->nonTableArray[] = $tempItemArray[$key];
427  }
428  }
429 
430  // Skip if not dealing with IRRE in a CSV list on a workspace
431  if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
432  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
433  || (int)$GLOBALS['BE_USER']->workspace === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
434  return;
435  }
436 
437  // Fetch live record data
438  if ($this->useLiveReferenceIds) {
439  foreach ($this->itemArray as &$item) {
440  $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
441  }
442  // Directly overlay workspace data
443  } else {
444  $this->itemArray = array();
445  $foreignTable = $configuration['foreign_table'];
446  $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
447  foreach ($ids as $id) {
448  $this->itemArray[] = array(
449  'id' => $id,
450  'table' => $foreignTable,
451  );
452  }
453  }
454  }
455  }
456 
465  public function sortList($sortby)
466  {
467  // Sort directly without fetching addional data
468  if ($sortby == 'uid') {
469  usort(
470  $this->itemArray,
471  function ($a, $b) {
472  return $a['id'] < $b['id'] ? -1 : 1;
473  }
474  );
475  } elseif (count($this->tableArray) === 1) {
476  reset($this->tableArray);
477  $table = key($this->tableArray);
478  $uidList = implode(',', current($this->tableArray));
479  if ($uidList) {
480  $this->itemArray = array();
481  $this->tableArray = array();
482  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'uid IN (' . $uidList . ')', '', $sortby);
483  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
484  $this->itemArray[] = array('id' => $row['uid'], 'table' => $table);
485  $this->tableArray[$table][] = $row['uid'];
486  }
487  $GLOBALS['TYPO3_DB']->sql_free_result($res);
488  }
489  }
490  }
491 
500  public function readMM($tableName, $uid)
501  {
502  $key = 0;
503  $additionalWhere = '';
504  $theTable = null;
505  // In case of a reverse relation
506  if ($this->MM_is_foreign) {
507  $uidLocal_field = 'uid_foreign';
508  $uidForeign_field = 'uid_local';
509  $sorting_field = 'sorting_foreign';
510  if ($this->MM_isMultiTableRelationship) {
511  $additionalWhere .= ' AND ( tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $tableName);
512  // Be backwards compatible! When allowing more than one table after
513  // having previously allowed only one table, this case applies.
514  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
515  $additionalWhere .= ' OR tablenames=\'\'';
516  }
517  $additionalWhere .= ' ) ';
518  }
519  $theTable = $this->MM_oppositeTable;
520  } else {
521  // Default
522  $uidLocal_field = 'uid_local';
523  $uidForeign_field = 'uid_foreign';
524  $sorting_field = 'sorting';
525  }
526  if ($this->MM_table_where) {
527  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
528  }
529  foreach ($this->MM_match_fields as $field => $value) {
530  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $tableName);
531  }
532  // Select all MM relations:
533  $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere;
534  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tableName, $where, '', $sorting_field);
535  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
536  // Default
537  if (!$this->MM_is_foreign) {
538  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
539  $theTable = $row['tablenames'] ?: $this->firstTable;
540  }
541  if (($row[$uidForeign_field] || $theTable == 'pages') && $theTable && isset($this->tableArray[$theTable])) {
542  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
543  $this->itemArray[$key]['table'] = $theTable;
544  $this->tableArray[$theTable][] = $row[$uidForeign_field];
545  } elseif ($this->registerNonTableValues) {
546  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
547  $this->itemArray[$key]['table'] = '_NO_TABLE';
548  $this->nonTableArray[] = $row[$uidForeign_field];
549  }
550  $key++;
551  }
552  $GLOBALS['TYPO3_DB']->sql_free_result($res);
553  }
554 
563  public function writeMM($MM_tableName, $uid, $prependTableName = false)
564  {
565  // In case of a reverse relation
566  if ($this->MM_is_foreign) {
567  $uidLocal_field = 'uid_foreign';
568  $uidForeign_field = 'uid_local';
569  $sorting_field = 'sorting_foreign';
570  } else {
571  // default
572  $uidLocal_field = 'uid_local';
573  $uidForeign_field = 'uid_foreign';
574  $sorting_field = 'sorting';
575  }
576  // If there are tables...
577  $tableC = count($this->tableArray);
578  if ($tableC) {
579  // Boolean: does the field "tablename" need to be filled?
580  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
581  $c = 0;
582  $additionalWhere_tablenames = '';
583  if ($this->MM_is_foreign && $prep) {
584  $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
585  }
586  $additionalWhere = '';
587  // Add WHERE clause if configured
588  if ($this->MM_table_where) {
589  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
590  }
591  // Select, update or delete only those relations that match the configured fields
592  foreach ($this->MM_match_fields as $field => $value) {
593  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
594  }
595  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
596  $uidForeign_field . ($prep ? ', tablenames' : '') . ($this->MM_hasUidField ? ', uid' : ''),
597  $MM_tableName,
598  $uidLocal_field . '=' . $uid . $additionalWhere_tablenames . $additionalWhere,
599  '',
600  $sorting_field
601  );
602  $oldMMs = array();
603  // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
604  // If the UID is present it will be used to update sorting and delete MM-records.
605  // This is necessary if the "multiple" feature is used for the MM relations.
606  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
607  $oldMMs_inclUid = array();
608  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
609  if (!$this->MM_is_foreign && $prep) {
610  $oldMMs[] = array($row['tablenames'], $row[$uidForeign_field]);
611  } else {
612  $oldMMs[] = $row[$uidForeign_field];
613  }
614  $oldMMs_inclUid[] = array($row['tablenames'], $row[$uidForeign_field], $row['uid']);
615  }
616  $GLOBALS['TYPO3_DB']->sql_free_result($res);
617  // For each item, insert it:
618  foreach ($this->itemArray as $val) {
619  $c++;
620  if ($prep || $val['table'] == '_NO_TABLE') {
621  // Insert current table if needed
622  if ($this->MM_is_foreign) {
623  $tablename = $this->currentTable;
624  } else {
625  $tablename = $val['table'];
626  }
627  } else {
628  $tablename = '';
629  }
630  if (!$this->MM_is_foreign && $prep) {
631  $item = array($val['table'], $val['id']);
632  } else {
633  $item = $val['id'];
634  }
635  if (in_array($item, $oldMMs)) {
636  $oldMMs_index = array_search($item, $oldMMs);
637  // In principle, selecting on the UID is all we need to do
638  // if a uid field is available since that is unique!
639  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
640  $whereClause = $uidLocal_field . '=' . $uid . ' AND ' . $uidForeign_field . '=' . $val['id']
641  . ($this->MM_hasUidField ? ' AND uid=' . (int)$oldMMs_inclUid[$oldMMs_index][2] : '');
642  if ($tablename) {
643  $whereClause .= ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tablename, $MM_tableName);
644  }
645  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $whereClause . $additionalWhere, array($sorting_field => $c));
646  // Remove the item from the $oldMMs array so after this
647  // foreach loop only the ones that need to be deleted are in there.
648  unset($oldMMs[$oldMMs_index]);
649  // Remove the item from the $oldMMs_inclUid array so after this
650  // foreach loop only the ones that need to be deleted are in there.
651  unset($oldMMs_inclUid[$oldMMs_index]);
652  } else {
653  $insertFields = $this->MM_insert_fields;
654  $insertFields[$uidLocal_field] = $uid;
655  $insertFields[$uidForeign_field] = $val['id'];
656  $insertFields[$sorting_field] = $c;
657  if ($tablename) {
658  $insertFields['tablenames'] = $tablename;
659  $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
660  }
661  $GLOBALS['TYPO3_DB']->exec_INSERTquery($MM_tableName, $insertFields);
662  if ($this->MM_is_foreign) {
663  $this->updateRefIndex($val['table'], $val['id']);
664  }
665  }
666  }
667  // Delete all not-used relations:
668  if (is_array($oldMMs) && !empty($oldMMs)) {
669  $removeClauses = array();
670  $updateRefIndex_records = array();
671  foreach ($oldMMs as $oldMM_key => $mmItem) {
672  // If UID field is present, of course we need only use that for deleting.
673  if ($this->MM_hasUidField) {
674  $removeClauses[] = 'uid=' . (int)$oldMMs_inclUid[$oldMM_key][2];
675  } else {
676  if (is_array($mmItem)) {
677  $removeClauses[] = 'tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mmItem[0], $MM_tableName)
678  . ' AND ' . $uidForeign_field . '=' . $mmItem[1];
679  } else {
680  $removeClauses[] = $uidForeign_field . '=' . $mmItem;
681  }
682  }
683  if ($this->MM_is_foreign) {
684  if (is_array($mmItem)) {
685  $updateRefIndex_records[] = array($mmItem[0], $mmItem[1]);
686  } else {
687  $updateRefIndex_records[] = array($this->firstTable, $mmItem);
688  }
689  }
690  }
691  $deleteAddWhere = ' AND (' . implode(' OR ', $removeClauses) . ')';
692  $where = $uidLocal_field . '=' . (int)$uid . $deleteAddWhere . $additionalWhere_tablenames . $additionalWhere;
693  $GLOBALS['TYPO3_DB']->exec_DELETEquery($MM_tableName, $where);
694  // Update ref index:
695  foreach ($updateRefIndex_records as $pair) {
696  $this->updateRefIndex($pair[0], $pair[1]);
697  }
698  }
699  // Update ref index; In tcemain it is not certain that this will happen because
700  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
701  // This could also have been fixed in updateDB in tcemain, however I decided to do it here ...
702  $this->updateRefIndex($this->currentTable, $uid);
703  }
704  }
705 
716  public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
717  {
718  // In case of a reverse relation
719  if ($this->MM_is_foreign) {
720  $uidLocal_field = 'uid_foreign';
721  } else {
722  // default
723  $uidLocal_field = 'uid_local';
724  }
725  // If there are tables...
726  $tableC = count($this->tableArray);
727  if ($tableC) {
728  // Boolean: does the field "tablename" need to be filled?
729  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
730  $additionalWhere_tablenames = '';
731  if ($this->MM_is_foreign && $prep) {
732  $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
733  }
734  $additionalWhere = '';
735  // Add WHERE clause if configured
736  if ($this->MM_table_where) {
737  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
738  }
739  // Select, update or delete only those relations that match the configured fields
740  foreach ($this->MM_match_fields as $field => $value) {
741  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
742  }
743  $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere_tablenames . $additionalWhere;
744  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $where, array($uidLocal_field => $newUid));
745  }
746  }
747 
756  public function readForeignField($uid, $conf)
757  {
758  if ($this->useLiveParentIds) {
759  $uid = $this->getLiveDefaultId($this->currentTable, $uid);
760  }
761 
762  $key = 0;
763  $uid = (int)$uid;
764  $foreign_table = $conf['foreign_table'];
765  $foreign_table_field = $conf['foreign_table_field'];
766  $useDeleteClause = !$this->undeleteRecord;
767  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
768  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
769  if ($conf['symmetric_field']) {
770  $whereClause = '(' . $conf['foreign_field'] . '=' . $uid . ' OR ' . $conf['symmetric_field'] . '=' . $uid . ')';
771  } else {
772  $whereClause = $conf['foreign_field'] . '=' . $uid;
773  }
774  // Use the deleteClause (e.g. "deleted=0") on this table
775  if ($useDeleteClause) {
776  $whereClause .= BackendUtility::deleteClause($foreign_table);
777  }
778  // If it's requested to look for the parent uid AND the parent table,
779  // add an additional SQL-WHERE clause
780  if ($foreign_table_field && $this->currentTable) {
781  $whereClause .= ' AND ' . $foreign_table_field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $foreign_table);
782  }
783  // Add additional where clause if foreign_match_fields are defined
784  foreach ($foreign_match_fields as $field => $value) {
785  $whereClause .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $foreign_table);
786  }
787  // Select children from the live(!) workspace only
788  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
789  $workspaceList = '0,' . $this->getWorkspaceId();
790  $whereClause .= ' AND ' . $foreign_table . '.t3ver_wsid IN (' . $workspaceList . ') AND ' . $foreign_table . '.pid<>-1';
791  }
792  // Get the correct sorting field
793  // Specific manual sortby for data handled by this field
794  $sortby = '';
795  if ($conf['foreign_sortby']) {
796  if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
797  // Sorting depends on, from which side of the relation we're looking at it
798  $sortby = '
799  CASE
800  WHEN ' . $conf['foreign_field'] . '=' . $uid . '
801  THEN ' . $conf['foreign_sortby'] . '
802  ELSE ' . $conf['symmetric_sortby'] . '
803  END';
804  } else {
805  // Regular single-side behaviour
806  $sortby = $conf['foreign_sortby'];
807  }
808  } elseif ($conf['foreign_default_sortby']) {
809  // Specific default sortby for data handled by this field
810  $sortby = $conf['foreign_default_sortby'];
811  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
812  // Manual sortby for all table records
813  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
814  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
815  // Default sortby for all table records
816  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
817  }
818  // Strip a possible "ORDER BY" in front of the $sortby value
819  $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
820  // Get the rows from storage
821  $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', $foreign_table, $whereClause, '', $sortby, '', 'uid');
822  if (!empty($rows)) {
823  $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
824  foreach ($ids as $id) {
825  $this->itemArray[$key]['id'] = $id;
826  $this->itemArray[$key]['table'] = $foreign_table;
827  $this->tableArray[$foreign_table][] = $id;
828  $key++;
829  }
830  }
831  }
832 
842  public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
843  {
844  if ($this->useLiveParentIds) {
845  $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
846  if (!empty($updateToUid)) {
847  $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
848  }
849  }
850 
851  $c = 0;
852  $foreign_table = $conf['foreign_table'];
853  $foreign_field = $conf['foreign_field'];
854  $symmetric_field = $conf['symmetric_field'];
855  $foreign_table_field = $conf['foreign_table_field'];
856  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : array();
857  // If there are table items and we have a proper $parentUid
858  if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
859  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
860  if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
861  $updateToUid = 0;
862  }
863  $considerWorkspaces = ($GLOBALS['BE_USER']->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreign_table));
864  $fields = 'uid,pid,' . $foreign_field;
865  // Consider the symmetric field if defined:
866  if ($symmetric_field) {
867  $fields .= ',' . $symmetric_field;
868  }
869  // Consider workspaces if defined and currently used:
870  if ($considerWorkspaces) {
871  $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
872  }
873  // Update all items
874  foreach ($this->itemArray as $val) {
875  $uid = $val['id'];
876  $table = $val['table'];
877  $row = array();
878  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
879  if ($symmetric_field || $considerWorkspaces) {
880  $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
881  if (empty($row)) {
882  continue;
883  }
884  }
885  $isOnSymmetricSide = false;
886  if ($symmetric_field) {
887  $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
888  }
889  $updateValues = $foreign_match_fields;
890  // No update to the uid is requested, so this is the normal behaviour
891  // just update the fields and care about sorting
892  if (!$updateToUid) {
893  // Always add the pointer to the parent uid
894  if ($isOnSymmetricSide) {
895  $updateValues[$symmetric_field] = $parentUid;
896  } else {
897  $updateValues[$foreign_field] = $parentUid;
898  }
899  // If it is configured in TCA also to store the parent table in the child record, just do it
900  if ($foreign_table_field && $this->currentTable) {
901  $updateValues[$foreign_table_field] = $this->currentTable;
902  }
903  // Update sorting columns if not to be skipped
904  if (!$skipSorting) {
905  // Get the correct sorting field
906  // Specific manual sortby for data handled by this field
907  $sortby = '';
908  if ($conf['foreign_sortby']) {
909  $sortby = $conf['foreign_sortby'];
910  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
911  // manual sortby for all table records
912  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
913  }
914  // Apply sorting on the symmetric side
915  // (it depends on who created the relation, so what uid is in the symmetric_field):
916  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
917  $sortby = $conf['symmetric_sortby'];
918  } else {
919  $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
920  }
921  if ($sortby) {
922  $updateValues[$sortby] = ++$c;
923  }
924  }
925  } else {
926  if ($isOnSymmetricSide) {
927  $updateValues[$symmetric_field] = $updateToUid;
928  } else {
929  $updateValues[$foreign_field] = $updateToUid;
930  }
931  }
932  // Update accordant fields in the database:
933  if (!empty($updateValues)) {
934  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateValues);
935  $this->updateRefIndex($table, $uid);
936  }
937  // Update accordant fields in the database for workspaces overlays/placeholders:
938  if ($considerWorkspaces) {
939  // It's the specific versioned record -> update placeholder (if any)
940  if (!empty($row['t3ver_oid']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
941  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$row['t3ver_oid'], $updateValues);
942  }
943  }
944  }
945  }
946  }
947 
954  public function getValueArray($prependTableName = false)
955  {
956  // INIT:
957  $valueArray = array();
958  $tableC = count($this->tableArray);
959  // If there are tables in the table array:
960  if ($tableC) {
961  // If there are more than ONE table in the table array, then always prepend table names:
962  $prep = $tableC > 1 || $prependTableName;
963  // Traverse the array of items:
964  foreach ($this->itemArray as $val) {
965  $valueArray[] = ($prep && $val['table'] != '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
966  }
967  }
968  // Return the array
969  return $valueArray;
970  }
971 
981  public function convertPosNeg($valueArray, $fTable, $nfTable)
982  {
984  if (is_array($valueArray) && $fTable) {
985  foreach ($valueArray as $key => $val) {
986  $val = strrev($val);
987  $parts = explode('_', $val, 2);
988  $theID = strrev($parts[0]);
989  $theTable = strrev($parts[1]);
991  && (!$theTable || $theTable === (string)$fTable || $theTable === (string)$nfTable)
992  ) {
993  $valueArray[$key] = $theTable && $theTable !== (string)$fTable ? $theID * -1 : $theID;
994  }
995  }
996  }
997  return $valueArray;
998  }
999 
1008  public function getFromDB()
1009  {
1010  // Traverses the tables listed:
1011  foreach ($this->tableArray as $key => $val) {
1012  if (is_array($val)) {
1013  $itemList = implode(',', $val);
1014  if ($itemList) {
1015  if ($this->fetchAllFields) {
1016  $from = '*';
1017  } else {
1018  $from = 'uid,pid';
1019  if ($GLOBALS['TCA'][$key]['ctrl']['label']) {
1020  // Titel
1021  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label'];
1022  }
1023  if ($GLOBALS['TCA'][$key]['ctrl']['label_alt']) {
1024  // Alternative Title-Fields
1025  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label_alt'];
1026  }
1027  if ($GLOBALS['TCA'][$key]['ctrl']['thumbnail']) {
1028  // Thumbnail
1029  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['thumbnail'];
1030  }
1031  }
1032  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($from, $key, 'uid IN (' . $itemList . ')' . $this->additionalWhere[$key]);
1033  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1034  $this->results[$key][$row['uid']] = $row;
1035  }
1036  $GLOBALS['TYPO3_DB']->sql_free_result($res);
1037  }
1038  }
1039  }
1040  return $this->results;
1041  }
1042 
1048  public function readyForInterface()
1049  {
1050  if (!is_array($this->itemArray)) {
1051  return false;
1052  }
1053  $output = array();
1054  $titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
1055  foreach ($this->itemArray as $val) {
1056  $theRow = $this->results[$val['table']][$val['id']];
1057  if ($theRow && is_array($GLOBALS['TCA'][$val['table']])) {
1058  $label = GeneralUtility::fixed_lgd_cs(strip_tags(
1059  BackendUtility::getRecordTitle($val['table'], $theRow)), $titleLen);
1060  $label = $label ? $label : '[...]';
1061  $output[] = str_replace(',', '', $val['table'] . '_' . $val['id'] . '|' . rawurlencode($label));
1062  }
1063  }
1064  return implode(',', $output);
1065  }
1066 
1073  public function countItems($returnAsArray = true)
1074  {
1075  $count = count($this->itemArray);
1076  if ($returnAsArray) {
1077  $count = array($count);
1078  }
1079  return $count;
1080  }
1081 
1091  public function updateRefIndex($table, $id)
1092  {
1093  $statisticsArray = array();
1094  if ($this->updateReferenceIndex) {
1096  $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
1098  $refIndexObj->setWorkspaceId($this->getWorkspaceId());
1099  }
1100  $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1101  }
1102  return $statisticsArray;
1103  }
1104 
1109  public function purgeItemArray($workspaceId = null)
1110  {
1111  if ($workspaceId === null) {
1112  $workspaceId = $this->getWorkspaceId();
1113  } else {
1114  $workspaceId = (int)$workspaceId;
1115  }
1116 
1117  // Ensure, only live relations are in the items Array
1118  if ($workspaceId === 0) {
1119  $purgeCallback = 'purgeVersionedIds';
1120  // Otherwise, ensure that live relations are purged if version exists
1121  } else {
1122  $purgeCallback = 'purgeLiveVersionedIds';
1123  }
1124 
1125  $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1126  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1127  return $itemArrayHasBeenPurged;
1128  }
1129 
1135  public function processDeletePlaceholder()
1136  {
1137  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1138  return false;
1139  }
1140 
1141  return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1142  }
1143 
1150  protected function purgeItemArrayHandler($purgeCallback)
1151  {
1152  $itemArrayHasBeenPurged = false;
1153 
1154  foreach ($this->tableArray as $itemTableName => $itemIds) {
1155  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1156  continue;
1157  }
1158 
1159  $purgedItemIds = call_user_func(array($this, $purgeCallback), $itemTableName, $itemIds);
1160  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1161  foreach ($removedItemIds as $removedItemId) {
1162  $this->removeFromItemArray($itemTableName, $removedItemId);
1163  }
1164  $this->tableArray[$itemTableName] = $purgedItemIds;
1165  if (!empty($removedItemIds)) {
1166  $itemArrayHasBeenPurged = true;
1167  }
1168  }
1169 
1170  return $itemArrayHasBeenPurged;
1171  }
1172 
1180  protected function purgeVersionedIds($tableName, array $ids)
1181  {
1182  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1183  $ids = array_combine($ids, $ids);
1184 
1185  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1186  'uid,t3ver_oid,t3ver_state',
1187  $tableName,
1188  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid<>0',
1189  '',
1190  't3ver_state DESC'
1191  );
1192 
1193  if (!empty($versions)) {
1194  foreach ($versions as $version) {
1195  $versionId = $version['uid'];
1196  if (isset($ids[$versionId])) {
1197  unset($ids[$versionId]);
1198  }
1199  }
1200  }
1201 
1202  return array_values($ids);
1203  }
1204 
1212  protected function purgeLiveVersionedIds($tableName, array $ids)
1213  {
1214  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1215  $ids = array_combine($ids, $ids);
1216 
1217  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1218  'uid,t3ver_oid,t3ver_state',
1219  $tableName,
1220  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid<>0',
1221  '',
1222  't3ver_state DESC'
1223  );
1224 
1225  if (!empty($versions)) {
1226  foreach ($versions as $version) {
1227  $versionId = $version['uid'];
1228  $liveId = $version['t3ver_oid'];
1229  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1230  unset($ids[$liveId]);
1231  }
1232  }
1233  }
1234 
1235  return array_values($ids);
1236  }
1237 
1245  protected function purgeDeletePlaceholder($tableName, array $ids)
1246  {
1247  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1248  $ids = array_combine($ids, $ids);
1249 
1250  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1251  'uid,t3ver_oid,t3ver_state',
1252  $tableName,
1253  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid=' . $this->getWorkspaceId() .
1255  );
1256 
1257  if (!empty($versions)) {
1258  foreach ($versions as $version) {
1259  $liveId = $version['t3ver_oid'];
1260  if (isset($ids[$liveId])) {
1261  unset($ids[$liveId]);
1262  }
1263  }
1264  }
1265 
1266  return array_values($ids);
1267  }
1268 
1269  protected function removeFromItemArray($tableName, $id)
1270  {
1271  foreach ($this->itemArray as $index => $item) {
1272  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1273  unset($this->itemArray[$index]);
1274  return true;
1275  }
1276  }
1277  return false;
1278  }
1279 
1288  public static function isOnSymmetricSide($parentUid, $parentConf, $childRec)
1289  {
1290  return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1291  && $parentConf['symmetric_field']
1292  && $parentUid == $childRec[$parentConf['symmetric_field']];
1293  }
1294 
1303  protected function completeOppositeUsageValues($tableName, array $referenceValues)
1304  {
1305  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1306  return $referenceValues;
1307  }
1308 
1309  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1310  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1311  return $referenceValues;
1312  }
1313 
1314  $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1315  if (!empty($configuration['MM_insert_fields'])) {
1316  $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1317  } elseif (!empty($configuration['MM_match_fields'])) {
1318  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1319  }
1320 
1321  return $referenceValues;
1322  }
1323 
1332  protected function getLiveDefaultId($tableName, $id)
1333  {
1334  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1335  if ($liveDefaultId === null) {
1336  $liveDefaultId = $id;
1337  }
1338  return (int)$liveDefaultId;
1339  }
1340 
1347  protected function getResolver($tableName, array $ids, $sortingStatement = null)
1348  {
1350  $resolver = GeneralUtility::makeInstance(
1351  PlainDataResolver::class,
1352  $tableName,
1353  $ids,
1354  $sortingStatement
1355  );
1356  $resolver->setWorkspaceId($this->getWorkspaceId());
1357  $resolver->setKeepDeletePlaceholder(true);
1358  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1359  return $resolver;
1360  }
1361 
1365  protected function getDatabaseConnection()
1366  {
1367  return $GLOBALS['TYPO3_DB'];
1368  }
1369 }