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
*/
/**
* 0 = rootFolder
*
* @var int
*/
/**
* Doktypes that define which should not be included in a menu
*
protected $excludedDoktypes = [PageRepository::DOKTYPE_BE_USER_SECTION, PageRepository::DOKTYPE_SYSFOLDER];
/**
* @var int[]
*/
protected $alwaysActivePIDlist = [];
/**
* Loaded with the parent cObj-object when a new HMENU is made
*
* @var ContentObjectRenderer
public $parent_cObj;
/**
* accumulation of mount point data
*
* @var string[]
*/
/**
* HMENU configuration
*
* @var array
*/
* xMENU configuration (TMENU etc)
*
* @var array
*/
/**
* 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
*/
/**
* The array of menuItems which is built
*
* @var array[]
*/
/**
* @var array
*/
/**
* 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)
*
/**
* @var mixed[]
*/
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.
*
*/
protected $parentMenuArrItemKey;
/**
* @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',
'USERDEF2',
/**
* 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 . '.'];
$this->request = $request;
// 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']);
}
// includeNotInMenu initialized:
$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),
$localRootLine
);
// 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 = [];
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'] .
(
!empty($rl_MParray)
? ':' . 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);
if ($value === '') {
$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) .
(
!empty($nextMParray)
? ':' . 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;
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) {
'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);
// 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();
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()
{
$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) {
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)
{
// 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,
'_SAFE' => true,
);
}
return $menuItems;
}
/**
* 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)
{
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']);
} 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'] : '');
$menuItems[] = $row;
return $menuItems;
}
/**
* Fetches all menuitems if special = list is set
*
* @param string $specialValue The value from special.value
* @return array
*/
protected function prepareMenuItemsForListMenu($specialValue)
{
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);
// Keep mount point?
$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);
unset($row['_MP_PARAM']);
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)
{
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));
if (!$limit) {
$limit = 10;
}
// '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));
}
// Get sortField (mode)
$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'));
}
if ($maxAge > 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
$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)
{
[$specialValue] = GeneralUtility::intExplode(',', $specialValue);
if (!$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.'] ?? []),
$localRootLine
$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);
$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',
)
);
}
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()
{
$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] ?? '')) {
$begin_end[1] = -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;
}
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)
{
[$specialValue] = GeneralUtility::intExplode(',', $specialValue);
if (!$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)) {
// 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
// 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';