‪TYPO3CMS  11.5
RootlineUtility.php
Go to the documentation of this file.
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 
17 
23 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
33 
38 {
42  protected ‪$pageUid;
43 
47  protected ‪$mountPointParameter;
48 
52  protected ‪$parsedMountPointParameters = [];
53 
57  protected ‪$languageUid = 0;
58 
62  protected ‪$workspaceUid = 0;
63 
67  protected static ‪$cache;
68 
72  protected static ‪$localCache = [];
73 
79  protected static ‪$rootlineFields = [
80  'pid',
81  'uid',
82  't3ver_oid',
83  't3ver_wsid',
84  't3ver_state',
85  'title',
86  'nav_title',
87  'media',
88  'layout',
89  'hidden',
90  'starttime',
91  'endtime',
92  'fe_group',
93  'extendToSubpages',
94  'doktype',
95  'TSconfig',
96  'tsconfig_includes',
97  'is_siteroot',
98  'mount_pid',
99  'mount_pid_ol',
100  'fe_login_mode',
101  'backend_layout_next_level',
102  ];
103 
109  protected ‪$pageRepository;
110 
116  protected ‪$context;
117 
121  protected ‪$cacheIdentifier;
122 
126  protected static ‪$pageRecordCache = [];
127 
134  public function ‪__construct($uid, ‪$mountPointParameter = '', ‪$context = null)
135  {
136  $this->mountPointParameter = $this->‪sanitizeMountPointParameter((string)‪$mountPointParameter);
137  if (!(‪$context instanceof ‪Context)) {
138  ‪$context = GeneralUtility::makeInstance(Context::class);
139  }
140  $this->context = ‪$context;
141  $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class, ‪$context);
142 
143  $this->languageUid = $this->context->getPropertyFromAspect('language', 'id', 0);
144  $this->workspaceUid = (int)$this->context->getPropertyFromAspect('workspace', 'id', 0);
145  if ($this->mountPointParameter !== '') {
146  if (!‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
147  throw new ‪MountPointsDisabledException('Mount-Point Pages are disabled for this installation. Cannot resolve a Rootline for a page with Mount-Points', 1343462896);
148  }
150  }
151 
152  $this->pageUid = $this->‪resolvePageId((int)$uid);
153  if (self::$cache === null) {
154  self::$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('rootline');
155  }
156  self::$rootlineFields = array_merge(self::$rootlineFields, ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], true));
157  self::$rootlineFields = array_unique(self::$rootlineFields);
158 
159  $this->cacheIdentifier = $this->‪getCacheIdentifier();
160  }
161 
167  public static function ‪purgeCaches()
168  {
169  self::$localCache = [];
170  self::$pageRecordCache = [];
171  }
172 
179  public function ‪getCacheIdentifier($otherUid = null)
180  {
181  ‪$mountPointParameter = (string)$this->mountPointParameter;
182  if (‪$mountPointParameter !== '' && str_contains(‪$mountPointParameter, ',')) {
183  ‪$mountPointParameter = str_replace(',', '__', ‪$mountPointParameter);
184  }
185 
186  return implode('_', [
187  $otherUid !== null ? (int)$otherUid : $this->pageUid,
189  $this->languageUid,
190  $this->workspaceUid,
191  $this->context->getAspect('visibility')->includeHiddenContent() ? '1' : '0',
192  ]);
193  }
194 
200  public function get()
201  {
202  if ($this->pageUid === 0) {
203  // pageUid 0 has no root line, return empty array right away
204  return [];
205  }
206  if (!isset(static::$localCache[$this->cacheIdentifier])) {
207  $entry = static::$cache->get($this->cacheIdentifier);
208  if (!$entry) {
209  $this->‪generateRootlineCache();
210  } else {
211  static::$localCache[‪$this->cacheIdentifier] = $entry;
212  $depth = count($entry);
213  // Populate the root-lines for parent pages as well
214  // since they are part of the current root-line
215  while ($depth > 1) {
216  --$depth;
217  $parentCacheIdentifier = $this->‪getCacheIdentifier($entry[$depth - 1]['uid']);
218  // Abort if the root-line of the parent page is
219  // already in the local cache data
220  if (isset(static::$localCache[$parentCacheIdentifier])) {
221  break;
222  }
223  // Behaves similar to array_shift(), but preserves
224  // the array keys - which contain the page ids here
225  $entry = array_slice($entry, 1, null, true);
226  static::$localCache[$parentCacheIdentifier] = $entry;
227  }
228  }
229  }
230  return static::$localCache[‪$this->cacheIdentifier];
231  }
232 
240  protected function ‪getRecordArray($uid)
241  {
242  $currentCacheIdentifier = $this->‪getCacheIdentifier($uid);
243  if (!isset(self::$pageRecordCache[$currentCacheIdentifier])) {
244  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
245  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
246  $row = $queryBuilder->select(...self::$rootlineFields)
247  ->from('pages')
248  ->where(
249  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)),
250  $queryBuilder->expr()->in('t3ver_wsid', $queryBuilder->createNamedParameter([0, $this->workspaceUid], Connection::PARAM_INT_ARRAY))
251  )
252  ->executeQuery()
253  ->fetchAssociative();
254  if (empty($row)) {
255  throw new PageNotFoundException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
256  }
257  $this->pageRepository->versionOL('pages', $row, false, true);
258  if (is_array($row)) {
259  if ($this->languageUid > 0) {
260  $row = $this->pageRepository->getPageOverlay($row, $this->languageUid);
261  }
262  $row = $this->‪enrichWithRelationFields($row['_PAGES_OVERLAY_UID'] ?? $uid, $row);
263  self::$pageRecordCache[$currentCacheIdentifier] = $row;
264  }
265  }
266  if (!is_array(self::$pageRecordCache[$currentCacheIdentifier] ?? false)) {
267  throw new PageNotFoundException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
268  }
269  return self::$pageRecordCache[$currentCacheIdentifier];
270  }
271 
280  protected function ‪enrichWithRelationFields($uid, array $pageRecord)
281  {
282  if (!isset(‪$GLOBALS['TCA']['pages']['columns']) || !is_array(‪$GLOBALS['TCA']['pages']['columns'])) {
283  return $pageRecord;
284  }
285 
286  foreach (‪$GLOBALS['TCA']['pages']['columns'] as $column => $configuration) {
287  // Ensure that only fields defined in $rootlineFields (and "addRootLineFields") are actually evaluated
288  if (array_key_exists($column, $pageRecord) && $this->‪columnHasRelationToResolve($configuration)) {
289  $fieldConfig = $configuration['config'];
290  $relatedUids = [];
291  if (($fieldConfig['MM'] ?? false) || (!empty($fieldConfig['foreign_table'] ?? $fieldConfig['allowed'] ?? ''))) {
292  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
293  // do not include hidden relational fields
294  $relationalTable = $fieldConfig['foreign_table'] ?? $fieldConfig['allowed'];
295  $hiddenFieldName = ‪$GLOBALS['TCA'][$relationalTable]['ctrl']['enablecolumns']['disabled'] ?? null;
296  if (!$this->context->getAspect('visibility')->includeHiddenContent() && $hiddenFieldName) {
297  $fieldConfig['foreign_match_fields'][$hiddenFieldName] = 0;
298  }
299  $relationHandler->setWorkspaceId($this->workspaceUid);
300  $relationHandler->start(
301  $pageRecord[$column],
302  $fieldConfig['foreign_table'] ?? $fieldConfig['allowed'],
303  $fieldConfig['MM'] ?? '',
304  $uid,
305  'pages',
306  $fieldConfig
307  );
308  $relatedUids = $relationHandler->getValueArray();
309  }
310  $pageRecord[$column] = implode(',', $relatedUids);
311  }
312  }
313  return $pageRecord;
314  }
315 
323  protected function ‪columnHasRelationToResolve(array $configuration)
324  {
325  $configuration = $configuration['config'] ?? [];
326  if (!empty($configuration['MM']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline', 'group'])) {
327  return true;
328  }
329  if (!empty($configuration['foreign_field']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline'])) {
330  return true;
331  }
332  if (($configuration['type'] ?? '') === 'category' && ($configuration['relationship'] ?? '') === 'manyToMany') {
333  return true;
334  }
335  return false;
336  }
337 
343  protected function ‪generateRootlineCache()
344  {
345  $page = $this->‪getRecordArray($this->pageUid);
346  // If the current page is a mounted (according to the MP parameter) handle the mount-point
347  if ($this->‪isMountedPage()) {
348  $mountPoint = $this->‪getRecordArray($this->parsedMountPointParameters[$this->pageUid]);
349  $page = $this->‪processMountedPage($page, $mountPoint);
350  $parentUid = $mountPoint['pid'];
351  // Anyhow after reaching the mount-point, we have to go up that rootline
352  unset($this->parsedMountPointParameters[$this->pageUid]);
353  } else {
354  $parentUid = $page['pid'];
355  }
356  $cacheTags = ['pageId_' . $page['uid']];
357  if ($parentUid > 0) {
358  // Get rootline of (and including) parent page
359  ‪$mountPointParameter = !empty($this->parsedMountPointParameters) ? $this->mountPointParameter : '';
360  $rootlineUtility = GeneralUtility::makeInstance(self::class, $parentUid, ‪$mountPointParameter, $this->context);
361  $rootline = $rootlineUtility->get();
362  // retrieve cache tags of parent rootline
363  foreach ($rootline as $entry) {
364  $cacheTags[] = 'pageId_' . $entry['uid'];
365  if ($entry['uid'] == $this->pageUid) {
366  throw new CircularRootLineException('Circular connection in rootline for page with uid ' . $this->pageUid . ' found. Check your mountpoint configuration.', 1343464103);
367  }
368  }
369  } else {
370  $rootline = [];
371  }
372  $rootline[] = $page;
373  krsort($rootline);
374  static::$cache->set($this->cacheIdentifier, $rootline, $cacheTags);
375  static::$localCache[‪$this->cacheIdentifier] = $rootline;
376  }
377 
384  public function ‪isMountedPage()
385  {
386  return array_key_exists($this->pageUid, $this->parsedMountPointParameters);
387  }
388 
397  protected function ‪processMountedPage(array $mountedPageData, array $mountPointPageData)
398  {
399  $mountPid = $mountPointPageData['mount_pid'] ?? null;
400  $uid = $mountedPageData['uid'] ?? null;
401  if ($mountPid != $uid) {
402  throw new ‪BrokenRootLineException('Broken rootline. Mountpoint parameter does not match the actual rootline. mount_pid (' . $mountPid . ') does not match page uid (' . $uid . ').', 1343464100);
403  }
404  // Current page replaces the original mount-page
405  $mountUid = $mountPointPageData['uid'] ?? null;
406  if (!empty($mountPointPageData['mount_pid_ol'])) {
407  $mountedPageData['_MOUNT_OL'] = true;
408  $mountedPageData['_MOUNT_PAGE'] = [
409  'uid' => $mountUid,
410  'pid' => $mountPointPageData['pid'] ?? null,
411  'title' => $mountPointPageData['title'] ?? null,
412  ];
413  } else {
414  // The mount-page is not replaced, the mount-page itself has to be used
415  $mountedPageData = $mountPointPageData;
416  }
417  $mountedPageData['_MOUNTED_FROM'] = ‪$this->pageUid;
418  $mountedPageData['_MP_PARAM'] = $this->pageUid . '-' . $mountUid;
419  return $mountedPageData;
420  }
421 
427  protected function ‪sanitizeMountPointParameter(string ‪$mountPointParameter): string
428  {
430  if (‪$mountPointParameter === '') {
431  return '';
432  }
434  foreach ($mountPoints as $key => $mP) {
435  // If MP has incorrect format, discard it
436  if (!preg_match('/^\d+-\d+$/', $mP)) {
437  unset($mountPoints[$key]);
438  }
439  }
440  return implode(',', $mountPoints);
441  }
442 
448  protected function ‪parseMountPointParameter()
449  {
450  $mountPoints = ‪GeneralUtility::trimExplode(',', $this->mountPointParameter);
451  foreach ($mountPoints as $mP) {
452  [$mountedPageUid, $mountPageUid] = ‪GeneralUtility::intExplode('-', $mP);
453  $this->parsedMountPointParameters[$mountedPageUid] = $mountPageUid;
454  }
455  }
456 
464  protected function ‪resolvePageId(int $pageId): int
465  {
466  if ($pageId === 0 || $this->workspaceUid === 0) {
467  return $pageId;
468  }
469 
470  $page = $this->‪resolvePageRecord($pageId);
471  if (!isset($page['t3ver_state']) || !‪VersionState::cast($page['t3ver_state'])->equals(‪VersionState::MOVE_POINTER)) {
472  return $pageId;
473  }
474 
475  $movePointerId = $this->‪resolveMovePointerId((int)$page['t3ver_oid']);
476  return $movePointerId ?: $pageId;
477  }
478 
483  protected function ‪resolvePageRecord(int $pageId): ?array
484  {
485  $queryBuilder = $this->‪createQueryBuilder('pages');
486  $queryBuilder->getRestrictions()->removeAll()
487  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
488 
489  $statement = $queryBuilder
490  ->from('pages')
491  ->select('uid', 't3ver_oid', 't3ver_state')
492  ->where(
493  $queryBuilder->expr()->eq(
494  'uid',
495  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
496  )
497  )
498  ->setMaxResults(1)
499  ->executeQuery();
500 
501  $record = $statement->fetchAssociative();
502  return $record ?: null;
503  }
504 
511  protected function ‪resolveMovePointerId(int $liveId): ?int
512  {
513  $queryBuilder = $this->‪createQueryBuilder('pages');
514  $queryBuilder->getRestrictions()->removeAll()
515  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
516 
517  $statement = $queryBuilder
518  ->from('pages')
519  ->select('uid')
520  ->setMaxResults(1)
521  ->where(
522  $queryBuilder->expr()->eq(
523  't3ver_wsid',
524  $queryBuilder->createNamedParameter($this->workspaceUid, ‪Connection::PARAM_INT)
525  ),
526  $queryBuilder->expr()->eq(
527  't3ver_state',
528  $queryBuilder->createNamedParameter(‪VersionState::MOVE_POINTER, ‪Connection::PARAM_INT)
529  ),
530  $queryBuilder->expr()->eq(
531  't3ver_oid',
532  $queryBuilder->createNamedParameter($liveId, ‪Connection::PARAM_INT)
533  )
534  )
535  ->executeQuery();
536 
537  $movePointerId = $statement->fetchOne();
538  return $movePointerId ? (int)$movePointerId : null;
539  }
540 
545  protected function ‪createQueryBuilder(string $tableName): QueryBuilder
546  {
547  return GeneralUtility::makeInstance(ConnectionPool::class)
548  ->getQueryBuilderForTable($tableName);
549  }
550 }
‪TYPO3\CMS\Core\Utility\RootlineUtility\$workspaceUid
‪int $workspaceUid
Definition: RootlineUtility.php:57
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Utility\RootlineUtility\$languageUid
‪int $languageUid
Definition: RootlineUtility.php:53
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageUid
‪int $pageUid
Definition: RootlineUtility.php:41
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Utility\RootlineUtility\getCacheIdentifier
‪string getCacheIdentifier($otherUid=null)
Definition: RootlineUtility.php:167
‪TYPO3\CMS\Core\Utility\RootlineUtility\isMountedPage
‪bool isMountedPage()
Definition: RootlineUtility.php:372
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolvePageId
‪int resolvePageId(int $pageId)
Definition: RootlineUtility.php:452
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:37
‪TYPO3\CMS\Core\Utility\RootlineUtility\parseMountPointParameter
‪parseMountPointParameter()
Definition: RootlineUtility.php:436
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolvePageRecord
‪array null resolvePageRecord(int $pageId)
Definition: RootlineUtility.php:471
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:38
‪TYPO3\CMS\Core\Utility\RootlineUtility\purgeCaches
‪static purgeCaches()
Definition: RootlineUtility.php:155
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:16
‪TYPO3\CMS\Core\Utility\RootlineUtility\sanitizeMountPointParameter
‪sanitizeMountPointParameter(string $mountPointParameter)
Definition: RootlineUtility.php:415
‪TYPO3\CMS\Core\Utility\RootlineUtility\getRecordArray
‪array getRecordArray($uid)
Definition: RootlineUtility.php:228
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolveMovePointerId
‪int null resolveMovePointerId(int $liveId)
Definition: RootlineUtility.php:499
‪TYPO3\CMS\Core\Versioning\VersionState\MOVE_POINTER
‪const MOVE_POINTER
Definition: VersionState.php:78
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Utility\RootlineUtility\columnHasRelationToResolve
‪bool columnHasRelationToResolve(array $configuration)
Definition: RootlineUtility.php:311
‪TYPO3\CMS\Core\Utility\RootlineUtility\generateRootlineCache
‪generateRootlineCache()
Definition: RootlineUtility.php:331
‪TYPO3\CMS\Core\Utility\RootlineUtility\$cacheIdentifier
‪string $cacheIdentifier
Definition: RootlineUtility.php:110
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageRecordCache
‪static array $pageRecordCache
Definition: RootlineUtility.php:114
‪TYPO3\CMS\Core\Type\Enumeration\cast
‪static static cast($value)
Definition: Enumeration.php:186
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageRepository
‪PageRepository $pageRepository
Definition: RootlineUtility.php:100
‪TYPO3\CMS\Core\Utility\RootlineUtility\$cache
‪static FrontendInterface $cache
Definition: RootlineUtility.php:61
‪TYPO3\CMS\Core\Utility\RootlineUtility\$mountPointParameter
‪string $mountPointParameter
Definition: RootlineUtility.php:45
‪TYPO3\CMS\Core\Exception\Page\CircularRootLineException
Definition: CircularRootLineException.php:23
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Utility\RootlineUtility\$localCache
‪static array $localCache
Definition: RootlineUtility.php:65
‪TYPO3\CMS\Core\Utility\RootlineUtility\$parsedMountPointParameters
‪array $parsedMountPointParameters
Definition: RootlineUtility.php:49
‪TYPO3\CMS\Core\Versioning\VersionState
Definition: VersionState.php:24
‪TYPO3\CMS\Core\Utility\RootlineUtility\__construct
‪__construct($uid, $mountPointParameter='', $context=null)
Definition: RootlineUtility.php:122
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Utility\RootlineUtility\enrichWithRelationFields
‪array enrichWithRelationFields($uid, array $pageRecord)
Definition: RootlineUtility.php:268
‪TYPO3\CMS\Core\Utility\RootlineUtility\createQueryBuilder
‪QueryBuilder createQueryBuilder(string $tableName)
Definition: RootlineUtility.php:533
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static int[] intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:927
‪TYPO3\CMS\Core\Utility\RootlineUtility\$context
‪Context $context
Definition: RootlineUtility.php:106
‪TYPO3\CMS\Core\Exception\Page\BrokenRootLineException
Definition: BrokenRootLineException.php:23
‪TYPO3\CMS\Core\Exception\Page\MountPointsDisabledException
Definition: MountPointsDisabledException.php:24
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Exception\Page\PageNotFoundException
Definition: PageNotFoundException.php:23
‪TYPO3\CMS\Core\Utility\RootlineUtility\$rootlineFields
‪static array $rootlineFields
Definition: RootlineUtility.php:71
‪TYPO3\CMS\Core\Utility\RootlineUtility\processMountedPage
‪array processMountedPage(array $mountedPageData, array $mountPointPageData)
Definition: RootlineUtility.php:385
‪TYPO3\CMS\Core\Exception\Page\PagePropertyRelationNotFoundException
Definition: PagePropertyRelationNotFoundException.php:23