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