Completed
Push — master ( a3f569...32d9f0 )
by
unknown
14:36
created

PageTreeRepository::applyCallbackToChildren()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 2
dl 0
loc 11
rs 10
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_tstamp',
65
        't3ver_move_id',
66
        'perms_userid',
67
        'perms_user',
68
        'perms_groupid',
69
        'perms_group',
70
        'perms_everybody',
71
        'mount_pid',
72
        'shortcut',
73
        'shortcut_mode',
74
        'mount_pid_ol',
75
        'url',
76
        'sys_language_uid',
77
        'l10n_parent',
78
    ];
79
80
    /**
81
     * The workspace ID to operate on
82
     *
83
     * @var int
84
     */
85
    protected $currentWorkspace = 0;
86
87
    /**
88
     * Full page tree when selected without permissions applied.
89
     *
90
     * @var array
91
     */
92
    protected $fullPageTree = [];
93
94
    /**
95
     * @var array
96
     */
97
    protected $additionalQueryRestrictions = [];
98
99
    /**
100
     * @param int $workspaceId the workspace ID to be checked for.
101
     * @param array $additionalFieldsToQuery an array with more fields that should be accessed.
102
     * @param array $additionalQueryRestrictions an array with more restrictions to add
103
     */
104
    public function __construct(int $workspaceId = 0, array $additionalFieldsToQuery = [], array $additionalQueryRestrictions = [])
105
    {
106
        $this->currentWorkspace = $workspaceId;
107
        if (!empty($additionalFieldsToQuery)) {
108
            $this->fields = array_merge($this->fields, $additionalFieldsToQuery);
109
        }
110
111
        if (!empty($additionalQueryRestrictions)) {
112
            $this->additionalQueryRestrictions = $additionalQueryRestrictions;
113
        }
114
    }
115
116
    /**
117
     * Main entry point for this repository, to fetch the tree data for a page.
118
     * Basically the page record, plus all child pages and their child pages recursively, stored within "_children" item.
119
     *
120
     * @param int $entryPoint the page ID to fetch the tree for
121
     * @param callable $callback a callback to be used to check for permissions and filter out pages not to be included.
122
     * @return array
123
     */
124
    public function getTree(int $entryPoint, callable $callback = null, array $dbMounts = []): array
125
    {
126
        $this->fetchAllPages($dbMounts);
127
        if ($entryPoint === 0) {
128
            $tree = $this->fullPageTree;
129
        } else {
130
            $tree = $this->findInPageTree($entryPoint, $this->fullPageTree);
131
        }
132
        if (!empty($tree) && $callback !== null) {
133
            $this->applyCallbackToChildren($tree, $callback);
134
        }
135
        return $tree;
136
    }
137
138
    /**
139
     * Removes items from a tree based on a callback, usually used for permission checks
140
     *
141
     * @param array $tree
142
     * @param callable $callback
143
     */
144
    protected function applyCallbackToChildren(array &$tree, callable $callback)
145
    {
146
        if (!isset($tree['_children'])) {
147
            return;
148
        }
149
        foreach ($tree['_children'] as $k => &$childPage) {
150
            if (!call_user_func_array($callback, [$childPage])) {
151
                unset($tree['_children'][$k]);
152
                continue;
153
            }
154
            $this->applyCallbackToChildren($childPage, $callback);
155
        }
156
    }
157
158
    /**
159
     * Fetch all non-deleted pages, regardless of permissions. That's why it's internal.
160
     *
161
     * @return array the full page tree of the whole installation
162
     */
163
    protected function fetchAllPages($dbMounts): array
164
    {
165
        if (!empty($this->fullPageTree)) {
166
            return $this->fullPageTree;
167
        }
168
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
169
            ->getQueryBuilderForTable('pages');
170
        $queryBuilder->getRestrictions()
171
            ->removeAll()
172
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
173
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->currentWorkspace));
174
175
        if (!empty($this->additionalQueryRestrictions)) {
176
            foreach ($this->additionalQueryRestrictions as $additionalQueryRestriction) {
177
                $queryBuilder->getRestrictions()->add($additionalQueryRestriction);
178
            }
179
        }
180
181
        $pageRecords = $queryBuilder
182
            ->select(...$this->fields)
183
            ->from('pages')
184
            ->where(
185
                // Only show records in default language
186
                $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
187
            )
188
            ->execute()
189
            ->fetchAll();
190
191
        $ids = array_column($pageRecords, 'uid');
192
        foreach ($dbMounts as $mount) {
193
            $entryPointRootLine = BackendUtility::BEgetRootLine($mount, '', false, $this->fields);
194
            foreach ($entryPointRootLine as $page) {
195
                $pageId = (int)$page['uid'];
196
                if (in_array($pageId, $ids) || $pageId === 0) {
197
                    continue;
198
                }
199
                $pageRecords[] = $page;
200
                $ids[] = $pageId;
201
            }
202
        }
203
204
        $livePagePids = [];
205
        $movePlaceholderData = [];
