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