‪TYPO3CMS  9.5
RootlineUtility.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
17 use Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\FetchMode;
31 
36 {
40  protected ‪$pageUid;
41 
45  protected ‪$mountPointParameter;
46 
50  protected ‪$parsedMountPointParameters = [];
51 
55  protected ‪$languageUid = 0;
56 
60  protected ‪$workspaceUid = 0;
61 
65  protected static ‪$cache;
66 
70  protected static ‪$localCache = [];
71 
77  protected static ‪$rootlineFields = [
78  'pid',
79  'uid',
80  't3ver_oid',
81  't3ver_wsid',
82  't3ver_state',
83  'title',
84  'alias',
85  'nav_title',
86  'media',
87  'layout',
88  'hidden',
89  'starttime',
90  'endtime',
91  'fe_group',
92  'extendToSubpages',
93  'doktype',
94  'TSconfig',
95  'tsconfig_includes',
96  'is_siteroot',
97  'mount_pid',
98  'mount_pid_ol',
99  'fe_login_mode',
100  'backend_layout_next_level'
101  ];
102 
108  protected ‪$pageContext;
109 
115  protected ‪$context;
116 
120  protected ‪$cacheIdentifier;
121 
125  protected static ‪$pageRecordCache = [];
126 
133  public function ‪__construct($uid, ‪$mountPointParameter = '', ‪$context = null)
134  {
135  $this->mountPointParameter = trim(‪$mountPointParameter);
136  if (‪$context instanceof ‪PageRepository) {
137  trigger_error('Calling RootlineUtility with PageRepository as third parameter will be unsupported with TYPO3 v10.0. Use a Context object directly.', E_USER_DEPRECATED);
138  $this->pageContext = ‪$context;
139  $this->context = GeneralUtility::makeInstance(Context::class);
140  } else {
141  if (!(‪$context instanceof ‪Context)) {
142  ‪$context = GeneralUtility::makeInstance(Context::class);
143  }
144  $this->context = ‪$context;
145  $this->pageContext = GeneralUtility::makeInstance(PageRepository::class, ‪$context);
146  }
147 
148  $this->languageUid = $this->context->getPropertyFromAspect('language', 'id', 0);
149  $this->workspaceUid = $this->context->getPropertyFromAspect('workspace', 'id', 0);
150  if ($this->mountPointParameter !== '') {
151  if (!‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
152  throw new ‪MountPointsDisabledException('Mount-Point Pages are disabled for this installation. Cannot resolve a Rootline for a page with Mount-Points', 1343462896);
153  }
155  }
156 
157  $this->pageUid = $this->‪resolvePageId(
158  (int)$uid,
159  (int)$this->workspaceUid
160  );
161 
162  if (self::$cache === null) {
163  self::$cache = GeneralUtility::makeInstance(\‪TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_rootline');
164  }
165  self::$rootlineFields = array_merge(self::$rootlineFields, GeneralUtility::trimExplode(',', ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], true));
166  self::$rootlineFields = array_unique(self::$rootlineFields);
167 
168  $this->cacheIdentifier = $this->‪getCacheIdentifier();
169  }
170 
176  public static function ‪purgeCaches()
177  {
178  self::$localCache = [];
179  self::$pageRecordCache = [];
180  }
181 
188  public function ‪getCacheIdentifier($otherUid = null)
189  {
190  ‪$mountPointParameter = (string)$this->mountPointParameter;
191  if (‪$mountPointParameter !== '' && strpos(‪$mountPointParameter, ',') !== false) {
192  ‪$mountPointParameter = str_replace(',', '__', ‪$mountPointParameter);
193  }
194 
195  return implode('_', [
196  $otherUid !== null ? (int)$otherUid : $this->pageUid,
198  $this->languageUid,
199  $this->workspaceUid
200  ]);
201  }
202 
208  public function get()
209  {
210  if ($this->pageUid === 0) {
211  // pageUid 0 has no root line, return empty array right away
212  return [];
213  }
214  if (!isset(static::$localCache[$this->cacheIdentifier])) {
215  $entry = static::$cache->get($this->cacheIdentifier);
216  if (!$entry) {
217  $this->‪generateRootlineCache();
218  } else {
219  static::$localCache[‪$this->cacheIdentifier] = $entry;
220  $depth = count($entry);
221  // Populate the root-lines for parent pages as well
222  // since they are part of the current root-line
223  while ($depth > 1) {
224  --$depth;
225  $parentCacheIdentifier = $this->‪getCacheIdentifier($entry[$depth - 1]['uid']);
226  // Abort if the root-line of the parent page is
227  // already in the local cache data
228  if (isset(static::$localCache[$parentCacheIdentifier])) {
229  break;
230  }
231  // Behaves similar to array_shift(), but preserves
232  // the array keys - which contain the page ids here
233  $entry = array_slice($entry, 1, null, true);
234  static::$localCache[$parentCacheIdentifier] = $entry;
235  }
236  }
237  }
238  return static::$localCache[‪$this->cacheIdentifier];
239  }
240 
248  protected function ‪getRecordArray($uid)
249  {
250  $currentCacheIdentifier = $this->‪getCacheIdentifier($uid);
251  if (!isset(self::$pageRecordCache[$currentCacheIdentifier])) {
252  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
253  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
254  $row = $queryBuilder->select(...self::$rootlineFields)
255  ->from('pages')
256  ->where(
257  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
258  )
259  ->execute()
260  ->fetch();
261  if (empty($row)) {
262  throw new PageNotFoundException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
263  }
264  $this->pageContext->versionOL('pages', $row, false, true);
265  $this->pageContext->fixVersioningPid('pages', $row);
266  if (is_array($row)) {
267  if ($this->languageUid > 0) {
268  $row = $this->pageContext->getPageOverlay($row, $this->languageUid);
269  }
270  $row = $this->‪enrichWithRelationFields($row['_PAGES_OVERLAY_UID'] ?? $uid, $row);
271  self::$pageRecordCache[$currentCacheIdentifier] = $row;
272  }
273  }
274  if (!is_array(self::$pageRecordCache[$currentCacheIdentifier])) {
275  throw new PageNotFoundException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
276  }
277  return self::$pageRecordCache[$currentCacheIdentifier];
278  }
279 
288  protected function ‪enrichWithRelationFields($uid, array $pageRecord)
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(\‪TYPO3\CMS\Core\Database\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  list($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:55
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:25
‪TYPO3\CMS\Core\Utility\RootlineUtility\$languageUid
‪int $languageUid
Definition: RootlineUtility.php:51
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageUid
‪int $pageUid
Definition: RootlineUtility.php:39
‪TYPO3\CMS\Core\Utility\RootlineUtility\getCacheIdentifier
‪string getCacheIdentifier($otherUid=null)
Definition: RootlineUtility.php:176
‪TYPO3\CMS\Core\Utility\RootlineUtility\isMountedPage
‪bool isMountedPage()
Definition: RootlineUtility.php:422
‪TYPO3\CMS\Core\Utility\RootlineUtility\parseMountPointParameter
‪parseMountPointParameter()
Definition: RootlineUtility.php:465
‪TYPO3
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:36
‪TYPO3\CMS\Core\Utility\RootlineUtility\purgeCaches
‪static purgeCaches()
Definition: RootlineUtility.php:164
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:2
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageContext
‪PageRepository $pageContext
Definition: RootlineUtility.php:99
‪TYPO3\CMS\Core\Utility\RootlineUtility\getRecordArray
‪array getRecordArray($uid)
Definition: RootlineUtility.php:236
‪TYPO3\CMS\Core\Versioning\VersionState\MOVE_POINTER
‪const MOVE_POINTER
Definition: VersionState.php:72
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:49
‪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:59
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:47
‪TYPO3\CMS\Core\Utility\RootlineUtility\generateRootlineCache
‪generateRootlineCache()
Definition: RootlineUtility.php:382
‪TYPO3\CMS\Core\Utility\RootlineUtility\$cacheIdentifier
‪string $cacheIdentifier
Definition: RootlineUtility.php:109
‪TYPO3\CMS\Core\Utility\RootlineUtility\$pageRecordCache
‪static array $pageRecordCache
Definition: RootlineUtility.php:113
‪TYPO3\CMS\Core\Type\Enumeration\cast
‪static static cast($value)
Definition: Enumeration.php:182
‪TYPO3\CMS\Core\Utility\RootlineUtility\resolveMovePlaceHolder
‪array null resolveMovePlaceHolder(int $liveId, array $fieldNames, int $workspaceId)
Definition: RootlineUtility.php:539
‪TYPO3\CMS\Frontend\Page\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Utility\RootlineUtility\$mountPointParameter
‪string $mountPointParameter
Definition: RootlineUtility.php:43
‪TYPO3\CMS\Core\Exception\Page\CircularRootLineException
Definition: CircularRootLineException.php:23
‪TYPO3\CMS\Core\Utility\RootlineUtility\$localCache
‪static array $localCache
Definition: RootlineUtility.php:63
‪TYPO3\CMS\Core\Utility\RootlineUtility\$parsedMountPointParameters
‪array $parsedMountPointParameters
Definition: RootlineUtility.php:47
‪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:23
‪TYPO3\CMS\Core\Utility\RootlineUtility\__construct
‪__construct($uid, $mountPointParameter='', $context=null)
Definition: RootlineUtility.php:121
‪TYPO3\CMS\Core\Utility\RootlineUtility\enrichWithRelationFields
‪array enrichWithRelationFields($uid, array $pageRecord)
Definition: RootlineUtility.php:276
‪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:26
‪TYPO3\CMS\Core\Utility\RootlineUtility\$context
‪Context $context
Definition: RootlineUtility.php:105
‪TYPO3\CMS\Core\Exception\Page\BrokenRootLineException
Definition: BrokenRootLineException.php:23
‪TYPO3\CMS\Core\Exception\Page\MountPointsDisabledException
Definition: MountPointsDisabledException.php:24
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Exception\Page\PageNotFoundException
Definition: PageNotFoundException.php:23
‪TYPO3\CMS\Core\Utility\RootlineUtility\$rootlineFields
‪static array $rootlineFields
Definition: RootlineUtility.php:69
‪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:23
‪TYPO3\CMS\Core\Versioning\VersionState\MOVE_PLACEHOLDER
‪const MOVE_PLACEHOLDER
Definition: VersionState.php:71