206
        // This is necessary to resolve all IDs in a workspace
207
        if ($this->currentWorkspace !== 0 && !empty($pageRecords)) {
208
            $livePageIds = [];
209
            foreach ($pageRecords as $pageRecord) {
210
                $livePageIds[] = (int)$pageRecord['uid'];
211
                $livePagePids[(int)$pageRecord['uid']] = (int)$pageRecord['pid'];
212
                if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
213
                    $movePlaceholderData[$pageRecord['t3ver_move_id']] = [
214
                        'pid' => (int)$pageRecord['pid'],
215
                        'sorting' => (int)$pageRecord['sorting']
216
                    ];
217
                }
218
            }
219
            // Resolve placeholders of workspace versions
220
            $resolver = GeneralUtility::makeInstance(
221
                PlainDataResolver::class,
222
                'pages',
223
                $livePageIds
224
            );
225
            $resolver->setWorkspaceId($this->currentWorkspace);
226
            $resolver->setKeepDeletePlaceholder(false);
227
            $resolver->setKeepMovePlaceholder(false);
228
            $resolver->setKeepLiveIds(false);
229
            $recordIds = $resolver->get();
230
231
            $queryBuilder->getRestrictions()->removeAll();
232
            $pageRecords = $queryBuilder
233
                ->select(...$this->fields)
234
                ->from('pages')
235
                ->where(
236
                    $queryBuilder->expr()->in('uid', $recordIds)
237
                )
238
                ->execute()
239
                ->fetchAll();
240
        }
241
242
        // Now set up sorting, nesting (tree-structure) for all pages based on pid+sorting fields
243
        $groupedAndSortedPagesByPid = [];
244
        foreach ($pageRecords as $pageRecord) {
245
            $parentPageId = (int)$pageRecord['pid'];
246
            // In case this is a record from a workspace
247
            // The uid+pid of the live-version record is fetched
248
            // This is done in order to avoid fetching records again (e.g. via BackendUtility::workspaceOL()
249
            if ((int)$pageRecord['t3ver_oid'] > 0) {
250
                // When a move pointer is found, the pid+sorting of the MOVE_PLACEHOLDER should be used (this is the
251
                // workspace record holding this information), also the t3ver_state is set to the MOVE_PLACEHOLDER
252
                // because the record is then added
253
                if ((int)$pageRecord['t3ver_state'] === VersionState::MOVE_POINTER && !empty($movePlaceholderData[$pageRecord['t3ver_oid']])) {
254
                    $parentPageId = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['pid'];
255
                    $pageRecord['sorting'] = (int)$movePlaceholderData[$pageRecord['t3ver_oid']]['sorting'];
256
                    $pageRecord['t3ver_state'] = VersionState::MOVE_PLACEHOLDER;
257
                } else {
258
                    // Just a record in a workspace (not moved etc)
259
                    $parentPageId = (int)$livePagePids[$pageRecord['t3ver_oid']];
260
                }
261
                // this is necessary so the links to the modules are still pointing to the live IDs
262
                $pageRecord['uid'] = (int)$pageRecord['t3ver_oid'];
263
                $pageRecord['pid'] = $parentPageId;
264
            }
265
266
            $sorting = (int)$pageRecord['sorting'];
267
            while (isset($groupedAndSortedPagesByPid[$parentPageId][$sorting])) {
268
                $sorting++;
269
            }
270
            $groupedAndSortedPagesByPid[$parentPageId][$sorting] = $pageRecord;
271
        }
272
273
        $this->fullPageTree = [
274
            'uid' => 0,
275
            'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
276
        ];
277
        $this->addChildrenToPage($this->fullPageTree, $groupedAndSortedPagesByPid);
278
        return $this->fullPageTree;
279
    }
280
281
    /**
282
     * Adds the property "_children" to a page record with the child pages
283
     *
284
     * @param array $page
285
     * @param array[] $groupedAndSortedPagesByPid
286
     */
287
    protected function addChildrenToPage(array &$page, array &$groupedAndSortedPagesByPid)
288
    {
289
        $page['_children'] = $groupedAndSortedPagesByPid[(int)$page['uid']] ?? [];
290
        ksort($page['_children']);
291
        foreach ($page['_children'] as &$child) {
292
            $this->addChildrenToPage($child, $groupedAndSortedPagesByPid);
293
        }
294
    }
295
296
    /**
297
     * Looking for a page by traversing the tree
298
     *
299
     * @param int $pageId the page ID to search for
300
     * @param array $pages the page tree to look for the page
301
     * @return array Array of the tree data, empty array if nothing was found
302
     */
303
    protected function findInPageTree(int $pageId, array $pages): array
304
    {
305
        foreach ($pages['_children'] as $childPage) {
306
            if ((int)$childPage['uid'] === $pageId) {
307
                return $childPage;
308
            }
309
            $result = $this->findInPageTree($pageId, $childPage);
310
            if (!empty($result)) {
311
                return $result;
312
            }
313
        }
314
        return [];
315
    }
316
}
317