Completed
Push — master ( d32f9d...c64bee )
by
unknown
26:12
created

PageTreeRepository::getTreeLevels()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 2
nop 2
dl 0
loc 16
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Tree\Repository;
19
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
24
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use TYPO3\CMS\Core\Versioning\VersionState;
27
28
/**
29
 * Fetches ALL pages in the page tree, possibly overlaid with the workspace
30
 * in a sorted way.
31
 *
32
 * This works agnostic of the Backend User, allows to be used in FE as well in the future.
33
 *
34
 * @internal this class is not public API yet, as it needs to be proven stable enough first.
35
 */
36
class PageTreeRepository
37
{
38
    /**
39
     * Fields to be queried from the database
40
     *
41
     * @var string[]
42
     */
43
    protected $fields = [
44
        'uid',
45
        'pid',
46
        'sorting',
47
        'starttime',
48
        'endtime',
49
        'hidden',
50
        'fe_group',
51
        'title',
52
        'nav_title',
53
        'nav_hide',
54
        'php_tree_stop',
55
        'doktype',
56
        'is_siteroot',
57
        'module',
58
        'extendToSubpages',
59
        'content_from_pid',
60
        't3ver_oid',
61
        't3ver_wsid',
62
        't3ver_state',
63
        't3ver_stage',
64
        't3ver_move_id',
65
        'perms_userid',
66
        'perms_user',
67
        'perms_groupid',
68
        'perms_group',
69
        'perms_everybody',
70
        'mount_pid',
71
        'shortcut',
72
        'shortcut_mode',
73
        'mount_pid_ol',
74
        'url',
75
        'sys_language_uid',
76
        'l10n_parent',
77
    ];
78
79
    /**
80
     * The workspace ID to operate on
81
     *
82
     * @var int
83
     */
84
    protected $currentWorkspace = 0;
85
86
    /**
87
     * Full page tree when selected without permissions applied.
88
     *
89
     * @var array
90
     */
91
    protected $fullPageTree = [];
92
93
    /**
94
     * @var array
95
     */
96
    protected $additionalQueryRestrictions = [];
97
98
    /**
99
     * @param int $workspaceId the workspace ID to be checked for.
100
     * @param array $additionalFieldsToQuery an array with more fields that should be accessed.
101
     * @param array $additionalQueryRestrictions an array with more restrictions to add
102
     */
103
    public function __construct(int $workspaceId = 0, array $additionalFieldsToQuery = [], array $additionalQueryRestrictions = [])
104
    {
105
        $this->currentWorkspace = $workspaceId;
106
        if (!empty($additionalFieldsToQuery)) {
107
            $this->fields = array_merge($this->fields, $additionalFieldsToQuery);
108
        }
109
110
        if (!empty($additionalQueryRestrictions)) {
111
            $this->additionalQueryRestrictions = $additionalQueryRestrictions;
112
        }
113
    }
114
115
    /**
116
     * Main entry point for this repository, to fetch the tree data for a page.
117
     * Basically the page record, plus all child pages and their child pages recursively, stored within "_children" item.
118
     *
119
     * @param int $entryPoint the page ID to fetch the tree for
120
     * @param callable $callback a callback to be used to check for permissions and filter out pages not to be included.
121
     * @return array
122
     */
123
    public function getTree(int $entryPoint, callable $callback = null, array $dbMounts = []): array
124
    {
125
        $this->fetchAllPages($dbMounts);
126
        if ($entryPoint === 0) {
127
            $tree = $this->fullPageTree;
128
        } else {
129
            $tree = $this->findInPageTree($entryPoint, $this->fullPageTree);
130
        }
131
        if (!empty($tree) && $callback !== null) {
132
            $this->applyCallbackToChildren($tree, $callback);
133
        }
134
        return $tree;
135
    }
136
137
    /**
138
     * Removes items from a tree based on a callback, usually used for permission checks
139
     *
140
     * @param array $tree
141
     * @param callable $callback
142
     */
143
    protected function applyCallbackToChildren(array &$tree, callable $callback)
144
    {
145
        if (!isset($tree['_children'])) {
146
            return;
147
        }
148
        foreach ($tree['_children'] as $k => &$childPage) {
149
            if (!call_user_func_array($callback, [$childPage])) {
150
                unset($tree['_children'][$k]);
151
                continue;
152
            }
153
            $this->applyCallbackToChildren($childPage, $callback);
154
        }
155
    }
156
157
    /**
158
     * Fetch all non-deleted pages, regardless of permissions. That's why it's internal.
159
     *
160
     * @return array the full page tree of the whole installation
161
     */
162
    protected function fetchAllPages($dbMounts): array
163
    {
164
        if (!empty($this->fullPageTree)) {
165
            return $this->fullPageTree;
166
        }
167
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
168
            ->getQueryBuilderForTable('pages');
169
        $queryBuilder->getRestrictions()
170
            ->removeAll()
171
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
172
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->currentWorkspace));
173
174
        if (!empty($this->additionalQueryRestrictions)) {
175
            foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
176
                $queryBuilder->getRestrictions()->add($additionalQueryRestriction);
177
            }
178
        }
179
180
        $pageRecords = $queryBuilder
181
            ->select(...$this->fields)
182
            ->from('pages')
183
            ->where(
184
                // Only show records in default language
185
                $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
186
            )
