Skip to content
Snippets Groups Projects
AbstractMenuContentObject.php 83.3 KiB
Newer Older
 * This file is part of the TYPO3 CMS project.
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 * The TYPO3 project - inspiring people to share!
 */
namespace TYPO3\CMS\Frontend\ContentObject\Menu;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Domain\Page;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\PageLinkBuilder;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
 * Generating navigation/menus from TypoScript
 * The HMENU content object uses this (or more precisely one of the extension classes).
 * Among others the class generates an array of menu items. Thereafter functions from the subclasses are called.
 * The class is always used through extension classes like TextMenuContentObject.
abstract class AbstractMenuContentObject
{
    /**
     * tells you which menu number this is. This is important when getting data from the setup
     */
    protected int $menuNumber = 1;
    protected $entryLevel = 0;

    /**
     * Doktypes that define which should not be included in a menu
     *
    protected $excludedDoktypes = [PageRepository::DOKTYPE_BE_USER_SECTION, PageRepository::DOKTYPE_SYSFOLDER];
    protected $alwaysActivePIDlist = [];

    /**
     * Loaded with the parent cObj-object when a new HMENU is made
     *
     * @var ContentObjectRenderer

    /**
     * accumulation of mount point data
     *
     * @var string[]
     */
    protected $MP_array = [];

    /**
     * HMENU configuration
     *
     * @var array
     */
    protected $conf = [];
     * xMENU configuration (TMENU etc)
    protected $mconf = [];
     * @var PageRepository
    protected $sys_page;

    /**
     * The base page-id of the menu.
     *
     * @var int
     */

    /**
     * Holds the page uid of the NEXT page in the root line from the page pointed to by entryLevel;
     * Used to expand the menu automatically if in a certain root line.
     *
     * @var string
     */
    protected $nextActive;

    /**
     * The array of menuItems which is built
     *
     * @var array[]
     */
    protected $menuArr;
     * @var string Unused
    protected $hash;
    protected $result = [];

    /**
     * Is filled with an array of page uid numbers + RL parameters which are in the current
     * root line (used to evaluate whether a menu item is in active state)
     *
    protected $rL_uidRegister;
    protected ServerRequestInterface $request;
    /**
     * Can be set to contain menu item arrays for sub-levels.
     */
    protected array $alternativeMenuTempArray = [];
    /**
     * Array key of the parentMenuItem in the parentMenuArr, if this menu is a subMenu.
     *
    /**
     * @var array
     */
    protected $parentMenuArr;

    protected bool $disableGroupAccessCheck = false;

