1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Core\Tree\TableConfiguration; 17 18use Psr\EventDispatcher\EventDispatcherInterface; 19use TYPO3\CMS\Backend\Tree\SortedTreeNodeCollection; 20use TYPO3\CMS\Backend\Tree\TreeNode; 21use TYPO3\CMS\Backend\Tree\TreeNodeCollection; 22use TYPO3\CMS\Backend\Utility\BackendUtility; 23use TYPO3\CMS\Core\Database\ConnectionPool; 24use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; 25use TYPO3\CMS\Core\Database\RelationHandler; 26use TYPO3\CMS\Core\Imaging\Icon; 27use TYPO3\CMS\Core\Imaging\IconFactory; 28use TYPO3\CMS\Core\Localization\LanguageService; 29use TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent; 30use TYPO3\CMS\Core\Utility\GeneralUtility; 31 32/** 33 * TCA tree data provider 34 */ 35class DatabaseTreeDataProvider extends AbstractTableConfigurationTreeDataProvider 36{ 37 /** 38 * @deprecated, will be removed in TYPO3 v11.0, use the EventDispatcher instead of Signal/Slot logic 39 */ 40 const SIGNAL_PostProcessTreeData = 'PostProcessTreeData'; 41 const MODE_CHILDREN = 1; 42 const MODE_PARENT = 2; 43 44 /** 45 * @var string 46 */ 47 protected $tableName = ''; 48 49 /** 50 * @var string 51 */ 52 protected $treeId = ''; 53 54 /** 55 * @var string 56 */ 57 protected $labelField = ''; 58 59 /** 60 * @var string 61 */ 62 protected $tableWhere = ''; 63 64 /** 65 * @var int 66 */ 67 protected $lookupMode = self::MODE_CHILDREN; 68 69 /** 70 * @var string 71 */ 72 protected $lookupField = ''; 73 74 /** 75 * @var int 76 */ 77 protected $rootUid = 0; 78 79 /** 80 * @var array 81 */ 82 protected $idCache = []; 83 84 /** 85 * Stores TCA-Configuration of the LookUpField in tableName 86 * 87 * @var array 88 */ 89 protected $columnConfiguration; 90 91 /** 92 * node sort values (the orderings from foreign_Table_where evaluation) 93 * 94 * @var array 95 */ 96 protected $nodeSortValues = []; 97 98 /** 99 * @var array TCEforms compiled TSConfig array 100 */ 101 protected $generatedTSConfig = []; 102 103 /** 104 * @var EventDispatcherInterface 105 */ 106 protected $eventDispatcher; 107 108 public function __construct(EventDispatcherInterface $eventDispatcher) 109 { 110 $this->eventDispatcher = $eventDispatcher; 111 } 112 113 /** 114 * Sets the label field 115 * 116 * @param string $labelField 117 */ 118 public function setLabelField($labelField) 119 { 120 $this->labelField = $labelField; 121 } 122 123 /** 124 * Gets the label field 125 * 126 * @return string 127 */ 128 public function getLabelField() 129 { 130 return $this->labelField; 131 } 132 133 /** 134 * Sets the table name 135 * 136 * @param string $tableName 137 */ 138 public function setTableName($tableName) 139 { 140 $this->tableName = $tableName; 141 } 142 143 /** 144 * Gets the table name 145 * 146 * @return string 147 */ 148 public function getTableName() 149 { 150 return $this->tableName; 151 } 152 153 /** 154 * Sets the lookup field 155 * 156 * @param string $lookupField 157 */ 158 public function setLookupField($lookupField) 159 { 160 $this->lookupField = $lookupField; 161 } 162 163 /** 164 * Gets the lookup field 165 * 166 * @return string 167 */ 168 public function getLookupField() 169 { 170 return $this->lookupField; 171 } 172 173 /** 174 * Sets the lookup mode 175 * 176 * @param int $lookupMode 177 */ 178 public function setLookupMode($lookupMode) 179 { 180 $this->lookupMode = $lookupMode; 181 } 182 183 /** 184 * Gets the lookup mode 185 * 186 * @return int 187 */ 188 public function getLookupMode() 189 { 190 return $this->lookupMode; 191 } 192 193 /** 194 * Gets the nodes 195 * 196 * @param \TYPO3\CMS\Backend\Tree\TreeNode $node 197 */ 198 public function getNodes(TreeNode $node) 199 { 200 } 201 202 /** 203 * Gets the root node 204 * 205 * @return \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode 206 */ 207 public function getRoot() 208 { 209 return $this->buildRepresentationForNode($this->treeData); 210 } 211 212 /** 213 * Sets the root uid 214 * 215 * @param int $rootUid 216 */ 217 public function setRootUid($rootUid) 218 { 219 $this->rootUid = $rootUid; 220 } 221 222 /** 223 * Gets the root uid 224 * 225 * @return int 226 */ 227 public function getRootUid() 228 { 229 return $this->rootUid; 230 } 231 232 /** 233 * Sets the tableWhere clause 234 * 235 * @param string $tableWhere 236 */ 237 public function setTableWhere($tableWhere) 238 { 239 $this->tableWhere = $tableWhere; 240 } 241 242 /** 243 * Gets the tableWhere clause 244 * 245 * @return string 246 */ 247 public function getTableWhere() 248 { 249 return $this->tableWhere; 250 } 251 252 /** 253 * Builds a complete node including childs 254 * 255 * @param \TYPO3\CMS\Backend\Tree\TreeNode $basicNode 256 * @param \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode|null $parent 257 * @param int $level 258 * @return \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode Node object 259 */ 260 protected function buildRepresentationForNode(TreeNode $basicNode, DatabaseTreeNode $parent = null, $level = 0) 261 { 262 /** @var \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode $node */ 263 $node = GeneralUtility::makeInstance(DatabaseTreeNode::class); 264 $row = []; 265 if ($basicNode->getId() == 0) { 266 $node->setSelected(false); 267 $node->setExpanded(true); 268 $node->setLabel($this->getLanguageService()->sL($GLOBALS['TCA'][$this->tableName]['ctrl']['title'])); 269 } else { 270 $row = BackendUtility::getRecordWSOL($this->tableName, (int)$basicNode->getId(), '*', '', false); 271 $node->setLabel(BackendUtility::getRecordTitle($this->tableName, $row) ?: $basicNode->getId()); 272 $node->setSelected(GeneralUtility::inList($this->getSelectedList(), $basicNode->getId())); 273 $node->setExpanded($this->isExpanded($basicNode)); 274 } 275 $node->setId($basicNode->getId()); 276 $node->setSelectable(!GeneralUtility::inList($this->getNonSelectableLevelList(), (string)$level) && !in_array($basicNode->getId(), $this->getItemUnselectableList())); 277 $node->setSortValue($this->nodeSortValues[$basicNode->getId()]); 278 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 279 $node->setIcon($iconFactory->getIconForRecord($this->tableName, $row, Icon::SIZE_SMALL)); 280 $node->setParentNode($parent); 281 if ($basicNode->hasChildNodes()) { 282 $node->setHasChildren(true); 283 /** @var \TYPO3\CMS\Backend\Tree\SortedTreeNodeCollection $childNodes */ 284 $childNodes = GeneralUtility::makeInstance(SortedTreeNodeCollection::class); 285 $tempNodes = []; 286 foreach ($basicNode->getChildNodes() as $child) { 287 $tempNodes[] = $this->buildRepresentationForNode($child, $node, $level + 1); 288 } 289 $childNodes->exchangeArray($tempNodes); 290 $childNodes->asort(); 291 $node->setChildNodes($childNodes); 292 } 293 return $node; 294 } 295 296 /** 297 * Init the tree data 298 */ 299 public function initializeTreeData() 300 { 301 parent::initializeTreeData(); 302 $this->nodeSortValues = array_flip($this->itemWhiteList); 303 $this->columnConfiguration = $GLOBALS['TCA'][$this->getTableName()]['columns'][$this->getLookupField()]['config']; 304 if (isset($this->columnConfiguration['foreign_table']) && $this->columnConfiguration['foreign_table'] != $this->getTableName()) { 305 throw new \InvalidArgumentException('TCA Tree configuration is invalid: tree for different node-Tables is not implemented yet', 1290944650); 306 } 307 $this->treeData = GeneralUtility::makeInstance(TreeNode::class); 308 $this->loadTreeData(); 309 /** @var ModifyTreeDataEvent $event */ 310 $event = $this->eventDispatcher->dispatch(new ModifyTreeDataEvent($this->treeData, $this)); 311 $this->treeData = $event->getTreeData(); 312 } 313 314 /** 315 * Loads the tree data (all possible children) 316 */ 317 protected function loadTreeData() 318 { 319 $this->treeData->setId($this->getRootUid()); 320 $this->treeData->setParentNode(null); 321 if ($this->levelMaximum >= 1) { 322 $childNodes = $this->getChildrenOf($this->treeData, 1); 323 if ($childNodes !== null) { 324 $this->treeData->setChildNodes($childNodes); 325 } 326 } 327 } 328 329 /** 330 * Gets node children 331 * 332 * @param \TYPO3\CMS\Backend\Tree\TreeNode $node 333 * @param int $level 334 * @return \TYPO3\CMS\Backend\Tree\TreeNodeCollection|null 335 */ 336 protected function getChildrenOf(TreeNode $node, $level) 337 { 338 $nodeData = null; 339 if ($node->getId() !== 0) { 340 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 341 ->getQueryBuilderForTable($this->getTableName()); 342 $queryBuilder->getRestrictions()->removeAll(); 343 $nodeData = $queryBuilder->select('*') 344 ->from($this->getTableName()) 345 ->where( 346 $queryBuilder->expr()->eq( 347 'uid', 348 $queryBuilder->createNamedParameter($node->getId(), \PDO::PARAM_INT) 349 ) 350 ) 351 ->setMaxResults(1) 352 ->execute() 353 ->fetch(); 354 } 355 if (empty($nodeData)) { 356 $nodeData = [ 357 'uid' => 0, 358 $this->getLookupField() => '' 359 ]; 360 } 361 $storage = null; 362 $children = $this->getRelatedRecords($nodeData); 363 if (!empty($children)) { 364 /** @var \TYPO3\CMS\Backend\Tree\TreeNodeCollection $storage */ 365 $storage = GeneralUtility::makeInstance(TreeNodeCollection::class); 366 foreach ($children as $child) { 367 $node = GeneralUtility::makeInstance(TreeNode::class); 368 $node->setId($child); 369 if ($level < $this->levelMaximum) { 370 $children = $this->getChildrenOf($node, $level + 1); 371 if ($children !== null) { 372 $node->setChildNodes($children); 373 } 374 } 375 $storage->append($node); 376 } 377 } 378 return $storage; 379 } 380 381 /** 382 * Gets related records depending on TCA configuration 383 * 384 * @param array $row 385 * @return array 386 */ 387 protected function getRelatedRecords(array $row) 388 { 389 if ($this->getLookupMode() == self::MODE_PARENT) { 390 $children = $this->getChildrenUidsFromParentRelation($row); 391 } else { 392 $children = $this->getChildrenUidsFromChildrenRelation($row); 393 } 394 $allowedArray = []; 395 foreach ($children as $child) { 396 if (!in_array($child, $this->idCache) && in_array($child, $this->itemWhiteList)) { 397 $allowedArray[] = $child; 398 } 399 } 400 $this->idCache = array_merge($this->idCache, $allowedArray); 401 return $allowedArray; 402 } 403 404 /** 405 * Gets related records depending on TCA configuration 406 * 407 * @param array $row 408 * @return array 409 */ 410 protected function getChildrenUidsFromParentRelation(array $row) 411 { 412 $uid = $row['uid']; 413 switch ((string)$this->columnConfiguration['type']) { 414 case 'inline': 415 416 case 'select': 417 if ($this->columnConfiguration['MM']) { 418 /** @var \TYPO3\CMS\Core\Database\RelationHandler $dbGroup */ 419 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class); 420 // Dummy field for setting "look from other site" 421 $this->columnConfiguration['MM_oppositeField'] = 'children'; 422 $dbGroup->start($row[$this->getLookupField()], $this->getTableName(), $this->columnConfiguration['MM'], $uid, $this->getTableName(), $this->columnConfiguration); 423 $relatedUids = $dbGroup->tableArray[$this->getTableName()]; 424 } elseif ($this->columnConfiguration['foreign_field']) { 425 $relatedUids = $this->listFieldQuery($this->columnConfiguration['foreign_field'], $uid); 426 } else { 427 $relatedUids = $this->listFieldQuery($this->getLookupField(), $uid); 428 } 429 break; 430 default: 431 $relatedUids = $this->listFieldQuery($this->getLookupField(), $uid); 432 } 433 return $relatedUids; 434 } 435 436 /** 437 * Gets related children records depending on TCA configuration 438 * 439 * @param array $row 440 * @return array 441 */ 442 protected function getChildrenUidsFromChildrenRelation(array $row) 443 { 444 $relatedUids = []; 445 $uid = $row['uid']; 446 $value = $row[$this->getLookupField()]; 447 switch ((string)$this->columnConfiguration['type']) { 448 case 'inline': 449 // Intentional fall-through 450 case 'select': 451 if ($this->columnConfiguration['MM']) { 452 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class); 453 $dbGroup->start( 454 $value, 455 $this->getTableName(), 456 $this->columnConfiguration['MM'], 457 $uid, 458 $this->getTableName(), 459 $this->columnConfiguration 460 ); 461 $relatedUids = $dbGroup->tableArray[$this->getTableName()]; 462 } elseif ($this->columnConfiguration['foreign_field']) { 463 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 464 ->getQueryBuilderForTable($this->getTableName()); 465 $queryBuilder->getRestrictions()->removeAll(); 466 $records = $queryBuilder->select('uid') 467 ->from($this->getTableName()) 468 ->where( 469 $queryBuilder->expr()->eq( 470 $this->columnConfiguration['foreign_field'], 471 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 472 ) 473 ) 474 ->execute() 475 ->fetchAll(); 476 477 if (!empty($records)) { 478 $relatedUids = array_column($records, 'uid'); 479 } 480 } else { 481 $relatedUids = GeneralUtility::intExplode(',', $value, true); 482 } 483 break; 484 default: 485 $relatedUids = GeneralUtility::intExplode(',', $value, true); 486 } 487 return $relatedUids; 488 } 489 490 /** 491 * Queries the table for a field which might contain a list. 492 * 493 * @param string $fieldName the name of the field to be queried 494 * @param int $queryId the uid to search for 495 * @return int[] all uids found 496 */ 497 protected function listFieldQuery($fieldName, $queryId) 498 { 499 $queryId = (int)$queryId; 500 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 501 ->getQueryBuilderForTable($this->getTableName()); 502 $queryBuilder->getRestrictions()->removeAll(); 503 504 $queryBuilder->select('uid') 505 ->from($this->getTableName()) 506 ->where($queryBuilder->expr()->inSet($fieldName, $queryBuilder->quote($queryId))); 507 508 if ($queryId === 0) { 509 $queryBuilder->orWhere( 510 $queryBuilder->expr()->comparison( 511 'CAST(' . $queryBuilder->quoteIdentifier($fieldName) . ' AS CHAR)', 512 ExpressionBuilder::EQ, 513 $queryBuilder->quote('') 514 ) 515 ); 516 } 517 518 $records = $queryBuilder->execute()->fetchAll(); 519 $uidArray = is_array($records) ? array_column($records, 'uid') : []; 520 521 return $uidArray; 522 } 523 524 /** 525 * @return LanguageService|null 526 */ 527 protected function getLanguageService(): ?LanguageService 528 { 529 return $GLOBALS['LANG'] ?? null; 530 } 531} 532