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