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