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