caf41e3cc25dc0280ffee830926e4ae15bff1eff
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / RootlineUtility.php
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
17 use TYPO3\CMS\Core\Database\ConnectionPool;
18 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
19 use TYPO3\CMS\Frontend\Page\PageRepository;
20
21 /**
22 * A utility resolving and Caching the Rootline generation
23 */
24 class RootlineUtility
25 {
26 /**
27 * @var int
28 */
29 protected $pageUid;
30
31 /**
32 * @var string
33 */
34 protected $mountPointParameter;
35
36 /**
37 * @var array
38 */
39 protected $parsedMountPointParameters = array();
40
41 /**
42 * @var int
43 */
44 protected $languageUid = 0;
45
46 /**
47 * @var int
48 */
49 protected $workspaceUid = 0;
50
51 /**
52 * @var bool
53 */
54 protected $versionPreview = false;
55
56 /**
57 * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
58 */
59 protected static $cache = null;
60
61 /**
62 * @var array
63 */
64 protected static $localCache = array();
65
66 /**
67 * Fields to fetch when populating rootline data
68 *
69 * @var array
70 */
71 protected static $rootlineFields = array(
72 'pid',
73 'uid',
74 't3ver_oid',
75 't3ver_wsid',
76 't3ver_state',
77 'title',
78 'alias',
79 'nav_title',
80 'media',
81 'layout',
82 'hidden',
83 'starttime',
84 'endtime',
85 'fe_group',
86 'extendToSubpages',
87 'doktype',
88 'TSconfig',
89 'tsconfig_includes',
90 'is_siteroot',
91 'mount_pid',
92 'mount_pid_ol',
93 'fe_login_mode',
94 'backend_layout_next_level'
95 );
96
97 /**
98 * Rootline Context
99 *
100 * @var \TYPO3\CMS\Frontend\Page\PageRepository
101 */
102 protected $pageContext;
103
104 /**
105 * @var string
106 */
107 protected $cacheIdentifier;
108
109 /**
110 * @var array
111 */
112 protected static $pageRecordCache = array();
113
114 /**
115 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
116 */
117 protected $databaseConnection;
118
119 /**
120 * @param int $uid
121 * @param string $mountPointParameter
122 * @param \TYPO3\CMS\Frontend\Page\PageRepository $context
123 * @throws \RuntimeException
124 */
125 public function __construct($uid, $mountPointParameter = '', PageRepository $context = null)
126 {
127 $this->pageUid = (int)$uid;
128 $this->mountPointParameter = trim($mountPointParameter);
129 if ($context === null) {
130 if (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']->sys_page)) {
131 $this->pageContext = $GLOBALS['TSFE']->sys_page;
132 } else {
133 $this->pageContext = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
134 }
135 } else {
136 $this->pageContext = $context;
137 }
138 $this->initializeObject();
139 }
140
141 /**
142 * Initialize a state to work with
143 *
144 * @throws \RuntimeException
145 * @return void
146 */
147 protected function initializeObject()
148 {
149 $this->languageUid = (int)$this->pageContext->sys_language_uid;
150 $this->workspaceUid = (int)$this->pageContext->versioningWorkspaceId;
151 $this->versionPreview = $this->pageContext->versioningPreview;
152 if ($this->mountPointParameter !== '') {
153 if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
154 throw new \RuntimeException('Mount-Point Pages are disabled for this installation. Cannot resolve a Rootline for a page with Mount-Points', 1343462896);
155 } else {
156 $this->parseMountPointParameter();
157 }
158 }
159 if (self::$cache === null) {
160 self::$cache = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_rootline');
161 }
162 self::$rootlineFields = array_merge(self::$rootlineFields, GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], true));
163 self::$rootlineFields = array_unique(self::$rootlineFields);
164 $this->databaseConnection = $GLOBALS['TYPO3_DB'];
165
166 $this->cacheIdentifier = $this->getCacheIdentifier();
167 }
168
169 /**
170 * Purges all rootline caches.
171 *
172 * Note: This function is intended to be used in unit tests only.
173 *
174 * @return void
175 */
176 public static function purgeCaches()
177 {
178 self::$localCache = array();
179 self::$pageRecordCache = array();
180 }
181
182 /**
183 * Constructs the cache Identifier
184 *
185 * @param int $otherUid
186 * @return string
187 */
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('_', array(
196 $otherUid !== null ? (int)$otherUid : $this->pageUid,
197 $mountPointParameter,
198 $this->languageUid,
199 $this->workspaceUid,
200 $this->versionPreview ? 1 : 0
201 ));
202 }
203
204 /**
205 * Returns the actual rootline
206 *
207 * @return array
208 */
209 public function get()
210 {
211 if (!isset(static::$localCache[$this->cacheIdentifier])) {
212 $entry = static::$cache->get($this->cacheIdentifier);
213 if (!$entry) {
214 $this->generateRootlineCache();
215 } else {
216 static::$localCache[$this->cacheIdentifier] = $entry;
217 $depth = count($entry);
218 // Populate the root-lines for parent pages as well
219 // since they are part of the current root-line
220 while ($depth > 1) {
221 --$depth;
222 $parentCacheIdentifier = $this->getCacheIdentifier($entry[$depth - 1]['uid']);
223 // Abort if the root-line of the parent page is
224 // already in the local cache data
225 if (isset(static::$localCache[$parentCacheIdentifier])) {
226 break;
227 }
228 // Behaves similar to array_shift(), but preserves
229 // the array keys - which contain the page ids here
230 $entry = array_slice($entry, 1, null, true);
231 static::$localCache[$parentCacheIdentifier] = $entry;
232 }
233 }
234 }
235 return static::$localCache[$this->cacheIdentifier];
236 }
237
238 /**
239 * Queries the database for the page record and returns it.
240 *
241 * @param int $uid Page id
242 * @throws \RuntimeException
243 * @return array
244 */
245 protected function getRecordArray($uid)
246 {
247 $currentCacheIdentifier = $this->getCacheIdentifier($uid);
248 if (!isset(self::$pageRecordCache[$currentCacheIdentifier])) {
249 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
250 $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
251 $row = $queryBuilder->select(...self::$rootlineFields)
252 ->from('pages')
253 ->where(
254 $queryBuilder->expr()->eq('uid', (int)$uid),
255 $queryBuilder->expr()->neq('doktype', (int)PageRepository::DOKTYPE_RECYCLER)
256 )
257 ->execute()
258 ->fetch();
259 if (empty($row)) {
260 throw new \RuntimeException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
261 }
262 $this->pageContext->versionOL('pages', $row, false, true);
263 $this->pageContext->fixVersioningPid('pages', $row);
264 if (is_array($row)) {
265 if ($this->languageUid > 0) {
266 $row = $this->pageContext->getPageOverlay($row, $this->languageUid);
267 }
268 $row = $this->enrichWithRelationFields(isset($row['_PAGES_OVERLAY_UID']) ? $row['_PAGES_OVERLAY_UID'] : $uid, $row);
269 self::$pageRecordCache[$currentCacheIdentifier] = $row;
270 }
271 }
272 if (!is_array(self::$pageRecordCache[$currentCacheIdentifier])) {
273 throw new \RuntimeException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
274 }
275 return self::$pageRecordCache[$currentCacheIdentifier];
276 }
277
278 /**
279 * Resolve relations as defined in TCA and add them to the provided $pageRecord array.
280 *
281 * @param int $uid Page id
282 * @param array $pageRecord Array with page data to add relation data to.
283 * @throws \RuntimeException
284 * @return array $pageRecord with additional relations
285 */
286 protected function enrichWithRelationFields($uid, array $pageRecord)
287 {
288 $pageOverlayFields = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['pageOverlayFields']);
289 foreach ($GLOBALS['TCA']['pages']['columns'] as $column => $configuration) {
290 if ($this->columnHasRelationToResolve($configuration)) {
291 $configuration = $configuration['config'];
292 if ($configuration['MM']) {
293 /** @var $loadDBGroup \TYPO3\CMS\Core\Database\RelationHandler */
294 $loadDBGroup = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
295 $loadDBGroup->start(
296 $pageRecord[$column],
297 isset($configuration['allowed']) ? $configuration['allowed'] : $configuration['foreign_table'],
298 $configuration['MM'],
299 $uid,
300 'pages',
301 $configuration
302 );
303 $relatedUids = isset($loadDBGroup->tableArray[$configuration['foreign_table']])
304 ? $loadDBGroup->tableArray[$configuration['foreign_table']]
305 : array();
306 } else {
307 $columnIsOverlaid = in_array($column, $pageOverlayFields, true);
308 $table = $configuration['foreign_table'];
309 $field = $configuration['foreign_field'];
310 $whereClauseParts = array($field . ' = ' . (int)($columnIsOverlaid ? $uid : $pageRecord['uid']));
311 if (isset($configuration['foreign_match_fields']) && is_array($configuration['foreign_match_fields'])) {
312 foreach ($configuration['foreign_match_fields'] as $field => $value) {
313 $whereClauseParts[] = $field . ' = ' . $this->databaseConnection->fullQuoteStr($value, $table);
314 }
315 }
316 if (isset($configuration['foreign_table_field'])) {
317 if ((int)$this->languageUid > 0 && $columnIsOverlaid) {
318 $whereClauseParts[] = trim($configuration['foreign_table_field']) . ' = \'pages_language_overlay\'';
319 } else {
320 $whereClauseParts[] = trim($configuration['foreign_table_field']) . ' = \'pages\'';
321 }
322 }
323 if (isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
324 $whereClauseParts[] = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'] . ' = 0';
325 }
326 $whereClause = implode(' AND ', $whereClauseParts);
327 $whereClause .= $this->pageContext->deleteClause($table);
328 $orderBy = isset($configuration['foreign_sortby']) ? $configuration['foreign_sortby'] : '';
329 $rows = $this->databaseConnection->exec_SELECTgetRows('uid', $table, $whereClause, '', $orderBy);
330 if (!is_array($rows)) {
331 throw new \RuntimeException('Could to resolve related records for page ' . $uid . ' and foreign_table ' . htmlspecialchars($configuration['foreign_table']), 1343589452);
332 }
333 $relatedUids = array();
334 foreach ($rows as $row) {
335 $relatedUids[] = $row['uid'];
336 }
337 }
338 $pageRecord[$column] = implode(',', $relatedUids);
339 }
340 }
341 return $pageRecord;
342 }
343
344 /**
345 * Checks whether the TCA Configuration array of a column
346 * describes a relation which is not stored as CSV in the record
347 *
348 * @param array $configuration TCA configuration to check
349 * @return bool TRUE, if it describes a non-CSV relation
350 */
351 protected function columnHasRelationToResolve(array $configuration)
352 {
353 $configuration = $configuration['config'];
354 if (!empty($configuration['MM']) && !empty($configuration['type']) && in_array($configuration['type'], array('select', 'inline', 'group'))) {
355 return true;
356 }
357 if (!empty($configuration['foreign_field']) && !empty($configuration['type']) && in_array($configuration['type'], array('select', 'inline'))) {
358 return true;
359 }
360 return false;
361 }
362
363 /**
364 * Actual function to generate the rootline and cache it
365 *
366 * @throws \RuntimeException
367 * @return void
368 */
369 protected function generateRootlineCache()
370 {
371 $page = $this->getRecordArray($this->pageUid);
372 // If the current page is a mounted (according to the MP parameter) handle the mount-point
373 if ($this->isMountedPage()) {
374 $mountPoint = $this->getRecordArray($this->parsedMountPointParameters[$this->pageUid]);
375 $page = $this->processMountedPage($page, $mountPoint);
376 $parentUid = $mountPoint['pid'];
377 // Anyhow after reaching the mount-point, we have to go up that rootline
378 unset($this->parsedMountPointParameters[$this->pageUid]);
379 } else {
380 $parentUid = $page['pid'];
381 }
382 $cacheTags = array('pageId_' . $page['uid']);
383 if ($parentUid > 0) {
384 // Get rootline of (and including) parent page
385 $mountPointParameter = !empty($this->parsedMountPointParameters) ? $this->mountPointParameter : '';
386 /** @var $rootline \TYPO3\CMS\Core\Utility\RootlineUtility */
387 $rootline = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Utility\RootlineUtility::class, $parentUid, $mountPointParameter, $this->pageContext);
388 $rootline = $rootline->get();
389 // retrieve cache tags of parent rootline
390 foreach ($rootline as $entry) {
391 $cacheTags[] = 'pageId_' . $entry['uid'];
392 if ($entry['uid'] == $this->pageUid) {
393 throw new \RuntimeException('Circular connection in rootline for page with uid ' . $this->pageUid . ' found. Check your mountpoint configuration.', 1343464103);
394 }
395 }
396 } else {
397 $rootline = array();
398 }
399 array_push($rootline, $page);
400 krsort($rootline);
401 static::$cache->set($this->cacheIdentifier, $rootline, $cacheTags);
402 static::$localCache[$this->cacheIdentifier] = $rootline;
403 }
404
405 /**
406 * Checks whether the current Page is a Mounted Page
407 * (according to the MP-URL-Parameter)
408 *
409 * @return bool
410 */
411 public function isMountedPage()
412 {
413 return in_array($this->pageUid, array_keys($this->parsedMountPointParameters));
414 }
415
416 /**
417 * Enhances with mount point information or replaces the node if needed
418 *
419 * @param array $mountedPageData page record array of mounted page
420 * @param array $mountPointPageData page record array of mount point page
421 * @throws \RuntimeException
422 * @return array
423 */
424 protected function processMountedPage(array $mountedPageData, array $mountPointPageData)
425 {
426 if ($mountPointPageData['mount_pid'] != $mountedPageData['uid']) {
427 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);
428 }
429 // Current page replaces the original mount-page
430 if ($mountPointPageData['mount_pid_ol']) {
431 $mountedPageData['_MOUNT_OL'] = true;
432 $mountedPageData['_MOUNT_PAGE'] = array(
433 'uid' => $mountPointPageData['uid'],
434 'pid' => $mountPointPageData['pid'],
435 'title' => $mountPointPageData['title']
436 );
437 } else {
438 // The mount-page is not replaced, the mount-page itself has to be used
439 $mountedPageData = $mountPointPageData;
440 }
441 $mountedPageData['_MOUNTED_FROM'] = $this->pageUid;
442 $mountedPageData['_MP_PARAM'] = $this->pageUid . '-' . $mountPointPageData['uid'];
443 return $mountedPageData;
444 }
445
446 /**
447 * Parse the MountPoint Parameters
448 * Splits the MP-Param via "," for several nested mountpoints
449 * and afterwords registers the mountpoint configurations
450 *
451 * @return void
452 */
453 protected function parseMountPointParameter()
454 {
455 $mountPoints = GeneralUtility::trimExplode(',', $this->mountPointParameter);
456 foreach ($mountPoints as $mP) {
457 list($mountedPageUid, $mountPageUid) = GeneralUtility::intExplode('-', $mP);
458 $this->parsedMountPointParameters[$mountedPageUid] = $mountPageUid;
459 }
460 }
461 }