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