TYPO3 CMS  TYPO3_7-6
RelationHandler.php
Go to the documentation of this file.
1 <?php
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 = [];
51 
57  public $itemArray = [];
58 
64  public $nonTableArray = [];
65 
69  public $additionalWhere = [];
70 
76  public $checkIfDeleted = true;
77 
81  public $dbPaths = [];
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 = [];
155 
162 
168  public $MM_insert_fields = [];
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 = [];
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 = [])
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'] : [];
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] = [];
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 (
411  (string)$theID != ''
412  // allow the default language '0' for the special languages configuration
413  && ($theID || isset($configuration['special']) && $configuration['special'] === 'languages')
414  && $theTable && isset($this->tableArray[$theTable])
415  ) {
416  // Get ID as the right value:
417  $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
418  // Register ID/table name in internal arrays:
419  $this->itemArray[$key]['id'] = $theID;
420  $this->itemArray[$key]['table'] = $theTable;
421  $this->tableArray[$theTable][] = $theID;
422  // Set update-flag:
423  $isSet = 1;
424  }
425  }
426  // If it turns out that the value from the list was NOT a valid reference to a table-record,
427  // then we might still set it as a NO_TABLE value:
428  if (!$isSet && $this->registerNonTableValues) {
429  $this->itemArray[$key]['id'] = $tempItemArray[$key];
430  $this->itemArray[$key]['table'] = '_NO_TABLE';
431  $this->nonTableArray[] = $tempItemArray[$key];
432  }
433  }
434 
435  // Skip if not dealing with IRRE in a CSV list on a workspace
436  if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
437  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
438  || (int)$GLOBALS['BE_USER']->workspace === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
439  return;
440  }
441 
442  // Fetch live record data
443  if ($this->useLiveReferenceIds) {
444  foreach ($this->itemArray as &$item) {
445  $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
446  }
447  // Directly overlay workspace data
448  } else {
449  $this->itemArray = [];
450  $foreignTable = $configuration['foreign_table'];
451  $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
452  foreach ($ids as $id) {
453  $this->itemArray[] = [
454  'id' => $id,
455  'table' => $foreignTable,
456  ];
457  }
458  }
459  }
460  }
461 
470  public function sortList($sortby)
471  {
472  // Sort directly without fetching addional data
473  if ($sortby == 'uid') {
474  usort(
475  $this->itemArray,
476  function ($a, $b) {
477  return $a['id'] < $b['id'] ? -1 : 1;
478  }
479  );
480  } elseif (count($this->tableArray) === 1) {
481  reset($this->tableArray);
482  $table = key($this->tableArray);
483  $uidList = implode(',', current($this->tableArray));
484  if ($uidList) {
485  $this->itemArray = [];
486  $this->tableArray = [];
487  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'uid IN (' . $uidList . ')', '', $sortby);
488  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
489  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
490  $this->tableArray[$table][] = $row['uid'];
491  }
492  $GLOBALS['TYPO3_DB']->sql_free_result($res);
493  }
494  }
495  }
496 
505  public function readMM($tableName, $uid)
506  {
507  $key = 0;
508  $additionalWhere = '';
509  $theTable = null;
510  // In case of a reverse relation
511  if ($this->MM_is_foreign) {
512  $uidLocal_field = 'uid_foreign';
513  $uidForeign_field = 'uid_local';
514  $sorting_field = 'sorting_foreign';
515  if ($this->MM_isMultiTableRelationship) {
516  $additionalWhere .= ' AND ( tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $tableName);
517  // Be backwards compatible! When allowing more than one table after
518  // having previously allowed only one table, this case applies.
519  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
520  $additionalWhere .= ' OR tablenames=\'\'';
521  }
522  $additionalWhere .= ' ) ';
523  }
524  $theTable = $this->MM_oppositeTable;
525  } else {
526  // Default
527  $uidLocal_field = 'uid_local';
528  $uidForeign_field = 'uid_foreign';
529  $sorting_field = 'sorting';
530  }
531  if ($this->MM_table_where) {
532  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
533  }
534  foreach ($this->MM_match_fields as $field => $value) {
535  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $tableName);
536  }
537  // Select all MM relations:
538  $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere;
539  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tableName, $where, '', $sorting_field);
540  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
541  // Default
542  if (!$this->MM_is_foreign) {
543  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
544  $theTable = $row['tablenames'] ?: $this->firstTable;
545  }
546  if (($row[$uidForeign_field] || $theTable == 'pages') && $theTable && isset($this->tableArray[$theTable])) {
547  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
548  $this->itemArray[$key]['table'] = $theTable;
549  $this->tableArray[$theTable][] = $row[$uidForeign_field];
550  } elseif ($this->registerNonTableValues) {
551  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
552  $this->itemArray[$key]['table'] = '_NO_TABLE';
553  $this->nonTableArray[] = $row[$uidForeign_field];
554  }
555  $key++;
556  }
557  $GLOBALS['TYPO3_DB']->sql_free_result($res);
558  }
559 
568  public function writeMM($MM_tableName, $uid, $prependTableName = false)
569  {
570  // In case of a reverse relation
571  if ($this->MM_is_foreign) {
572  $uidLocal_field = 'uid_foreign';
573  $uidForeign_field = 'uid_local';
574  $sorting_field = 'sorting_foreign';
575  } else {
576  // default
577  $uidLocal_field = 'uid_local';
578  $uidForeign_field = 'uid_foreign';
579  $sorting_field = 'sorting';
580  }
581  // If there are tables...
582  $tableC = count($this->tableArray);
583  if ($tableC) {
584  // Boolean: does the field "tablename" need to be filled?
585  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
586  $c = 0;
587  $additionalWhere_tablenames = '';
588  if ($this->MM_is_foreign && $prep) {
589  $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
590  }
591  $additionalWhere = '';
592  // Add WHERE clause if configured
593  if ($this->MM_table_where) {
594  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
595  }
596  // Select, update or delete only those relations that match the configured fields
597  foreach ($this->MM_match_fields as $field => $value) {
598  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
599  }
600  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
601  $uidForeign_field . ($prep ? ', tablenames' : '') . ($this->MM_hasUidField ? ', uid' : ''),
602  $MM_tableName,
603  $uidLocal_field . '=' . $uid . $additionalWhere_tablenames . $additionalWhere,
604  '',
605  $sorting_field
606  );
607  $oldMMs = [];
608  // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
609  // If the UID is present it will be used to update sorting and delete MM-records.
610  // This is necessary if the "multiple" feature is used for the MM relations.
611  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
612  $oldMMs_inclUid = [];
613  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
614  if (!$this->MM_is_foreign && $prep) {
615  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
616  } else {
617  $oldMMs[] = $row[$uidForeign_field];
618  }
619  $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid']];
620  }
621  $GLOBALS['TYPO3_DB']->sql_free_result($res);
622  // For each item, insert it:
623  foreach ($this->itemArray as $val) {
624  $c++;
625  if ($prep || $val['table'] == '_NO_TABLE') {
626  // Insert current table if needed
627  if ($this->MM_is_foreign) {
628  $tablename = $this->currentTable;
629  } else {
630  $tablename = $val['table'];
631  }
632  } else {
633  $tablename = '';
634  }
635  if (!$this->MM_is_foreign && $prep) {
636  $item = [$val['table'], $val['id']];
637  } else {
638  $item = $val['id'];
639  }
640  if (in_array($item, $oldMMs)) {
641  $oldMMs_index = array_search($item, $oldMMs);
642  // In principle, selecting on the UID is all we need to do
643  // if a uid field is available since that is unique!
644  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
645  $whereClause = $uidLocal_field . '=' . $uid . ' AND ' . $uidForeign_field . '=' . $val['id']
646  . ($this->MM_hasUidField ? ' AND uid=' . (int)$oldMMs_inclUid[$oldMMs_index][2] : '');
647  if ($tablename) {
648  $whereClause .= ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tablename, $MM_tableName);
649  }
650  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $whereClause . $additionalWhere, [$sorting_field => $c]);
651  // Remove the item from the $oldMMs array so after this
652  // foreach loop only the ones that need to be deleted are in there.
653  unset($oldMMs[$oldMMs_index]);
654  // Remove the item from the $oldMMs_inclUid array so after this
655  // foreach loop only the ones that need to be deleted are in there.
656  unset($oldMMs_inclUid[$oldMMs_index]);
657  } else {
658  $insertFields = $this->MM_insert_fields;
659  $insertFields[$uidLocal_field] = $uid;
660  $insertFields[$uidForeign_field] = $val['id'];
661  $insertFields[$sorting_field] = $c;
662  if ($tablename) {
663  $insertFields['tablenames'] = $tablename;
664  $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
665  }
666  $GLOBALS['TYPO3_DB']->exec_INSERTquery($MM_tableName, $insertFields);
667  if ($this->MM_is_foreign) {
668  $this->updateRefIndex($val['table'], $val['id']);
669  }
670  }
671  }
672  // Delete all not-used relations:
673  if (is_array($oldMMs) && !empty($oldMMs)) {
674  $removeClauses = [];
675  $updateRefIndex_records = [];
676  foreach ($oldMMs as $oldMM_key => $mmItem) {
677  // If UID field is present, of course we need only use that for deleting.
678  if ($this->MM_hasUidField) {
679  $removeClauses[] = 'uid=' . (int)$oldMMs_inclUid[$oldMM_key][2];
680  } else {
681  if (is_array($mmItem)) {
682  $removeClauses[] = 'tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($mmItem[0], $MM_tableName)
683  . ' AND ' . $uidForeign_field . '=' . $mmItem[1];
684  } else {
685  $removeClauses[] = $uidForeign_field . '=' . $mmItem;
686  }
687  }
688  if ($this->MM_is_foreign) {
689  if (is_array($mmItem)) {
690  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
691  } else {
692  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
693  }
694  }
695  }
696  $deleteAddWhere = ' AND (' . implode(' OR ', $removeClauses) . ')';
697  $where = $uidLocal_field . '=' . (int)$uid . $deleteAddWhere . $additionalWhere_tablenames . $additionalWhere;
698  $GLOBALS['TYPO3_DB']->exec_DELETEquery($MM_tableName, $where);
699  // Update ref index:
700  foreach ($updateRefIndex_records as $pair) {
701  $this->updateRefIndex($pair[0], $pair[1]);
702  }
703  }
704  // Update ref index; In tcemain it is not certain that this will happen because
705  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
706  // This could also have been fixed in updateDB in tcemain, however I decided to do it here ...
707  $this->updateRefIndex($this->currentTable, $uid);
708  }
709  }
710 
721  public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
722  {
723  // In case of a reverse relation
724  if ($this->MM_is_foreign) {
725  $uidLocal_field = 'uid_foreign';
726  } else {
727  // default
728  $uidLocal_field = 'uid_local';
729  }
730  // If there are tables...
731  $tableC = count($this->tableArray);
732  if ($tableC) {
733  // Boolean: does the field "tablename" need to be filled?
734  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
735  $additionalWhere_tablenames = '';
736  if ($this->MM_is_foreign && $prep) {
737  $additionalWhere_tablenames = ' AND tablenames=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $MM_tableName);
738  }
739  $additionalWhere = '';
740  // Add WHERE clause if configured
741  if ($this->MM_table_where) {
742  $additionalWhere .= LF . str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where);
743  }
744  // Select, update or delete only those relations that match the configured fields
745  foreach ($this->MM_match_fields as $field => $value) {
746  $additionalWhere .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $MM_tableName);
747  }
748  $where = $uidLocal_field . '=' . (int)$uid . $additionalWhere_tablenames . $additionalWhere;
749  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($MM_tableName, $where, [$uidLocal_field => $newUid]);
750  }
751  }
752 
761  public function readForeignField($uid, $conf)
762  {
763  if ($this->useLiveParentIds) {
764  $uid = $this->getLiveDefaultId($this->currentTable, $uid);
765  }
766 
767  $key = 0;
768  $uid = (int)$uid;
769  $foreign_table = $conf['foreign_table'];
770  $foreign_table_field = $conf['foreign_table_field'];
771  $useDeleteClause = !$this->undeleteRecord;
772  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
773  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
774  if ($conf['symmetric_field']) {
775  $whereClause = '(' . $conf['foreign_field'] . '=' . $uid . ' OR ' . $conf['symmetric_field'] . '=' . $uid . ')';
776  } else {
777  $whereClause = $conf['foreign_field'] . '=' . $uid;
778  }
779  // Use the deleteClause (e.g. "deleted=0") on this table
780  if ($useDeleteClause) {
781  $whereClause .= BackendUtility::deleteClause($foreign_table);
782  }
783  // If it's requested to look for the parent uid AND the parent table,
784  // add an additional SQL-WHERE clause
785  if ($foreign_table_field && $this->currentTable) {
786  $whereClause .= ' AND ' . $foreign_table_field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->currentTable, $foreign_table);
787  }
788  // Add additional where clause if foreign_match_fields are defined
789  foreach ($foreign_match_fields as $field => $value) {
790  $whereClause .= ' AND ' . $field . '=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($value, $foreign_table);
791  }
792  // Select children from the live(!) workspace only
793  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
794  $workspaceList = '0,' . $this->getWorkspaceId();
795  $whereClause .= ' AND ' . $foreign_table . '.t3ver_wsid IN (' . $workspaceList . ') AND ' . $foreign_table . '.pid<>-1';
796  }
797  // Get the correct sorting field
798  // Specific manual sortby for data handled by this field
799  $sortby = '';
800  if ($conf['foreign_sortby']) {
801  if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
802  // Sorting depends on, from which side of the relation we're looking at it
803  $sortby = '
804  CASE
805  WHEN ' . $conf['foreign_field'] . '=' . $uid . '
806  THEN ' . $conf['foreign_sortby'] . '
807  ELSE ' . $conf['symmetric_sortby'] . '
808  END';
809  } else {
810  // Regular single-side behaviour
811  $sortby = $conf['foreign_sortby'];
812  }
813  } elseif ($conf['foreign_default_sortby']) {
814  // Specific default sortby for data handled by this field
815  $sortby = $conf['foreign_default_sortby'];
816  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
817  // Manual sortby for all table records
818  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
819  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
820  // Default sortby for all table records
821  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
822  }
823  // Strip a possible "ORDER BY" in front of the $sortby value
824  $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
825  // Get the rows from storage
826  $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', $foreign_table, $whereClause, '', $sortby, '', 'uid');
827  if (!empty($rows)) {
828  $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
829  foreach ($ids as $id) {
830  $this->itemArray[$key]['id'] = $id;
831  $this->itemArray[$key]['table'] = $foreign_table;
832  $this->tableArray[$foreign_table][] = $id;
833  $key++;
834  }
835  }
836  }
837 
847  public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
848  {
849  if ($this->useLiveParentIds) {
850  $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
851  if (!empty($updateToUid)) {
852  $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
853  }
854  }
855 
856  $c = 0;
857  $foreign_table = $conf['foreign_table'];
858  $foreign_field = $conf['foreign_field'];
859  $symmetric_field = $conf['symmetric_field'];
860  $foreign_table_field = $conf['foreign_table_field'];
861  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
862  // If there are table items and we have a proper $parentUid
863  if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
864  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
865  if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
866  $updateToUid = 0;
867  }
868  $considerWorkspaces = ($GLOBALS['BE_USER']->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreign_table));
869  $fields = 'uid,pid,' . $foreign_field;
870  // Consider the symmetric field if defined:
871  if ($symmetric_field) {
872  $fields .= ',' . $symmetric_field;
873  }
874  // Consider workspaces if defined and currently used:
875  if ($considerWorkspaces) {
876  $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
877  }
878  // Update all items
879  foreach ($this->itemArray as $val) {
880  $uid = $val['id'];
881  $table = $val['table'];
882  $row = [];
883  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
884  if ($symmetric_field || $considerWorkspaces) {
885  $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
886  if (empty($row)) {
887  continue;
888  }
889  }
890  $isOnSymmetricSide = false;
891  if ($symmetric_field) {
892  $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
893  }
894  $updateValues = $foreign_match_fields;
895  // No update to the uid is requested, so this is the normal behaviour
896  // just update the fields and care about sorting
897  if (!$updateToUid) {
898  // Always add the pointer to the parent uid
899  if ($isOnSymmetricSide) {
900  $updateValues[$symmetric_field] = $parentUid;
901  } else {
902  $updateValues[$foreign_field] = $parentUid;
903  }
904  // If it is configured in TCA also to store the parent table in the child record, just do it
905  if ($foreign_table_field && $this->currentTable) {
906  $updateValues[$foreign_table_field] = $this->currentTable;
907  }
908  // Update sorting columns if not to be skipped
909  if (!$skipSorting) {
910  // Get the correct sorting field
911  // Specific manual sortby for data handled by this field
912  $sortby = '';
913  if ($conf['foreign_sortby']) {
914  $sortby = $conf['foreign_sortby'];
915  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
916  // manual sortby for all table records
917  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
918  }
919  // Apply sorting on the symmetric side
920  // (it depends on who created the relation, so what uid is in the symmetric_field):
921  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
922  $sortby = $conf['symmetric_sortby'];
923  } else {
924  $sortby = $GLOBALS['TYPO3_DB']->stripOrderBy($sortby);
925  }
926  if ($sortby) {
927  $updateValues[$sortby] = ++$c;
928  }
929  }
930  } else {
931  if ($isOnSymmetricSide) {
932  $updateValues[$symmetric_field] = $updateToUid;
933  } else {
934  $updateValues[$foreign_field] = $updateToUid;
935  }
936  }
937  // Update accordant fields in the database:
938  if (!empty($updateValues)) {
939  // Update tstamp if any foreign field value has changed
940  if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
941  $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
942  }
943 
944  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$uid, $updateValues);
945  $this->updateRefIndex($table, $uid);
946  }
947  // Update accordant fields in the database for workspaces overlays/placeholders:
948  if ($considerWorkspaces) {
949  // It's the specific versioned record -> update placeholder (if any)
950  if (!empty($row['t3ver_oid']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
951  $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$row['t3ver_oid'], $updateValues);
952  }
953  }
954  }
955  }
956  }
957 
964  public function getValueArray($prependTableName = false)
965  {
966  // INIT:
967  $valueArray = [];
968  $tableC = count($this->tableArray);
969  // If there are tables in the table array:
970  if ($tableC) {
971  // If there are more than ONE table in the table array, then always prepend table names:
972  $prep = $tableC > 1 || $prependTableName;
973  // Traverse the array of items:
974  foreach ($this->itemArray as $val) {
975  $valueArray[] = ($prep && $val['table'] != '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
976  }
977  }
978  // Return the array
979  return $valueArray;
980  }
981 
991  public function convertPosNeg($valueArray, $fTable, $nfTable)
992  {
994  if (is_array($valueArray) && $fTable) {
995  foreach ($valueArray as $key => $val) {
996  $val = strrev($val);
997  $parts = explode('_', $val, 2);
998  $theID = strrev($parts[0]);
999  $theTable = strrev($parts[1]);
1001  && (!$theTable || $theTable === (string)$fTable || $theTable === (string)$nfTable)
1002  ) {
1003  $valueArray[$key] = $theTable && $theTable !== (string)$fTable ? $theID * -1 : $theID;
1004  }
1005  }
1006  }
1007  return $valueArray;
1008  }
1009 
1018  public function getFromDB()
1019  {
1020  // Traverses the tables listed:
1021  foreach ($this->tableArray as $key => $val) {
1022  if (is_array($val)) {
1023  $itemList = implode(',', $val);
1024  if ($itemList) {
1025  if ($this->fetchAllFields) {
1026  $from = '*';
1027  } else {
1028  $from = 'uid,pid';
1029  if ($GLOBALS['TCA'][$key]['ctrl']['label']) {
1030  // Titel
1031  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label'];
1032  }
1033  if ($GLOBALS['TCA'][$key]['ctrl']['label_alt']) {
1034  // Alternative Title-Fields
1035  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['label_alt'];
1036  }
1037  if ($GLOBALS['TCA'][$key]['ctrl']['thumbnail']) {
1038  // Thumbnail
1039  $from .= ',' . $GLOBALS['TCA'][$key]['ctrl']['thumbnail'];
1040  }
1041  }
1042  $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($from, $key, 'uid IN (' . $itemList . ')' . $this->additionalWhere[$key]);
1043  while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1044  $this->results[$key][$row['uid']] = $row;
1045  }
1046  $GLOBALS['TYPO3_DB']->sql_free_result($res);
1047  }
1048  }
1049  }
1050  return $this->results;
1051  }
1052 
1058  public function readyForInterface()
1059  {
1060  if (!is_array($this->itemArray)) {
1061  return false;
1062  }
1063  $output = [];
1064  $titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
1065  foreach ($this->itemArray as $val) {
1066  $theRow = $this->results[$val['table']][$val['id']];
1067  if ($theRow && is_array($GLOBALS['TCA'][$val['table']])) {
1068  $label = GeneralUtility::fixed_lgd_cs(strip_tags(
1069  BackendUtility::getRecordTitle($val['table'], $theRow)), $titleLen);
1070  $label = $label ? $label : '[...]';
1071  $output[] = str_replace(',', '', $val['table'] . '_' . $val['id'] . '|' . rawurlencode($label));
1072  }
1073  }
1074  return implode(',', $output);
1075  }
1076 
1083  public function countItems($returnAsArray = true)
1084  {
1085  $count = count($this->itemArray);
1086  if ($returnAsArray) {
1087  $count = [$count];
1088  }
1089  return $count;
1090  }
1091 
1101  public function updateRefIndex($table, $id)
1102  {
1103  $statisticsArray = [];
1104  if ($this->updateReferenceIndex) {
1106  $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
1108  $refIndexObj->setWorkspaceId($this->getWorkspaceId());
1109  }
1110  $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1111  }
1112  return $statisticsArray;
1113  }
1114 
1124  public function convertItemArray()
1125  {
1126  $hasBeenConverted = false;
1127 
1128  // conversion is only required in a workspace context
1129  // (the case that version ids are submitted in a live context are rare)
1130  if ($this->getWorkspaceId() === 0) {
1131  return $hasBeenConverted;
1132  }
1133 
1134  foreach ($this->tableArray as $tableName => $ids) {
1135  if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1136  continue;
1137  }
1138 
1139  // convert live ids to version ids if available
1140  $convertedIds = $this->getResolver($tableName, $ids)
1141  ->setKeepDeletePlaceholder(false)
1142  ->setKeepMovePlaceholder(false)
1143  ->processVersionOverlays($ids);
1144  foreach ($this->itemArray as $index => $item) {
1145  if ($item['table'] !== $tableName) {
1146  continue;
1147  }
1148  $currentItemId = $item['id'];
1149  if (
1150  !isset($convertedIds[$currentItemId])
1151  || $currentItemId === $convertedIds[$currentItemId]
1152  ) {
1153  continue;
1154  }
1155  // adjust local item to use resolved version id
1156  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1157  $hasBeenConverted = true;
1158  }
1159  // update per-table reference for ids
1160  if ($hasBeenConverted) {
1161  $this->tableArray[$tableName] = array_values($convertedIds);
1162  }
1163  }
1164 
1165  return $hasBeenConverted;
1166  }
1167 
1172  public function purgeItemArray($workspaceId = null)
1173  {
1174  if ($workspaceId === null) {
1175  $workspaceId = $this->getWorkspaceId();
1176  } else {
1177  $workspaceId = (int)$workspaceId;
1178  }
1179 
1180  // Ensure, only live relations are in the items Array
1181  if ($workspaceId === 0) {
1182  $purgeCallback = 'purgeVersionedIds';
1183  // Otherwise, ensure that live relations are purged if version exists
1184  } else {
1185  $purgeCallback = 'purgeLiveVersionedIds';
1186  }
1187 
1188  $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1189  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1190  return $itemArrayHasBeenPurged;
1191  }
1192 
1198  public function processDeletePlaceholder()
1199  {
1200  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1201  return false;
1202  }
1203 
1204  return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1205  }
1206 
1213  protected function purgeItemArrayHandler($purgeCallback)
1214  {
1215  $itemArrayHasBeenPurged = false;
1216 
1217  foreach ($this->tableArray as $itemTableName => $itemIds) {
1218  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1219  continue;
1220  }
1221 
1222  $purgedItemIds = call_user_func([$this, $purgeCallback], $itemTableName, $itemIds);
1223  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1224  foreach ($removedItemIds as $removedItemId) {
1225  $this->removeFromItemArray($itemTableName, $removedItemId);
1226  }
1227  $this->tableArray[$itemTableName] = $purgedItemIds;
1228  if (!empty($removedItemIds)) {
1229  $itemArrayHasBeenPurged = true;
1230  }
1231  }
1232 
1233  return $itemArrayHasBeenPurged;
1234  }
1235 
1243  protected function purgeVersionedIds($tableName, array $ids)
1244  {
1245  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1246  $ids = array_combine($ids, $ids);
1247 
1248  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1249  'uid,t3ver_oid,t3ver_state',
1250  $tableName,
1251  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid<>0',
1252  '',
1253  't3ver_state DESC'
1254  );
1255 
1256  if (!empty($versions)) {
1257  foreach ($versions as $version) {
1258  $versionId = $version['uid'];
1259  if (isset($ids[$versionId])) {
1260  unset($ids[$versionId]);
1261  }
1262  }
1263  }
1264 
1265  return array_values($ids);
1266  }
1267 
1275  protected function purgeLiveVersionedIds($tableName, array $ids)
1276  {
1277  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1278  $ids = array_combine($ids, $ids);
1279 
1280  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1281  'uid,t3ver_oid,t3ver_state',
1282  $tableName,
1283  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid<>0',
1284  '',
1285  't3ver_state DESC'
1286  );
1287 
1288  if (!empty($versions)) {
1289  foreach ($versions as $version) {
1290  $versionId = $version['uid'];
1291  $liveId = $version['t3ver_oid'];
1292  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1293  unset($ids[$liveId]);
1294  }
1295  }
1296  }
1297 
1298  return array_values($ids);
1299  }
1300 
1308  protected function purgeDeletePlaceholder($tableName, array $ids)
1309  {
1310  $ids = $this->getDatabaseConnection()->cleanIntArray($ids);
1311  $ids = array_combine($ids, $ids);
1312 
1313  $versions = $this->getDatabaseConnection()->exec_SELECTgetRows(
1314  'uid,t3ver_oid,t3ver_state',
1315  $tableName,
1316  'pid=-1 AND t3ver_oid IN (' . implode(',', $ids) . ') AND t3ver_wsid=' . $this->getWorkspaceId() .
1318  );
1319 
1320  if (!empty($versions)) {
1321  foreach ($versions as $version) {
1322  $liveId = $version['t3ver_oid'];
1323  if (isset($ids[$liveId])) {
1324  unset($ids[$liveId]);
1325  }
1326  }
1327  }
1328 
1329  return array_values($ids);
1330  }
1331 
1332  protected function removeFromItemArray($tableName, $id)
1333  {
1334  foreach ($this->itemArray as $index => $item) {
1335  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1336  unset($this->itemArray[$index]);
1337  return true;
1338  }
1339  }
1340  return false;
1341  }
1342 
1351  public static function isOnSymmetricSide($parentUid, $parentConf, $childRec)
1352  {
1353  return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1354  && $parentConf['symmetric_field']
1355  && $parentUid == $childRec[$parentConf['symmetric_field']];
1356  }
1357 
1366  protected function completeOppositeUsageValues($tableName, array $referenceValues)
1367  {
1368  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1369  return $referenceValues;
1370  }
1371 
1372  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1373  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1374  return $referenceValues;
1375  }
1376 
1377  $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1378  if (!empty($configuration['MM_insert_fields'])) {
1379  $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1380  } elseif (!empty($configuration['MM_match_fields'])) {
1381  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1382  }
1383 
1384  return $referenceValues;
1385  }
1386 
1395  protected function getLiveDefaultId($tableName, $id)
1396  {
1397  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1398  if ($liveDefaultId === null) {
1399  $liveDefaultId = $id;
1400  }
1401  return (int)$liveDefaultId;
1402  }
1403 
1410  protected function getResolver($tableName, array $ids, $sortingStatement = null)
1411  {
1413  $resolver = GeneralUtility::makeInstance(
1414  PlainDataResolver::class,
1415  $tableName,
1416  $ids,
1417  $sortingStatement
1418  );
1419  $resolver->setWorkspaceId($this->getWorkspaceId());
1420  $resolver->setKeepDeletePlaceholder(true);
1421  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1422  return $resolver;
1423  }
1424 
1428  protected function getDatabaseConnection()
1429  {
1430  return $GLOBALS['TYPO3_DB'];
1431  }
1432 }
completeOppositeUsageValues($tableName, array $referenceValues)
writeMM($MM_tableName, $uid, $prependTableName=false)
setUpdateReferenceIndex($updateReferenceIndex)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static isOnSymmetricSide($parentUid, $parentConf, $childRec)
writeForeignField($conf, $parentUid, $updateToUid=0, $skipSorting=false)
readList($itemlist, array $configuration)
purgeLiveVersionedIds($tableName, array $ids)
convertPosNeg($valueArray, $fTable, $nfTable)
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
purgeDeletePlaceholder($tableName, array $ids)
static fixed_lgd_cs($string, $chars, $appendString='...')
$uid
Definition: server.php:38
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
start($itemlist, $tablelist, $MMtable='', $MMuid=0, $currentTable='', $conf=[])
static deleteClause($table, $tableAlias='')
remapMM($MM_tableName, $uid, $newUid, $prependTableName=false)