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