    protected const customItemStates = [
        // IFSUB is TRUE if there exist submenu items to the current item
        'IFSUB',
        'ACT',
        // ACTIFSUB is TRUE if there exist submenu items to the current item and the current item is active
        'ACTIFSUB',
        // CUR is TRUE if the current page equals the item here!
        'CUR',
        // CURIFSUB is TRUE if there exist submenu items to the current item and the current page equals the item here!
        'CURIFSUB',
        'USR',
        'SPC',
        'USERDEF1',
    /**
     * The initialization of the object. This just sets some internal variables.
     *
     * @param null $_ Obsolete argument
     * @param PageRepository $sys_page
     * @param int|string $id A starting point page id. This should probably be blank since the 'entryLevel' value will be used then.
     * @param array $conf The TypoScript configuration for the HMENU cObject
     * @param int $menuNumber Menu number; 1,2,3. Should probably be 1
     * @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")
     * @return bool Returns TRUE on success
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::HMENU()
     */
    public function start($_, $sys_page, $id, $conf, int $menuNumber, string $objSuffix, ServerRequestInterface $request): bool
        $this->conf = (array)$conf;
        $this->menuNumber = $menuNumber;
        $this->mconf = (array)$conf[$this->menuNumber . $objSuffix . '.'];
        // Sets the internal vars. $sys_page MUST be the PageRepository object
        if ($this->conf[$this->menuNumber . $objSuffix] && is_object($sys_page)) {
            $localRootLine = $request->getAttribute('frontend.page.information')->getLocalRootLine();
            $this->sys_page = $sys_page;
            // alwaysActivePIDlist initialized:
            $this->conf['alwaysActivePIDlist'] = (string)$this->parent_cObj->stdWrapValue('alwaysActivePIDlist', $this->conf);
            if (trim($this->conf['alwaysActivePIDlist'])) {
                $this->alwaysActivePIDlist = GeneralUtility::intExplode(',', $this->conf['alwaysActivePIDlist']);
            }
            $this->conf['includeNotInMenu'] = $this->parent_cObj->stdWrapValue('includeNotInMenu', $this->conf, false);
            // exclude doktypes that should not be shown in menu (e.g. backend user section)
            if ($this->conf['excludeDoktypes'] ?? false) {
                $this->excludedDoktypes = GeneralUtility::intExplode(',', (string)($this->conf['excludeDoktypes']));
            }
            // EntryLevel
            $this->entryLevel = $this->parent_cObj->getKey(
                $this->parent_cObj->stdWrapValue('entryLevel', $this->conf),
            );
            // Set parent page: If $id not stated with start() then the base-id will be found from rootLine[$this->entryLevel]
            // Called as the next level in a menu. It is assumed that $this->MP_array is set from parent menu.
            if ($id) {
                $this->id = (int)$id;
            } else {
                // This is a BRAND NEW menu, first level. So we take ID from rootline and also find MP_array (mount points)
                $this->id = (int)($localRootLine[$this->entryLevel]['uid'] ?? 0);
                // Traverse rootline to build MP_array of pages BEFORE the entryLevel
                // (MP var for ->id is picked up in the next part of the code...)
                foreach ($localRootLine as $entryLevel => $levelRec) {
                    // For overlaid mount points, set the variable right now:
                    if (($levelRec['_MP_PARAM'] ?? false) && ($levelRec['_MOUNT_OL'] ?? false)) {
                        $this->MP_array[] = $levelRec['_MP_PARAM'];
                    }
                    // Break when entry level is reached:
                    if ($entryLevel >= $this->entryLevel) {
                        break;
                    }
                    // For normal mount points, set the variable for next level.
                    if (!empty($levelRec['_MP_PARAM']) && empty($levelRec['_MOUNT_OL'])) {
                        $this->MP_array[] = $levelRec['_MP_PARAM'];
                    }
                }
            }
            // Return FALSE if no page ID was set (thus no menu of subpages can be made).
            if ($this->id <= 0) {
                return false;
            }
            // Check if page is a mount point, and if so set id and MP_array
            // (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...)
            $mount_info = $this->sys_page->getMountPointInfo($this->id);
            if (is_array($mount_info)) {
                $this->MP_array[] = $mount_info['MPvar'];
                $this->id = $mount_info['mount_pid'];
            }
            // Gather list of page uids in root line (for "isActive" evaluation). Also adds the MP params in the path so Mount Points are respected.
            // (List is specific for this rootline, so it may be supplied from parent menus for speed...)
            if ($this->rL_uidRegister === null) {
                $this->rL_uidRegister = [];
                $rl_MParray = [];
                foreach ($localRootLine as $v_rl) {
                    // For overlaid mount points, set the variable right now:
                    if (($v_rl['_MP_PARAM'] ?? false) && ($v_rl['_MOUNT_OL'] ?? false)) {
                        $rl_MParray[] = $v_rl['_MP_PARAM'];
                    }
                    // Add to register:
                    $this->rL_uidRegister[] = 'ITEM:' . $v_rl['uid'] .
                            ? ':' . implode(',', $rl_MParray)
                            : ''
                        );
                    // For normal mount points, set the variable for next level.
                    if (($v_rl['_MP_PARAM'] ?? false) && !($v_rl['_MOUNT_OL'] ?? false)) {
                        $rl_MParray[] = $v_rl['_MP_PARAM'];
                    }
                }
            }
            // Set $directoryLevel so the following evaluation of the nextActive will not return
            // an invalid value if .special=directory was set
            $directoryLevel = 0;
            if (($this->conf['special'] ?? '') === 'directory') {
                $value = $this->parent_cObj->stdWrapValue('value', $this->conf['special.'] ?? [], null);
                    $value = $this->request->getAttribute('frontend.page.information')->getId();
                $directoryLevel = $this->getRootlineLevel($localRootLine, (string)$value);
            }
            // 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
            // Notice: The automatic expansion of a menu is designed to work only when no "special" modes (except "directory") are used.
            $startLevel = $directoryLevel ?: $this->entryLevel;
            $currentLevel = $startLevel + $this->menuNumber;
            if (is_array($localRootLine[$currentLevel] ?? null)) {
                $nextMParray = $this->MP_array;
                if (empty($nextMParray) && !($localRootLine[$currentLevel]['_MOUNT_OL'] ?? false) && $currentLevel > 0) {
                    // Make sure to slide-down any mount point information (_MP_PARAM) to children records in the rootline
                    // otherwise automatic expansion will not work
                    $parentRecord = $localRootLine[$currentLevel - 1] ?? [];
                    if (isset($parentRecord['_MP_PARAM'])) {
                        $nextMParray[] = $parentRecord['_MP_PARAM'];
                    }
                }
                // In overlay mode, add next level MPvars as well:
                if ($localRootLine[$currentLevel]['_MOUNT_OL'] ?? false) {
                    $nextMParray[] = $localRootLine[$currentLevel]['_MP_PARAM'] ?? [];
                $this->nextActive = ($localRootLine[$currentLevel]['uid']  ?? 0) .
                        ? ':' . implode(',', $nextMParray)
                        : ''
                    );
            } else {
                $this->nextActive = '';
            }
        $this->getTimeTracker()->setTSlogMessage('ERROR in menu', LogLevel::ERROR);
    }

