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