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