    /**
     * Creates the menu in the internal variables, ready for output.
     * Basically this will read the page records needed and fill in the internal $this->menuArr
     * Based on a hash of this array and some other variables the $this->result variable will be
     * loaded either from cache OR by calling the generate() method of the class to create the menu for real.
     */
    public function makeMenu()
    {
        if (!$this->id) {
            return;
        }

        // Initializing showAccessRestrictedPages
        if ($this->mconf['showAccessRestrictedPages'] ?? false) {
            $this->disableGroupAccessCheck = true;
        }

        $menuItems = $this->prepareMenuItems();

        $c = 0;
        $c_b = 0;

        $minItems = (int)(($this->mconf['minItems'] ?? 0) ?: ($this->conf['minItems'] ?? 0));
        $maxItems = (int)(($this->mconf['maxItems'] ?? 0) ?: ($this->conf['maxItems'] ?? 0));
        $begin = $this->parent_cObj->calc(($this->mconf['begin'] ?? 0) ?: ($this->conf['begin'] ?? 0));
        $minItemsConf = $this->mconf['minItems.'] ?? $this->conf['minItems.'] ?? null;
        $minItems = is_array($minItemsConf) ? $this->parent_cObj->stdWrap((string)$minItems, $minItemsConf) : $minItems;
        $maxItemsConf = $this->mconf['maxItems.'] ?? $this->conf['maxItems.'] ?? null;
        $maxItems = is_array($maxItemsConf) ? $this->parent_cObj->stdWrap((string)$maxItems, $maxItemsConf) : $maxItems;
        $beginConf = $this->mconf['begin.'] ?? $this->conf['begin.'] ?? null;
        $begin = is_array($beginConf) ? $this->parent_cObj->stdWrap((string)$begin, $beginConf) : $begin;
        $this->menuArr = [];
        foreach ($menuItems as &$data) {
            $data['isSpacer'] = ($data['isSpacer'] ?? false) || (int)($data['doktype'] ?? 0) === PageRepository::DOKTYPE_SPACER || ($data['ITEM_STATE'] ?? '') === 'SPC';
        }
        $menuItems = $this->removeInaccessiblePages($menuItems);
        // Fill in the menuArr with elements that should go into the menu
        foreach ($menuItems as $menuItem) {
            $c_b++;
            // If the beginning item has been reached, add the items.
            if ($begin <= $c_b) {
                $this->menuArr[$c] = $menuItem;
                $c++;
                if ($maxItems && $c >= $maxItems) {
                    break;
                }
            }
        }
        // Fill in fake items, if min-items is set.
        if ($minItems) {
            while ($c < $minItems) {
                $this->menuArr[$c] = [
                    'uid' => $this->request->getAttribute('frontend.page.information')->getId(),
                $c++;
            }
        }
        //	Passing the menuArr through a user defined function:
        if ($this->mconf['itemArrayProcFunc'] ?? false) {
            $this->menuArr = $this->userProcess('itemArrayProcFunc', $this->menuArr);
        }
        // Setting number of menu items
        $frontendController = $this->getTypoScriptFrontendController();
        $frontendController->register['count_menuItems'] = count($this->menuArr);
        $this->generate();
        // End showAccessRestrictedPages
        if ($this->mconf['showAccessRestrictedPages'] ?? false) {
            $this->disableGroupAccessCheck = false;
     * Calls processItemStates() so that the common configuration for the menu items are resolved into individual configuration per item.
     * Sets the result for the new "normal state" in $this->result
     * @see AbstractMenuContentObject::processItemStates()
     */
    public function generate()
    {
        $itemConfiguration = [];
        $splitCount = count($this->menuArr);
        if ($splitCount) {
            $itemConfiguration = $this->processItemStates($splitCount);
        }
        $this->result = $itemConfiguration;
     * @return string The HTML for the menu
     */
    public function writeMenu()
    {
        return '';
    }

    /**
     * Gets an array of page rows and removes all, which are not accessible
     */
    protected function removeInaccessiblePages(array $pages): array
    {
        $banned = $this->getBannedUids();
        $filteredPages = [];
        foreach ($pages as $aPage) {
            $isSpacerPage = ((int)($aPage['doktype'] ?? 0) === PageRepository::DOKTYPE_SPACER) || ($aPage['isSpacer'] ?? false);
            if ($this->filterMenuPages($aPage, $banned, $isSpacerPage)) {
                $filteredPages[] = $aPage;
        $event = new FilterMenuItemsEvent(
            $pages,
            $filteredPages,
            $this->mconf,
            $this->conf,
            $banned,
            $this->excludedDoktypes,
            $this->getCurrentSite(),
            GeneralUtility::makeInstance(Context::class),
            $this->request->getAttribute('frontend.page.information')->getPageRecord()
        $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
        return $event->getFilteredMenuItems();
    }

    /**
     * Main function for retrieving menu items based on the menu type (special or sectionIndex or "normal")
     *
     * @return array
     */
    protected function prepareMenuItems()
    {
        $menuItems = [];
        $alternativeSortingField = trim($this->mconf['alternativeSortingField'] ?? '') ?: 'sorting';

        // Additional where clause, usually starts with AND (as usual with all additionalWhere functionality in TS)
        $additionalWhere = $this->parent_cObj->stdWrapValue('additionalWhere', $this->mconf);
        $additionalWhere .= $this->getDoktypeExcludeWhere();

        // ... only for the FIRST level of a HMENU
        if ($this->menuNumber == 1 && ($this->conf['special'] ?? false)) {
            $value = (string)$this->parent_cObj->stdWrapValue('value', $this->conf['special.'] ?? [], null);
            switch ($this->conf['special']) {
                case 'userfunction':
                    $menuItems = $this->prepareMenuItemsForUserSpecificMenu($value, $alternativeSortingField);
                    break;
                case 'language':
                    $menuItems = $this->prepareMenuItemsForLanguageMenu($value);
                    break;
                case 'directory':
                    $menuItems = $this->prepareMenuItemsForDirectoryMenu($value, $alternativeSortingField);
                    break;
                case 'list':
                    $menuItems = $this->prepareMenuItemsForListMenu($value);
                    break;
                case 'updated':
                    $menuItems = $this->prepareMenuItemsForUpdatedMenu(
                        $value,
                        $this->mconf['alternativeSortingField'] ?? ''
                    );
                    break;
                case 'keywords':
                    $menuItems = $this->prepareMenuItemsForKeywordsMenu(
                        $value,
                        $this->mconf['alternativeSortingField'] ?? ''
                    );
                    break;
                case 'categories':
                    $categoryMenuUtility = GeneralUtility::makeInstance(CategoryMenuUtility::class);
                    $menuItems = $categoryMenuUtility->collectPages($value, $this->conf['special.'], $this);
                    break;
                case 'rootline':
                    $menuItems = $this->prepareMenuItemsForRootlineMenu();
                    break;
                case 'browse':
                    $menuItems = $this->prepareMenuItemsForBrowseMenu($value, $alternativeSortingField, $additionalWhere);
            if ($this->mconf['sectionIndex'] ?? false) {
                $sectionIndexes = [];
                foreach ($menuItems as $page) {
                    $sectionIndexes = $sectionIndexes + $this->sectionIndex($alternativeSortingField, $page['uid']);
                }
                $menuItems = $sectionIndexes;
            }
        } elseif ($this->alternativeMenuTempArray !== []) {
            // Setting $menuItems array if not level 1.
            $menuItems = $this->alternativeMenuTempArray;
        } elseif ($this->mconf['sectionIndex'] ?? false) {
            $menuItems = $this->sectionIndex($alternativeSortingField);
        } else {
            // Default: Gets a hierarchical menu based on subpages of $this->id
            $subMenuDecision = $this->getRuntimeCache()->get($this->getCacheIdentifierForSubMenuDecision($this->id));
            if (!isset($subMenuDecision['result']) || $subMenuDecision['result'] === true) {
                $menuItems = $this->sys_page->getMenu($this->id, '*', $alternativeSortingField, $additionalWhere, true, $this->disableGroupAccessCheck);
            }
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = userfunction is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForUserSpecificMenu($specialValue, $sortingField)
    {
        $menuItems = $this->parent_cObj->callUserFunction(
            $this->conf['special.']['userFunc'],
            array_merge($this->conf['special.'], ['value' => $specialValue, '_altSortField' => $sortingField]),
        return is_array($menuItems) ? $menuItems : [];
    }

    /**
     * Fetches all menuitems if special = language is set
     *
     * @param string $specialValue The value from special.value
     * @return array
     */
    protected function prepareMenuItemsForLanguageMenu($specialValue)
    {
        $menuItems = [];
        // Getting current page record NOT overlaid by any translation:
        $pageRecord = $this->request->getAttribute('frontend.page.information')->getPageRecord();
        $currentPageWithNoOverlay = ($pageRecord['_TRANSLATION_SOURCE'] ?? null)?->toArray(true) ?? $pageRecord;
        $languages = $this->getCurrentSite()->getLanguages();
        if ($specialValue === 'auto') {
            $languageItems = array_keys($languages);
        } else {
            $languageItems = GeneralUtility::intExplode(',', $specialValue);
        }

        $tsfe = $this->getTypoScriptFrontendController();
        $tsfe->register['languages_HMENU'] = implode(',', $languageItems);

        $currentLanguageId = $this->getCurrentLanguageAspect()->getId();

        // @todo Fetch all language overlays in a single query
        foreach ($languageItems as $sUid) {
            // Find overlay record:
            if ($sUid) {
                $languageAspect = LanguageAspectFactory::createFromSiteLanguage($languages[$sUid]);
                $pageRepository = $this->buildPageRepository($languageAspect);
                $lRecs = $pageRepository->getPageOverlay($currentPageWithNoOverlay, $languageAspect);
                // getPageOverlay() might return the original record again, if so this is emptied
                // this should be fixed in PageRepository in the future.
                if (!empty($lRecs) && !isset($lRecs['_LOCALIZED_UID'])) {
                $lRecs = [];
            }
            // Checking if the "disabled" state should be set.
            $pageTranslationVisibility = new PageTranslationVisibility((int)($currentPageWithNoOverlay['l18n_cfg'] ?? 0));
            if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() && $sUid &&
                empty($lRecs) || $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() &&
                (!$sUid || empty($lRecs)) ||
                !($this->conf['special.']['normalWhenNoLanguage'] ?? false) && $sUid && empty($lRecs)
                $iState = $currentLanguageId === $sUid ? 'USERDEF2' : 'USERDEF1';
                $iState = $currentLanguageId === $sUid ? 'ACT' : 'NO';
            }
            // Adding menu item:
            $menuItems[] = array_merge(
                array_merge($currentPageWithNoOverlay, $lRecs),
                    '_REQUESTED_OVERLAY_LANGUAGE' => $sUid,
                    'ITEM_STATE' => $iState,
                    '_ADD_GETVARS' => $this->conf['addQueryString'] ?? false,
    /**
     * Builds PageRepository instance without depending on global context, e.g.
     * not automatically overlaying records based on current request language.
     */
    protected function buildPageRepository(?LanguageAspect $languageAspect = null): PageRepository
    {
        // clone global context object (singleton)
        $context = clone GeneralUtility::makeInstance(Context::class);
        $context->setAspect(
            'language',
            $languageAspect ?? GeneralUtility::makeInstance(LanguageAspect::class)
        );
        return GeneralUtility::makeInstance(
            PageRepository::class,
            $context
        );
    }

    /**
     * Fetches all menuitems if special = directory is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForDirectoryMenu($specialValue, $sortingField)
    {
        $menuItems = [];
        if ($specialValue == '') {
            $specialValue = $this->request->getAttribute('frontend.page.information')->getId();
        $items = GeneralUtility::intExplode(',', (string)$specialValue);
        $pageLinkBuilder = GeneralUtility::makeInstance(PageLinkBuilder::class, $this->parent_cObj);
        foreach ($items as $id) {
            $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($id);
            // Checking if a page is a mount page and if so, change the ID and set the MP var properly.
            $mount_info = $this->sys_page->getMountPointInfo($id);
            if (is_array($mount_info)) {
                if ($mount_info['overlay']) {
                    // Overlays should already have their full MPvars calculated:
                    $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps((int)$mount_info['mount_pid']);
                    $MP = $MP ?: $mount_info['MPvar'];
                } else {
                    $MP = ($MP ? $MP . ',' : '') . $mount_info['MPvar'];
                }
                $id = $mount_info['mount_pid'];
            }
            $subPages = $this->sys_page->getMenu($id, '*', $sortingField, '', true, $this->disableGroupAccessCheck);
            foreach ($subPages as $row) {
                // Add external MP params
                if ($MP) {
                    $row['_MP_PARAM'] = $MP . (($row['_MP_PARAM'] ?? '') ? ',' . $row['_MP_PARAM'] : '');
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = list is set
     *
     * @param string $specialValue The value from special.value
     * @return array
     */
    protected function prepareMenuItemsForListMenu($specialValue)
    {
        $menuItems = [];
        if ($specialValue == '') {
            $specialValue = $this->id;
        }
        $pageIds = GeneralUtility::intExplode(',', (string)$specialValue);
        $disableGroupAccessCheck = !empty($this->mconf['showAccessRestrictedPages']);
        $pageRecords = $this->sys_page->getMenuForPages($pageIds, '*', 'sorting', '', true, $disableGroupAccessCheck);
        // After fetching the page records, restore the initial order by using the page id list as arrays keys and
        // replace them with the resolved page records. The id list is cleaned up first, since ids might be invalid.
        $pageRecords = array_replace(
            array_flip(array_intersect(array_values($pageIds), array_keys($pageRecords))),
            $pageRecords
        );
        $pageLinkBuilder = GeneralUtility::makeInstance(PageLinkBuilder::class, $this->parent_cObj);
        foreach ($pageRecords as $row) {
            $pageId = (int)$row['uid'];
            $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($pageId);
            $mount_info = $this->sys_page->getMountPointInfo($pageId, $row);
            // $pageId is a valid mount point
            if (is_array($mount_info) && $mount_info['overlay']) {
                $mountedPageId = (int)$mount_info['mount_pid'];
                // Using "getPage" is OK since we need the check for enableFields
                // AND for type 2 of mount pids we DO require a doktype < 200!
                $mountedPageRow = $this->sys_page->getPage($mountedPageId, $disableGroupAccessCheck);
                if (empty($mountedPageRow)) {
                    // If the mount point could not be fetched with respect to
                    // enableFields, the page should not become a part of the menu!
                    continue;
                $row = $mountedPageRow;
                $row['_MP_PARAM'] = $mount_info['MPvar'];
                // Overlays should already have their full MPvars calculated, that's why we unset the
                // existing $row['_MP_PARAM'], as the full $MP will be added again below
                $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($mountedPageId);
            if ($MP) {
                $row['_MP_PARAM'] = $MP . ($row['_MP_PARAM'] ? ',' . $row['_MP_PARAM'] : '');
            }
            $menuItems[] = $row;
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = updated is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForUpdatedMenu($specialValue, $sortingField)
    {
        $menuItems = [];
        if ($specialValue == '') {
            $specialValue = $this->request->getAttribute('frontend.page.information')->getId();
        $items = GeneralUtility::intExplode(',', (string)$specialValue);
        if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'] ?? null)) {
            $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 1, 20);
        } else {
            $depth = 20;
        }
        // Max number of items
        $limit = MathUtility::forceIntegerInRange(($this->conf['special.']['limit'] ?? 0), 0, 100);
        $maxAge = (int)($this->parent_cObj->calc($this->conf['special.']['maxAge'] ?? 0));
        // 'auto', 'manual', 'tstamp'
        $mode = $this->conf['special.']['mode'] ?? '';
        $beginAtLevel = MathUtility::forceIntegerInRange(($this->conf['special.']['beginAtLevel'] ?? 0), 0, 100);
        foreach ($items as $id) {
            // Exclude the current ID if beginAtLevel is > 0
            if ($beginAtLevel > 0) {
                $pageIds = array_merge($pageIds, $this->sys_page->getDescendantPageIdsRecursive($id, $depth - 1 + $beginAtLevel, $beginAtLevel - 1));
                $pageIds = array_merge($pageIds, [$id], $this->sys_page->getDescendantPageIdsRecursive($id, $depth - 1 + $beginAtLevel, $beginAtLevel - 1));
        $sortField = $this->getMode($mode);

        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
        $extraWhere = ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
        if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
            $extraWhere .= sprintf(' AND %s=%s', $connection->quoteIdentifier('pages.no_search'), $connection->quote('0'));
            $extraWhere .= sprintf(' AND %s>%s', $connection->quoteIdentifier($sortField), $connection->quote((string)($GLOBALS['SIM_ACCESS_TIME'] - $maxAge)));
        $extraWhere = sprintf('%s>=%s', $connection->quoteIdentifier($sortField), $connection->quote('0')) . $extraWhere;
        $pageRecords = $this->sys_page->getMenuForPages($pageIds, '*', $sortingField ?: $sortField . ' DESC', $extraWhere, true, $this->disableGroupAccessCheck);
        foreach ($pageRecords as $row) {
            // Build a custom LIMIT clause as "getMenuForPages()" does not support this
            if (++$i > $limit) {
            $menuItems[$row['uid']] = $row;
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = keywords is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForKeywordsMenu($specialValue, $sortingField)
    {
        $menuItems = [];
        [$specialValue] = GeneralUtility::intExplode(',', $specialValue);
            $specialValue = $this->request->getAttribute('frontend.page.information')->getId();
        if (($this->conf['special.']['setKeywords'] ?? false) || ($this->conf['special.']['setKeywords.'] ?? false)) {
            $kw = (string)$this->parent_cObj->stdWrapValue('setKeywords', $this->conf['special.'] ?? []);
        } else {
            // The page record of the 'value'.
            $value_rec = $this->sys_page->getPage((int)$specialValue);
            $kfieldSrc = ($this->conf['special.']['keywordsField.']['sourceField'] ?? false) ? $this->conf['special.']['keywordsField.']['sourceField'] : 'keywords';
            $kw = trim($this->parent_cObj->keywords($value_rec[$kfieldSrc] ?? ''));
        }
        // *'auto', 'manual', 'tstamp'
        $mode = $this->conf['special.']['mode'] ?? '';
        $sortField = $this->getMode($mode);
        // Depth, limit, extra where
        if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'] ?? null)) {
            $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 0, 20);
        } else {
            $depth = 20;
        }
        // Max number of items
        $limit = MathUtility::forceIntegerInRange(($this->conf['special.']['limit'] ?? 0), 0, 100);
        $localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
        $eLevel = $this->parent_cObj->getKey(
            $this->parent_cObj->stdWrapValue('entryLevel', $this->conf['special.'] ?? []),
        $startUid = (int)($localRootLine[$eLevel]['uid'] ?? 0);
        // Which field is for keywords
        $kfield = 'keywords';
        if ($this->conf['special.']['keywordsField'] ?? false) {
            [$kfield] = explode(' ', trim($this->conf['special.']['keywordsField']));
        // If there are keywords and the startUid is present
        if ($kw && $startUid) {
            $bA = MathUtility::forceIntegerInRange(($this->conf['special.']['beginAtLevel'] ?? 0), 0, 100);
            $id_list = $this->sys_page->getDescendantPageIdsRecursive($startUid, $depth - 1 + $bA, $bA - 1);
            $id_list = array_merge([(int)$startUid], $id_list);
            $kwArr = GeneralUtility::trimExplode(',', $kw, true);
            $keyWordsWhereArr = [];
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
            foreach ($kwArr as $word) {
                $keyWordsWhereArr[] = $queryBuilder->expr()->like(
                    $kfield,
                    $queryBuilder->createNamedParameter(
                        '%' . $queryBuilder->escapeLikeWildcards($word) . '%'
            $queryBuilder
                ->select('*')
                ->from('pages')
                ->where(
                    $queryBuilder->expr()->in(
                        'uid',
                    ),
                    $queryBuilder->expr()->neq(
                        'uid',
                        $queryBuilder->createNamedParameter($specialValue, Connection::PARAM_INT)
            if (!empty($keyWordsWhereArr)) {
                $queryBuilder->andWhere($queryBuilder->expr()->or(...$keyWordsWhereArr));
            if (!empty($this->excludedDoktypes)) {
                $queryBuilder->andWhere(
                    $queryBuilder->expr()->notIn(
                        'pages.doktype',
                        $this->excludedDoktypes
                    )
                );
            }

            if (!$this->conf['includeNotInMenu']) {
                $queryBuilder->andWhere($queryBuilder->expr()->eq('pages.nav_hide', 0));
            }

            if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
                $queryBuilder->andWhere($queryBuilder->expr()->eq('pages.no_search', 0));
            }

            if ($limit > 0) {
                $queryBuilder->setMaxResults($limit);
            }

            if ($sortingField) {
                $queryBuilder->orderBy($sortingField);
            } else {
                $queryBuilder->orderBy($sortField, 'desc');
            }

            $result = $queryBuilder->executeQuery();
            while ($row = $result->fetchAssociative()) {
                $this->sys_page->versionOL('pages', $row, true);
                if (is_array($row)) {
                    $menuItems[$row['uid']] = $this->sys_page->getPageOverlay($row);
                }
            }
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = rootline is set
     *
     * @return array
     */
    protected function prepareMenuItemsForRootlineMenu()
    {
        $menuItems = [];
        $range = (string)$this->parent_cObj->stdWrapValue('range', $this->conf['special.'] ?? []);
        $begin_end = explode('|', $range);
        $begin_end[0] = (int)$begin_end[0];
        if (!MathUtility::canBeInterpretedAsInteger($begin_end[1] ?? '')) {
        $localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
        $beginKey = $this->parent_cObj->getKey($begin_end[0], $localRootLine ?? []);
        $endKey = $this->parent_cObj->getKey($begin_end[1], $localRootLine ?? []);
        if ($endKey < $beginKey) {
            $endKey = $beginKey;
        }
        $rl_MParray = [];
        foreach ($localRootLine as $k_rl => $v_rl) {
            // For overlaid mount points, set the variable right now:
            if (($v_rl['_MP_PARAM'] ?? false) && ($v_rl['_MOUNT_OL'] ?? false)) {
                $rl_MParray[] = $v_rl['_MP_PARAM'];
            }
            // Traverse rootline:
            if ($k_rl >= $beginKey && $k_rl <= $endKey) {
                $temp_key = $k_rl;
                $menuItems[$temp_key] = $this->sys_page->getPage((int)$v_rl['uid']);
                if (!empty($menuItems[$temp_key])) {
                    // If there are no specific target for the page, put the level specific target on.
                    if (!$menuItems[$temp_key]['target']) {
                        $menuItems[$temp_key]['target'] = $this->conf['special.']['targets.'][$k_rl] ?? '';
                        $menuItems[$temp_key]['_MP_PARAM'] = implode(',', $rl_MParray);
                    }
                } else {
                    unset($menuItems[$temp_key]);
                }
            }
            // For normal mount points, set the variable for next level.
            if (($v_rl['_MP_PARAM'] ?? false) && !($v_rl['_MOUNT_OL'] ?? false)) {
                $rl_MParray[] = $v_rl['_MP_PARAM'];
            }
        }
        // Reverse order of elements (e.g. "1,2,3,4" gets "4,3,2,1"):
        if (isset($this->conf['special.']['reverseOrder']) && $this->conf['special.']['reverseOrder']) {
            $menuItems = array_reverse($menuItems);
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = browse is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @param string $additionalWhere Additional WHERE clause
     * @return array
     */
    protected function prepareMenuItemsForBrowseMenu($specialValue, $sortingField, $additionalWhere)
    {
        $menuItems = [];
        [$specialValue] = GeneralUtility::intExplode(',', $specialValue);
            $specialValue = $this->request->getAttribute('frontend.page.information')->getPageRecord()['uid'];
        $localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
        // Will not work out of rootline
        if ($specialValue != ($localRootLine[0]['uid'] ?? null)) {
            $recArr = [];
            // The page id of the 'value'
            $value_rec_pid = $this->sys_page->getPage((int)$specialValue, $this->disableGroupAccessCheck)['pid'] ?? null;
            // 'up' page cannot be outside rootline
            if ($value_rec_pid) {
                // The page record of 'up'.
                $recArr['up'] = $this->sys_page->getPage((int)$value_rec_pid, $this->disableGroupAccessCheck);
            }
            // If the 'up' item was NOT level 0 in rootline...
            if (($recArr['up']['pid'] ?? 0) && $value_rec_pid != ($localRootLine[0]['uid'] ?? null)) {
                // The page record of "index".
                $recArr['index'] = $this->sys_page->getPage((int)$recArr['up']['pid']);
            }
            // check if certain pages should be excluded
            $additionalWhere .= ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
            if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
                $additionalWhere .= ' AND pages.no_search=0';