AbstractMenuContentObject.php 81.4 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
15

16
17
namespace TYPO3\CMS\Frontend\ContentObject\Menu;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use Psr\Log\LogLevel;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
23
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
26
use TYPO3\CMS\Core\Site\Entity\Site;
27
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
28
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
29
use TYPO3\CMS\Core\TypoScript\TemplateService;
30
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Core\Utility\MathUtility;
33
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
34
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
35
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
36
use TYPO3\CMS\Frontend\Event\FilterMenuItemsEvent;
37
use TYPO3\CMS\Frontend\Typolink\PageLinkBuilder;
38

39
/**
40
 * Generating navigation/menus from TypoScript
41
 *
42
43
 * The HMENU content object uses this (or more precisely one of the extension classes).
 * Among others the class generates an array of menu items. Thereafter functions from the subclasses are called.
44
 * The class is always used through extension classes like TextMenuContentObject.
45
 */
46
47
48
49
50
51
52
abstract class AbstractMenuContentObject
{
    /**
     * tells you which menu number this is. This is important when getting data from the setup
     *
     * @var int
     */
53
    protected $menuNumber = 1;
54
55
56
57
58
59

    /**
     * 0 = rootFolder
     *
     * @var int
     */
60
    protected $entryLevel = 0;
61
62
63
64

    /**
     * Doktypes that define which should not be included in a menu
     *
65
     * @var int[]
66
     */
67
    protected $excludedDoktypes = [PageRepository::DOKTYPE_BE_USER_SECTION, PageRepository::DOKTYPE_SYSFOLDER];
68
69
70
71

    /**
     * @var int[]
     */
72
    protected $alwaysActivePIDlist = [];
73
74
75
76

    /**
     * Loaded with the parent cObj-object when a new HMENU is made
     *
77
     * @var ContentObjectRenderer
78
     */
79
    public $parent_cObj;
80
81
82
83
84
85

    /**
     * accumulation of mount point data
     *
     * @var string[]
     */
86
    protected $MP_array = [];
87
88
89
90
91
92

    /**
     * HMENU configuration
     *
     * @var array
     */
93
    protected $conf = [];
94
95

    /**
96
     * xMENU configuration (TMENU etc)
97
98
99
     *
     * @var array
     */
100
    protected $mconf = [];
101
102

    /**
103
     * @var TemplateService
104
     */
105
    protected $tmpl;
106
107

    /**
108
     * @var PageRepository
109
     */
110
    protected $sys_page;
111
112
113
114
115
116

    /**
     * The base page-id of the menu.
     *
     * @var int
     */
117
    protected $id;
118
119
120
121
122
123
124

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

    /**
     * The array of menuItems which is built
     *
     * @var array[]
     */
132
    protected $menuArr;
133
134
135
136

    /**
     * @var string
     */
137
    protected $hash;
138
139
140
141

    /**
     * @var array
     */
142
    protected $result = [];
143
144
145
146
147

    /**
     * Is filled with an array of page uid numbers + RL parameters which are in the current
     * root line (used to evaluate whether a menu item is in active state)
     *
148
     * @var array
149
     */
150
    protected $rL_uidRegister;
151
152
153
154

    /**
     * @var mixed[]
     */
155
    protected $I;
156
157
158
159

    /**
     * @var string
     */
160
    protected $WMresult;
161
162
163
164

    /**
     * @var int
     */
165
    protected $WMmenuItems;
166
167
168
169

    /**
     * @var array[]
     */
170
    protected $WMsubmenuObjSuffixes;
171
172
173
174

