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