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\Backend\Security;
17
18use TYPO3\CMS\Backend\Tree\TreeNode;
19use TYPO3\CMS\Backend\Tree\TreeNodeCollection;
20use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21use TYPO3\CMS\Core\Database\ConnectionPool;
22use TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent;
23use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25/**
26 * This event listener deals with tree data security which reacts on a PSR-14 event
27 * on data object initialization.
28 *
29 * The aspect defines category mount points according to BE User permissions.
30 *
31 * @internal This class is TYPO3-internal hook and is not considered part of the Public TYPO3 API.
32 */
33final class CategoryPermissionsAspect
34{
35    /**
36     * @var string
37     */
38    private $categoryTableName = 'sys_category';
39
40    /**
41     * @var BackendUserAuthentication
42     */
43    private $backendUserAuthentication;
44
45    /**
46     * @param BackendUserAuthentication|null $backendUserAuthentication
47     */
48    public function __construct($backendUserAuthentication = null)
49    {
50        $this->backendUserAuthentication = $backendUserAuthentication ?: $GLOBALS['BE_USER'];
51    }
52
53    /**
54     * The listener for the event in DatabaseTreeDataProvider, which only affects the TYPO3 Backend
55     *
56     * @param ModifyTreeDataEvent $event
57     */
58    public function addUserPermissionsToCategoryTreeData(ModifyTreeDataEvent $event): void
59    {
60        // Only evaluate this in the backend
61        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE)) {
62            return;
63        }
64
65        $dataProvider = $event->getProvider();
66        $treeData = $event->getTreeData();
67
68        if (!$this->backendUserAuthentication->isAdmin() && $dataProvider->getTableName() === $this->categoryTableName) {
69
70            // Get User permissions related to category
71            $categoryMountPoints = $this->backendUserAuthentication->getCategoryMountPoints();
72
73            // Backup child nodes to be processed.
74            $treeNodeCollection = $treeData->getChildNodes();
75
76            if (!empty($categoryMountPoints) && !empty($treeNodeCollection)) {
77
78                // Check the rootline against categoryMountPoints when tree was filtered
79                if ($dataProvider->getRootUid() !== null) {
80                    if (in_array($dataProvider->getRootUid(), $categoryMountPoints)) {
81                        return;
82                    }
83                    $uidsInRootline = $this->findUidsInRootline($dataProvider->getRootUid());
84                    if (!empty(array_intersect($categoryMountPoints, $uidsInRootline))) {
85                        // One of the parents was found in categoryMountPoints so all children are secure
86                        return;
87                    }
88                }
89
90                // First, remove all child nodes which must be analyzed to be considered as "secure".
91                // The nodes were backed up in variable $treeNodeCollection beforehand.
92                $treeData->removeChildNodes();
93
94                // Create an empty tree node collection to receive the secured nodes.
95                /** @var TreeNodeCollection $securedTreeNodeCollection */
96                $securedTreeNodeCollection = GeneralUtility::makeInstance(TreeNodeCollection::class);
97
98                foreach ($categoryMountPoints as $categoryMountPoint) {
99                    $treeNode = $this->lookUpCategoryMountPointInTreeNodes((int)$categoryMountPoint, $treeNodeCollection);
100                    if ($treeNode !== null) {
101                        $securedTreeNodeCollection->append($treeNode);
102                    }
103                }
104
105                // Reset child nodes.
106                $treeData->setChildNodes($securedTreeNodeCollection);
107            }
108        }
109    }
110
111    /**
112     * Recursively look up for a category mount point within a tree.
113     *
114     * @param int $categoryMountPoint
115     * @param TreeNodeCollection $treeNodeCollection
116     * @return TreeNode|null
117     */
118    private function lookUpCategoryMountPointInTreeNodes($categoryMountPoint, TreeNodeCollection $treeNodeCollection)
119    {
120        $result = null;
121
122        // If any User permission, recursively traverse the tree and set tree part as mount point
123        foreach ($treeNodeCollection as $treeNode) {
124
125            /** @var TreeNode $treeNode */
126            if ((int)$treeNode->getId() === $categoryMountPoint) {
127                $result = $treeNode;
128                break;
129            }
130
131            if ($treeNode->hasChildNodes()) {
132
133                /** @var TreeNode $node */
134                $node = $this->lookUpCategoryMountPointInTreeNodes($categoryMountPoint, $treeNode->getChildNodes());
135                if ($node !== null) {
136                    $result = $node;
137                    break;
138                }
139            }
140        }
141        return $result;
142    }
143
144    /**
145     * Find parent uids in rootline
146     *
147     * @param int $uid
148     * @return array
149     */
150    private function findUidsInRootline($uid)
151    {
152        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
153            ->getQueryBuilderForTable($this->categoryTableName);
154        $row = $queryBuilder
155            ->select('parent')
156            ->from($this->categoryTableName)
157            ->where(
158                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
159            )
160            ->execute()
161            ->fetch();
162
163        $parentUids = [];
164        if ($row['parent'] > 0) {
165            $parentUids = $this->findUidsInRootline($row['parent']);
166            $parentUids[] = $row['parent'];
167        }
168        return $parentUids;
169    }
170}
171