‪TYPO3CMS  10.4
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 
18 use Doctrine\DBAL\DBALException;
19 use Doctrine\DBAL\FetchMode;
34 
39 {
43  protected ‪$pageUid;
44 
48  protected ‪$mountPointParameter;
49 
53  protected ‪$parsedMountPointParameters = [];
54 
58  protected ‪$languageUid = 0;
59 
63  protected ‪$workspaceUid = 0;
64 
68  protected static ‪$cache;
69 
73  protected static ‪$localCache = [];
74 
80  protected static ‪$rootlineFields = [
81  'pid',
82  'uid',
83  't3ver_oid',
84  't3ver_wsid',
85  't3ver_state',
86  'title',
87  'nav_title',
88  'media',
89  'layout',
90  'hidden',
91  'starttime',
92  'endtime',
93  'fe_group',
94  'extendToSubpages',
95  'doktype',
96  'TSconfig',
97  'tsconfig_includes',
98  'is_siteroot',
99  'mount_pid',
100  'mount_pid_ol',
101  'fe_login_mode',
102  'backend_layout_next_level'
103  ];
104 
110  protected ‪$pageRepository;
111 
117  protected ‪$context;
118 
122  protected ‪$cacheIdentifier;
123 
127  protected static ‪$pageRecordCache = [];
128 
135  public function ‪__construct($uid, ‪$mountPointParameter = '', ‪$context = null)
136  {
137  $this->mountPointParameter = trim(‪$mountPointParameter);
138  if (!(‪$context instanceof ‪Context)) {
139  ‪$context = GeneralUtility::makeInstance(Context::class);
140  }
141  $this->context = ‪$context;
142  $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class, ‪$context);
143 
144  $this->languageUid = $this->context->getPropertyFromAspect('language', 'id', 0);
145  $this->workspaceUid = $this->context->getPropertyFromAspect('workspace', 'id', 0);
146  if ($this->mountPointParameter !== '') {
147  if (!‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
148  throw new ‪MountPointsDisabledException('Mount-Point Pages are disabled for this installation. Cannot resolve a Rootline for a page with Mount-Points', 1343462896);
149  }
151  }
152 
153  $this->pageUid = $this->‪resolvePageId(
154  (int)$uid,
155  (int)$this->workspaceUid
156  );
157 
158  if (self::$cache === null) {
159  self::$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('rootline');
160  }
161  self::$rootlineFields = array_merge(self::$rootlineFields, ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], true));
162  self::$rootlineFields = array_unique(self::$rootlineFields);
163 
164  $this->cacheIdentifier = $this->‪getCacheIdentifier();
165  }
166 
172  public static function ‪purgeCaches()
173  {
174  self::$localCache = [];
175  self::$pageRecordCache = [];
176  }
177 
184  public function ‪getCacheIdentifier($otherUid = null)
185  {
186  ‪$mountPointParameter = (string)$this->mountPointParameter;
187  if (‪$mountPointParameter !== '' && strpos(‪$mountPointParameter, ',') !== false) {
188  ‪$mountPointParameter = str_replace(',', '__', ‪$mountPointParameter);
189  }
190 
191  return implode('_', [
192  $otherUid !== null ? (int)$otherUid : $this->pageUid,
194  $this->languageUid,
195  $this->workspaceUid
196  ]);
197  }
198 
204  public function get()
205  {
206  if ($this->pageUid === 0) {
207  // pageUid 0 has no root line, return empty array right away
208  return [];
209  }
210  if (!isset(static::$localCache[$this->cacheIdentifier])) {
211  $entry = static::$cache->get($this->cacheIdentifier);
212  if (!$entry) {
213  $this->‪generateRootlineCache();
214  } else {
215  static::$localCache[‪$this->cacheIdentifier] = $entry;
216  $depth = count($entry);
217  // Populate the root-lines for parent pages as well
218  // since they are part of the current root-line
219  while ($depth > 1) {
220  --$depth;
221  $parentCacheIdentifier = $this->‪getCacheIdentifier($entry[$depth - 1]['uid']);
222  // Abort if the root-line of the parent page is
223  // already in the local cache data
224  if (isset(static::$localCache[$parentCacheIdentifier])) {
225  break;
226  }
227  // Behaves similar to array_shift(), but preserves
228  // the array keys - which contain the page ids here
229  $entry = array_slice($entry, 1, null, true);
230  static::$localCache[$parentCacheIdentifier] = $entry;
231  }
232  }
233  }
234  return static::$localCache[‪$this->cacheIdentifier];
235  }
236 
244  protected function ‪getRecordArray($uid)
245  {
246  $currentCacheIdentifier = $this->‪getCacheIdentifier($uid);
247  if (!isset(self::$pageRecordCache[$currentCacheIdentifier])) {
248  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
249  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
250  $row = $queryBuilder->select(...self::$rootlineFields)
251  ->from('pages')
252  ->where(
253  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
254  )
255  ->execute()
256  ->fetch();
257  if (empty($row)) {
258  throw new PageNotFoundException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
259  }
260  $this->pageRepository->versionOL('pages', $row, false, true);
261  $this->pageRepository->fixVersioningPid('pages', $row);
262  if (is_array($row)) {
263  if ($this->languageUid > 0) {
264  $row = $this->pageRepository->getPageOverlay($row, $this->languageUid);
265  }
266  $row = $this->‪enrichWithRelationFields($row['_PAGES_OVERLAY_UID'] ?? $uid, $row);
267  self::$pageRecordCache[$currentCacheIdentifier] = $row;
268  }
269  }
270  if (!is_array(self::$pageRecordCache[$currentCacheIdentifier])) {
271  throw new PageNotFoundException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
272  }
273  return self::$pageRecordCache[$currentCacheIdentifier];
274  }
275 
284  protected function ‪enrichWithRelationFields($uid, array $pageRecord)
285  {
286  if (!isset(‪$GLOBALS['TCA']['pages']['columns']) || !is_array(‪$GLOBALS['TCA']['pages']['columns'])) {
287  return $pageRecord;
288  }
289 
290  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
291 
292  // @todo Remove this special interpretation of relations by consequently using RelationHandler
293  foreach (‪$GLOBALS['TCA']['pages']['columns'] as $column => $configuration) {
294  // Ensure that only fields defined in $rootlineFields (and "addRootLineFields") are actually evaluated
295  if (array_key_exists($column, $pageRecord) && $this->‪columnHasRelationToResolve($configuration)) {
296  $configuration = $configuration['config'];
297  if ($configuration['MM']) {
299  $loadDBGroup = GeneralUtility::makeInstance(RelationHandler::class);
300  $loadDBGroup->start(
301  $pageRecord[$column],
302  // @todo That depends on the type (group, select, inline)
303  $configuration['allowed'] ?? $configuration['foreign_table'],
304  $configuration['MM'],
305  $uid,
306  'pages',
307  $configuration
308  );
309  $relatedUids = $loadDBGroup->tableArray[$configuration['foreign_table']] ?? [];
310  } else {
311  // @todo The assumption is wrong, since group can be used without "MM", but having "allowed"
312  $table = $configuration['foreign_table'];
313 
314  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
315  $queryBuilder->getRestrictions()->removeAll()
316  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
317  ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
318  $queryBuilder->select('uid')
319  ->from($table)
320  ->where(
321  $queryBuilder->expr()->eq(
322  $configuration['foreign_field'],
323  $queryBuilder->createNamedParameter(
324  $uid,
325  \PDO::PARAM_INT
326  )
327  )
328  );
329 
330  if (isset($configuration['foreign_match_fields']) && is_array($configuration['foreign_match_fields'])) {
331  foreach ($configuration['foreign_match_fields'] as $field => $value) {
332  $queryBuilder->andWhere(
333  $queryBuilder->expr()->eq(
334  $field,
335  $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
336  )
337  );
338  }
339  }
340  if (isset($configuration['foreign_table_field'])) {
341  $queryBuilder->andWhere(
342  $queryBuilder->expr()->eq(
343  trim($configuration['foreign_table_field']),
344  $queryBuilder->createNamedParameter(
345  'pages',
346  \PDO::PARAM_STR
347  )
348  )
349  );
350  }
351  if (isset($configuration['foreign_sortby'])) {
352  $queryBuilder->orderBy($configuration['foreign_sortby']);
353  }
354  try {
355  $statement = $queryBuilder->execute();
356  } catch (DBALException $e) {
357  throw new PagePropertyRelationNotFoundException('Could to resolve related records for page ' . $uid . ' and foreign_table ' . htmlspecialchars($table), 1343589452);
358  }
359  $relatedUids = [];
360  while ($row = $statement->fetch()) {
361  $relatedUids[] = $row['uid'];
362  }
363  }
364  $pageRecord[$column] = implode(',', $relatedUids);
365  }
366  }
367  return $pageRecord;
368  }
369 
377  protected function ‪columnHasRelationToResolve(array $configuration)
378  {
379  $configuration = $configuration['config'] ?? null;
380  if (!empty($configuration['MM']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline', 'group'])) {
381  return true;
382  }
383  if (!empty($configuration['foreign_field']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline'])) {
384  return true;
385  }
386  return false;
387  }
388 
394  protected function ‪generateRootlineCache()
395  {
396  $page = $this->‪getRecordArray($this->pageUid);
397  // If the current page is a mounted (according to the MP parameter) handle the mount-point
398  if ($this->‪isMountedPage()) {
399  $mountPoint = $this->‪getRecordArray($this->parsedMountPointParameters[$this->pageUid]);
400  $page = $this->‪processMountedPage($page, $mountPoint);
401  $parentUid = $mountPoint['pid'];
402  // Anyhow after reaching the mount-point, we have to go up that rootline
403  unset($this->parsedMountPointParameters[$this->pageUid]);
404  } else {
405  $parentUid = $page['pid'];
406  }
407  $cacheTags = ['pageId_' . $page['uid']];
408  if ($parentUid > 0) {
409  // Get rootline of (and including) parent page
410  ‪$mountPointParameter = !empty($this->parsedMountPointParameters) ? $this->mountPointParameter : '';
411  $rootline = GeneralUtility::makeInstance(self::class, $parentUid, ‪$mountPointParameter, $this->context)->get();
412  // retrieve cache tags of parent rootline
413  foreach ($rootline as $entry) {
414  $cacheTags[] = 'pageId_' . $entry['uid'];
415  if ($entry['uid'] == $this->pageUid) {
416  throw new CircularRootLineException('Circular connection in rootline for page with uid ' . $this->pageUid . ' found. Check your mountpoint configuration.', 1343464103);
417  }
418  }
419  } else {
420  $rootline = [];
421  }
422  $rootline[] = $page;
423  krsort($rootline);
424  static::$cache->set($this->cacheIdentifier, $rootline, $cacheTags);
425  static::$localCache[‪$this->cacheIdentifier] = $rootline;
426  }
427 
434  public function ‪isMountedPage()
435  {
436  return array_key_exists($this->pageUid, $this->parsedMountPointParameters);
437  }
438 
447  protected function ‪processMountedPage(array $mountedPageData, array $mountPointPageData)
448  {
449  $mountPid = $mountPointPageData['mount_pid'] ?? null;
450  $uid = $mountedPageData['uid'] ?? null;
451  if ($mountPid != $uid) {
452  throw new ‪BrokenRootLineException('Broken rootline. Mountpoint parameter does not match the actual rootline. mount_pid (' . $mountPid . ') does not match page uid (' . $uid . ').', 1343464100);
453  }
454  // Current page replaces the original mount-page
455  $mountUid = $mountPointPageData['uid'] ?? null;
456  if (!empty($mountPointPageData['mount_pid_ol'])) {
457  $mountedPageData['_MOUNT_OL'] = true;
458  $mountedPageData['_MOUNT_PAGE'] = [
459  'uid' => $mountUid,
460  'pid' => $mountPointPageData['pid'] ?? null,
461  'title' => $mountPointPageData['title'] ?? null
462  ];
463  } else {
464  // The mount-page is not replaced, the mount-page itself has to be used
465  $mountedPageData = $mountPointPageData;
466  }
467  $mountedPageData['_MOUNTED_FROM'] = ‪$this->pageUid;
468  $mountedPageData['_MP_PARAM'] = $this->pageUid . '-' . $mountUid;
469  return $mountedPageData;
470  }
471 
477  protected function ‪parseMountPointParameter()
478  {
479  $mountPoints = ‪GeneralUtility::trimExplode(',', $this->mountPointParameter);
480  foreach ($mountPoints as $mP) {
481  [$mountedPageUid, $mountPageUid] = ‪GeneralUtility::intExplode('-', $mP);
482  $this->parsedMountPointParameters[$mountedPageUid] = $mountPageUid;
483  }
484  }
485 
491  protected function ‪resolvePageId(int $pageId, int $workspaceId): int
492  {
493  if ($pageId === 0 || $workspaceId === 0) {
494  return $pageId;
495  }
496 
497  $page = $this->‪resolvePageRecord(
498  $pageId,
499  ['uid', 't3ver_oid', 't3ver_state']
500  );
501  if (!‪VersionState::cast($page['t3ver_state'])
502  ->equals(‪VersionState::MOVE_POINTER)) {
503  return $pageId;
504  }
505 
506  $movePlaceholder = $this->‪resolveMovePlaceHolder(
507  (int)$page['t3ver_oid'],
508  ['uid'],
509  $workspaceId
510  );
511  if (empty($movePlaceholder['uid'])) {
512  return $pageId;
513  }
514 
515  return (int)$movePlaceholder['uid'];
516  }
517 
523  protected function ‪resolvePageRecord(int $pageId, array $fieldNames): ?array
524  {
525  $queryBuilder = $this->‪createQueryBuilder('pages');
526  $queryBuilder->getRestrictions()->removeAll()
527  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
528 
529  $statement = $queryBuilder
530  ->from('pages')
531  ->select(...$fieldNames)
532  ->where(
533  $queryBuilder->expr()->eq(
534  'uid',
535  $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
536  )
537  )
538  ->setMaxResults(1)
539  ->execute();
540 
541  $record = $statement->fetch(FetchMode::ASSOCIATIVE);
542  return $record ?: null;
543  }
544 
551  protected function ‪resolveMovePlaceHolder(int $liveId, array $fieldNames, int $workspaceId): ?array
552  {
553  $queryBuilder = $this->‪createQueryBuilder('pages');
554  $queryBuilder->getRestrictions()->removeAll()
555  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
556 
557  $statement = $queryBuilder
558  ->from('pages')
559  ->select(...$fieldNames)
560  ->setMaxResults(1)
561  ->where(
562  $queryBuilder->expr()->eq(
563  't3ver_wsid',
564  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
565  ),
566  $queryBuilder->expr()->eq(
567  't3ver_state',
568  $queryBuilder->createNamedParameter(‪VersionState::MOVE_PLACEHOLDER, \PDO::PARAM_INT)
569  ),
570  $queryBuilder->expr()->eq(
571  't3ver_move_id',
572  $queryBuilder->createNamedParameter($liveId, \PDO::PARAM_INT)
573  )
574  )
575  ->execute();
576 
577  $record = $statement->fetch(FetchMode::ASSOCIATIVE);
578  return $record ?: null;
579  }
580 
585  protected function ‪createQueryBuilder(string $tableName): ‪QueryBuilder
586  {
587  return GeneralUtility::makeInstance(ConnectionPool::class)
588  ->getQueryBuilderForTable($tableName);
589  }
590 }
‪TYPO3\CMS\Core\Utility\RootlineUtility\$workspaceUid
‪int $workspaceUid
Definition: RootlineUtility.php:58
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Utility\RootlineUtility\$languageUid
‪int $languageUid
Definition: RootlineUtility.php:54
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageUid
‪int $pageUid
Definition: RootlineUtility.php:42
‪TYPO3\CMS\Core\Utility\RootlineUtility\getCacheIdentifier
‪string getCacheIdentifier($otherUid=null)
Definition: RootlineUtility.php:172
‪TYPO3\CMS\Core\Utility\RootlineUtility\isMountedPage
‪bool isMountedPage()
Definition: RootlineUtility.php:422
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:35
‪TYPO3\CMS\Core\Utility\RootlineUtility\parseMountPointParameter
‪parseMountPointParameter()
Definition: RootlineUtility.php:465
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:39
‪TYPO3\CMS\Core\Utility\RootlineUtility\purgeCaches
‪static purgeCaches()
Definition: RootlineUtility.php:160
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:16
‪TYPO3\CMS\Core\Utility\RootlineUtility\getRecordArray
‪array getRecordArray($uid)
Definition: RootlineUtility.php:232
‪TYPO3\CMS\Core\Versioning\VersionState\MOVE_POINTER
‪const MOVE_POINTER
Definition: VersionState.php:73
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Utility\RootlineUtility\columnHasRelationToResolve
‪bool columnHasRelationToResolve(array $configuration)
Definition: RootlineUtility.php:365
‪TYPO3\CMS\Core\Utility\RootlineUtility\$cache
‪static TYPO3 CMS Core Cache Frontend FrontendInterface $cache
Definition: RootlineUtility.php:62
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:52
‪TYPO3\CMS\Core\Utility\RootlineUtility\generateRootlineCache
‪generateRootlineCache()
Definition: RootlineUtility.php:382
‪TYPO3\CMS\Core\Utility\RootlineUtility\$cacheIdentifier
‪string $cacheIdentifier
Definition: RootlineUtility.php:111
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageRecordCache
‪static array $pageRecordCache
Definition: RootlineUtility.php:115
‪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:101
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolveMovePlaceHolder
‪array null resolveMovePlaceHolder(int $liveId, array $fieldNames, int $workspaceId)
Definition: RootlineUtility.php:539
‪TYPO3\CMS\Core\Utility\RootlineUtility\$mountPointParameter
‪string $mountPointParameter
Definition: RootlineUtility.php:46
‪TYPO3\CMS\Core\Exception\Page\CircularRootLineException
Definition: CircularRootLineException.php:24
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:35
‪TYPO3\CMS\Core\Utility\RootlineUtility\$localCache
‪static array $localCache
Definition: RootlineUtility.php:66
‪TYPO3\CMS\Core\Utility\RootlineUtility\$parsedMountPointParameters
‪array $parsedMountPointParameters
Definition: RootlineUtility.php:50
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolvePageRecord
‪array null resolvePageRecord(int $pageId, array $fieldNames)
Definition: RootlineUtility.php:511
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolvePageId
‪int resolvePageId(int $pageId, int $workspaceId)
Definition: RootlineUtility.php:479
‪TYPO3\CMS\Core\Versioning\VersionState
Definition: VersionState.php:24
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static string[] trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:1059
‪TYPO3\CMS\Core\Utility\RootlineUtility\__construct
‪__construct($uid, $mountPointParameter='', $context=null)
Definition: RootlineUtility.php:123
‪TYPO3\CMS\Core\Utility\RootlineUtility\enrichWithRelationFields
‪array enrichWithRelationFields($uid, array $pageRecord)
Definition: RootlineUtility.php:272
‪TYPO3\CMS\Core\Utility\RootlineUtility\createQueryBuilder
‪QueryBuilder createQueryBuilder(string $tableName)
Definition: RootlineUtility.php:573
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪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:988
‪TYPO3\CMS\Core\Utility\RootlineUtility\$context
‪Context $context
Definition: RootlineUtility.php:107
‪TYPO3\CMS\Core\Exception\Page\BrokenRootLineException
Definition: BrokenRootLineException.php:24
‪TYPO3\CMS\Core\Exception\Page\MountPointsDisabledException
Definition: MountPointsDisabledException.php:25
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:52
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Exception\Page\PageNotFoundException
Definition: PageNotFoundException.php:24
‪TYPO3\CMS\Core\Utility\RootlineUtility\$rootlineFields
‪static array $rootlineFields
Definition: RootlineUtility.php:72
‪TYPO3\CMS\Core\Utility\RootlineUtility\processMountedPage
‪array processMountedPage(array $mountedPageData, array $mountPointPageData)
Definition: RootlineUtility.php:435
‪TYPO3\CMS\Core\Exception\Page\PagePropertyRelationNotFoundException
Definition: PagePropertyRelationNotFoundException.php:24
‪TYPO3\CMS\Core\Versioning\VersionState\MOVE_PLACEHOLDER
‪const MOVE_PLACEHOLDER
Definition: VersionState.php:72