TYPO3 CMS  TYPO3_8-7
RootlineUtility.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Utility;
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 
22 
27 {
31  protected $pageUid;
32 
37 
42 
46  protected $languageUid = 0;
47 
51  protected $workspaceUid = 0;
52 
56  protected $versionPreview = false;
57 
61  protected static $cache = null;
62 
66  protected static $localCache = [];
67 
73  protected static $rootlineFields = [
74  'pid',
75  'uid',
76  't3ver_oid',
77  't3ver_wsid',
78  't3ver_state',
79  'title',
80  'alias',
81  'nav_title',
82  'media',
83  'layout',
84  'hidden',
85  'starttime',
86  'endtime',
87  'fe_group',
88  'extendToSubpages',
89  'doktype',
90  'TSconfig',
91  'tsconfig_includes',
92  'is_siteroot',
93  'mount_pid',
94  'mount_pid_ol',
95  'fe_login_mode',
96  'backend_layout_next_level'
97  ];
98 
104  protected $pageContext;
105 
109  protected $cacheIdentifier;
110 
114  protected static $pageRecordCache = [];
115 
122  public function __construct($uid, $mountPointParameter = '', PageRepository $context = null)
123  {
124  $this->pageUid = (int)$uid;
125  $this->mountPointParameter = trim($mountPointParameter);
126  if ($context === null) {
127  if (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']->sys_page)) {
128  $this->pageContext = $GLOBALS['TSFE']->sys_page;
129  } else {
130  $this->pageContext = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
131  }
132  } else {
133  $this->pageContext = $context;
134  }
135  $this->initializeObject();
136  }
137 
143  protected function initializeObject()
144  {
145  $this->languageUid = (int)$this->pageContext->sys_language_uid;
146  $this->workspaceUid = (int)$this->pageContext->versioningWorkspaceId;
147  $this->versionPreview = $this->pageContext->versioningPreview;
148  if ($this->mountPointParameter !== '') {
149  if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
150  throw new \RuntimeException('Mount-Point Pages are disabled for this installation. Cannot resolve a Rootline for a page with Mount-Points', 1343462896);
151  }
152  $this->parseMountPointParameter();
153  }
154  if (self::$cache === null) {
155  self::$cache = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_rootline');
156  }
157  self::$rootlineFields = array_merge(self::$rootlineFields, GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], true));
158  self::$rootlineFields = array_unique(self::$rootlineFields);
159 
160  $this->cacheIdentifier = $this->getCacheIdentifier();
161  }
162 
168  public static function purgeCaches()
169  {
170  self::$localCache = [];
171  self::$pageRecordCache = [];
172  }
173 
180  public function getCacheIdentifier($otherUid = null)
181  {
182  $mountPointParameter = (string)$this->mountPointParameter;
183  if ($mountPointParameter !== '' && strpos($mountPointParameter, ',') !== false) {
184  $mountPointParameter = str_replace(',', '__', $mountPointParameter);
185  }
186 
187  return implode('_', [
188  $otherUid !== null ? (int)$otherUid : $this->pageUid,
190  $this->languageUid,
191  $this->workspaceUid,
192  $this->versionPreview ? 1 : 0
193  ]);
194  }
195 
201  public function get()
202  {
203  if (!isset(static::$localCache[$this->cacheIdentifier])) {
204  $entry = static::$cache->get($this->cacheIdentifier);
205  if (!$entry) {
206  $this->generateRootlineCache();
207  } else {
208  static::$localCache[$this->cacheIdentifier] = $entry;
209  $depth = count($entry);
210  // Populate the root-lines for parent pages as well
211  // since they are part of the current root-line
212  while ($depth > 1) {
213  --$depth;
214  $parentCacheIdentifier = $this->getCacheIdentifier($entry[$depth - 1]['uid']);
215  // Abort if the root-line of the parent page is
216  // already in the local cache data
217  if (isset(static::$localCache[$parentCacheIdentifier])) {
218  break;
219  }
220  // Behaves similar to array_shift(), but preserves
221  // the array keys - which contain the page ids here
222  $entry = array_slice($entry, 1, null, true);
223  static::$localCache[$parentCacheIdentifier] = $entry;
224  }
225  }
226  }
227  return static::$localCache[$this->cacheIdentifier];
228  }
229 
237  protected function getRecordArray($uid)
238  {
239  $currentCacheIdentifier = $this->getCacheIdentifier($uid);
240  if (!isset(self::$pageRecordCache[$currentCacheIdentifier])) {
241  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
242  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
243  $row = $queryBuilder->select(...self::$rootlineFields)
244  ->from('pages')
245  ->where(
246  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
247  $queryBuilder->expr()->neq(
248  'doktype',
249  $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_RECYCLER, \PDO::PARAM_INT)
250  )
251  )
252  ->execute()
253  ->fetch();
254  if (empty($row)) {
255  throw new \RuntimeException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
256  }
257  $this->pageContext->versionOL('pages', $row, false, true);
258  $this->pageContext->fixVersioningPid('pages', $row);
259  if (is_array($row)) {
260  if ($this->languageUid > 0) {
261  $row = $this->pageContext->getPageOverlay($row, $this->languageUid);
262  }
263  $row = $this->enrichWithRelationFields($row['_PAGES_OVERLAY_UID'] ?? $uid, $row);
264  self::$pageRecordCache[$currentCacheIdentifier] = $row;
265  }
266  }
267  if (!is_array(self::$pageRecordCache[$currentCacheIdentifier])) {
268  throw new \RuntimeException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
269  }
270  return self::$pageRecordCache[$currentCacheIdentifier];
271  }
272 
281  protected function enrichWithRelationFields($uid, array $pageRecord)
282  {
283  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
284 
285  // @todo Remove this special interpretation of relations by consequently using RelationHandler
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  $configuration = $configuration['config'];
290  if ($configuration['MM']) {
292  $loadDBGroup = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
293  $loadDBGroup->start(
294  $pageRecord[$column],
295  // @todo That depends on the type (group, select, inline)
296  isset($configuration['allowed']) ? $configuration['allowed'] : $configuration['foreign_table'],
297  $configuration['MM'],
298  $uid,
299  'pages',
300  $configuration
301  );
302  $relatedUids = isset($loadDBGroup->tableArray[$configuration['foreign_table']])
303  ? $loadDBGroup->tableArray[$configuration['foreign_table']]
304  : [];
305  } else {
306  // @todo The assumption is wrong, since group can be used without "MM", but having "allowed"
307  $table = $configuration['foreign_table'];
308 
309  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
310  $queryBuilder->getRestrictions()->removeAll()
311  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
312  ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
313  $queryBuilder->select('uid')
314  ->from($table)
315  ->where(
316  $queryBuilder->expr()->eq(
317  $configuration['foreign_field'],
318  $queryBuilder->createNamedParameter(
319  $uid,
320  \PDO::PARAM_INT
321  )
322  )
323  );
324 
325  if (isset($configuration['foreign_match_fields']) && is_array($configuration['foreign_match_fields'])) {
326  foreach ($configuration['foreign_match_fields'] as $field => $value) {
327  $queryBuilder->andWhere(
328  $queryBuilder->expr()->eq(
329  $field,
330  $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
331  )
332  );
333  }
334  }
335  if (isset($configuration['foreign_table_field'])) {
336  $queryBuilder->andWhere(
337  $queryBuilder->expr()->eq(
338  trim($configuration['foreign_table_field']),
339  $queryBuilder->createNamedParameter(
340  (int)$this->languageUid > 0 ? 'pages_language_overlay' : 'pages',
341  \PDO::PARAM_STR
342  )
343  )
344  );
345  }
346  if (isset($configuration['foreign_sortby'])) {
347  $queryBuilder->orderBy($configuration['foreign_sortby']);
348  }
349  try {
350  $statement = $queryBuilder->execute();
351  } catch (DBALException $e) {
352  throw new \RuntimeException('Could to resolve related records for page ' . $uid . ' and foreign_table ' . htmlspecialchars($table), 1343589452);
353  }
354  $relatedUids = [];
355  while ($row = $statement->fetch()) {
356  $relatedUids[] = $row['uid'];
357  }
358  }
359  $pageRecord[$column] = implode(',', $relatedUids);
360  }
361  }
362  return $pageRecord;
363  }
364 
372  protected function columnHasRelationToResolve(array $configuration)
373  {
374  $configuration = $configuration['config'];
375  if (!empty($configuration['MM']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline', 'group'])) {
376  return true;
377  }
378  if (!empty($configuration['foreign_field']) && !empty($configuration['type']) && in_array($configuration['type'], ['select', 'inline'])) {
379  return true;
380  }
381  return false;
382  }
383 
389  protected function generateRootlineCache()
390  {
391  $page = $this->getRecordArray($this->pageUid);
392  // If the current page is a mounted (according to the MP parameter) handle the mount-point
393  if ($this->isMountedPage()) {
394  $mountPoint = $this->getRecordArray($this->parsedMountPointParameters[$this->pageUid]);
395  $page = $this->processMountedPage($page, $mountPoint);
396  $parentUid = $mountPoint['pid'];
397  // Anyhow after reaching the mount-point, we have to go up that rootline
398  unset($this->parsedMountPointParameters[$this->pageUid]);
399  } else {
400  $parentUid = $page['pid'];
401  }
402  $cacheTags = ['pageId_' . $page['uid']];
403  if ($parentUid > 0) {
404  // Get rootline of (and including) parent page
405  $mountPointParameter = !empty($this->parsedMountPointParameters) ? $this->mountPointParameter : '';
407  $rootline = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Utility\RootlineUtility::class, $parentUid, $mountPointParameter, $this->pageContext);
408  $rootline = $rootline->get();
409  // retrieve cache tags of parent rootline
410  foreach ($rootline as $entry) {
411  $cacheTags[] = 'pageId_' . $entry['uid'];
412  if ($entry['uid'] == $this->pageUid) {
413  throw new \RuntimeException('Circular connection in rootline for page with uid ' . $this->pageUid . ' found. Check your mountpoint configuration.', 1343464103);
414  }
415  }
416  } else {
417  $rootline = [];
418  }
419  $rootline[] = $page;
420  krsort($rootline);
421  static::$cache->set($this->cacheIdentifier, $rootline, $cacheTags);
422  static::$localCache[$this->cacheIdentifier] = $rootline;
423  }
424 
431  public function isMountedPage()
432  {
433  return in_array($this->pageUid, array_keys($this->parsedMountPointParameters));
434  }
435 
444  protected function processMountedPage(array $mountedPageData, array $mountPointPageData)
445  {
446  if ($mountPointPageData['mount_pid'] != $mountedPageData['uid']) {
447  throw new \RuntimeException('Broken rootline. Mountpoint parameter does not match the actual rootline. mount_pid (' . $mountPointPageData['mount_pid'] . ') does not match page uid (' . $mountedPageData['uid'] . ').', 1343464100);
448  }
449  // Current page replaces the original mount-page
450  if ($mountPointPageData['mount_pid_ol']) {
451  $mountedPageData['_MOUNT_OL'] = true;
452  $mountedPageData['_MOUNT_PAGE'] = [
453  'uid' => $mountPointPageData['uid'],
454  'pid' => $mountPointPageData['pid'],
455  'title' => $mountPointPageData['title']
456  ];
457  } else {
458  // The mount-page is not replaced, the mount-page itself has to be used
459  $mountedPageData = $mountPointPageData;
460  }
461  $mountedPageData['_MOUNTED_FROM'] = $this->pageUid;
462  $mountedPageData['_MP_PARAM'] = $this->pageUid . '-' . $mountPointPageData['uid'];
463  return $mountedPageData;
464  }
465 
471  protected function parseMountPointParameter()
472  {
473  $mountPoints = GeneralUtility::trimExplode(',', $this->mountPointParameter);
474  foreach ($mountPoints as $mP) {
475  list($mountedPageUid, $mountPageUid) = GeneralUtility::intExplode('-', $mP);
476  $this->parsedMountPointParameters[$mountedPageUid] = $mountPageUid;
477  }
478  }
479 }
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
processMountedPage(array $mountedPageData, array $mountPointPageData)
columnHasRelationToResolve(array $configuration)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
__construct($uid, $mountPointParameter='', PageRepository $context=null)