    /**
     * @var ContentObjectRenderer
     */
175
    protected $WMcObj;
176

177
178
    protected ?ServerRequestInterface $request = null;

179
180
181
    /**
     * Can be set to contain menu item arrays for sub-levels.
     *
182
     * @var array
183
     */
184
    protected $alternativeMenuTempArray = [];
185

186
187
188
    /**
     * Array key of the parentMenuItem in the parentMenuArr, if this menu is a subMenu.
     *
189
     * @var int|null
190
191
192
     */
    protected $parentMenuArrItemKey;

193
194
195
196
197
    /**
     * @var array
     */
    protected $parentMenuArr;

198
199
200
201
202
203
204
205
206
207
208
209
210
    protected const customItemStates = [
        // IFSUB is TRUE if there exist submenu items to the current item
        'IFSUB',
        'ACT',
        // ACTIFSUB is TRUE if there exist submenu items to the current item and the current item is active
        'ACTIFSUB',
        // CUR is TRUE if the current page equals the item here!
        'CUR',
        // CURIFSUB is TRUE if there exist submenu items to the current item and the current page equals the item here!
        'CURIFSUB',
        'USR',
        'SPC',
        'USERDEF1',
211
        'USERDEF2',
212
213
    ];

214
215
216
217
218
219
220
221
222
    /**
     * The initialization of the object. This just sets some internal variables.
     *
     * @param TemplateService $tmpl The $this->getTypoScriptFrontendController()->tmpl object
     * @param PageRepository $sys_page The $this->getTypoScriptFrontendController()->sys_page object
     * @param int|string $id A starting point page id. This should probably be blank since the 'entryLevel' value will be used then.
     * @param array $conf The TypoScript configuration for the HMENU cObject
     * @param int $menuNumber Menu number; 1,2,3. Should probably be 1
     * @param string $objSuffix Submenu Object suffix. This offers submenus a way to use alternative configuration for specific positions in the menu; By default "1 = TMENU" would use "1." for the TMENU configuration, but if this string is set to eg. "a" then "1a." would be used for configuration instead (while "1 = " is still used for the overall object definition of "TMENU")
223
     * @param ServerRequestInterface|null $request
224
225
226
     * @return bool Returns TRUE on success
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::HMENU()
     */
227
    public function start($tmpl, $sys_page, $id, $conf, $menuNumber, $objSuffix = '', ?ServerRequestInterface $request = null)
228
229
230
231
232
    {
        $tsfe = $this->getTypoScriptFrontendController();
        $this->conf = $conf;
        $this->menuNumber = $menuNumber;
        $this->mconf = $conf[$this->menuNumber . $objSuffix . '.'];
233
        $this->request = $request;
234
        $this->WMcObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
235
        // Sets the internal vars. $tmpl MUST be the template-object. $sys_page MUST be the PageRepository object
236
237
238
239
        if ($this->conf[$this->menuNumber . $objSuffix] && is_object($tmpl) && is_object($sys_page)) {
            $this->tmpl = $tmpl;
            $this->sys_page = $sys_page;
            // alwaysActivePIDlist initialized:
240
            $this->conf['alwaysActivePIDlist'] = (string)$this->parent_cObj->stdWrapValue('alwaysActivePIDlist', $this->conf ?? []);
241
            if (trim($this->conf['alwaysActivePIDlist'])) {
242
243
                $this->alwaysActivePIDlist = GeneralUtility::intExplode(',', $this->conf['alwaysActivePIDlist']);
            }
244
            // includeNotInMenu initialized:
245
            $this->conf['includeNotInMenu'] = $this->parent_cObj->stdWrapValue('includeNotInMenu', $this->conf, false);
246
            // exclude doktypes that should not be shown in menu (e.g. backend user section)
247
            if ($this->conf['excludeDoktypes'] ?? false) {
248
                $this->excludedDoktypes = GeneralUtility::intExplode(',', $this->conf['excludeDoktypes']);
249
250
251
            }
            // EntryLevel
            $this->entryLevel = $this->parent_cObj->getKey(
252
                $this->parent_cObj->stdWrapValue('entryLevel', $this->conf ?? []),
253
254
255
256
257
258
259
260
                $this->tmpl->rootLine
            );
            // Set parent page: If $id not stated with start() then the base-id will be found from rootLine[$this->entryLevel]
            // Called as the next level in a menu. It is assumed that $this->MP_array is set from parent menu.
            if ($id) {
                $this->id = (int)$id;
            } else {
                // This is a BRAND NEW menu, first level. So we take ID from rootline and also find MP_array (mount points)
261
262
                $this->id = (int)($this->tmpl->rootLine[$this->entryLevel]['uid'] ?? 0);

263
264
265
266
                // Traverse rootline to build MP_array of pages BEFORE the entryLevel
                // (MP var for ->id is picked up in the next part of the code...)
                foreach ($this->tmpl->rootLine as $entryLevel => $levelRec) {
                    // For overlaid mount points, set the variable right now:
267
                    if (($levelRec['_MP_PARAM'] ?? false) && ($levelRec['_MOUNT_OL'] ?? false)) {
268
269
                        $this->MP_array[] = $levelRec['_MP_PARAM'];
                    }
270

271
272
273
274
                    // Break when entry level is reached:
                    if ($entryLevel >= $this->entryLevel) {
                        break;
                    }
275

276
                    // For normal mount points, set the variable for next level.
277
                    if (!empty($levelRec['_MP_PARAM']) && empty($levelRec['_MOUNT_OL'])) {
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
                        $this->MP_array[] = $levelRec['_MP_PARAM'];
                    }
                }
            }
            // Return FALSE if no page ID was set (thus no menu of subpages can be made).
            if ($this->id <= 0) {
                return false;
            }
            // Check if page is a mount point, and if so set id and MP_array
            // (basically this is ONLY for non-overlay mode, but in overlay mode an ID with a mount point should never reach this point anyways, so no harm done...)
            $mount_info = $this->sys_page->getMountPointInfo($this->id);
            if (is_array($mount_info)) {
                $this->MP_array[] = $mount_info['MPvar'];
                $this->id = $mount_info['mount_pid'];
            }
            // Gather list of page uids in root line (for "isActive" evaluation). Also adds the MP params in the path so Mount Points are respected.
            // (List is specific for this rootline, so it may be supplied from parent menus for speed...)
295
296
            if ($this->rL_uidRegister === null) {
                $this->rL_uidRegister = [];
297
                $rl_MParray = [];
298
299
                foreach ($this->tmpl->rootLine as $v_rl) {
                    // For overlaid mount points, set the variable right now:
300
                    if (($v_rl['_MP_PARAM'] ?? false) && ($v_rl['_MOUNT_OL'] ?? false)) {
301
302
303
304
                        $rl_MParray[] = $v_rl['_MP_PARAM'];
                    }
                    // Add to register:
                    $this->rL_uidRegister[] = 'ITEM:' . $v_rl['uid'] .
305
306
                        (
                            !empty($rl_MParray)
307
308
309
310
                            ? ':' . implode(',', $rl_MParray)
                            : ''
                        );
                    // For normal mount points, set the variable for next level.
311
                    if (($v_rl['_MP_PARAM'] ?? false) && !($v_rl['_MOUNT_OL'] ?? false)) {
312
313
314
315
                        $rl_MParray[] = $v_rl['_MP_PARAM'];
                    }
                }
            }
316
            // Set $directoryLevel so the following evaluation of the nextActive will not return
317
318
            // an invalid value if .special=directory was set
            $directoryLevel = 0;
319
            if (($this->conf['special'] ?? '') === 'directory') {
320
                $value = $this->parent_cObj->stdWrapValue('value', $this->conf['special.'] ?? [], null);
321
                if ($value === '') {
322
                    $value = (string)$tsfe->id;
323
324
325
326
327
328
329
                }
                $directoryLevel = (int)$tsfe->tmpl->getRootlineLevel($value);
            }
            // Setting "nextActive": This is the page uid + MPvar of the NEXT page in rootline. Used to expand the menu if we are in the right branch of the tree
            // Notice: The automatic expansion of a menu is designed to work only when no "special" modes (except "directory") are used.
            $startLevel = $directoryLevel ?: $this->entryLevel;
            $currentLevel = $startLevel + $this->menuNumber;
330
            if (is_array($this->tmpl->rootLine[$currentLevel] ?? null)) {
331
                $nextMParray = $this->MP_array;
332
                if (empty($nextMParray) && !($this->tmpl->rootLine[$currentLevel]['_MOUNT_OL'] ?? false) && $currentLevel > 0) {
333
334
335
336
337
338
339
340
                    // Make sure to slide-down any mount point information (_MP_PARAM) to children records in the rootline
                    // otherwise automatic expansion will not work
                    $parentRecord = $this->tmpl->rootLine[$currentLevel - 1];
                    if (isset($parentRecord['_MP_PARAM'])) {
                        $nextMParray[] = $parentRecord['_MP_PARAM'];
                    }
                }
                // In overlay mode, add next level MPvars as well:
341
                if ($this->tmpl->rootLine[$currentLevel]['_MOUNT_OL'] ?? false) {
342
343
344
                    $nextMParray[] = $this->tmpl->rootLine[$currentLevel]['_MP_PARAM'];
                }
                $this->nextActive = $this->tmpl->rootLine[$currentLevel]['uid'] .
345
346
                    (
                        !empty($nextMParray)
347
348
349
350
351
352
                        ? ':' . implode(',', $nextMParray)
                        : ''
                    );
            } else {
                $this->nextActive = '';
            }
353
            return true;
354
        }
355
        $this->getTimeTracker()->setTSlogMessage('ERROR in menu', LogLevel::ERROR);
356
        return false;
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
    }

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

