[TASK] Merge submodule dbal into core
[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 array
113 */
114 static protected $pageRecordCache = array();
115
116 /**
117 * @var \TYPO3\CMS\Core\Database\DatabaseConnection
118 */
119 protected $databaseConnection;
120
121 /**
122 * @param int $uid
123 * @param string $mountPointParameter
124 * @param \TYPO3\CMS\Frontend\Page\PageRepository $context
125 * @throws \RuntimeException
126 */
127 public function __construct($uid, $mountPointParameter = '', \TYPO3\CMS\Frontend\Page\PageRepository $context = NULL) {
128 $this->pageUid = intval($uid);
129 $this->mountPointParameter = trim($mountPointParameter);
130 if ($context === NULL) {
131 if (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE']->sys_page)) {
132 $this->pageContext = $GLOBALS['TSFE']->sys_page;
133 } else {
134 $this->pageContext = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\Page\\PageRepository');
135 }
136 } else {
137 $this->pageContext = $context;
138 }
139 $this->initializeObject();
140 }
141
142 /**
143 * Initialize a state to work with
144 *
145 * @throws \RuntimeException
146 * @return void
147 */
148 protected function initializeObject() {
149 $this->languageUid = intval($this->pageContext->sys_language_uid);
150 $this->workspaceUid = intval($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 = $GLOBALS['typo3CacheManager']->getCache('cache_rootline');
161 }
162 self::$rootlineFields = array_merge(self::$rootlineFields, \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'], TRUE));
163 self::$rootlineFields = array_unique(self::$rootlineFields);
164 $this->databaseConnection = $GLOBALS['TYPO3_DB'];
165 }
166
167 /**
168 * Constructs the cache Identifier
169 *
170 * @param integer $otherUid
171 * @return string
172 */
173 public function getCacheIdentifier($otherUid = NULL) {
174 return implode('_', array(
175 $otherUid !== NULL ? intval($otherUid) : $this->pageUid,
176 $this->mountPointParameter,
177 $this->languageUid,
178 $this->workspaceUid,
179 $this->versionPreview ? 1 : 0
180 ));
181 }
182
183 /**
184 * Returns the actual rootline
185 *
186 * @return array
187 */
188 public function get() {
189 $cacheIdentifier = $this->getCacheIdentifier();
190 if (!isset(self::$localCache[$cacheIdentifier])) {
191 $entry = self::$cache->get($cacheIdentifier);
192 if (!$entry) {
193 $this->generateRootlineCache();
194 } else {
195 self::$localCache[$cacheIdentifier] = $entry;
196 }
197 }
198 return self::$localCache[$cacheIdentifier];
199 }
200
201 /**
202 * Queries the database for the page record and returns it.
203 *
204 * @param integer $uid Page id
205 * @throws \RuntimeException
206 * @return array
207 */
208 protected function getRecordArray($uid) {
209 if (!isset(self::$pageRecordCache[$this->getCacheIdentifier($uid)])) {
210 $row = $this->databaseConnection->exec_SELECTgetSingleRow(implode(',', self::$rootlineFields), 'pages', 'uid = ' . intval($uid) . ' AND pages.deleted = 0 AND pages.doktype <> ' . \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_RECYCLER);
211 if (empty($row)) {
212 throw new \RuntimeException('Could not fetch page data for uid ' . $uid . '.', 1343589451);
213 }
214 $this->pageContext->versionOL('pages', $row, FALSE, TRUE);
215 $this->pageContext->fixVersioningPid('pages', $row);
216 if (is_array($row)) {
217 if ($this->languageUid > 0) {
218 $row = $this->pageContext->getPageOverlay($row, $this->languageUid);
219 }
220 $row = $this->enrichWithRelationFields(isset($row['_PAGES_OVERLAY_UID']) ? $row['_PAGES_OVERLAY_UID'] : $uid, $row);
221 self::$pageRecordCache[$this->getCacheIdentifier($uid)] = $row;
222 }
223 }
224 if (!is_array(self::$pageRecordCache[$this->getCacheIdentifier($uid)])) {
225 throw new \RuntimeException('Broken rootline. Could not resolve page with uid ' . $uid . '.', 1343464101);
226 }
227 return self::$pageRecordCache[$this->getCacheIdentifier($uid)];
228 }
229
230 /**
231 * Resolve relations as defined in TCA and add them to the provided $pageRecord array.
232 *
233 * @param integer $uid Page id
234 * @param array $pageRecord Array with page data to add relation data to.
235 * @throws \RuntimeException
236 * @return array $pageRecord with additional relations
237 */
238 protected function enrichWithRelationFields($uid, array $pageRecord) {
239 foreach ($GLOBALS['TCA']['pages']['columns'] as $column => $configuration) {
240 if ($this->columnHasRelationToResolve($configuration)) {
241 $configuration = $configuration['config'];
242 if ($configuration['MM']) {
243 /** @var $loadDBGroup \TYPO3\CMS\Core\Database\RelationHandler */
244 $loadDBGroup = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\RelationHandler');
245 $loadDBGroup->start($pageRecord[$column], $configuration['foreign_table'], $configuration['MM'], $uid, 'pages', $configuration);
246 $relatedUids = $loadDBGroup->tableArray[$configuration['foreign_table']];
247 } else {
248 $table = $configuration['foreign_table'];
249 $field = $configuration['foreign_field'];
250 $whereClauseParts = array($field . ' = ' . intval($uid));
251 if (isset($configuration['foreign_match_fields']) && is_array($configuration['foreign_match_fields'])) {
252 foreach ($configuration['foreign_match_fields'] as $field => $value) {
253 $whereClauseParts[] = $field . ' = ' . $this->databaseConnection->fullQuoteStr($value, $table);
254 }
255 }
256 if (isset($configuration['foreign_table_field'])) {
257 if (intval($this->languageUid) > 0) {
258 $whereClauseParts[] = trim($configuration['foreign_table_field']) . ' = \'pages_language_overlay\'';
259 } else {
260 $whereClauseParts[] = trim($configuration['foreign_table_field']) . ' = \'pages\'';
261 }
262 }
263 $whereClause = implode(' AND ', $whereClauseParts);
264 $whereClause .= $this->pageContext->deleteClause($table);
265 $rows = $this->databaseConnection->exec_SELECTgetRows('uid', $table, $whereClause);
266 if (!is_array($rows)) {
267 throw new \RuntimeException('Could to resolve related records for page ' . $uid . ' and foreign_table ' . htmlspecialchars($configuration['foreign_table']), 1343589452);
268 }
269 $relatedUids = array();
270 foreach ($rows as $row) {
271 $relatedUids[] = $row['uid'];
272 }
273 }
274 $pageRecord[$column] = implode(',', $relatedUids);
275 }
276 }
277 return $pageRecord;
278 }
279
280 /**
281 * Checks whether the TCA Configuration array of a column
282 * describes a relation which is not stored as CSV in the record
283 *
284 * @param array $configuration TCA configuration to check
285 * @return boolean TRUE, if it describes a non-CSV relation
286 */
287 protected function columnHasRelationToResolve(array $configuration) {
288 $configuration = $configuration['config'];
289 if (!empty($configuration['MM']) && !empty($configuration['type']) && in_array($configuration['type'], array('select', 'inline', 'group'))) {
290 return TRUE;
291 }
292 if (!empty($configuration['foreign_field']) && !empty($configuration['type']) && in_array($configuration['type'], array('select', 'inline'))) {
293 return TRUE;
294 }
295 return FALSE;
296 }
297
298 /**
299 * Actual function to generate the rootline and cache it
300 *
301 * @throws \RuntimeException
302 * @return void
303 */
304 protected function generateRootlineCache() {
305 $page = $this->getRecordArray($this->pageUid);
306 // If the current page is a mounted (according to the MP parameter) handle the mount-point
307 if ($this->isMountedPage()) {
308 $mountPoint = $this->getRecordArray($this->parsedMountPointParameters[$this->pageUid]);
309 $page = $this->processMountedPage($page, $mountPoint);
310 $parentUid = $mountPoint['pid'];
311 // Anyhow after reaching the mount-point, we have to go up that rootline
312 unset($this->parsedMountPointParameters[$this->pageUid]);
313 } else {
314 $parentUid = $page['pid'];
315 }
316 $cacheTags = array('pageId_' . $page['uid']);
317 if ($parentUid > 0) {
318 // Get rootline of (and including) parent page
319 $mountPointParameter = count($this->parsedMountPointParameters) > 0 ? $this->mountPointParameter : '';
320 /** @var $rootline \TYPO3\CMS\Core\Utility\RootlineUtility */
321 $rootline = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Utility\\RootlineUtility', $parentUid, $mountPointParameter, $this->pageContext);
322 $rootline = $rootline->get();
323 // retrieve cache tags of parent rootline
324 foreach ($rootline as $entry) {
325 $cacheTags[] = 'pageId_' . $entry['uid'];
326 if ($entry['uid'] == $this->pageUid) {
327 throw new \RuntimeException('Circular connection in rootline for page with uid ' . $this->pageUid . ' found. Check your mountpoint configuration.', 1343464103);
328 }
329 }
330 } else {
331 $rootline = array();
332 }
333 array_push($rootline, $page);
334 krsort($rootline);
335 self::$cache->set($this->getCacheIdentifier(), $rootline, $cacheTags);
336 self::$localCache[$this->getCacheIdentifier()] = $rootline;
337 }
338
339 /**
340 * Checks whether the current Page is a Mounted Page
341 * (according to the MP-URL-Parameter)
342 *
343 * @return boolean
344 */
345 public function isMountedPage() {
346 return in_array($this->pageUid, array_keys($this->parsedMountPointParameters));
347 }
348
349 /**
350 * Enhances with mount point information or replaces the node if needed
351 *
352 * @param array $mountedPageData page record array of mounted page
353 * @param array $mountPointPageData page record array of mount point page
354 * @throws \RuntimeException
355 * @return array
356 */
357 protected function processMountedPage(array $mountedPageData, array $mountPointPageData) {
358 if ($mountPointPageData['mount_pid'] != $mountedPageData['uid']) {
359 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);
360 }
361 // Current page replaces the original mount-page
362 if ($mountPointPageData['mount_pid_ol']) {
363 $mountedPageData['_MOUNT_OL'] = TRUE;
364 $mountedPageData['_MOUNT_PAGE'] = array(
365 'uid' => $mountPointPageData['uid'],
366 'pid' => $mountPointPageData['pid'],
367 'title' => $mountPointPageData['title']
368 );
369 } else {
370 // The mount-page is not replaced, the mount-page itself has to be used
371 $mountedPageData = $mountPointPageData;
372 }
373 $mountedPageData['_MOUNTED_FROM'] = $this->pageUid;
374 $mountedPageData['_MP_PARAM'] = $this->pageUid . '-' . $mountPointPageData['uid'];
375 return $mountedPageData;
376 }
377
378 /**
379 * Parse the MountPoint Parameters
380 * Splits the MP-Param via "," for several nested mountpoints
381 * and afterwords registers the mountpoint configurations
382 *
383 * @return void
384 */
385 protected function parseMountPointParameter() {
386 $mountPoints = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $this->mountPointParameter);
387 foreach ($mountPoints as $mP) {
388 list($mountedPageUid, $mountPageUid) = \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode('-', $mP);
389 $this->parsedMountPointParameters[$mountedPageUid] = $mountPageUid;
390 }
391 }
392
393 }
394
395
396 ?>