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