187
            ->execute()
188
            ->fetchAll();
189
190
        $ids = array_column($pageRecords, 'uid');
191
        foreach ($dbMounts as $mount) {
192
            $entryPointRootLine = BackendUtility::BEgetRootLine($mount, '', false, $this->fields);
193
            foreach ($entryPointRootLine as $page) {
194
                $pageId = (int)$page['uid'];
195
                if (in_array($pageId, $ids) || $pageId === 0) {
196
                    continue;
197
                }
198
                $pageRecords[] = $page;
199
                $ids[] = $pageId;
200
            }
201
        }
202
203
        $livePagePids = [];
204
        $movePlaceholderData = [];
205
        // This is necessary to resolve all IDs in a workspace
206
        if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
207
            $livePageIds = [];
208
            foreach ($pageRecords as $pageRecord) {
209
                $livePageIds[] = (int)$pageRecord['uid'];
210
                $livePagePids[(int)$pageRecord['uid']] = (int)$pageRecord['pid'];
211
                if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
212
                    $movePlaceholderData[$pageRecord['t3ver_move_id']] = [
213
                        'pid' => (int)$pageRecord['pid'],
214
                        'sorting' => (int)$pageRecord['sorting']
215
                    ];
216
                }
217
            }
218
            // Resolve placeholders of workspace versions
219
            $resolver = GeneralUtility::makeInstance(
220
                PlainDataResolver::class,
221
                'pages',
222
                $livePageIds
223
            );
224
            $resolver->setWorkspaceId($this->currentWorkspace);
225
            $resolver->setKeepDeletePlaceholder(false);
226
            $resolver->setKeepMovePlaceholder(false);
227
            $resolver->setKeepLiveIds(false);
228
            $recordIds = $resolver->get();
229
230
            $queryBuilder->getRestrictions()->removeAll();
231
            $pageRecords = $queryBuilder
232
                ->select(...$this->fields)
233
                ->from('pages')
234
                ->where(
235
                    $queryBuilder->expr()->in('uid', $recordIds)
236
                )
237
                ->execute()
238
                ->fetchAll();
239
        }
240
241
        // Now set up sorting, nesting (tree-structure) for all pages based on pid+sorting fields
242
        $groupedAndSortedPagesByPid = [];
243
        foreach ($pageRecords as $pageRecord) {
244
            $parentPageId = (int)$pageRecord['pid'];
245
            // In case this is a record from a workspace
246
            // The uid+pid of the live-version record is fetched
247
            // This is done in order to avoid fetching records again (e.g. via BackendUtility::workspaceOL()
248
            if ((int)$pageRecord['t3ver_oid'] > 0) {
249
                // When a move pointer is found, the pid+sorting of the MOVE_PLACEHOLDER should be used (this is the
250
                // workspace record holding this information), also the t3ver_state is set to the MOVE_PLACEHOLDER
251
                // because the record is then added
252
                if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_POINTER && !empty($movePlaceholderData[$pageRecord['t3ver_oid']])) {
253
                    $parentPageId = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['pid'];
254
                    $pageRecord['sorting'] = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['sorting'];
255
                    $pageRecord['t3ver_state'] = VersionState::MOVE_PLACEHOLDER;
256
                } else {
257
                    // Just a record in a workspace (not moved etc)
258
                    $parentPageId = (int)$livePagePids[$pageRecord['t3ver_oid']];
259
                }
260
                // this is necessary so the links to the modules are still pointing to the live IDs
261
                $pageRecord['uid'] = (int)$pageRecord['t3ver_oid'];
262
                $pageRecord['pid'] = $parentPageId;
263
            }
264
265
            $sorting = (int)$pageRecord['sorting'];
266
            while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
267
                $sorting++;
268
            }
269
            $groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
270
        }
271
272
        $this->fullPageTree = [
273
            'uid' => 0,
274
            'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
275
        ];
276
        $this->addChildrenToPage($this->fullPageTree, $groupedAndSortedPagesByPid);
277
        return $this->fullPageTree;
278
    }
279
280
    /**
281
     * Adds the property "_children" to a page record with the child pages
282
     *
283
     * @param array $page
284
     * @param array[] $groupedAndSortedPagesByPid
285
     */
286
    protected function addChildrenToPage(array &$page, array &$groupedAndSortedPagesByPid)
287
    {
288
        $page['_children'] = $groupedAndSortedPagesByPid[(int)$page['uid']] ?? [];
289
        ksort($page['_children']);
290
        foreach ($page['_children'] as &$child) {
291
            $this->addChildrenToPage($child, $groupedAndSortedPagesByPid);
292
        }
293
    }
294
295
    /**
296
     * Looking for a page by traversing the tree
297
     *
298
     * @param int $pageId the page ID to search for
299
     * @param array $pages the page tree to look for the page
300
     * @return array Array of the tree data, empty array if nothing was found
301
     */
302
    protected function findInPageTree(int $pageId, array $pages): array
303
    {
304
        foreach ($pages['_children'] as $childPage) {
305
            if ((int)$childPage['uid'] === $pageId) {
306
                return $childPage;
307
            }
308
            $result = $this->findInPageTree($pageId, $childPage);
309
            if (!empty($result)) {
310
                return $result;
311
            }
312
        }
313
        return [];
314
    }
315
}
316