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