[!!!][TASK] Make TimeTracker a singleton
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / ContentObject / Menu / AbstractMenuContentObject.php
1 <?php
2 namespace TYPO3\CMS\Frontend\ContentObject\Menu;
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\Core\Cache\CacheManager;
18 use TYPO3\CMS\Core\Database\DatabaseConnection;
19 use TYPO3\CMS\Core\Database\RelationHandler;
20 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
21 use TYPO3\CMS\Core\TypoScript\TemplateService;
22 use TYPO3\CMS\Core\Utility\ArrayUtility;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\MathUtility;
25 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
26 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
27 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
28 use TYPO3\CMS\Frontend\Page\PageRepository;
29
30 /**
31 * Generating navigation/menus from TypoScript
32 *
33 * The HMENU content object uses this (or more precisely one of the extension classes).
34 * Among others the class generates an array of menu items. Thereafter functions from the subclasses are called.
35 * The class is always used through extension classes (like GraphicalMenuContentObject or TextMenuContentObject).
36 */
37 abstract class AbstractMenuContentObject
38 {
39 /**
40 * tells you which menu number this is. This is important when getting data from the setup
41 *
42 * @var int
43 */
44 public $menuNumber = 1;
45
46 /**
47 * 0 = rootFolder
48 *
49 * @var int
50 */
51 public $entryLevel = 0;
52
53 /**
54 * The doktype-number that defines a spacer
55 *
56 * @var string
57 */
58 public $spacerIDList = '199';
59
60 /**
61 * Doktypes that define which should not be included in a menu
62 *
63 * @var string
64 */
65 public $doktypeExcludeList = '6';
66
67 /**
68 * @var int[]
69 */
70 public $alwaysActivePIDlist = array();
71
72 /**
73 * @var string
74 */
75 public $imgNamePrefix = 'img';
76
77 /**
78 * @var int
79 */
80 public $imgNameNotRandom = 0;
81
82 /**
83 * @var bool
84 */
85 public $debug = false;
86
87 /**
88 * Loaded with the parent cObj-object when a new HMENU is made
89 *
90 * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
91 */
92 public $parent_cObj = null;
93
94 /**
95 * @var string
96 */
97 public $GMENU_fixKey = 'gmenu';
98
99 /**
100 * accumulation of mount point data
101 *
102 * @var string[]
103 */
104 public $MP_array = array();
105
106 /**
107 * HMENU configuration
108 *
109 * @var array
110 */
111 public $conf = array();
112
113 /**
114 * xMENU configuration (TMENU, GMENU etc)
115 *
116 * @var array
117 */
118 public $mconf = array();
119
120 /**
121 * @var \TYPO3\CMS\Core\TypoScript\TemplateService
122 */
123 public $tmpl = null;
124
125 /**
126 * @var \TYPO3\CMS\Frontend\Page\PageRepository
127 */
128 public $sys_page = null;
129
130 /**
131 * The base page-id of the menu.
132 *
133 * @var int
134 */
135 public $id;
136
137 /**
138 * Holds the page uid of the NEXT page in the root line from the page pointed to by entryLevel;
139 * Used to expand the menu automatically if in a certain root line.
140 *
141 * @var string
142 */
143 public $nextActive;
144
145 /**
146 * The array of menuItems which is built
147 *
148 * @var array[]
149 */
150 public $menuArr;
151
152 /**
153 * @var string
154 */
155 public $hash;
156
157 /**
158 * @var array
159 */
160 public $result = array();
161
162 /**
163 * Is filled with an array of page uid numbers + RL parameters which are in the current
164 * root line (used to evaluate whether a menu item is in active state)
165 *
166 * @var string
167 */
168 public $rL_uidRegister = '';
169
170 /**
171 * @var string
172 */
173 public $INPfixMD5;
174
175 /**
176 * @var mixed[]
177 */
178 public $I;
179
180 /**
181 * @var string
182 */
183 public $WMresult;
184
185 /**
186 * @var string
187 */
188 public $WMfreezePrefix;
189
190 /**
191 * @var int
192 */
193 public $WMmenuItems;
194
195 /**
196 * @var array[]
197 */
198 public $WMsubmenuObjSuffixes;
199
200 /**
201 * @var string
202 */
203 public $WMextraScript;
204
205 /**
206 * @var ContentObjectRenderer
207 */
208 public $WMcObj = null;
209
210 /**
211 * Can be set to contain menu item arrays for sub-levels.
212 *
213 * @var string
214 */
215 public $alternativeMenuTempArray = '';
216
217 /**
218 * Will be 'id' in XHTML-mode
219 *
220 * @var string
221 */
222 public $nameAttribute = 'name';
223
224 /**
225 * TRUE to use cHash in generated link (normally only for the language
226 * selector and if parameters exist in the URL).
227 *
228 * @var bool
229 */
230 protected $useCacheHash = false;
231
232 /**
233 * The initialization of the object. This just sets some internal variables.
234 *
235 * @param TemplateService $tmpl The $this->getTypoScriptFrontendController()->tmpl object
236 * @param PageRepository $sys_page The $this->getTypoScriptFrontendController()->sys_page object
237 * @param int|string $id A starting point page id. This should probably be blank since the 'entryLevel' value will be used then.
238 * @param array $conf The TypoScript configuration for the HMENU cObject
239 * @param int $menuNumber Menu number; 1,2,3. Should probably be 1
240 * @param string $objSuffix Submenu Object suffix. This offers submenus a way to use alternative configuration for specific positions in the menu; By default "1 = TMENU" would use "1." for the TMENU configuration, but if this string is set to eg. "a" then "1a." would be used for configuration instead (while "1 = " is still used for the overall object definition of "TMENU")
241 * @return bool Returns TRUE on success
242 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::HMENU()
243 */
244 public function start($tmpl, $sys_page, $id, $conf, $menuNumber, $objSuffix = '')
245 {
246 $tsfe = $this->getTypoScriptFrontendController();
247 // Init:
248 $this->conf = $conf;
249 $this->menuNumber = $menuNumber;
250 $this->mconf = $conf[$this->menuNumber . $objSuffix . '.'];
251 $this->debug = $tsfe->debug;
252 $this->WMcObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
253 // In XHTML and HTML5 there is no "name" attribute anymore
254 switch ($tsfe->xhtmlDoctype) {
255 case 'xhtml_strict':
256 // intended fall-through
257 case 'xhtml_11':
258 // intended fall-through
259 case 'html5':
260 // intended fall-through
261 case '':
262 // empty means that it's HTML5 by default
263 $this->nameAttribute = 'id';
264 break;
265 default:
266 $this->nameAttribute = 'name';
267 }
268 // Sets the internal vars. $tmpl MUST be the template-object. $sys_page MUST be the sys_page object
269 if ($this->conf[$this->menuNumber . $objSuffix] && is_object($tmpl) && is_object($sys_page)) {
270 $this->tmpl = $tmpl;
271 $this->sys_page = $sys_page;
272 // alwaysActivePIDlist initialized:
273 if (trim($this->conf['alwaysActivePIDlist']) || isset($this->conf['alwaysActivePIDlist.'])) {
274 if (isset($this->conf['alwaysActivePIDlist.'])) {
275 $this->conf['alwaysActivePIDlist'] = $this->parent_cObj->stdWrap(
276 $this->conf['alwaysActivePIDlist'],
277 $this->conf['alwaysActivePIDlist.']
278 );
279 }
280 $this->alwaysActivePIDlist = GeneralUtility::intExplode(',', $this->conf['alwaysActivePIDlist']);
281 }
282 // 'not in menu' doktypes
283 if ($this->conf['excludeDoktypes']) {
284 $this->doktypeExcludeList = $this->getDatabaseConnection()->cleanIntList($this->conf['excludeDoktypes']);
285 }
286 // EntryLevel
287 $this->entryLevel = $this->parent_cObj->getKey(
288 isset($conf['entryLevel.']) ? $this->parent_cObj->stdWrap(
289 $conf['entryLevel'],
290 $conf['entryLevel.']
291 ) : $conf['entryLevel'],
292 $this->tmpl->rootLine
293 );
294 // Set parent page: If $id not stated with start() then the base-id will be found from rootLine[$this->entryLevel]
295 // Called as the next level in a menu. It is assumed that $this->MP_array is set from parent menu.
296 if ($id) {
297 $this->id = (int)$id;
298 } else {
299 // This is a BRAND NEW menu, first level. So we take ID from rootline and also find MP_array (mount points)
300 $this->id = (int)$this->tmpl->rootLine[$this->entryLevel]['uid'];
301 // Traverse rootline to build MP_array of pages BEFORE the entryLevel
302 // (MP var for ->id is picked up in the next part of the code...)
303 foreach ($this->tmpl->rootLine as $entryLevel => $levelRec) {
304 // For overlaid mount points, set the variable right now:
305 if ($levelRec['_MP_PARAM'] && $levelRec['_MOUNT_OL']) {
306 $this->MP_array[] = $levelRec['_MP_PARAM'];
307 }
308 // Break when entry level is reached:
309 if ($entryLevel >= $this->entryLevel) {
310 break;
311 }
312 // For normal mount points, set the variable for next level.
313 if ($levelRec['_MP_PARAM'] && !$levelRec['_MOUNT_OL']) {
314 $this->MP_array[] = $levelRec['_MP_PARAM'];
315 }
316 }
317 }
318 // Return FALSE if no page ID was set (thus no menu of subpages can be made).
319 if ($this->id <= 0) {
320 return false;
321 }
322 // Check if page is a mount point, and if so set id and MP_array
323 // (basically this is ONLY for non-overlay mode, but in overlay mode an ID with a mount point should never reach this point anyways, so no harm done...)
324 $mount_info = $this->sys_page->getMountPointInfo($this->id);
325 if (is_array($mount_info)) {
326 $this->MP_array[] = $mount_info['MPvar'];
327 $this->id = $mount_info['mount_pid'];
328 }
329 // Gather list of page uids in root line (for "isActive" evaluation). Also adds the MP params in the path so Mount Points are respected.
330 // (List is specific for this rootline, so it may be supplied from parent menus for speed...)
331 if (!is_array($this->rL_uidRegister)) {
332 $rl_MParray = array();
333 foreach ($this->tmpl->rootLine as $v_rl) {
334 // For overlaid mount points, set the variable right now:
335 if ($v_rl['_MP_PARAM'] && $v_rl['_MOUNT_OL']) {
336 $rl_MParray[] = $v_rl['_MP_PARAM'];
337 }
338 // Add to register:
339 $this->rL_uidRegister[] = 'ITEM:' . $v_rl['uid'] .
340 (!empty($rl_MParray)
341 ? ':' . implode(',', $rl_MParray)
342 : ''
343 );
344 // For normal mount points, set the variable for next level.
345 if ($v_rl['_MP_PARAM'] && !$v_rl['_MOUNT_OL']) {
346 $rl_MParray[] = $v_rl['_MP_PARAM'];
347 }
348 }
349 }
350 // Set $directoryLevel so the following evalution of the nextActive will not return
351 // an invalid value if .special=directory was set
352 $directoryLevel = 0;
353 if ($this->conf['special'] === 'directory') {
354 $value = isset($this->conf['special.']['value.']) ? $this->parent_cObj->stdWrap(
355 $this->conf['special.']['value'],
356 $this->conf['special.']['value.']
357 ) : $this->conf['special.']['value'];
358 if ($value === '') {
359 $value = $tsfe->page['uid'];
360 }
361 $directoryLevel = (int)$tsfe->tmpl->getRootlineLevel($value);
362 }
363 // Setting "nextActive": This is the page uid + MPvar of the NEXT page in rootline. Used to expand the menu if we are in the right branch of the tree
364 // Notice: The automatic expansion of a menu is designed to work only when no "special" modes (except "directory") are used.
365 $startLevel = $directoryLevel ?: $this->entryLevel;
366 $currentLevel = $startLevel + $this->menuNumber;
367 if (is_array($this->tmpl->rootLine[$currentLevel])) {
368 $nextMParray = $this->MP_array;
369 if (empty($nextMParray) && !$this->tmpl->rootLine[$currentLevel]['_MOUNT_OL'] && $currentLevel > 0) {
370 // Make sure to slide-down any mount point information (_MP_PARAM) to children records in the rootline
371 // otherwise automatic expansion will not work
372 $parentRecord = $this->tmpl->rootLine[$currentLevel - 1];
373 if (isset($parentRecord['_MP_PARAM'])) {
374 $nextMParray[] = $parentRecord['_MP_PARAM'];
375 }
376 }
377 // In overlay mode, add next level MPvars as well:
378 if ($this->tmpl->rootLine[$currentLevel]['_MOUNT_OL']) {
379 $nextMParray[] = $this->tmpl->rootLine[$currentLevel]['_MP_PARAM'];
380 }
381 $this->nextActive = $this->tmpl->rootLine[$currentLevel]['uid'] .
382 (!empty($nextMParray)
383 ? ':' . implode(',', $nextMParray)
384 : ''
385 );
386 } else {
387 $this->nextActive = '';
388 }
389 // imgNamePrefix
390 if ($this->mconf['imgNamePrefix']) {
391 $this->imgNamePrefix = $this->mconf['imgNamePrefix'];
392 }
393 $this->imgNameNotRandom = $this->mconf['imgNameNotRandom'];
394 $retVal = true;
395 } else {
396 $this->getTimeTracker()->setTSlogMessage('ERROR in menu', 3);
397 $retVal = false;
398 }
399 return $retVal;
400 }
401
402 /**
403 * Creates the menu in the internal variables, ready for output.
404 * Basically this will read the page records needed and fill in the internal $this->menuArr
405 * Based on a hash of this array and some other variables the $this->result variable will be
406 * loaded either from cache OR by calling the generate() method of the class to create the menu for real.
407 *
408 * @return void
409 */
410 public function makeMenu()
411 {
412 if (!$this->id) {
413 return;
414 }
415
416 $this->useCacheHash = false;
417
418 // Initializing showAccessRestrictedPages
419 $SAVED_where_groupAccess = '';
420 if ($this->mconf['showAccessRestrictedPages']) {
421 // SAVING where_groupAccess
422 $SAVED_where_groupAccess = $this->sys_page->where_groupAccess;
423 // Temporarily removing fe_group checking!
424 $this->sys_page->where_groupAccess = '';
425 }
426
427 $menuItems = $this->prepareMenuItems();
428
429 $c = 0;
430 $c_b = 0;
431 $minItems = (int)($this->mconf['minItems'] ?: $this->conf['minItems']);
432 $maxItems = (int)($this->mconf['maxItems'] ?: $this->conf['maxItems']);
433 $begin = $this->parent_cObj->calc($this->mconf['begin'] ? $this->mconf['begin'] : $this->conf['begin']);
434 $minItemsConf = isset($this->mconf['minItems.']) ? $this->mconf['minItems.'] : (isset($this->conf['minItems.']) ? $this->conf['minItems.'] : null);
435 $minItems = is_array($minItemsConf) ? $this->parent_cObj->stdWrap($minItems, $minItemsConf) : $minItems;
436 $maxItemsConf = isset($this->mconf['maxItems.']) ? $this->mconf['maxItems.'] : (isset($this->conf['maxItems.']) ? $this->conf['maxItems.'] : null);
437 $maxItems = is_array($maxItemsConf) ? $this->parent_cObj->stdWrap($maxItems, $maxItemsConf) : $maxItems;
438 $beginConf = isset($this->mconf['begin.']) ? $this->mconf['begin.'] : (isset($this->conf['begin.']) ? $this->conf['begin.'] : null);
439 $begin = is_array($beginConf) ? $this->parent_cObj->stdWrap($begin, $beginConf) : $begin;
440 $banUidArray = $this->getBannedUids();
441 // Fill in the menuArr with elements that should go into the menu:
442 $this->menuArr = array();
443 foreach ($menuItems as $data) {
444 $spacer = GeneralUtility::inList($this->spacerIDList, $data['doktype']) || $data['ITEM_STATE'] === 'SPC';
445 // if item is a spacer, $spacer is set
446 if ($this->filterMenuPages($data, $banUidArray, $spacer)) {
447 $c_b++;
448 // If the beginning item has been reached.
449 if ($begin <= $c_b) {
450 $this->menuArr[$c] = $data;
451 $this->menuArr[$c]['isSpacer'] = $spacer;
452 $c++;
453 if ($maxItems && $c >= $maxItems) {
454 break;
455 }
456 }
457 }
458 }
459 // Fill in fake items, if min-items is set.
460 if ($minItems) {
461 while ($c < $minItems) {
462 $this->menuArr[$c] = array(
463 'title' => '...',
464 'uid' => $this->getTypoScriptFrontendController()->id
465 );
466 $c++;
467 }
468 }
469 // Passing the menuArr through a user defined function:
470 if ($this->mconf['itemArrayProcFunc']) {
471 $this->menuArr = $this->userProcess('itemArrayProcFunc', $this->menuArr);
472 }
473 // Setting number of menu items
474 $this->getTypoScriptFrontendController()->register['count_menuItems'] = count($this->menuArr);
475 $this->hash = md5(
476 serialize($this->menuArr) .
477 serialize($this->mconf) .
478 serialize($this->tmpl->rootLine) .
479 serialize($this->MP_array)
480 );
481 // Get the cache timeout:
482 if ($this->conf['cache_period']) {
483 $cacheTimeout = $this->conf['cache_period'];
484 } else {
485 $cacheTimeout = $this->getTypoScriptFrontendController()->get_cache_timeout();
486 }
487 $cache = $this->getCache();
488 $cachedData = $cache->get($this->hash);
489 if (!is_array($cachedData)) {
490 $this->generate();
491 $cache->set($this->hash, $this->result, array('ident_MENUDATA'), (int)$cacheTimeout);
492 } else {
493 $this->result = $cachedData;
494 }
495 // End showAccessRestrictedPages
496 if ($this->mconf['showAccessRestrictedPages']) {
497 // RESTORING where_groupAccess
498 $this->sys_page->where_groupAccess = $SAVED_where_groupAccess;
499 }
500 }
501
502 /**
503 * Generates the the menu data.
504 *
505 * Subclasses should overwrite this method.
506 *
507 * @return void
508 */
509 public function generate()
510 {
511 }
512
513 /**
514 * @return string The HTML for the menu
515 */
516 public function writeMenu()
517 {
518 return '';
519 }
520
521 /**
522 * Gets an array of page rows and removes all, which are not accessible
523 *
524 * @param array $pages
525 * @return array
526 */
527 protected function removeInaccessiblePages(array $pages)
528 {
529 $banned = $this->getBannedUids();
530 $filteredPages = array();
531 foreach ($pages as $aPage) {
532 if ($this->filterMenuPages($aPage, $banned, $aPage['doktype'] === PageRepository::DOKTYPE_SPACER)) {
533 $filteredPages[$aPage['uid']] = $aPage;
534 }
535 }
536 return $filteredPages;
537 }
538
539 /**
540 * Main function for retrieving menu items based on the menu type (special or sectionIndex or "normal")
541 *
542 * @return array
543 */
544 protected function prepareMenuItems()
545 {
546 $menuItems = array();
547 $alternativeSortingField = trim($this->mconf['alternativeSortingField']) ?: 'sorting';
548
549 // Additional where clause, usually starts with AND (as usual with all additionalWhere functionality in TS)
550 $additionalWhere = isset($this->mconf['additionalWhere']) ? $this->mconf['additionalWhere'] : '';
551 if (isset($this->mconf['additionalWhere.'])) {
552 $additionalWhere = $this->parent_cObj->stdWrap($additionalWhere, $this->mconf['additionalWhere.']);
553 }
554
555 // ... only for the FIRST level of a HMENU
556 if ($this->menuNumber == 1 && $this->conf['special']) {
557 $value = isset($this->conf['special.']['value.'])
558 ? $this->parent_cObj->stdWrap($this->conf['special.']['value'], $this->conf['special.']['value.'])
559 : $this->conf['special.']['value'];
560 switch ($this->conf['special']) {
561 case 'userfunction':
562 $menuItems = $this->prepareMenuItemsForUserSpecificMenu($value, $alternativeSortingField);
563 break;
564 case 'language':
565 $menuItems = $this->prepareMenuItemsForLanguageMenu($value);
566 break;
567 case 'directory':
568 $menuItems = $this->prepareMenuItemsForDirectoryMenu($value, $alternativeSortingField);
569 break;
570 case 'list':
571 $menuItems = $this->prepareMenuItemsForListMenu($value);
572 break;
573 case 'updated':
574 $menuItems = $this->prepareMenuItemsForUpdatedMenu(
575 $value,
576 $this->mconf['alternativeSortingField'] ?: false
577 );
578 break;
579 case 'keywords':
580 $menuItems = $this->prepareMenuItemsForKeywordsMenu(
581 $value,
582 $this->mconf['alternativeSortingField'] ?: false
583 );
584 break;
585 case 'categories':
586 /** @var CategoryMenuUtility $categoryMenuUtility */
587 $categoryMenuUtility = GeneralUtility::makeInstance(CategoryMenuUtility::class);
588 $menuItems = $categoryMenuUtility->collectPages($value, $this->conf['special.'], $this);
589 break;
590 case 'rootline':
591 $menuItems = $this->prepareMenuItemsForRootlineMenu();
592 break;
593 case 'browse':
594 $menuItems = $this->prepareMenuitemsForBrowseMenu($value, $alternativeSortingField, $additionalWhere);
595 break;
596 }
597 if ($this->mconf['sectionIndex']) {
598 $sectionIndexes = array();
599 foreach ($menuItems as $page) {
600 $sectionIndexes = $sectionIndexes + $this->sectionIndex($alternativeSortingField, $page['uid']);
601 }
602 $menuItems = $sectionIndexes;
603 }
604 } elseif (is_array($this->alternativeMenuTempArray)) {
605 // Setting $menuItems array if not level 1.
606 $menuItems = $this->alternativeMenuTempArray;
607 } elseif ($this->mconf['sectionIndex']) {
608 $menuItems = $this->sectionIndex($alternativeSortingField);
609 } else {
610 // Default: Gets a hierarchical menu based on subpages of $this->id
611 $menuItems = $this->sys_page->getMenu($this->id, '*', $alternativeSortingField, $additionalWhere);
612 }
613 return $menuItems;
614 }
615
616 /**
617 * Fetches all menuitems if special = userfunction is set
618 *
619 * @param string $specialValue The value from special.value
620 * @param string $sortingField The sorting field
621 * @return array
622 */
623 protected function prepareMenuItemsForUserSpecificMenu($specialValue, $sortingField)
624 {
625 $menuItems = $this->parent_cObj->callUserFunction(
626 $this->conf['special.']['userFunc'],
627 array_merge($this->conf['special.'], array('value' => $specialValue, '_altSortField' => $sortingField)),
628 ''
629 );
630 if (!is_array($menuItems)) {
631 $menuItems = array();
632 }
633 return $menuItems;
634 }
635
636 /**
637 * Fetches all menuitems if special = language is set
638 *
639 * @param string $specialValue The value from special.value
640 * @return array
641 */
642 protected function prepareMenuItemsForLanguageMenu($specialValue)
643 {
644 $menuItems = array();
645 // Getting current page record NOT overlaid by any translation:
646 $tsfe = $this->getTypoScriptFrontendController();
647 $currentPageWithNoOverlay = $this->sys_page->getRawRecord('pages', $tsfe->page['uid']);
648 // Traverse languages set up:
649 $languageItems = GeneralUtility::intExplode(',', $specialValue);
650 foreach ($languageItems as $sUid) {
651 // Find overlay record:
652 if ($sUid) {
653 $lRecs = $this->sys_page->getPageOverlay($tsfe->page['uid'], $sUid);
654 } else {
655 $lRecs = array();
656 }
657 // Checking if the "disabled" state should be set.
658 if (GeneralUtility::hideIfNotTranslated($tsfe->page['l18n_cfg']) && $sUid &&
659 empty($lRecs) || GeneralUtility::hideIfDefaultLanguage($tsfe->page['l18n_cfg']) &&
660 (!$sUid || empty($lRecs)) ||
661 !$this->conf['special.']['normalWhenNoLanguage'] && $sUid && empty($lRecs)
662 ) {
663 $iState = $tsfe->sys_language_uid == $sUid ? 'USERDEF2' : 'USERDEF1';
664 } else {
665 $iState = $tsfe->sys_language_uid == $sUid ? 'ACT' : 'NO';
666 }
667 if ($this->conf['addQueryString']) {
668 $getVars = $this->parent_cObj->getQueryArguments(
669 $this->conf['addQueryString.'],
670 array('L' => $sUid),
671 true
672 );
673 $this->analyzeCacheHashRequirements($getVars);
674 } else {
675 $getVars = '&L=' . $sUid;
676 }
677 // Adding menu item:
678 $menuItems[] = array_merge(
679 array_merge($currentPageWithNoOverlay, $lRecs),
680 array(
681 'ITEM_STATE' => $iState,
682 '_ADD_GETVARS' => $getVars,
683 '_SAFE' => true
684 )
685 );
686 }
687 return $menuItems;
688 }
689
690 /**
691 * Fetches all menuitems if special = directory is set
692 *
693 * @param string $specialValue The value from special.value
694 * @param string $sortingField The sorting field
695 * @return array
696 */
697 protected function prepareMenuItemsForDirectoryMenu($specialValue, $sortingField)
698 {
699 $tsfe = $this->getTypoScriptFrontendController();
700 $databaseConnection = $this->getDatabaseConnection();
701 $menuItems = array();
702 if ($specialValue == '') {
703 $specialValue = $tsfe->page['uid'];
704 }
705 $items = GeneralUtility::intExplode(',', $specialValue);
706 foreach ($items as $id) {
707 $MP = $this->tmpl->getFromMPmap($id);
708 // Checking if a page is a mount page and if so, change the ID and set the MP var properly.
709 $mount_info = $this->sys_page->getMountPointInfo($id);
710 if (is_array($mount_info)) {
711 if ($mount_info['overlay']) {
712 // Overlays should already have their full MPvars calculated:
713 $MP = $this->tmpl->getFromMPmap($mount_info['mount_pid']);
714 $MP = $MP ? $MP : $mount_info['MPvar'];
715 } else {
716 $MP = ($MP ? $MP . ',' : '') . $mount_info['MPvar'];
717 }
718 $id = $mount_info['mount_pid'];
719 }
720 // Get sub-pages:
721 $res = $this->parent_cObj->exec_getQuery('pages', array('pidInList' => $id, 'orderBy' => $sortingField));
722 while ($row = $databaseConnection->sql_fetch_assoc($res)) {
723 $tsfe->sys_page->versionOL('pages', $row, true);
724 if (!empty($row)) {
725 // Keep mount point?
726 $mount_info = $this->sys_page->getMountPointInfo($row['uid'], $row);
727 // There is a valid mount point.
728 if (is_array($mount_info) && $mount_info['overlay']) {
729 // Using "getPage" is OK since we need the check for enableFields
730 // AND for type 2 of mount pids we DO require a doktype < 200!
731 $mp_row = $this->sys_page->getPage($mount_info['mount_pid']);
732 if (!empty($mp_row)) {
733 $row = $mp_row;
734 $row['_MP_PARAM'] = $mount_info['MPvar'];
735 } else {
736 // If the mount point could not be fetched with respect
737 // to enableFields, unset the row so it does not become a part of the menu!
738 unset($row);
739 }
740 }
741 // Add external MP params, then the row:
742 if (!empty($row)) {
743 if ($MP) {
744 $row['_MP_PARAM'] = $MP . ($row['_MP_PARAM'] ? ',' . $row['_MP_PARAM'] : '');
745 }
746 $menuItems[$row['uid']] = $this->sys_page->getPageOverlay($row);
747 }
748 }
749 }
750 $databaseConnection->sql_free_result($res);
751 }
752 return $menuItems;
753 }
754
755 /**
756 * Fetches all menuitems if special = list is set
757 *
758 * @param string $specialValue The value from special.value
759 * @return array
760 */
761 protected function prepareMenuItemsForListMenu($specialValue)
762 {
763 $menuItems = array();
764 if ($specialValue == '') {
765 $specialValue = $this->id;
766 }
767 $skippedEnableFields = array();
768 if (!empty($this->mconf['showAccessRestrictedPages'])) {
769 $skippedEnableFields = array('fe_group' => 1);
770 }
771 /** @var RelationHandler $loadDB*/
772 $loadDB = GeneralUtility::makeInstance(RelationHandler::class);
773 $loadDB->setFetchAllFields(true);
774 $loadDB->start($specialValue, 'pages');
775 $loadDB->additionalWhere['pages'] = $this->parent_cObj->enableFields('pages', false, $skippedEnableFields);
776 $loadDB->getFromDB();
777 foreach ($loadDB->itemArray as $val) {
778 $MP = $this->tmpl->getFromMPmap($val['id']);
779 // Keep mount point?
780 $mount_info = $this->sys_page->getMountPointInfo($val['id']);
781 // There is a valid mount point.
782 if (is_array($mount_info) && $mount_info['overlay']) {
783 // Using "getPage" is OK since we need the check for enableFields
784 // AND for type 2 of mount pids we DO require a doktype < 200!
785 $mp_row = $this->sys_page->getPage($mount_info['mount_pid']);
786 if (!empty($mp_row)) {
787 $row = $mp_row;
788 $row['_MP_PARAM'] = $mount_info['MPvar'];
789 // Overlays should already have their full MPvars calculated
790 if ($mount_info['overlay']) {
791 $MP = $this->tmpl->getFromMPmap($mount_info['mount_pid']);
792 if ($MP) {
793 unset($row['_MP_PARAM']);
794 }
795 }
796 } else {
797 // If the mount point could not be fetched with respect to
798 // enableFields, unset the row so it does not become a part of the menu!
799 unset($row);
800 }
801 } else {
802 $row = $loadDB->results['pages'][$val['id']];
803 }
804 // Add versioning overlay for current page (to respect workspaces)
805 if (isset($row) && is_array($row)) {
806 $this->sys_page->versionOL('pages', $row, true);
807 }
808 // Add external MP params, then the row:
809 if (isset($row) && is_array($row)) {
810 if ($MP) {
811 $row['_MP_PARAM'] = $MP . ($row['_MP_PARAM'] ? ',' . $row['_MP_PARAM'] : '');
812 }
813 $menuItems[] = $this->sys_page->getPageOverlay($row);
814 }
815 }
816 return $menuItems;
817 }
818
819 /**
820 * Fetches all menuitems if special = updated is set
821 *
822 * @param string $specialValue The value from special.value
823 * @param string $sortingField The sorting field
824 * @return array
825 */
826 protected function prepareMenuItemsForUpdatedMenu($specialValue, $sortingField)
827 {
828 $tsfe = $this->getTypoScriptFrontendController();
829 $menuItems = array();
830 if ($specialValue == '') {
831 $specialValue = $tsfe->page['uid'];
832 }
833 $items = GeneralUtility::intExplode(',', $specialValue);
834 if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'])) {
835 $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 1, 20);
836 } else {
837 $depth = 20;
838 }
839 // Max number of items
840 $limit = MathUtility::forceIntegerInRange($this->conf['special.']['limit'], 0, 100);
841 $maxAge = (int)$this->parent_cObj->calc($this->conf['special.']['maxAge']);
842 if (!$limit) {
843 $limit = 10;
844 }
845 // *'auto', 'manual', 'tstamp'
846 $mode = $this->conf['special.']['mode'];
847 // Get id's
848 $id_list_arr = array();
849 foreach ($items as $id) {
850 $bA = MathUtility::forceIntegerInRange($this->conf['special.']['beginAtLevel'], 0, 100);
851 $id_list_arr[] = $this->parent_cObj->getTreeList(-1 * $id, $depth - 1 + $bA, $bA - 1);
852 }
853 $id_list = implode(',', $id_list_arr);
854 // Get sortField (mode)
855 switch ($mode) {
856 case 'starttime':
857 $sortField = 'starttime';
858 break;
859 case 'lastUpdated':
860 case 'manual':
861 $sortField = 'lastUpdated';
862 break;
863 case 'tstamp':
864 $sortField = 'tstamp';
865 break;
866 case 'crdate':
867 $sortField = 'crdate';
868 break;
869 default:
870 $sortField = 'SYS_LASTCHANGED';
871 }
872 $extraWhere = ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
873 if ($this->conf['special.']['excludeNoSearchPages']) {
874 $extraWhere .= ' AND pages.no_search=0';
875 }
876 if ($maxAge > 0) {
877 $extraWhere .= ' AND ' . $sortField . '>' . ($GLOBALS['SIM_ACCESS_TIME'] - $maxAge);
878 }
879 $res = $this->parent_cObj->exec_getQuery('pages', array(
880 'pidInList' => '0',
881 'uidInList' => $id_list,
882 'where' => $sortField . '>=0' . $extraWhere,
883 'orderBy' => $sortingField ?: $sortField . ' DESC',
884 'max' => $limit
885 ));
886 while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
887 $tsfe->sys_page->versionOL('pages', $row, true);
888 if (is_array($row)) {
889 $menuItems[$row['uid']] = $this->sys_page->getPageOverlay($row);
890 }
891 }
892 $this->getDatabaseConnection()->sql_free_result($res);
893 return $menuItems;
894 }
895
896 /**
897 * Fetches all menuitems if special = keywords is set
898 *
899 * @param string $specialValue The value from special.value
900 * @param string $sortingField The sorting field
901 * @return array
902 */
903 protected function prepareMenuItemsForKeywordsMenu($specialValue, $sortingField)
904 {
905 $tsfe = $this->getTypoScriptFrontendController();
906 $menuItems = array();
907 list($specialValue) = GeneralUtility::intExplode(',', $specialValue);
908 if (!$specialValue) {
909 $specialValue = $tsfe->page['uid'];
910 }
911 if ($this->conf['special.']['setKeywords'] || $this->conf['special.']['setKeywords.']) {
912 $kw = isset($this->conf['special.']['setKeywords.']) ? $this->parent_cObj->stdWrap($this->conf['special.']['setKeywords'], $this->conf['special.']['setKeywords.']) : $this->conf['special.']['setKeywords'];
913 } else {
914 // The page record of the 'value'.
915 $value_rec = $this->sys_page->getPage($specialValue);
916 $kfieldSrc = $this->conf['special.']['keywordsField.']['sourceField'] ? $this->conf['special.']['keywordsField.']['sourceField'] : 'keywords';
917 // keywords.
918 $kw = trim($this->parent_cObj->keywords($value_rec[$kfieldSrc]));
919 }
920 // *'auto', 'manual', 'tstamp'
921 $mode = $this->conf['special.']['mode'];
922 switch ($mode) {
923 case 'starttime':
924 $sortField = 'starttime';
925 break;
926 case 'lastUpdated':
927 case 'manual':
928 $sortField = 'lastUpdated';
929 break;
930 case 'tstamp':
931 $sortField = 'tstamp';
932 break;
933 case 'crdate':
934 $sortField = 'crdate';
935 break;
936 default:
937 $sortField = 'SYS_LASTCHANGED';
938 }
939 // Depth, limit, extra where
940 if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'])) {
941 $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 0, 20);
942 } else {
943 $depth = 20;
944 }
945 // Max number of items
946 $limit = MathUtility::forceIntegerInRange($this->conf['special.']['limit'], 0, 100);
947 $extraWhere = ' AND pages.uid<>' . $specialValue . ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
948 if ($this->conf['special.']['excludeNoSearchPages']) {
949 $extraWhere .= ' AND pages.no_search=0';
950 }
951 // Start point
952 $eLevel = $this->parent_cObj->getKey(isset($this->conf['special.']['entryLevel.'])
953 ? $this->parent_cObj->stdWrap($this->conf['special.']['entryLevel'], $this->conf['special.']['entryLevel.'])
954 : $this->conf['special.']['entryLevel'], $this->tmpl->rootLine
955 );
956 $startUid = (int)$this->tmpl->rootLine[$eLevel]['uid'];
957 // Which field is for keywords
958 $kfield = 'keywords';
959 if ($this->conf['special.']['keywordsField']) {
960 list($kfield) = explode(' ', trim($this->conf['special.']['keywordsField']));
961 }
962 // If there are keywords and the startuid is present
963 if ($kw && $startUid) {
964 $bA = MathUtility::forceIntegerInRange($this->conf['special.']['beginAtLevel'], 0, 100);
965 $id_list = $this->parent_cObj->getTreeList(-1 * $startUid, $depth - 1 + $bA, $bA - 1);
966 $kwArr = explode(',', $kw);
967 $keyWordsWhereArr = array();
968 foreach ($kwArr as $word) {
969 $word = trim($word);
970 if ($word) {
971 $keyWordsWhereArr[] = $kfield . ' LIKE \'%' . $this->getDatabaseConnection()->quoteStr($word, 'pages') . '%\'';
972 }
973 }
974 $where = empty($keyWordsWhereArr) ? '' : '(' . implode(' OR ', $keyWordsWhereArr) . ')';
975 $res = $this->parent_cObj->exec_getQuery('pages', array(
976 'pidInList' => '0',
977 'uidInList' => $id_list,
978 'where' => $where . $extraWhere,
979 'orderBy' => $sortingField ?: $sortField . ' desc',
980 'max' => $limit
981 ));
982 while (($row = $this->getDatabaseConnection()->sql_fetch_assoc($res))) {
983 $tsfe->sys_page->versionOL('pages', $row, true);
984 if (is_array($row)) {
985 $menuItems[$row['uid']] = $this->sys_page->getPageOverlay($row);
986 }
987 }
988 $this->getDatabaseConnection()->sql_free_result($res);
989 }
990 return $menuItems;
991 }
992
993 /**
994 * Fetches all menuitems if special = rootline is set
995 *
996 * @return array
997 */
998 protected function prepareMenuItemsForRootlineMenu()
999 {
1000 $menuItems = array();
1001 $range = isset($this->conf['special.']['range.'])
1002 ? $this->parent_cObj->stdWrap($this->conf['special.']['range'], $this->conf['special.']['range.'])
1003 : $this->conf['special.']['range'];
1004 $begin_end = explode('|', $range);
1005 $begin_end[0] = (int)$begin_end[0];
1006 if (!MathUtility::canBeInterpretedAsInteger($begin_end[1])) {
1007 $begin_end[1] = -1;
1008 }
1009 $beginKey = $this->parent_cObj->getKey($begin_end[0], $this->tmpl->rootLine);
1010 $endKey = $this->parent_cObj->getKey($begin_end[1], $this->tmpl->rootLine);
1011 if ($endKey < $beginKey) {
1012 $endKey = $beginKey;
1013 }
1014 $rl_MParray = array();
1015 foreach ($this->tmpl->rootLine as $k_rl => $v_rl) {
1016 // For overlaid mount points, set the variable right now:
1017 if ($v_rl['_MP_PARAM'] && $v_rl['_MOUNT_OL']) {
1018 $rl_MParray[] = $v_rl['_MP_PARAM'];
1019 }
1020 // Traverse rootline:
1021 if ($k_rl >= $beginKey && $k_rl <= $endKey) {
1022 $temp_key = $k_rl;
1023 $menuItems[$temp_key] = $this->sys_page->getPage($v_rl['uid']);
1024 if (!empty($menuItems[$temp_key])) {
1025 // If there are no specific target for the page, put the level specific target on.
1026 if (!$menuItems[$temp_key]['target']) {
1027 $menuItems[$temp_key]['target'] = $this->conf['special.']['targets.'][$k_rl];
1028 $menuItems[$temp_key]['_MP_PARAM'] = implode(',', $rl_MParray);
1029 }
1030 } else {
1031 unset($menuItems[$temp_key]);
1032 }
1033 }
1034 // For normal mount points, set the variable for next level.
1035 if ($v_rl['_MP_PARAM'] && !$v_rl['_MOUNT_OL']) {
1036 $rl_MParray[] = $v_rl['_MP_PARAM'];
1037 }
1038 }
1039 // Reverse order of elements (e.g. "1,2,3,4" gets "4,3,2,1"):
1040 if (isset($this->conf['special.']['reverseOrder']) && $this->conf['special.']['reverseOrder']) {
1041 $menuItems = array_reverse($menuItems);
1042 }
1043 return $menuItems;
1044 }
1045
1046 /**
1047 * Fetches all menuitems if special = browse is set
1048 *
1049 * @param string $specialValue The value from special.value
1050 * @param string $sortingField The sorting field
1051 * @param string $additionalWhere Additional WHERE clause
1052 * @return array
1053 */
1054 protected function prepareMenuItemsForBrowseMenu($specialValue, $sortingField, $additionalWhere)
1055 {
1056 $menuItems = array();
1057 list($specialValue) = GeneralUtility::intExplode(',', $specialValue);
1058 if (!$specialValue) {
1059 $specialValue = $this->getTypoScriptFrontendController()->page['uid'];
1060 }
1061 // Will not work out of rootline
1062 if ($specialValue != $this->tmpl->rootLine[0]['uid']) {
1063 $recArr = array();
1064 // The page record of the 'value'.
1065 $value_rec = $this->sys_page->getPage($specialValue);
1066 // 'up' page cannot be outside rootline
1067 if ($value_rec['pid']) {
1068 // The page record of 'up'.
1069 $recArr['up'] = $this->sys_page->getPage($value_rec['pid']);
1070 }
1071 // If the 'up' item was NOT level 0 in rootline...
1072 if ($recArr['up']['pid'] && $value_rec['pid'] != $this->tmpl->rootLine[0]['uid']) {
1073 // The page record of "index".
1074 $recArr['index'] = $this->sys_page->getPage($recArr['up']['pid']);
1075 }
1076 // check if certain pages should be excluded
1077 $additionalWhere .= ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
1078 if ($this->conf['special.']['excludeNoSearchPages']) {
1079 $additionalWhere .= ' AND pages.no_search=0';
1080 }
1081 // prev / next is found
1082 $prevnext_menu = $this->removeInaccessiblePages($this->sys_page->getMenu($value_rec['pid'], '*', $sortingField, $additionalWhere));
1083 $lastKey = 0;
1084 $nextActive = 0;
1085 foreach ($prevnext_menu as $k_b => $v_b) {
1086 if ($nextActive) {
1087 $recArr['next'] = $v_b;
1088 $nextActive = 0;
1089 }
1090 if ($v_b['uid'] == $specialValue) {
1091 if ($lastKey) {
1092 $recArr['prev'] = $prevnext_menu[$lastKey];
1093 }
1094 $nextActive = 1;
1095 }
1096 $lastKey = $k_b;
1097 }
1098
1099 $recArr['first'] = reset($prevnext_menu);
1100 $recArr['last'] = end($prevnext_menu);
1101 // prevsection / nextsection is found
1102 // You can only do this, if there is a valid page two levels up!
1103 if (is_array($recArr['index'])) {
1104 $prevnextsection_menu = $this->removeInaccessiblePages($this->sys_page->getMenu($recArr['index']['uid'], '*', $sortingField, $additionalWhere));
1105 $lastKey = 0;
1106 $nextActive = 0;
1107 foreach ($prevnextsection_menu as $k_b => $v_b) {
1108 if ($nextActive) {
1109 $sectionRec_temp = $this->removeInaccessiblePages($this->sys_page->getMenu($v_b['uid'], '*', $sortingField, $additionalWhere));
1110 if (!empty($sectionRec_temp)) {
1111 $recArr['nextsection'] = reset($sectionRec_temp);
1112 $recArr['nextsection_last'] = end($sectionRec_temp);
1113 $nextActive = 0;
1114 }
1115 }
1116 if ($v_b['uid'] == $value_rec['pid']) {
1117 if ($lastKey) {
1118 $sectionRec_temp = $this->removeInaccessiblePages($this->sys_page->getMenu($prevnextsection_menu[$lastKey]['uid'], '*', $sortingField, $additionalWhere));
1119 if (!empty($sectionRec_temp)) {
1120 $recArr['prevsection'] = reset($sectionRec_temp);
1121 $recArr['prevsection_last'] = end($sectionRec_temp);
1122 }
1123 }
1124 $nextActive = 1;
1125 }
1126 $lastKey = $k_b;
1127 }
1128 }
1129 if ($this->conf['special.']['items.']['prevnextToSection']) {
1130 if (!is_array($recArr['prev']) && is_array($recArr['prevsection_last'])) {
1131 $recArr['prev'] = $recArr['prevsection_last'];
1132 }
1133 if (!is_array($recArr['next']) && is_array($recArr['nextsection'])) {
1134 $recArr['next'] = $recArr['nextsection'];
1135 }
1136 }
1137 $items = explode('|', $this->conf['special.']['items']);
1138 $c = 0;
1139 foreach ($items as $k_b => $v_b) {
1140 $v_b = strtolower(trim($v_b));
1141 if ((int)$this->conf['special.'][$v_b . '.']['uid']) {
1142 $recArr[$v_b] = $this->sys_page->getPage((int)$this->conf['special.'][$v_b . '.']['uid']);
1143 }
1144 if (is_array($recArr[$v_b])) {
1145 $menuItems[$c] = $recArr[$v_b];
1146 if ($this->conf['special.'][$v_b . '.']['target']) {
1147 $menuItems[$c]['target'] = $this->conf['special.'][$v_b . '.']['target'];
1148 }
1149 $tmpSpecialFields = $this->conf['special.'][$v_b . '.']['fields.'];
1150 if (is_array($tmpSpecialFields)) {
1151 foreach ($tmpSpecialFields as $fk => $val) {
1152 $menuItems[$c][$fk] = $val;
1153 }
1154 }
1155 $c++;
1156 }
1157 }
1158 }
1159 return $menuItems;
1160 }
1161
1162 /**
1163 * Analyzes the parameters to find if the link needs a cHash parameter.
1164 *
1165 * @param string $queryString
1166 * @return void
1167 */
1168 protected function analyzeCacheHashRequirements($queryString)
1169 {
1170 $parameters = GeneralUtility::explodeUrl2Array($queryString);
1171 if (!empty($parameters)) {
1172 /** @var CacheHashCalculator $cacheHashCalculator */
1173 $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
1174 $cHashParameters = $cacheHashCalculator->getRelevantParameters($queryString);
1175 if (count($cHashParameters) > 1) {
1176 $this->useCacheHash = (
1177 $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter'] ||
1178 !isset($parameters['no_cache']) ||
1179 !$parameters['no_cache']
1180 );
1181 }
1182 }
1183 }
1184
1185 /**
1186 * Checks if a page is OK to include in the final menu item array. Pages can be excluded if the doktype is wrong,
1187 * if they are hidden in navigation, have a uid in the list of banned uids etc.
1188 *
1189 * @param array $data Array of menu items
1190 * @param array $banUidArray Array of page uids which are to be excluded
1191 * @param bool $spacer If set, then the page is a spacer.
1192 * @return bool Returns TRUE if the page can be safely included.
1193 *
1194 * @throws \UnexpectedValueException
1195 */
1196 public function filterMenuPages(&$data, $banUidArray, $spacer)
1197 {
1198 $includePage = true;
1199 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/tslib/class.tslib_menu.php']['filterMenuPages'])) {
1200 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/tslib/class.tslib_menu.php']['filterMenuPages'] as $classRef) {
1201 $hookObject = GeneralUtility::getUserObj($classRef);
1202 if (!$hookObject instanceof AbstractMenuFilterPagesHookInterface) {
1203 throw new \UnexpectedValueException('$hookObject must implement interface ' . AbstractMenuFilterPagesHookInterface::class, 1269877402);
1204 }
1205 $includePage = $includePage && $hookObject->processFilter($data, $banUidArray, $spacer, $this);
1206 }
1207 }
1208 if (!$includePage) {
1209 return false;
1210 }
1211 if ($data['_SAFE']) {
1212 return true;
1213 }
1214
1215 if (
1216 ($this->mconf['SPC'] || !$spacer) // If the spacer-function is not enabled, spacers will not enter the $menuArr
1217 && (!$data['nav_hide'] || $this->conf['includeNotInMenu']) // Not hidden in navigation
1218 && !GeneralUtility::inList($this->doktypeExcludeList, $data['doktype']) // Page may not be 'not_in_menu' or 'Backend User Section'
1219 && !ArrayUtility::inArray($banUidArray, $data['uid']) // not in banned uid's
1220 ) {
1221 // Checks if the default language version can be shown:
1222 // Block page is set, if l18n_cfg allows plus: 1) Either default language or 2) another language but NO overlay record set for page!
1223 $tsfe = $this->getTypoScriptFrontendController();
1224 $blockPage = GeneralUtility::hideIfDefaultLanguage($data['l18n_cfg']) && (!$tsfe->sys_language_uid || $tsfe->sys_language_uid && !$data['_PAGES_OVERLAY']);
1225 if (!$blockPage) {
1226 // Checking if a page should be shown in the menu depending on whether a translation exists:
1227 $tok = true;
1228 // There is an alternative language active AND the current page requires a translation:
1229 if ($tsfe->sys_language_uid && GeneralUtility::hideIfNotTranslated($data['l18n_cfg'])) {
1230 if (!$data['_PAGES_OVERLAY']) {
1231 $tok = false;
1232 }
1233 }
1234 // Continue if token is TRUE:
1235 if ($tok) {
1236 // Checking if "&L" should be modified so links to non-accessible pages will not happen.
1237 if ($this->conf['protectLvar']) {
1238 $languageUid = (int)$tsfe->config['config']['sys_language_uid'];
1239 if ($languageUid && ($this->conf['protectLvar'] == 'all' || GeneralUtility::hideIfNotTranslated($data['l18n_cfg']))) {
1240 $olRec = $tsfe->sys_page->getPageOverlay($data['uid'], $languageUid);
1241 if (empty($olRec)) {
1242 // If no pages_language_overlay record then page can NOT be accessed in
1243 // the language pointed to by "&L" and therefore we protect the link by setting "&L=0"
1244 $data['_ADD_GETVARS'] .= '&L=0';
1245 }
1246 }
1247 }
1248 return true;
1249 }
1250 }
1251 }
1252 return false;
1253 }
1254
1255 /**
1256 * Generating the per-menu-item configuration arrays based on the settings for item states (NO, RO, ACT, CUR etc)
1257 * set in ->mconf (config for the current menu object)
1258 * Basically it will produce an individual array for each menu item based on the item states.
1259 * BUT in addition the "optionSplit" syntax for the values is ALSO evaluated here so that all property-values
1260 * are "option-splitted" and the output will thus be resolved.
1261 * Is called from the "generate" functions in the extension classes. The function is processor intensive due to
1262 * the option split feature in particular. But since the generate function is not always called
1263 * (since the ->result array may be cached, see makeMenu) it doesn't hurt so badly.
1264 *
1265 * @param int $splitCount Number of menu items in the menu
1266 * @return array An array with two keys: array($NOconf,$ROconf) - where $NOconf contains the resolved configuration for each item when NOT rolled-over and $ROconf contains the ditto for the mouseover state (if any)
1267 *
1268 * @internal
1269 */
1270 public function procesItemStates($splitCount)
1271 {
1272 // Prepare normal settings
1273 if (!is_array($this->mconf['NO.']) && $this->mconf['NO']) {
1274 // Setting a blank array if NO=1 and there are no properties.
1275 $this->mconf['NO.'] = array();
1276 }
1277 $NOconf = $this->tmpl->splitConfArray($this->mconf['NO.'], $splitCount);
1278 // Prepare rollOver settings, overriding normal settings
1279 $ROconf = array();
1280 if ($this->mconf['RO']) {
1281 $ROconf = $this->tmpl->splitConfArray($this->mconf['RO.'], $splitCount);
1282 }
1283 // Prepare IFSUB settings, overriding normal settings
1284 // IFSUB is TRUE if there exist submenu items to the current item
1285 if (!empty($this->mconf['IFSUB'])) {
1286 $IFSUBconf = null;
1287 $IFSUBROconf = null;
1288 foreach ($NOconf as $key => $val) {
1289 if ($this->isItemState('IFSUB', $key)) {
1290 // if this is the first IFSUB element, we must generate IFSUB.
1291 if ($IFSUBconf === null) {
1292 $IFSUBconf = $this->tmpl->splitConfArray($this->mconf['IFSUB.'], $splitCount);
1293 if (!empty($this->mconf['IFSUBRO'])) {
1294 $IFSUBROconf = $this->tmpl->splitConfArray($this->mconf['IFSUBRO.'], $splitCount);
1295 }
1296 }
1297 // Substitute normal with ifsub
1298 if (isset($IFSUBconf[$key])) {
1299 $NOconf[$key] = $IFSUBconf[$key];
1300 }
1301 // If rollOver on normal, we must apply a state for rollOver on the active
1302 if ($ROconf) {
1303 // If RollOver on active then apply this
1304 $ROconf[$key] = !empty($IFSUBROconf[$key]) ? $IFSUBROconf[$key] : $IFSUBconf[$key];
1305 }
1306 }
1307 }
1308 }
1309 // Prepare active settings, overriding normal settings
1310 if (!empty($this->mconf['ACT'])) {
1311 $ACTconf = null;
1312 $ACTROconf = null;
1313 // Find active
1314 foreach ($NOconf as $key => $val) {
1315 if ($this->isItemState('ACT', $key)) {
1316 // If this is the first 'active', we must generate ACT.
1317 if ($ACTconf === null) {
1318 $ACTconf = $this->tmpl->splitConfArray($this->mconf['ACT.'], $splitCount);
1319 // Prepare active rollOver settings, overriding normal active settings
1320 if (!empty($this->mconf['ACTRO'])) {
1321 $ACTROconf = $this->tmpl->splitConfArray($this->mconf['ACTRO.'], $splitCount);
1322 }
1323 }
1324 // Substitute normal with active
1325 if (isset($ACTconf[$key])) {
1326 $NOconf[$key] = $ACTconf[$key];
1327 }
1328 // If rollOver on normal, we must apply a state for rollOver on the active
1329 if ($ROconf) {
1330 // If RollOver on active then apply this
1331 $ROconf[$key] = !empty($ACTROconf[$key]) ? $ACTROconf[$key] : $ACTconf[$key];
1332 }
1333 }
1334 }
1335 }
1336 // Prepare ACT (active)/IFSUB settings, overriding normal settings
1337 // ACTIFSUB is TRUE if there exist submenu items to the current item and the current item is active
1338 if (!empty($this->mconf['ACTIFSUB'])) {
1339 $ACTIFSUBconf = null;
1340 $ACTIFSUBROconf = null;
1341 // Find active
1342 foreach ($NOconf as $key => $val) {
1343 if ($this->isItemState('ACTIFSUB', $key)) {
1344 // If this is the first 'active', we must generate ACTIFSUB.
1345 if ($ACTIFSUBconf === null) {
1346 $ACTIFSUBconf = $this->tmpl->splitConfArray($this->mconf['ACTIFSUB.'], $splitCount);
1347 // Prepare active rollOver settings, overriding normal active settings
1348 if (!empty($this->mconf['ACTIFSUBRO'])) {
1349 $ACTIFSUBROconf = $this->tmpl->splitConfArray($this->mconf['ACTIFSUBRO.'], $splitCount);
1350 }
1351 }
1352 // Substitute normal with active
1353 if (isset($ACTIFSUBconf[$key])) {
1354 $NOconf[$key] = $ACTIFSUBconf[$key];
1355 }
1356 // If rollOver on normal, we must apply a state for rollOver on the active
1357 if ($ROconf) {
1358 // If RollOver on active then apply this
1359 $ROconf[$key] = !empty($ACTIFSUBROconf[$key]) ? $ACTIFSUBROconf[$key] : $ACTIFSUBconf[$key];
1360 }
1361 }
1362 }
1363 }
1364 // Prepare CUR (current) settings, overriding normal settings
1365 // CUR is TRUE if the current page equals the item here!
1366 if (!empty($this->mconf['CUR'])) {
1367 $CURconf = null;
1368 $CURROconf = null;
1369 foreach ($NOconf as $key => $val) {
1370 if ($this->isItemState('CUR', $key)) {
1371 // if this is the first 'current', we must generate CUR. Basically this control is just inherited
1372 // from the other implementations as current would only exist one time and that's it
1373 // (unless you use special-features of HMENU)
1374 if ($CURconf === null) {
1375 $CURconf = $this->tmpl->splitConfArray($this->mconf['CUR.'], $splitCount);
1376 if (!empty($this->mconf['CURRO'])) {
1377 $CURROconf = $this->tmpl->splitConfArray($this->mconf['CURRO.'], $splitCount);
1378 }
1379 }
1380 // Substitute normal with current
1381 if (isset($CURconf[$key])) {
1382 $NOconf[$key] = $CURconf[$key];
1383 }
1384 // If rollOver on normal, we must apply a state for rollOver on the active
1385 if ($ROconf) {
1386 // If RollOver on active then apply this
1387 $ROconf[$key] = !empty($CURROconf[$key]) ? $CURROconf[$key] : $CURconf[$key];
1388 }
1389 }
1390 }
1391 }
1392 // Prepare CUR (current)/IFSUB settings, overriding normal settings
1393 // CURIFSUB is TRUE if there exist submenu items to the current item and the current page equals the item here!
1394 if (!empty($this->mconf['CURIFSUB'])) {
1395 $CURIFSUBconf = null;
1396 $CURIFSUBROconf = null;
1397 foreach ($NOconf as $key => $val) {
1398 if ($this->isItemState('CURIFSUB', $key)) {
1399 // If this is the first 'current', we must generate CURIFSUB.
1400 if ($CURIFSUBconf === null) {
1401 $CURIFSUBconf = $this->tmpl->splitConfArray($this->mconf['CURIFSUB.'], $splitCount);
1402 // Prepare current rollOver settings, overriding normal current settings
1403 if (!empty($this->mconf['CURIFSUBRO'])) {
1404 $CURIFSUBROconf = $this->tmpl->splitConfArray($this->mconf['CURIFSUBRO.'], $splitCount);
1405 }
1406 }
1407 // Substitute normal with active
1408 if ($CURIFSUBconf[$key]) {
1409 $NOconf[$key] = $CURIFSUBconf[$key];
1410 }
1411 // If rollOver on normal, we must apply a state for rollOver on the current
1412 if ($ROconf) {
1413 // If RollOver on current then apply this
1414 $ROconf[$key] = !empty($CURIFSUBROconf[$key]) ? $CURIFSUBROconf[$key] : $CURIFSUBconf[$key];
1415 }
1416 }
1417 }
1418 }
1419 // Prepare active settings, overriding normal settings
1420 if (!empty($this->mconf['USR'])) {
1421 $USRconf = null;
1422 $USRROconf = null;
1423 // Find active
1424 foreach ($NOconf as $key => $val) {
1425 if ($this->isItemState('USR', $key)) {
1426 // if this is the first active, we must generate USR.
1427 if ($USRconf === null) {
1428 $USRconf = $this->tmpl->splitConfArray($this->mconf['USR.'], $splitCount);
1429 // Prepare active rollOver settings, overriding normal active settings
1430 if (!empty($this->mconf['USRRO'])) {
1431 $USRROconf = $this->tmpl->splitConfArray($this->mconf['USRRO.'], $splitCount);
1432 }
1433 }
1434 // Substitute normal with active
1435 if ($USRconf[$key]) {
1436 $NOconf[$key] = $USRconf[$key];
1437 }
1438 // If rollOver on normal, we must apply a state for rollOver on the active
1439 if ($ROconf) {
1440 // If RollOver on active then apply this
1441 $ROconf[$key] = !empty($USRROconf[$key]) ? $USRROconf[$key] : $USRconf[$key];
1442 }
1443 }
1444 }
1445 }
1446 // Prepare spacer settings, overriding normal settings
1447 if (!empty($this->mconf['SPC'])) {
1448 $SPCconf = null;
1449 // Find spacers
1450 foreach ($NOconf as $key => $val) {
1451 if ($this->isItemState('SPC', $key)) {
1452 // If this is the first spacer, we must generate SPC.
1453 if ($SPCconf === null) {
1454 $SPCconf = $this->tmpl->splitConfArray($this->mconf['SPC.'], $splitCount);
1455 }
1456 // Substitute normal with spacer
1457 if (isset($SPCconf[$key])) {
1458 $NOconf[$key] = $SPCconf[$key];
1459 }
1460 }
1461 }
1462 }
1463 // Prepare Userdefined settings
1464 if (!empty($this->mconf['USERDEF1'])) {
1465 $USERDEF1conf = null;
1466 $USERDEF1ROconf = null;
1467 // Find active
1468 foreach ($NOconf as $key => $val) {
1469 if ($this->isItemState('USERDEF1', $key)) {
1470 // If this is the first active, we must generate USERDEF1.
1471 if ($USERDEF1conf === null) {
1472 $USERDEF1conf = $this->tmpl->splitConfArray($this->mconf['USERDEF1.'], $splitCount);
1473 // Prepare active rollOver settings, overriding normal active settings
1474 if (!empty($this->mconf['USERDEF1RO'])) {
1475 $USERDEF1ROconf = $this->tmpl->splitConfArray($this->mconf['USERDEF1RO.'], $splitCount);
1476 }
1477 }
1478 // Substitute normal with active
1479 if (isset($USERDEF1conf[$key])) {
1480 $NOconf[$key] = $USERDEF1conf[$key];
1481 }
1482 // If rollOver on normal, we must apply a state for rollOver on the active
1483 if ($ROconf) {
1484 // If RollOver on active then apply this
1485 $ROconf[$key] = !empty($USERDEF1ROconf[$key]) ? $USERDEF1ROconf[$key] : $USERDEF1conf[$key];
1486 }
1487 }
1488 }
1489 }
1490 // Prepare Userdefined settings
1491 if (!empty($this->mconf['USERDEF2'])) {
1492 $USERDEF2conf = null;
1493 $USERDEF2ROconf = null;
1494 // Find active
1495 foreach ($NOconf as $key => $val) {
1496 if ($this->isItemState('USERDEF2', $key)) {
1497 // If this is the first active, we must generate USERDEF2.
1498 if ($USERDEF2conf) {
1499 $USERDEF2conf = $this->tmpl->splitConfArray($this->mconf['USERDEF2.'], $splitCount);
1500 // Prepare active rollOver settings, overriding normal active settings
1501 if (!empty($this->mconf['USERDEF2RO'])) {
1502 $USERDEF2ROconf = $this->tmpl->splitConfArray($this->mconf['USERDEF2RO.'], $splitCount);
1503 }
1504 }
1505 // Substitute normal with active
1506 if (isset($USERDEF2conf[$key])) {
1507 $NOconf[$key] = $USERDEF2conf[$key];
1508 }
1509 // If rollOver on normal, we must apply a state for rollOver on the active
1510 if ($ROconf) {
1511 // If RollOver on active then apply this
1512 $ROconf[$key] = !empty($USERDEF2ROconf[$key]) ? $USERDEF2ROconf[$key] : $USERDEF2conf[$key];
1513 }
1514 }
1515 }
1516 }
1517 return array($NOconf, $ROconf);
1518 }
1519
1520 /**
1521 * Creates the URL, target and onclick values for the menu item link. Returns them in an array as key/value pairs for <A>-tag attributes
1522 * This function doesn't care about the url, because if we let the url be redirected, it will be logged in the stat!!!
1523 *
1524 * @param int $key Pointer to a key in the $this->menuArr array where the value for that key represents the menu item we are linking to (page record)
1525 * @param string $altTarget Alternative target
1526 * @param string $typeOverride Alternative type
1527 * @return array Returns an array with A-tag attributes as key/value pairs (HREF, TARGET and onClick)
1528 * @internal
1529 */
1530 public function link($key, $altTarget = '', $typeOverride = '')
1531 {
1532 // Mount points:
1533 $MP_var = $this->getMPvar($key);
1534 $MP_params = $MP_var ? '&MP=' . rawurlencode($MP_var) : '';
1535 // Setting override ID
1536 if ($this->mconf['overrideId'] || $this->menuArr[$key]['overrideId']) {
1537 $overrideArray = array();
1538 // If a user script returned the value overrideId in the menu array we use that as page id
1539 $overrideArray['uid'] = $this->mconf['overrideId'] ?: $this->menuArr[$key]['overrideId'];
1540 $overrideArray['alias'] = '';
1541 // Clear MP parameters since ID was changed.
1542 $MP_params = '';
1543 } else {
1544 $overrideArray = '';
1545 }
1546 // Setting main target:
1547 if ($altTarget) {
1548 $mainTarget = $altTarget;
1549 } elseif ($this->mconf['target.']) {
1550 $mainTarget = $this->parent_cObj->stdWrap($this->mconf['target'], $this->mconf['target.']);
1551 } else {
1552 $mainTarget = $this->mconf['target'];
1553 }
1554 // Creating link:
1555 $addParams = $this->mconf['addParams'] . $MP_params;
1556 if ($this->mconf['collapse'] && $this->isActive($this->menuArr[$key]['uid'], $this->getMPvar($key))) {
1557 $thePage = $this->sys_page->getPage($this->menuArr[$key]['pid']);
1558 $addParams .= $this->menuArr[$key]['_ADD_GETVARS'];
1559 $LD = $this->menuTypoLink($thePage, $mainTarget, '', '', $overrideArray, $addParams, $typeOverride);
1560 } else {
1561 $addParams .= $this->I['val']['additionalParams'] . $this->menuArr[$key]['_ADD_GETVARS'];
1562 $LD = $this->menuTypoLink($this->menuArr[$key], $mainTarget, '', '', $overrideArray, $addParams, $typeOverride);
1563 }
1564 // Override URL if using "External URL"
1565 if ($this->menuArr[$key]['doktype'] == PageRepository::DOKTYPE_LINK) {
1566 if ($this->menuArr[$key]['urltype'] == 3 && GeneralUtility::validEmail($this->menuArr[$key]['url'])) {
1567 // Create mailto-link using \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink (concerning spamProtectEmailAddresses):
1568 $LD['totalURL'] = $this->parent_cObj->typoLink_URL(array('parameter' => $this->menuArr[$key]['url']));
1569 $LD['target'] = '';
1570 } else {
1571 $LD['totalURL'] = $this->parent_cObj->typoLink_URL(array('parameter' => $this->getSysPage()->getExtURL($this->menuArr[$key])));
1572 }
1573 }
1574
1575 $tsfe = $this->getTypoScriptFrontendController();
1576
1577 // Override url if current page is a shortcut
1578 $shortcut = null;
1579 if ($this->menuArr[$key]['doktype'] == PageRepository::DOKTYPE_SHORTCUT && $this->menuArr[$key]['shortcut_mode'] != PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
1580 $menuItem = $this->determineOriginalShortcutPage($this->menuArr[$key]);
1581 try {
1582 $shortcut = $tsfe->getPageShortcut(
1583 $menuItem['shortcut'],
1584 $menuItem['shortcut_mode'],
1585 $menuItem['uid'],
1586 20,
1587 array(),
1588 true
1589 );
1590 } catch (\Exception $ex) {
1591 }
1592 if (!is_array($shortcut)) {
1593 return array();
1594 }
1595 // Only setting url, not target
1596 $LD['totalURL'] = $this->parent_cObj->typoLink_URL(array(
1597 'parameter' => $shortcut['uid'],
1598 'additionalParams' => $addParams . $this->I['val']['additionalParams'] . $menuItem['_ADD_GETVARS'],
1599 'linkAccessRestrictedPages' => $this->mconf['showAccessRestrictedPages'] && $this->mconf['showAccessRestrictedPages'] !== 'NONE'
1600 ));
1601 }
1602 if ($shortcut) {
1603 $pageData = $shortcut;
1604 $pageData['_SHORTCUT_PAGE_UID'] = $this->menuArr[$key]['uid'];
1605 } else {
1606 $pageData = $this->menuArr[$key];
1607 }
1608 // Manipulation in case of access restricted pages:
1609 $this->changeLinksForAccessRestrictedPages($LD, $pageData, $mainTarget, $typeOverride);
1610 // Overriding URL / Target if set to do so:
1611 if ($this->menuArr[$key]['_OVERRIDE_HREF']) {
1612 $LD['totalURL'] = $this->menuArr[$key]['_OVERRIDE_HREF'];
1613 if ($this->menuArr[$key]['_OVERRIDE_TARGET']) {
1614 $LD['target'] = $this->menuArr[$key]['_OVERRIDE_TARGET'];
1615 }
1616 }
1617 // OnClick open in windows.
1618 $onClick = '';
1619 if ($this->mconf['JSWindow']) {
1620 $conf = $this->mconf['JSWindow.'];
1621 $url = $LD['totalURL'];
1622 $LD['totalURL'] = '#';
1623 $onClick = 'openPic('
1624 . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($url)) . ','
1625 . '\'' . ($conf['newWindow'] ? md5($url) : 'theNewPage') . '\','
1626 . GeneralUtility::quoteJSvalue($conf['params']) . '); return false;';
1627 $tsfe->setJS('openPic');
1628 }
1629 // look for type and popup
1630 // following settings are valid in field target:
1631 // 230 will add type=230 to the link
1632 // 230 500x600 will add type=230 to the link and open in popup window with 500x600 pixels
1633 // 230 _blank will add type=230 to the link and open with target "_blank"
1634 // 230x450:resizable=0,location=1 will open in popup window with 500x600 pixels with settings "resizable=0,location=1"
1635 $matches = array();
1636 $targetIsType = $LD['target'] && MathUtility::canBeInterpretedAsInteger($LD['target']) ? (int)$LD['target'] : false;
1637 if (preg_match('/([0-9]+[\\s])?(([0-9]+)x([0-9]+))?(:.+)?/s', $LD['target'], $matches) || $targetIsType) {
1638 // has type?
1639 if ((int)$matches[1] || $targetIsType) {
1640 $LD['totalURL'] = $this->parent_cObj->URLqMark($LD['totalURL'], '&type=' . ($targetIsType ?: (int)$matches[1]));
1641 $LD['target'] = $targetIsType ? '' : trim(substr($LD['target'], strlen($matches[1]) + 1));
1642 }
1643 // Open in popup window?
1644 if ($matches[3] && $matches[4]) {
1645 $JSparamWH = 'width=' . $matches[3] . ',height=' . $matches[4] . ($matches[5] ? ',' . substr($matches[5], 1) : '');
1646 $onClick = 'vHWin=window.open('
1647 . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($LD['totalURL']))
1648 . ',\'FEopenLink\',' . GeneralUtility::quoteJSvalue($JSparamWH) . ');vHWin.focus();return false;';
1649 $LD['target'] = '';
1650 }
1651 }
1652 // out:
1653 $list = array();
1654 // Added this check: What it does is to enter the baseUrl (if set, which it should for "realurl" based sites)
1655 // as URL if the calculated value is empty. The problem is that no link is generated with a blank URL
1656 // and blank URLs might appear when the realurl encoding is used and a link to the frontpage is generated.
1657 $list['HREF'] = (string)$LD['totalURL'] !== '' ? $LD['totalURL'] : $tsfe->baseUrl;
1658 $list['TARGET'] = $LD['target'];
1659 $list['onClick'] = $onClick;
1660 return $list;
1661 }
1662
1663 /**
1664 * Determines original shortcut destination in page overlays.
1665 *
1666 * Since the pages records used for menu rendering are overlaid by default,
1667 * the original 'shortcut' value is lost, if a translation did not define one.
1668 * The behaviour in TSFE can be compared to the 'mergeIfNotBlank' feature, but
1669 * it's hardcoded there and not related to the mentioned setting at all.
1670 *
1671 * @param array $page
1672 * @return array
1673 */
1674 protected function determineOriginalShortcutPage(array $page)
1675 {
1676 // Check if modification is required
1677 if (
1678 $this->getTypoScriptFrontendController()->sys_language_uid > 0
1679 && empty($page['shortcut'])
1680 && !empty($page['uid'])
1681 && !empty($page['_PAGES_OVERLAY'])
1682 && !empty($page['_PAGES_OVERLAY_UID'])
1683 ) {
1684 // Using raw record since the record was overlaid and is correct already:
1685 $originalPage = $this->sys_page->getRawRecord('pages', $page['uid']);
1686
1687 if ($originalPage['shortcut_mode'] === $page['shortcut_mode'] && !empty($originalPage['shortcut'])) {
1688 $page['shortcut'] = $originalPage['shortcut'];
1689 }
1690 }
1691
1692 return $page;
1693 }
1694
1695 /**
1696 * Will change $LD (passed by reference) if the page is access restricted
1697 *
1698 * @param array $LD The array from the linkData() function
1699 * @param array $page Page array
1700 * @param string $mainTarget Main target value
1701 * @param string $typeOverride Type number override if any
1702 * @return void ($LD passed by reference might be changed.)
1703 */
1704 public function changeLinksForAccessRestrictedPages(&$LD, $page, $mainTarget, $typeOverride)
1705 {
1706 // If access restricted pages should be shown in menus, change the link of such pages to link to a redirection page:
1707 if ($this->mconf['showAccessRestrictedPages'] && $this->mconf['showAccessRestrictedPages'] !== 'NONE' && !$this->getTypoScriptFrontendController()->checkPageGroupAccess($page)) {
1708 $thePage = $this->sys_page->getPage($this->mconf['showAccessRestrictedPages']);
1709 $addParams = str_replace(
1710 array(
1711 '###RETURN_URL###',
1712 '###PAGE_ID###'
1713 ),
1714 array(
1715 rawurlencode($LD['totalURL']),
1716 isset($page['_SHORTCUT_PAGE_UID']) ? $page['_SHORTCUT_PAGE_UID'] : $page['uid']
1717 ),
1718 $this->mconf['showAccessRestrictedPages.']['addParams']
1719 );
1720 $LD = $this->menuTypoLink($thePage, $mainTarget, '', '', '', $addParams, $typeOverride);
1721 }
1722 }
1723
1724 /**
1725 * Creates a submenu level to the current level - if configured for.
1726 *
1727 * @param int $uid Page id of the current page for which a submenu MAY be produced (if conditions are met)
1728 * @param string $objSuffix Object prefix, see ->start()
1729 * @return string HTML content of the submenu
1730 * @internal
1731 */
1732 public function subMenu($uid, $objSuffix = '')
1733 {
1734 // Setting alternative menu item array if _SUB_MENU has been defined in the current ->menuArr
1735 $altArray = '';
1736 if (is_array($this->menuArr[$this->I['key']]['_SUB_MENU']) && !empty($this->menuArr[$this->I['key']]['_SUB_MENU'])) {
1737 $altArray = $this->menuArr[$this->I['key']]['_SUB_MENU'];
1738 }
1739 // Make submenu if the page is the next active
1740 $menuType = $this->conf[($this->menuNumber + 1) . $objSuffix];
1741 // stdWrap for expAll
1742 if (isset($this->mconf['expAll.'])) {
1743 $this->mconf['expAll'] = $this->parent_cObj->stdWrap($this->mconf['expAll'], $this->mconf['expAll.']);
1744 }
1745 if (($this->mconf['expAll'] || $this->isNext($uid, $this->getMPvar($this->I['key'])) || is_array($altArray)) && !$this->mconf['sectionIndex']) {
1746 try {
1747 $menuObjectFactory = GeneralUtility::makeInstance(MenuContentObjectFactory::class);
1748 /** @var $submenu AbstractMenuContentObject */
1749 $submenu = $menuObjectFactory->getMenuObjectByType($menuType);
1750 $submenu->entryLevel = $this->entryLevel + 1;
1751 $submenu->rL_uidRegister = $this->rL_uidRegister;
1752 $submenu->MP_array = $this->MP_array;
1753 if ($this->menuArr[$this->I['key']]['_MP_PARAM']) {
1754 $submenu->MP_array[] = $this->menuArr[$this->I['key']]['_MP_PARAM'];
1755 }
1756 // Especially scripts that build the submenu needs the parent data
1757 $submenu->parent_cObj = $this->parent_cObj;
1758 // Setting alternativeMenuTempArray (will be effective only if an array)
1759 if (is_array($altArray)) {
1760 $submenu->alternativeMenuTempArray = $altArray;
1761 }
1762 if ($submenu->start($this->tmpl, $this->sys_page, $uid, $this->conf, $this->menuNumber + 1, $objSuffix)) {
1763 $submenu->makeMenu();
1764 // Memorize the current menu item count
1765 $tsfe = $this->getTypoScriptFrontendController();
1766 $tempCountMenuObj = $tsfe->register['count_MENUOBJ'];
1767 // Reset the menu item count for the submenu
1768 $tsfe->register['count_MENUOBJ'] = 0;
1769 $content = $submenu->writeMenu();
1770 // Restore the item count now that the submenu has been handled
1771 $tsfe->register['count_MENUOBJ'] = $tempCountMenuObj;
1772 $tsfe->register['count_menuItems'] = count($this->menuArr);
1773 return $content;
1774 }
1775 } catch (Exception\NoSuchMenuTypeException $e) {
1776 }
1777 }
1778 return '';
1779 }
1780
1781 /**
1782 * Returns TRUE if the page with UID $uid is the NEXT page in root line (which means a submenu should be drawn)
1783 *
1784 * @param int $uid Page uid to evaluate.
1785 * @param string $MPvar MPvar for the current position of item.
1786 * @return bool TRUE if page with $uid is active
1787 * @internal
1788 * @see subMenu()
1789 */
1790 public function isNext($uid, $MPvar = '')
1791 {
1792 // Check for always active PIDs:
1793 if (!empty($this->alwaysActivePIDlist) && in_array((int)$uid, $this->alwaysActivePIDlist, true)) {
1794 return true;
1795 }
1796 $testUid = $uid . ($MPvar ? ':' . $MPvar : '');
1797 if ($uid && $testUid == $this->nextActive) {
1798 return true;
1799 }
1800 return false;
1801 }
1802
1803 /**
1804 * Returns TRUE if the page with UID $uid is active (in the current rootline)
1805 *
1806 * @param int $uid Page uid to evaluate.
1807 * @param string $MPvar MPvar for the current position of item.
1808 * @return bool TRUE if page with $uid is active
1809 * @internal
1810 */
1811 public function isActive($uid, $MPvar = '')
1812 {
1813 // Check for always active PIDs:
1814 if (!empty($this->alwaysActivePIDlist) && in_array((int)$uid, $this->alwaysActivePIDlist, true)) {
1815 return true;
1816 }
1817 $testUid = $uid . ($MPvar ? ':' . $MPvar : '');
1818 if ($uid && in_array('ITEM:' . $testUid, $this->rL_uidRegister, true)) {
1819 return true;
1820 }
1821 return false;
1822 }
1823
1824 /**
1825 * Returns TRUE if the page with UID $uid is the CURRENT page (equals $this->getTypoScriptFrontendController()->id)
1826 *
1827 * @param int $uid Page uid to evaluate.
1828 * @param string $MPvar MPvar for the current position of item.
1829 * @return bool TRUE if page $uid = $this->getTypoScriptFrontendController()->id
1830 * @internal
1831 */
1832 public function isCurrent($uid, $MPvar = '')
1833 {
1834 $testUid = $uid . ($MPvar ? ':' . $MPvar : '');
1835 return $uid && end($this->rL_uidRegister) === 'ITEM:' . $testUid;
1836 }
1837
1838 /**
1839 * Returns TRUE if there is a submenu with items for the page id, $uid
1840 * Used by the item states "IFSUB", "ACTIFSUB" and "CURIFSUB" to check if there is a submenu
1841 *
1842 * @param int $uid Page uid for which to search for a submenu
1843 * @return bool Returns TRUE if there was a submenu with items found
1844 * @internal
1845 */
1846 public function isSubMenu($uid)
1847 {
1848 // Looking for a mount-pid for this UID since if that
1849 // exists we should look for a subpages THERE and not in the input $uid;
1850 $mount_info = $this->sys_page->getMountPointInfo($uid);
1851 if (is_array($mount_info)) {
1852 $uid = $mount_info['mount_pid'];
1853 }
1854 $recs = $this->sys_page->getMenu($uid, 'uid,pid,doktype,mount_pid,mount_pid_ol,nav_hide,shortcut,shortcut_mode,l18n_cfg');
1855 $hasSubPages = false;
1856 $bannedUids = $this->getBannedUids();
1857 foreach ($recs as $theRec) {
1858 // no valid subpage if the document type is excluded from the menu
1859 if (GeneralUtility::inList($this->doktypeExcludeList, $theRec['doktype'])) {
1860 continue;
1861 }
1862 // No valid subpage if the page is hidden inside menus and
1863 // it wasn't forced to show such entries
1864 if ($theRec['nav_hide'] && !$this->conf['includeNotInMenu']) {
1865 continue;
1866 }
1867 // No valid subpage if the default language should be shown and the page settings
1868 // are excluding the visibility of the default language
1869 if (!$this->getTypoScriptFrontendController()->sys_language_uid && GeneralUtility::hideIfDefaultLanguage($theRec['l18n_cfg'])) {
1870 continue;
1871 }
1872 // No valid subpage if the alternative language should be shown and the page settings
1873 // are requiring a valid overlay but it doesn't exists
1874 $hideIfNotTranslated = GeneralUtility::hideIfNotTranslated($theRec['l18n_cfg']);
1875 if ($this->getTypoScriptFrontendController()->sys_language_uid && $hideIfNotTranslated && !$theRec['_PAGES_OVERLAY']) {
1876 continue;
1877 }
1878 // No valid subpage if the subpage is banned by excludeUidList
1879 if (in_array($theRec['uid'], $bannedUids)) {
1880 continue;
1881 }
1882 $hasSubPages = true;
1883 break;
1884 }
1885 return $hasSubPages;
1886 }
1887
1888 /**
1889 * Used by procesItemStates() to evaluate if a menu item (identified by $key) is in a certain state.
1890 *
1891 * @param string $kind The item state to evaluate (SPC, IFSUB, ACT etc... but no xxxRO states of course)
1892 * @param int $key Key pointing to menu item from ->menuArr
1893 * @return bool Returns TRUE if state matches
1894 * @internal
1895 * @see procesItemStates()
1896 */
1897 public function isItemState($kind, $key)
1898 {
1899 $natVal = false;
1900 // If any value is set for ITEM_STATE the normal evaluation is discarded
1901 if ($this->menuArr[$key]['ITEM_STATE']) {
1902 if ((string)$this->menuArr[$key]['ITEM_STATE'] === (string)$kind) {
1903 $natVal = true;
1904 }
1905 } else {
1906 switch ($kind) {
1907 case 'SPC':
1908 $natVal = (bool)$this->menuArr[$key]['isSpacer'];
1909 break;
1910 case 'IFSUB':
1911 $natVal = $this->isSubMenu($this->menuArr[$key]['uid']);
1912 break;
1913 case 'ACT':
1914 $natVal = $this->isActive($this->menuArr[$key]['uid'], $this->getMPvar($key));
1915 break;
1916 case 'ACTIFSUB':
1917 $natVal = $this->isActive($this->menuArr[$key]['uid'], $this->getMPvar($key)) && $this->isSubMenu($this->menuArr[$key]['uid']);
1918 break;
1919 case 'CUR':
1920 $natVal = $this->isCurrent($this->menuArr[$key]['uid'], $this->getMPvar($key));
1921 break;
1922 case 'CURIFSUB':
1923 $natVal = $this->isCurrent($this->menuArr[$key]['uid'], $this->getMPvar($key)) && $this->isSubMenu($this->menuArr[$key]['uid']);
1924 break;
1925 case 'USR':
1926 $natVal = (bool)$this->menuArr[$key]['fe_group'];
1927 break;
1928 }
1929 }
1930 return $natVal;
1931 }
1932
1933 /**
1934 * Creates an access-key for a TMENU/GMENU menu item based on the menu item titles first letter
1935 *
1936 * @param string $title Menu item title.
1937 * @return array Returns an array with keys "code" ("accesskey" attribute for the img-tag) and "alt" (text-addition to the "alt" attribute) if an access key was defined. Otherwise array was empty
1938 * @internal
1939 */
1940 public function accessKey($title)
1941 {
1942 $tsfe = $this->getTypoScriptFrontendController();
1943 // The global array ACCESSKEY is used to globally control if letters are already used!!
1944 $result = array();
1945 $title = trim(strip_tags($title));
1946 $titleLen = strlen($title);
1947 for ($a = 0; $a < $titleLen; $a++) {
1948 $key = strtoupper(substr($title, $a, 1));
1949 if (preg_match('/[A-Z]/', $key) && !isset($tsfe->accessKey[$key])) {
1950 $tsfe->accessKey[$key] = 1;
1951 $result['code'] = ' accesskey="' . $key . '"';
1952 $result['alt'] = ' (ALT+' . $key . ')';
1953 $result['key'] = $key;
1954 break;
1955 }
1956 }
1957 return $result;
1958 }
1959
1960 /**
1961 * Calls a user function for processing of internal data.
1962 * Used for the properties "IProcFunc" and "itemArrayProcFunc"
1963 *
1964 * @param string $mConfKey Key pointing for the property in the current ->mconf array holding possibly parameters to pass along to the function/method. Currently the keys used are "IProcFunc" and "itemArrayProcFunc".
1965 * @param mixed $passVar A variable to pass to the user function and which should be returned again from the user function. The idea is that the user function modifies this variable according to what you want to achieve and then returns it. For "itemArrayProcFunc" this variable is $this->menuArr, for "IProcFunc" it is $this->I
1966 * @return mixed The processed $passVar
1967 * @internal
1968 */
1969 public function userProcess($mConfKey, $passVar)
1970 {
1971 if ($this->mconf[$mConfKey]) {
1972 $funcConf = $this->mconf[$mConfKey . '.'];
1973 $funcConf['parentObj'] = $this;
1974 $passVar = $this->parent_cObj->callUserFunction($this->mconf[$mConfKey], $funcConf, $passVar);
1975 }
1976 return $passVar;
1977 }
1978
1979 /**
1980 * Creates the <A> tag parts for the current item (in $this->I, [A1] and [A2]) based on other information in this array (like $this->I['linkHREF'])
1981 *
1982 * @return void
1983 * @internal
1984 */
1985 public function setATagParts()
1986 {
1987 $params = trim($this->I['val']['ATagParams']) . $this->I['accessKey']['code'];
1988 $params = $params !== '' ? ' ' . $params : '';
1989 $this->I['A1'] = '<a ' . GeneralUtility::implodeAttributes($this->I['linkHREF'], 1) . $params . '>';
1990 $this->I['A2'] = '</a>';
1991 }
1992
1993 /**
1994 * Returns the title for the navigation
1995 *
1996 * @param string $title The current page title
1997 * @param string $nav_title The current value of the navigation title
1998 * @return string Returns the navigation title if it is NOT blank, otherwise the page title.
1999 * @internal
2000 */
2001 public function getPageTitle($title, $nav_title)
2002 {
2003 return trim($nav_title) !== '' ? $nav_title : $title;
2004 }
2005
2006 /**
2007 * Return MPvar string for entry $key in ->menuArr
2008 *
2009 * @param int $key Pointer to element in ->menuArr
2010 * @return string MP vars for element.
2011 * @see link()
2012 */
2013 public function getMPvar($key)
2014 {
2015 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
2016 $localMP_array = $this->MP_array;
2017 // NOTICE: "_MP_PARAM" is allowed to be a commalist of PID pairs!
2018 if ($this->menuArr[$key]['_MP_PARAM']) {
2019 $localMP_array[] = $this->menuArr[$key]['_MP_PARAM'];
2020 }
2021 return !empty($localMP_array) ? implode(',', $localMP_array) : '';
2022 }
2023 return '';
2024 }
2025
2026 /**
2027 * Returns where clause part to exclude 'not in menu' pages
2028 *
2029 * @return string where clause part.
2030 * @internal
2031 */
2032 public function getDoktypeExcludeWhere()
2033 {
2034 return $this->doktypeExcludeList ? ' AND pages.doktype NOT IN (' . $this->doktypeExcludeList . ')' : '';
2035 }
2036
2037 /**
2038 * Returns an array of banned UIDs (from excludeUidList)
2039 *
2040 * @return array Array of banned UIDs
2041 * @internal
2042 */
2043 public function getBannedUids()
2044 {
2045 $excludeUidList = isset($this->conf['excludeUidList.'])
2046 ? $this->parent_cObj->stdWrap($this->conf['excludeUidList'], $this->conf['excludeUidList.'])
2047 : $this->conf['excludeUidList'];
2048
2049 if (!trim($excludeUidList)) {
2050 return array();
2051 }
2052
2053 $banUidList = str_replace('current', $this->getTypoScriptFrontendController()->page['uid'], $excludeUidList);
2054 return GeneralUtility::intExplode(',', $banUidList);
2055 }
2056
2057 /**
2058 * Calls typolink to create menu item links.
2059 *
2060 * @param array $page Page record (uid points where to link to)
2061 * @param string $oTarget Target frame/window
2062 * @param bool $no_cache TRUE if caching should be disabled
2063 * @param string $script Alternative script name (unused)
2064 * @param array|string $overrideArray Array to override values in $page, empty string to skip override
2065 * @param string $addParams Parameters to add to URL
2066 * @param int|string $typeOverride "type" value, empty string means "not set"
2067 * @return array See linkData
2068 */
2069 public function menuTypoLink($page, $oTarget, $no_cache, $script, $overrideArray = '', $addParams = '', $typeOverride = '')
2070 {
2071 $conf = array(
2072 'parameter' => is_array($overrideArray) && $overrideArray['uid'] ? $overrideArray['uid'] : $page['uid']
2073 );
2074 if (MathUtility::canBeInterpretedAsInteger($typeOverride)) {
2075 $conf['parameter'] .= ',' . (int)$typeOverride;
2076 }
2077 if ($addParams) {
2078 $conf['additionalParams'] = $addParams;
2079 }
2080 if ($no_cache) {
2081 $conf['no_cache'] = true;
2082 } elseif ($this->useCacheHash) {
2083 $conf['useCacheHash'] = true;
2084 }
2085 if ($oTarget) {
2086 $conf['target'] = $oTarget;
2087 }
2088 if ($page['sectionIndex_uid']) {
2089 $conf['section'] = $page['sectionIndex_uid'];
2090 }
2091 $conf['linkAccessRestrictedPages'] = $this->mconf['showAccessRestrictedPages'] && $this->mconf['showAccessRestrictedPages'] !== 'NONE';
2092 $this->parent_cObj->typoLink('|', $conf);
2093 $LD = $this->parent_cObj->lastTypoLinkLD;
2094 $LD['totalURL'] = $this->parent_cObj->lastTypoLinkUrl;
2095 return $LD;
2096 }
2097
2098 /**
2099 * Generates a list of content objects with sectionIndex enabled
2100 * available on a specific page
2101 *
2102 * Used for menus with sectionIndex enabled
2103 *
2104 * @param string $altSortField Alternative sorting field
2105 * @param int $pid The page id to search for sections
2106 * @throws \UnexpectedValueException if the query to fetch the content elements unexpectedly fails
2107 * @return array
2108 */
2109 protected function sectionIndex($altSortField, $pid = null)
2110 {
2111 $pid = (int)($pid ?: $this->id);
2112 $basePageRow = $this->sys_page->getPage($pid);
2113 if (!is_array($basePageRow)) {
2114 return array();
2115 }
2116 $tsfe = $this->getTypoScriptFrontendController();
2117 $configuration = $this->mconf['sectionIndex.'];
2118 $useColPos = 0;
2119 if (trim($configuration['useColPos']) !== '' || is_array($configuration['useColPos.'])) {
2120 $useColPos = $tsfe->cObj->stdWrap($configuration['useColPos'], $configuration['useColPos.']);
2121 $useColPos = (int)$useColPos;
2122 }
2123 $selectSetup = array(
2124 'pidInList' => $pid,
2125 'orderBy' => $altSortField,
2126 'languageField' => 'sys_language_uid',
2127 'where' => $useColPos >= 0 ? 'colPos=' . $useColPos : ''
2128 );
2129 if ($basePageRow['content_from_pid']) {
2130 // If the page is configured to show content from a referenced page the sectionIndex contains only contents of
2131 // the referenced page
2132 $selectSetup['pidInList'] = $basePageRow['content_from_pid'];
2133 }
2134 $resource = $this->parent_cObj->exec_getQuery('tt_content', $selectSetup);
2135 if (!$resource) {
2136 $message = 'SectionIndex: Query to fetch the content elements failed!';
2137 throw new \UnexpectedValueException($message, 1337334849);
2138 }
2139 $result = array();
2140 while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($resource)) {
2141 $this->sys_page->versionOL('tt_content', $row);
2142 if ($tsfe->sys_language_contentOL && $basePageRow['_PAGES_OVERLAY_LANGUAGE']) {
2143 $row = $this->sys_page->getRecordOverlay('tt_content', $row, $basePageRow['_PAGES_OVERLAY_LANGUAGE'], $tsfe->sys_language_contentOL);
2144 }
2145 if ($this->mconf['sectionIndex.']['type'] !== 'all') {
2146 $doIncludeInSectionIndex = $row['sectionIndex'] >= 1;
2147 $doHeaderCheck = $this->mconf['sectionIndex.']['type'] === 'header';
2148 $isValidHeader = ((int)$row['header_layout'] !== 100 || !empty($this->mconf['sectionIndex.']['includeHiddenHeaders'])) && trim($row['header']) !== '';
2149 if (!$doIncludeInSectionIndex || $doHeaderCheck && !$isValidHeader) {
2150 continue;
2151 }
2152 }
2153 if (is_array($row)) {
2154 $uid = $row['uid'];
2155 $result[$uid] = $basePageRow;
2156 $result[$uid]['title'] = $row['header'];
2157 $result[$uid]['nav_title'] = $row['header'];
2158 // Prevent false exclusion in filterMenuPages, thus: Always show tt_content records
2159 $result[$uid]['nav_hide'] = 0;
2160 $result[$uid]['subtitle'] = $row['subheader'];
2161 $result[$uid]['starttime'] = $row['starttime'];
2162 $result[$uid]['endtime'] = $row['endtime'];
2163 $result[$uid]['fe_group'] = $row['fe_group'];
2164 $result[$uid]['media'] = $row['media'];
2165 $result[$uid]['header_layout'] = $row['header_layout'];
2166 $result[$uid]['bodytext'] = $row['bodytext'];
2167 $result[$uid]['image'] = $row['image'];
2168 $result[$uid]['sectionIndex_uid'] = $uid;
2169 }
2170 }
2171 $this->getDatabaseConnection()->sql_free_result($resource);
2172 return $result;
2173 }
2174
2175 /**
2176 * Returns the sys_page object
2177 *
2178 * @return \TYPO3\CMS\Frontend\Page\PageRepository
2179 */
2180 public function getSysPage()
2181 {
2182 return $this->sys_page;
2183 }
2184
2185 /**
2186 * Returns the parent content object
2187 *
2188 * @return \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
2189 */
2190 public function getParentContentObject()
2191 {
2192 return $this->parent_cObj;
2193 }
2194
2195 /**
2196 * @return DatabaseConnection
2197 */
2198 protected function getDatabaseConnection()
2199 {
2200 return $GLOBALS['TYPO3_DB'];
2201 }
2202
2203 /**
2204 * @return TypoScriptFrontendController
2205 */
2206 protected function getTypoScriptFrontendController()
2207 {
2208 return $GLOBALS['TSFE'];
2209 }
2210
2211 /**
2212 * @return TimeTracker
2213 */
2214 protected function getTimeTracker()
2215 {
2216 return GeneralUtility::makeInstance(TimeTracker::class);
2217 }
2218
2219 /**
2220 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
2221 */
2222 protected function getCache()
2223 {
2224 return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
2225 }
2226 }