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