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