DatabaseTreeDataProvider.php 16.9 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
Wouter Wolters's avatar
Wouter Wolters committed
15

16
17
namespace TYPO3\CMS\Core\Tree\TableConfiguration;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
20
21
use TYPO3\CMS\Backend\Tree\SortedTreeNodeCollection;
use TYPO3\CMS\Backend\Tree\TreeNode;
use TYPO3\CMS\Backend\Tree\TreeNodeCollection;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
24
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
25
use TYPO3\CMS\Core\Database\RelationHandler;
26
27
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
28
use TYPO3\CMS\Core\Localization\LanguageService;
29
use TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent;
30
31
use TYPO3\CMS\Core\Utility\GeneralUtility;

32
33
34
/**
 * TCA tree data provider
 */
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class DatabaseTreeDataProvider extends AbstractTableConfigurationTreeDataProvider
{
    const MODE_CHILDREN = 1;
    const MODE_PARENT = 2;

    /**
     * @var string
     */
    protected $tableName = '';

    /**
     * @var string
     */
    protected $treeId = '';

    /**
     * @var string
     */
    protected $labelField = '';

    /**
     * @var string
     */
    protected $tableWhere = '';

    /**
     * @var int
     */
    protected $lookupMode = self::MODE_CHILDREN;

    /**
     * @var string
     */
    protected $lookupField = '';

70
71
72
73
74
    /**
     * @var int[]
     */
    protected array $startingPoints = [0];

75
76
77
    /**
     * @var array
     */
78
    protected $idCache = [];
79
80
81
82
83
84
85
86
87
88
89
90
91

    /**
     * Stores TCA-Configuration of the LookUpField in tableName
     *
     * @var array
     */
    protected $columnConfiguration;

    /**
     * node sort values (the orderings from foreign_Table_where evaluation)
     *
     * @var array
     */
92
    protected $nodeSortValues = [];
93
94
95
96

    /**
     * @var array TCEforms compiled TSConfig array
     */
97
    protected $generatedTSConfig = [];
98
99

    /**
100
     * @var EventDispatcherInterface
101
     */
102
103
104
105
106
107
    protected $eventDispatcher;

    public function __construct(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
    }
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

    /**
     * Sets the label field
     *
     * @param string $labelField
     */
    public function setLabelField($labelField)
    {
        $this->labelField = $labelField;
    }

    /**
     * Gets the label field
     *
     * @return string
     */
    public function getLabelField()
    {
        return $this->labelField;
    }

    /**
     * Sets the table name
     *
     * @param string $tableName
     */
    public function setTableName($tableName)
    {
        $this->tableName = $tableName;
    }

    /**
     * Gets the table name
     *
     * @return string
     */
    public function getTableName()
    {
        return $this->tableName;
    }

    /**
     * Sets the lookup field
     *
     * @param string $lookupField
     */
    public function setLookupField($lookupField)
    {
        $this->lookupField = $lookupField;
    }

    /**
     * Gets the lookup field
     *
     * @return string
     */
    public function getLookupField()
    {
        return $this->lookupField;
    }

    /**
     * Sets the lookup mode
     *
     * @param int $lookupMode
     */
    public function setLookupMode($lookupMode)
    {
        $this->lookupMode = $lookupMode;
    }

    /**
     * Gets the lookup mode
     *
     * @return int
     */
    public function getLookupMode()
    {
        return $this->lookupMode;
    }

    /**
     * Gets the nodes
     *
192
     * @param TreeNode $node
193
     */
194
    public function getNodes(TreeNode $node)
195
196
197
198
199
200
    {
    }

    /**
     * Gets the root node
     *
201
     * @return DatabaseTreeNode
202
203
204
205
206
207
     */
    public function getRoot()
    {
        return $this->buildRepresentationForNode($this->treeData);
    }

208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
    /**
     * Sets the root uids
     *
     * @param int[] $startingPoints
     */
    public function setStartingPoints(array $startingPoints): void
    {
        $this->startingPoints = $startingPoints;
    }

    /**
     * Gets the root uids
     *
     * @return int[]
     */
    public function getStartingPoints(): array
    {
        return $this->startingPoints;
    }

228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    /**
     * Sets the tableWhere clause
     *
     * @param string $tableWhere
     */
    public function setTableWhere($tableWhere)
    {
        $this->tableWhere = $tableWhere;
    }

    /**
     * Gets the tableWhere clause
     *
     * @return string
     */
    public function getTableWhere()
    {
        return $this->tableWhere;
    }

    /**
249
     * Builds a complete node including children
250
     *
251
252
     * @param TreeNode $basicNode
     * @param DatabaseTreeNode|null $parent
253
     * @param int $level
254
     * @return DatabaseTreeNode Node object
255
     */
256
    protected function buildRepresentationForNode(TreeNode $basicNode, DatabaseTreeNode $parent = null, $level = 0)
257
    {
258
        $node = GeneralUtility::makeInstance(DatabaseTreeNode::class);
259
        $row = [];
260
261
262
        if ($basicNode->getId() == 0) {
            $node->setSelected(false);
            $node->setExpanded(true);
263
            $node->setLabel($this->getLanguageService()->sL($GLOBALS['TCA'][$this->tableName]['ctrl']['title']));
264
        } else {
265
            $row = BackendUtility::getRecordWSOL($this->tableName, (int)$basicNode->getId(), '*', '', false) ?? [];
266
267
268
269
270
            $node->setLabel(BackendUtility::getRecordTitle($this->tableName, $row) ?: $basicNode->getId());
            $node->setSelected(GeneralUtility::inList($this->getSelectedList(), $basicNode->getId()));
            $node->setExpanded($this->isExpanded($basicNode));
        }
        $node->setId($basicNode->getId());
271
        $node->setSelectable(!GeneralUtility::inList($this->getNonSelectableLevelList(), (string)$level) && !in_array($basicNode->getId(), $this->getItemUnselectableList()));
272
        $node->setSortValue($this->nodeSortValues[$basicNode->getId()] ?? '');
273
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
274
        $node->setIcon($iconFactory->getIconForRecord($this->tableName, $row, Icon::SIZE_SMALL));
275
276
277
        $node->setParentNode($parent);
        if ($basicNode->hasChildNodes()) {
            $node->setHasChildren(true);
278
            $childNodes = GeneralUtility::makeInstance(SortedTreeNodeCollection::class);
279
            $tempNodes = [];
280
            foreach ($basicNode->getChildNodes() as $child) {
281
                $tempNodes[] = $this->buildRepresentationForNode($child, $node, $level + 1);
282
            }
283
284
            $childNodes->exchangeArray($tempNodes);
            $childNodes->asort();
285
286
287
288
289
290
291
292
293
294
295
296
            $node->setChildNodes($childNodes);
        }
        return $node;
    }

    /**
     * Init the tree data
     */
    public function initializeTreeData()
    {
        parent::initializeTreeData();
        $this->nodeSortValues = array_flip($this->itemWhiteList);
crell's avatar
crell committed
297
298
        $this->columnConfiguration = $GLOBALS['TCA'][$this->getTableName()]['columns'][$this->getLookupField()]['config'] ?? [];
        if (isset($this->columnConfiguration['foreign_table']) && $this->columnConfiguration['foreign_table'] !== $this->getTableName()) {
299
300
            throw new \InvalidArgumentException('TCA Tree configuration is invalid: tree for different node-Tables is not implemented yet', 1290944650);
        }
301
        $this->treeData = GeneralUtility::makeInstance(TreeNode::class);
302
        $this->loadTreeData();
303
304
305
        /** @var ModifyTreeDataEvent $event */
        $event = $this->eventDispatcher->dispatch(new ModifyTreeDataEvent($this->treeData, $this));
        $this->treeData = $event->getTreeData();
306
307
308
309
310
311
312
    }

    /**
     * Loads the tree data (all possible children)
     */
    protected function loadTreeData()
    {
313
        if ($this->getStartingPoints()) {
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
            $startingPoints = $this->getStartingPoints();
        } else {
            $startingPoints = [0];
        }

        if (count($startingPoints) === 1) {
            // Only one starting point is available, grab it and set it as root node
            $startingPoint = current($startingPoints);
            $this->treeData->setId($startingPoint);
            $this->treeData->setParentNode(null);

            if ($this->levelMaximum >= 1) {
                $childNodes = $this->getChildrenOf($this->treeData, 1);
                if ($childNodes !== null) {
                    $this->treeData->setChildNodes($childNodes);
                }
            }
        } else {
            // The current tree implementation disallows multiple elements on root level, thus we have to work around
            // this with a separate TreeNodeCollection that gets attached to the root node with uid 0. This has the
            // nasty side effect we cannot avoid the root node being rendered.

            $treeNodeCollection = GeneralUtility::makeInstance(TreeNodeCollection::class);
            foreach ($startingPoints as $startingPoint) {
                $treeData = GeneralUtility::makeInstance(TreeNode::class);
                $treeData->setId($startingPoint);

                if ($this->levelMaximum >= 1) {
                    $childNodes = $this->getChildrenOf($treeData, 1);
                    if ($childNodes !== null) {
                        $treeData->setChildNodes($childNodes);
                    }
                }
                $treeNodeCollection->append($treeData);
348
            }
349
350
            $this->treeData->setId(0);
            $this->treeData->setChildNodes($treeNodeCollection);
351
352
353
354
355
356
        }
    }

    /**
     * Gets node children
     *
357
     * @param TreeNode $node
358
     * @param int $level
359
     * @return TreeNodeCollection|null
360
     */
361
    protected function getChildrenOf(TreeNode $node, $level): ?TreeNodeCollection
362
363
364
    {
        $nodeData = null;
        if ($node->getId() !== 0) {
365
366
367
368
369
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable($this->getTableName());
            $queryBuilder->getRestrictions()->removeAll();
            $nodeData = $queryBuilder->select('*')
                ->from($this->getTableName())
370
371
372
373
374
375
                ->where(
                    $queryBuilder->expr()->eq(
                        'uid',
                        $queryBuilder->createNamedParameter($node->getId(), \PDO::PARAM_INT)
                    )
                )
376
                ->setMaxResults(1)
377
                ->executeQuery()
378
                ->fetchAssociative();
379
        }
380
        if (empty($nodeData)) {
381
            $nodeData = [
382
                'uid' => 0,
383
                $this->getLookupField() => '',
384
            ];
385
386
387
388
        }
        $storage = null;
        $children = $this->getRelatedRecords($nodeData);
        if (!empty($children)) {
389
            $storage = GeneralUtility::makeInstance(TreeNodeCollection::class);
390
            foreach ($children as $child) {
391
                $node = GeneralUtility::makeInstance(TreeNode::class);
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
                $node->setId($child);
                if ($level < $this->levelMaximum) {
                    $children = $this->getChildrenOf($node, $level + 1);
                    if ($children !== null) {
                        $node->setChildNodes($children);
                    }
                }
                $storage->append($node);
            }
        }
        return $storage;
    }

    /**
     * Gets related records depending on TCA configuration
     *
     * @param array $row
     * @return array
     */
    protected function getRelatedRecords(array $row)
    {
413
        if ($this->getLookupMode() == self::MODE_PARENT) {
414
415
416
417
            $children = $this->getChildrenUidsFromParentRelation($row);
        } else {
            $children = $this->getChildrenUidsFromChildrenRelation($row);
        }
418
        $allowedArray = [];
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
        foreach ($children as $child) {
            if (!in_array($child, $this->idCache) && in_array($child, $this->itemWhiteList)) {
                $allowedArray[] = $child;
            }
        }
        $this->idCache = array_merge($this->idCache, $allowedArray);
        return $allowedArray;
    }

    /**
     * Gets related records depending on TCA configuration
     *
     * @param array $row
     * @return array
     */
    protected function getChildrenUidsFromParentRelation(array $row)
    {
        $uid = $row['uid'];
437
438
439
440
441
442
443
444
445
446
        if (in_array($this->columnConfiguration['type'] ?? '', ['select', 'category', 'inline'], true)) {
            if ($this->columnConfiguration['MM'] ?? null) {
                $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
                // Dummy field for setting "look from other site"
                $this->columnConfiguration['MM_oppositeField'] = 'children';
                $dbGroup->start($row[$this->getLookupField()], $this->getTableName(), $this->columnConfiguration['MM'], $uid, $this->getTableName(), $this->columnConfiguration);
                $relatedUids = $dbGroup->tableArray[$this->getTableName()];
            } elseif ($this->columnConfiguration['foreign_field'] ?? null) {
                $relatedUids = $this->listFieldQuery($this->columnConfiguration['foreign_field'], $uid);
            } else {
447
                $relatedUids = $this->listFieldQuery($this->getLookupField(), $uid);
448
449
450
            }
        } else {
            $relatedUids = $this->listFieldQuery($this->getLookupField(), $uid);
451
        }
452

453
454
455
456
457
458
459
460
461
462
463
        return $relatedUids;
    }

    /**
     * Gets related children records depending on TCA configuration
     *
     * @param array $row
     * @return array
     */
    protected function getChildrenUidsFromChildrenRelation(array $row)
    {
464
        $relatedUids = [];
465
466
467
468
        $uid = $row['uid'];
        $value = $row[$this->getLookupField()];
        switch ((string)$this->columnConfiguration['type']) {
            case 'inline':
469
                // Intentional fall-through
470
            case 'select':
471
            case 'category':
472
                if ($this->columnConfiguration['MM']) {
473
                    $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
474
475
476
477
478
479
480
481
                    $dbGroup->start(
                        $value,
                        $this->getTableName(),
                        $this->columnConfiguration['MM'],
                        $uid,
                        $this->getTableName(),
                        $this->columnConfiguration
                    );
482
483
                    $relatedUids = $dbGroup->tableArray[$this->getTableName()];
                } elseif ($this->columnConfiguration['foreign_field']) {
484
485
486
487
488
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                        ->getQueryBuilderForTable($this->getTableName());
                    $queryBuilder->getRestrictions()->removeAll();
                    $records = $queryBuilder->select('uid')
                        ->from($this->getTableName())
489
490
491
492
493
494
                        ->where(
                            $queryBuilder->expr()->eq(
                                $this->columnConfiguration['foreign_field'],
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
                            )
                        )
495
                        ->executeQuery()
496
                        ->fetchAllAssociative();
497
498
499

                    if (!empty($records)) {
                        $relatedUids = array_column($records, 'uid');
500
501
502
503
504
505
506
507
508
509
510
511
                    }
                } else {
                    $relatedUids = GeneralUtility::intExplode(',', $value, true);
                }
                break;
            default:
                $relatedUids = GeneralUtility::intExplode(',', $value, true);
        }
        return $relatedUids;
    }

    /**
512
     * Queries the table for a field which might contain a list.
513
514
515
516
517
518
519
     *
     * @param string $fieldName the name of the field to be queried
     * @param int $queryId the uid to search for
     * @return int[] all uids found
     */
    protected function listFieldQuery($fieldName, $queryId)
    {
520
521
522
523
524
525
526
        $queryId = (int)$queryId;
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable($this->getTableName());
        $queryBuilder->getRestrictions()->removeAll();

        $queryBuilder->select('uid')
            ->from($this->getTableName())
527
            ->where($queryBuilder->expr()->inSet($fieldName, $queryBuilder->quote($queryId)));
528
529
530
531
532
533
534
535
536

        if ($queryId === 0) {
            $queryBuilder->orWhere(
                $queryBuilder->expr()->comparison(
                    'CAST(' . $queryBuilder->quoteIdentifier($fieldName) . ' AS CHAR)',
                    ExpressionBuilder::EQ,
                    $queryBuilder->quote('')
                )
            );
537
        }
538

539
        $records = $queryBuilder->executeQuery()->fetchAllAssociative();
540
        return array_column($records, 'uid');
541
542
    }

543
544
545
546
    protected function getLanguageService(): ?LanguageService
    {
        return $GLOBALS['LANG'] ?? null;
    }
547
}