        // Initializing showAccessRestrictedPages
        $SAVED_where_groupAccess = '';
373
        if ($this->mconf['showAccessRestrictedPages'] ?? false) {
374
375
376
377
378
379
380
381
382
383
            // SAVING where_groupAccess
            $SAVED_where_groupAccess = $this->sys_page->where_groupAccess;
            // Temporarily removing fe_group checking!
            $this->sys_page->where_groupAccess = '';
        }

        $menuItems = $this->prepareMenuItems();

        $c = 0;
        $c_b = 0;
384
385
386
387

        $minItems = (int)(($this->mconf['minItems'] ?? 0) ?: ($this->conf['minItems'] ?? 0));
        $maxItems = (int)(($this->mconf['maxItems'] ?? 0) ?: ($this->conf['maxItems'] ?? 0));
        $begin = $this->parent_cObj->calc(($this->mconf['begin'] ?? 0) ?: ($this->conf['begin'] ?? 0));
388
        $minItemsConf = $this->mconf['minItems.'] ?? $this->conf['minItems.'] ?? null;
389
        $minItems = is_array($minItemsConf) ? $this->parent_cObj->stdWrap($minItems, $minItemsConf) : $minItems;
390
        $maxItemsConf = $this->mconf['maxItems.'] ?? $this->conf['maxItems.'] ?? null;
391
        $maxItems = is_array($maxItemsConf) ? $this->parent_cObj->stdWrap($maxItems, $maxItemsConf) : $maxItems;
392
        $beginConf = $this->mconf['begin.'] ?? $this->conf['begin.'] ?? null;
393
        $begin = is_array($beginConf) ? $this->parent_cObj->stdWrap($begin, $beginConf) : $begin;
394
        $this->menuArr = [];
395
396
397
398
399
400
401
402
403
404
405
406
407
408
        foreach ($menuItems as &$data) {
            $data = $this->determineOriginalShortcutPage($data);
            $data['isSpacer'] = ($data['isSpacer'] ?? false) || (int)($data['doktype'] ?? 0) === PageRepository::DOKTYPE_SPACER || ($data['ITEM_STATE'] ?? '') === 'SPC';
        }
        $menuItems = $this->removeInaccessiblePages($menuItems);
        // Fill in the menuArr with elements that should go into the menu
        foreach ($menuItems as $menuItem) {
            $c_b++;
            // If the beginning item has been reached, add the items.
            if ($begin <= $c_b) {
                $this->menuArr[$c] = $menuItem;
                $c++;
                if ($maxItems && $c >= $maxItems) {
                    break;
409
410
411
412
413
414
                }
            }
        }
        // Fill in fake items, if min-items is set.
        if ($minItems) {
            while ($c < $minItems) {
415
                $this->menuArr[$c] = [
416
                    'title' => '...',
417
                    'uid' => $this->getTypoScriptFrontendController()->id,
418
                ];
419
420
421
422
                $c++;
            }
        }
        //	Passing the menuArr through a user defined function:
423
        if ($this->mconf['itemArrayProcFunc'] ?? false) {
424
425
426
427
428
            $this->menuArr = $this->userProcess('itemArrayProcFunc', $this->menuArr);
        }
        // Setting number of menu items
        $this->getTypoScriptFrontendController()->register['count_menuItems'] = count($this->menuArr);
        $this->hash = md5(
429
430
431
432
            json_encode($this->menuArr) .
            json_encode($this->mconf) .
            json_encode($this->tmpl->rootLine) .
            json_encode($this->MP_array)
433
434
        );
        // Get the cache timeout:
435
        if ($this->conf['cache_period'] ?? false) {
436
437
438
439
440
441
442
443
            $cacheTimeout = $this->conf['cache_period'];
        } else {
            $cacheTimeout = $this->getTypoScriptFrontendController()->get_cache_timeout();
        }
        $cache = $this->getCache();
        $cachedData = $cache->get($this->hash);
        if (!is_array($cachedData)) {
            $this->generate();
444
            $cache->set($this->hash, $this->result, ['ident_MENUDATA'], (int)$cacheTimeout);
445
446
447
448
        } else {
            $this->result = $cachedData;
        }
        // End showAccessRestrictedPages
449
        if ($this->mconf['showAccessRestrictedPages'] ?? false) {
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
            // RESTORING where_groupAccess
            $this->sys_page->where_groupAccess = $SAVED_where_groupAccess;
        }
    }

    /**
     * Generates the the menu data.
     *
     * Subclasses should overwrite this method.
     */
    public function generate()
    {
    }

    /**
465
466
     * @return string The HTML for the menu
     */
467
468
469
470
471
472
473
474
    public function writeMenu()
    {
        return '';
    }

    /**
     * Gets an array of page rows and removes all, which are not accessible
     */
475
    protected function removeInaccessiblePages(array $pages): array
476
477
    {
        $banned = $this->getBannedUids();
478
        $filteredPages = [];
479
        foreach ($pages as $aPage) {
480
481
482
            $isSpacerPage = ((int)($aPage['doktype'] ?? 0) === PageRepository::DOKTYPE_SPACER) || ($aPage['isSpacer'] ?? false);
            if ($this->filterMenuPages($aPage, $banned, $isSpacerPage)) {
                $filteredPages[] = $aPage;
483
484
            }
        }
485
486
487
488
489
490
491
492
493
494
495
496
497
        $event = new FilterMenuItemsEvent(
            $pages,
            $filteredPages,
            $this->mconf,
            $this->conf,
            $banned,
            $this->excludedDoktypes,
            $this->getCurrentSite(),
            $this->getTypoScriptFrontendController()->getContext(),
            $this->getTypoScriptFrontendController()->page
        );
        $event = GeneralUtility::getContainer()->get(EventDispatcherInterface::class)->dispatch($event);
        return $event->getFilteredMenuItems();
498
499
500
501
502
503
504
505
506
    }

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

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

        // ... only for the FIRST level of a HMENU
515
        if ($this->menuNumber == 1 && ($this->conf['special'] ?? false)) {
516
            $value = (string)$this->parent_cObj->stdWrapValue('value', $this->conf['special.'] ?? [], null);
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
            switch ($this->conf['special']) {
                case 'userfunction':
                    $menuItems = $this->prepareMenuItemsForUserSpecificMenu($value, $alternativeSortingField);
                    break;
                case 'language':
                    $menuItems = $this->prepareMenuItemsForLanguageMenu($value);
                    break;
                case 'directory':
                    $menuItems = $this->prepareMenuItemsForDirectoryMenu($value, $alternativeSortingField);
                    break;
                case 'list':
                    $menuItems = $this->prepareMenuItemsForListMenu($value);
                    break;
                case 'updated':
                    $menuItems = $this->prepareMenuItemsForUpdatedMenu(
                        $value,
533
                        $this->mconf['alternativeSortingField'] ?? ''
534
535
536
537
538
                    );
                    break;
                case 'keywords':
                    $menuItems = $this->prepareMenuItemsForKeywordsMenu(
                        $value,
539
                        $this->mconf['alternativeSortingField'] ?? ''
540
541
542
543
544
545
546
547
548
549
550
                    );
                    break;
                case 'categories':
                    /** @var CategoryMenuUtility $categoryMenuUtility */
                    $categoryMenuUtility = GeneralUtility::makeInstance(CategoryMenuUtility::class);
                    $menuItems = $categoryMenuUtility->collectPages($value, $this->conf['special.'], $this);
                    break;
                case 'rootline':
                    $menuItems = $this->prepareMenuItemsForRootlineMenu();
                    break;
                case 'browse':
551
                    $menuItems = $this->prepareMenuItemsForBrowseMenu($value, $alternativeSortingField, $additionalWhere);
552
553
                    break;
            }
554
            if ($this->mconf['sectionIndex'] ?? false) {
555
                $sectionIndexes = [];
556
557
558
559
560
                foreach ($menuItems as $page) {
                    $sectionIndexes = $sectionIndexes + $this->sectionIndex($alternativeSortingField, $page['uid']);
                }
                $menuItems = $sectionIndexes;
            }
561
        } elseif ($this->alternativeMenuTempArray !== []) {
562
563
            // Setting $menuItems array if not level 1.
            $menuItems = $this->alternativeMenuTempArray;
564
        } elseif ($this->mconf['sectionIndex'] ?? false) {
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
            $menuItems = $this->sectionIndex($alternativeSortingField);
        } else {
            // Default: Gets a hierarchical menu based on subpages of $this->id
            $menuItems = $this->sys_page->getMenu($this->id, '*', $alternativeSortingField, $additionalWhere);
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = userfunction is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForUserSpecificMenu($specialValue, $sortingField)
    {
        $menuItems = $this->parent_cObj->callUserFunction(
            $this->conf['special.']['userFunc'],
584
            array_merge($this->conf['special.'], ['value' => $specialValue, '_altSortField' => $sortingField]),
585
586
            ''
        );
587
        return is_array($menuItems) ? $menuItems : [];
588
589
590
591
592
593
594
595
596
597
    }

    /**
     * Fetches all menuitems if special = language is set
     *
     * @param string $specialValue The value from special.value
     * @return array
     */
    protected function prepareMenuItemsForLanguageMenu($specialValue)
    {
598
        $menuItems = [];
599
600
        // Getting current page record NOT overlaid by any translation:
        $tsfe = $this->getTypoScriptFrontendController();
601
        $currentPageWithNoOverlay = $this->sys_page->getRawRecord('pages', $tsfe->id);
602
603
604
605

        if ($specialValue === 'auto') {
            $site = $this->getCurrentSite();
            $languages = $site->getLanguages();
606
            $languageItems = array_keys($languages);
607
608
609
610
611
612
        } else {
            $languageItems = GeneralUtility::intExplode(',', $specialValue);
        }

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

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

615
616
617
        foreach ($languageItems as $sUid) {
            // Find overlay record:
            if ($sUid) {
618
                $lRecs = $this->sys_page->getPageOverlay($currentPageWithNoOverlay, $sUid);
619
            } else {
620
                $lRecs = [];
621
622
            }
            // Checking if the "disabled" state should be set.
623
            $pageTranslationVisibility = new PageTranslationVisibility((int)($currentPageWithNoOverlay['l18n_cfg'] ?? 0));
624
625
            if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() && $sUid &&
                empty($lRecs) || $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() &&
626
                (!$sUid || empty($lRecs)) ||
627
                !($this->conf['special.']['normalWhenNoLanguage'] ?? false) && $sUid && empty($lRecs)
628
            ) {
629
                $iState = $currentLanguageId === $sUid ? 'USERDEF2' : 'USERDEF1';
630
            } else {
631
                $iState = $currentLanguageId === $sUid ? 'ACT' : 'NO';
632
633
634
635
            }
            // Adding menu item:
            $menuItems[] = array_merge(
                array_merge($currentPageWithNoOverlay, $lRecs),
636
                [
637
                    '_PAGES_OVERLAY_REQUESTEDLANGUAGE' => $sUid,
638
                    'ITEM_STATE' => $iState,
639
                    '_ADD_GETVARS' => $this->conf['addQueryString'] ?? false,
640
                    '_SAFE' => true,
641
                ]
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
            );
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = directory is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForDirectoryMenu($specialValue, $sortingField)
    {
        $tsfe = $this->getTypoScriptFrontendController();
657
        $menuItems = [];
658
        if ($specialValue == '') {
659
            $specialValue = $tsfe->id;
660
        }
661
        $items = GeneralUtility::intExplode(',', (string)$specialValue);
662
        $pageLinkBuilder = GeneralUtility::makeInstance(PageLinkBuilder::class, $this->parent_cObj);
663
        foreach ($items as $id) {
664
            $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($id);
665
666
667
668
669
            // Checking if a page is a mount page and if so, change the ID and set the MP var properly.
            $mount_info = $this->sys_page->getMountPointInfo($id);
            if (is_array($mount_info)) {
                if ($mount_info['overlay']) {
                    // Overlays should already have their full MPvars calculated:
670
                    $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps((int)$mount_info['mount_pid']);
671
                    $MP = $MP ?: $mount_info['MPvar'];
672
673
674
675
676
                } else {
                    $MP = ($MP ? $MP . ',' : '') . $mount_info['MPvar'];
                }
                $id = $mount_info['mount_pid'];
            }
677
678
679
680
681
            $subPages = $this->sys_page->getMenu($id, '*', $sortingField);
            foreach ($subPages as $row) {
                // Add external MP params
                if ($MP) {
                    $row['_MP_PARAM'] = $MP . (($row['_MP_PARAM'] ?? '') ? ',' . $row['_MP_PARAM'] : '');
682
                }
683
                $menuItems[] = $row;
684
685
            }
        }
686

687
688
689
690
691
692
693
694
695
696
697
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = list is set
     *
     * @param string $specialValue The value from special.value
     * @return array
     */
    protected function prepareMenuItemsForListMenu($specialValue)
    {
698
        $menuItems = [];
699
700
701
        if ($specialValue == '') {
            $specialValue = $this->id;
        }
702
703
        $pageIds = GeneralUtility::intExplode(',', (string)$specialValue);
        $disableGroupAccessCheck = !empty($this->mconf['showAccessRestrictedPages']);
704
        $pageRecords = $this->sys_page->getMenuForPages($pageIds);
705
        $pageLinkBuilder = GeneralUtility::makeInstance(PageLinkBuilder::class, $this->parent_cObj);
706
707
        foreach ($pageRecords as $row) {
            $pageId = (int)$row['uid'];
708
            $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($pageId);
709
            // Keep mount point?
710
711
            $mount_info = $this->sys_page->getMountPointInfo($pageId, $row);
            // $pageId is a valid mount point
712
            if (is_array($mount_info) && $mount_info['overlay']) {
713
                $mountedPageId = (int)$mount_info['mount_pid'];
714
715
                // Using "getPage" is OK since we need the check for enableFields
                // AND for type 2 of mount pids we DO require a doktype < 200!
716
717
                $mountedPageRow = $this->sys_page->getPage($mountedPageId, $disableGroupAccessCheck);
                if (empty($mountedPageRow)) {
718
                    // If the mount point could not be fetched with respect to
719
720
                    // enableFields, the page should not become a part of the menu!
                    continue;
721
                }
722
723
724
725
726
                $row = $mountedPageRow;
                $row['_MP_PARAM'] = $mount_info['MPvar'];
                // Overlays should already have their full MPvars calculated, that's why we unset the
                // existing $row['_MP_PARAM'], as the full $MP will be added again below
                $MP = $pageLinkBuilder->getMountPointParameterFromRootPointMaps($mountedPageId);
727
                if ($MP) {
728
                    unset($row['_MP_PARAM']);
729
730
                }
            }
731
732
733
734
            if ($MP) {
                $row['_MP_PARAM'] = $MP . ($row['_MP_PARAM'] ? ',' . $row['_MP_PARAM'] : '');
            }
            $menuItems[] = $row;
735
736
737
738
739
740
741
742
743
744
745
746
747
748
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = updated is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForUpdatedMenu($specialValue, $sortingField)
    {
        $tsfe = $this->getTypoScriptFrontendController();
749
        $menuItems = [];
750
        if ($specialValue == '') {
751
            $specialValue = $tsfe->id;
752
        }
753
        $items = GeneralUtility::intExplode(',', (string)$specialValue);
754
        if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'] ?? null)) {
755
756
757
758
759
            $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 1, 20);
        } else {
            $depth = 20;
        }
        // Max number of items
760
        $limit = MathUtility::forceIntegerInRange(($this->conf['special.']['limit'] ?? 0), 0, 100);
761
        $maxAge = (int)($this->parent_cObj->calc($this->conf['special.']['maxAge'] ?? 0));
762
763
764
        if (!$limit) {
            $limit = 10;
        }
765
        // 'auto', 'manual', 'tstamp'
766
        $mode = $this->conf['special.']['mode'] ?? '';
767
        // Get id's
768
        $beginAtLevel = MathUtility::forceIntegerInRange(($this->conf['special.']['beginAtLevel'] ?? 0), 0, 100);
769
        $id_list_arr = [];
770
        foreach ($items as $id) {
771
772
773
774
775
776
            // Exclude the current ID if beginAtLevel is > 0
            if ($beginAtLevel > 0) {
                $id_list_arr[] = $this->parent_cObj->getTreeList($id, $depth - 1 + $beginAtLevel, $beginAtLevel - 1);
            } else {
                $id_list_arr[] = $this->parent_cObj->getTreeList(-1 * $id, $depth - 1 + $beginAtLevel, $beginAtLevel - 1);
            }
777
778
        }
        $id_list = implode(',', $id_list_arr);
779
        $pageIds = GeneralUtility::intExplode(',', $id_list);
780
        // Get sortField (mode)
781
782
        $sortField = $this->getMode($mode);

783
        $extraWhere = ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
784
        if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
785
786
787
788
789
            $extraWhere .= ' AND pages.no_search=0';
        }
        if ($maxAge > 0) {
            $extraWhere .= ' AND ' . $sortField . '>' . ($GLOBALS['SIM_ACCESS_TIME'] - $maxAge);
        }
790
791
792
793
794
795
796
797
        $extraWhere = $sortField . '>=0' . $extraWhere;

        $i = 0;
        $pageRecords = $this->sys_page->getMenuForPages($pageIds, '*', $sortingField ?: $sortField . ' DESC', $extraWhere);
        foreach ($pageRecords as $row) {
            // Build a custom LIMIT clause as "getMenuForPages()" does not support this
            if ($limit && ++$i > $limit) {
                continue;
798
            }
799
            $menuItems[$row['uid']] = $row;
800
        }
801

802
803
804
805
806
807
808
809
810
811
812
813
814
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = keywords is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @return array
     */
    protected function prepareMenuItemsForKeywordsMenu($specialValue, $sortingField)
    {
        $tsfe = $this->getTypoScriptFrontendController();
815
        $menuItems = [];
816
        [$specialValue] = GeneralUtility::intExplode(',', $specialValue);
817
        if (!$specialValue) {
818
            $specialValue = $tsfe->id;
819
        }
820
        if (($this->conf['special.']['setKeywords'] ?? false) || ($this->conf['special.']['setKeywords.'] ?? false)) {
821
            $kw = (string)$this->parent_cObj->stdWrapValue('setKeywords', $this->conf['special.'] ?? []);
822
823
824
        } else {
            // The page record of the 'value'.
            $value_rec = $this->sys_page->getPage($specialValue);
825
            $kfieldSrc = ($this->conf['special.']['keywordsField.']['sourceField'] ?? false) ? $this->conf['special.']['keywordsField.']['sourceField'] : 'keywords';
826
827
828
829
            // keywords.
            $kw = trim($this->parent_cObj->keywords($value_rec[$kfieldSrc]));
        }
        // *'auto', 'manual', 'tstamp'
830
831
        $mode = $this->conf['special.']['mode'] ?? '';
        $sortField = $this->getMode($mode);
832
        // Depth, limit, extra where
833
        if (MathUtility::canBeInterpretedAsInteger($this->conf['special.']['depth'] ?? null)) {
834
835
836
837
838
            $depth = MathUtility::forceIntegerInRange($this->conf['special.']['depth'], 0, 20);
        } else {
            $depth = 20;
        }
        // Max number of items
839
        $limit = MathUtility::forceIntegerInRange(($this->conf['special.']['limit'] ?? 0), 0, 100);
840
        // Start point
841
        $eLevel = $this->parent_cObj->getKey(
842
            $this->parent_cObj->stdWrapValue('entryLevel', $this->conf['special.'] ?? []),
843
            $this->tmpl->rootLine
844
        );
845
        $startUid = (int)($this->tmpl->rootLine[$eLevel]['uid'] ?? 0);
846
847
        // Which field is for keywords
        $kfield = 'keywords';
848
        if ($this->conf['special.']['keywordsField'] ?? false) {
849
            [$kfield] = explode(' ', trim($this->conf['special.']['keywordsField']));
850
851
852
        }
        // If there are keywords and the startuid is present
        if ($kw && $startUid) {
853
            $bA = MathUtility::forceIntegerInRange(($this->conf['special.']['beginAtLevel'] ?? 0), 0, 100);
854
            $id_list = $this->parent_cObj->getTreeList(-1 * $startUid, $depth - 1 + $bA, $bA - 1);
855
            $kwArr = GeneralUtility::trimExplode(',', $kw, true);
856
            $keyWordsWhereArr = [];
857
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
858
            foreach ($kwArr as $word) {
859
860
                $keyWordsWhereArr[] = $queryBuilder->expr()->like(
                    $kfield,
861
862
863
864
                    $queryBuilder->createNamedParameter(
                        '%' . $queryBuilder->escapeLikeWildcards($word) . '%',
                        \PDO::PARAM_STR
                    )
865
                );
866
            }
867
868
869
870
871
872
873
874
875
876
877
878
879
880
            $queryBuilder
                ->select('*')
                ->from('pages')
                ->where(
                    $queryBuilder->expr()->in(
                        'uid',
                        GeneralUtility::intExplode(',', $id_list, true)
                    ),
                    $queryBuilder->expr()->neq(
                        'uid',
                        $queryBuilder->createNamedParameter($specialValue, \PDO::PARAM_INT)
                    )
                );

881
            if (!empty($keyWordsWhereArr)) {
882
883
884
                $queryBuilder->andWhere($queryBuilder->expr()->orX(...$keyWordsWhereArr));
            }

885
            if (!empty($this->excludedDoktypes)) {
886
887
888
                $queryBuilder->andWhere(
                    $queryBuilder->expr()->notIn(
                        'pages.doktype',
889
                        $this->excludedDoktypes
890
891
892
893
894
895
896
897
                    )
                );
            }

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

898
            if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
899
900
901
902
903
904
905
906
907
908
909
910
911
912
                $queryBuilder->andWhere($queryBuilder->expr()->eq('pages.no_search', 0));
            }

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

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

            $result = $queryBuilder->execute();
913
            while ($row = $result->fetchAssociative()) {
914
                $this->sys_page->versionOL('pages', $row, true);
915
916
917
918
919
                if (is_array($row)) {
                    $menuItems[$row['uid']] = $this->sys_page->getPageOverlay($row);
                }
            }
        }
920

921
922
923
924
925
926
927
928
929
930
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = rootline is set
     *
     * @return array
     */
    protected function prepareMenuItemsForRootlineMenu()
    {
931
        $menuItems = [];
932
        $range = (string)$this->parent_cObj->stdWrapValue('range', $this->conf['special.'] ?? []);
933
934
        $begin_end = explode('|', $range);
        $begin_end[0] = (int)$begin_end[0];
935
        if (!MathUtility::canBeInterpretedAsInteger($begin_end[1] ?? '')) {
936
937
938
939
940
941
942
            $begin_end[1] = -1;
        }
        $beginKey = $this->parent_cObj->getKey($begin_end[0], $this->tmpl->rootLine);
        $endKey = $this->parent_cObj->getKey($begin_end[1], $this->tmpl->rootLine);
        if ($endKey < $beginKey) {
            $endKey = $beginKey;
        }
943
        $rl_MParray = [];
944
945
        foreach ($this->tmpl->rootLine as $k_rl => $v_rl) {
            // For overlaid mount points, set the variable right now:
946
            if (($v_rl['_MP_PARAM'] ?? false) && ($v_rl['_MOUNT_OL'] ?? false)) {
947
948
949
950
951
952
953
954
955
                $rl_MParray[] = $v_rl['_MP_PARAM'];
            }
            // Traverse rootline:
            if ($k_rl >= $beginKey && $k_rl <= $endKey) {
                $temp_key = $k_rl;
                $menuItems[$temp_key] = $this->sys_page->getPage($v_rl['uid']);
                if (!empty($menuItems[$temp_key])) {
                    // If there are no specific target for the page, put the level specific target on.
                    if (!$menuItems[$temp_key]['target']) {
956
                        $menuItems[$temp_key]['target'] = $this->conf['special.']['targets.'][$k_rl] ?? '';
957
958
959
960
961
962
963
                        $menuItems[$temp_key]['_MP_PARAM'] = implode(',', $rl_MParray);
                    }
                } else {
                    unset($menuItems[$temp_key]);
                }
            }
            // For normal mount points, set the variable for next level.
964
            if (($v_rl['_MP_PARAM'] ?? false) && !($v_rl['_MOUNT_OL'] ?? false)) {
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
                $rl_MParray[] = $v_rl['_MP_PARAM'];
            }
        }
        // Reverse order of elements (e.g. "1,2,3,4" gets "4,3,2,1"):
        if (isset($this->conf['special.']['reverseOrder']) && $this->conf['special.']['reverseOrder']) {
            $menuItems = array_reverse($menuItems);
        }
        return $menuItems;
    }

    /**
     * Fetches all menuitems if special = browse is set
     *
     * @param string $specialValue The value from special.value
     * @param string $sortingField The sorting field
     * @param string $additionalWhere Additional WHERE clause
     * @return array
     */
    protected function prepareMenuItemsForBrowseMenu($specialValue, $sortingField, $additionalWhere)
    {
985
        $menuItems = [];
986
        [$specialValue] = GeneralUtility::intExplode(',', $specialValue);
987
988
989
990
991
        if (!$specialValue) {
            $specialValue = $this->getTypoScriptFrontendController()->page['uid'];
        }
        // Will not work out of rootline
        if ($specialValue != $this->tmpl->rootLine[0]['uid']) {
992
            $recArr = [];
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
            // The page record of the 'value'.
            $value_rec = $this->sys_page->getPage($specialValue);
            // 'up' page cannot be outside rootline
            if ($value_rec['pid']) {
                // The page record of 'up'.
                $recArr['up'] = $this->sys_page->getPage($value_rec['pid']);
            }
            // If the 'up' item was NOT level 0 in rootline...
            if ($recArr['up']['pid'] && $value_rec['pid'] != $this->tmpl->rootLine[0]['uid']) {
                // The page record of "index".
                $recArr['index'] = $this->sys_page->getPage($recArr['up']['pid']);
            }
            // check if certain pages should be excluded
            $additionalWhere .= ($this->conf['includeNotInMenu'] ? '' : ' AND pages.nav_hide=0') . $this->getDoktypeExcludeWhere();
1007
            if ($this->conf['special.']['excludeNoSearchPages'] ?? false) {
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
                $additionalWhere .= ' AND pages.no_search=0';
            }
            // prev / next is found
            $prevnext_menu = $this->removeInaccessiblePages($this->sys_page->getMenu($value_rec['pid'], '*', $sortingField, $additionalWhere));
            $lastKey = 0;
            $nextActive = 0;
            foreach ($prevnext_menu as $k_b => $v_b) {
                if ($nextActive) {
                    $recArr['next'] = $v_b;
                    $nextActive = 0;
                }
                if ($v_b['uid'] == $specialValue) {
                    if ($lastKey) {
                        $recArr['prev'] = $prevnext_menu[$lastKey];
                    }
                    $nextActive = 1;
                }
                $lastKey = $k_b;