PageLayoutController.php 49.8 KB
Newer Older
1
<?php
2
declare(strict_types = 1);
3
4
namespace TYPO3\CMS\Backend\Controller;

5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
9
10
 * 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.
11
 *
12
13
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
16
 * The TYPO3 project - inspiring people to share!
 */
17

18
19
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
20
use TYPO3\CMS\Backend\Module\ModuleLoader;
21
use TYPO3\CMS\Backend\Routing\UriBuilder;
22
23
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
Nicole Cordes's avatar
Nicole Cordes committed
24
use TYPO3\CMS\Backend\Utility\BackendUtility;
25
26
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\PageLayoutView;
27
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
28
use TYPO3\CMS\Core\Database\ConnectionPool;
29
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
30
31
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
32
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
33
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
34
use TYPO3\CMS\Core\Http\HtmlResponse;
35
36
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
37
use TYPO3\CMS\Core\Localization\LanguageService;
38
use TYPO3\CMS\Core\Page\PageRenderer;
39
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
40
use TYPO3\CMS\Core\Type\Bitmask\Permission;
Nicole Cordes's avatar
Nicole Cordes committed
41
42
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
43
use TYPO3\CMS\Core\Versioning\VersionState;
44
use TYPO3\CMS\Fluid\View\StandaloneView;
45
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
Nicole Cordes's avatar
Nicole Cordes committed
46

47
48
49
/**
 * Script Class for Web > Layout module
 */
50
51
52
53
54
55
class PageLayoutController
{
    /**
     * Page Id for which to make the listing
     *
     * @var int
56
     * @internal
57
58
59
60
61
62
63
64
     */
    public $id;

    /**
     * Pointer - for browsing list of records.
     *
     * @var int
     */
65
    protected $pointer;
66
67
68
69
70
71

    /**
     * Thumbnails or not
     *
     * @var string
     */
72
    protected $imagemode;
73
74
75
76
77
78

    /**
     * Search-fields
     *
     * @var string
     */
79
    protected $search_field;
80
81
82
83
84
85

    /**
     * Search-levels
     *
     * @var int
     */
86
    protected $search_levels;
87
88
89
90
91
92

    /**
     * Show-limit
     *
     * @var int
     */
93
    protected $showLimit;
94
95
96
97
98
99

    /**
     * Return URL
     *
     * @var string
     */
100
    protected $returnUrl;
101
102
103
104
105
106

    /**
     * PopView id - for opening a window with the page
     *
     * @var bool
     */
107
    protected $popView;
108
109
110
111
112
113

    /**
     * Page select perms clause
     *
     * @var string
     */
114
    protected $perms_clause;
115
116
117
118
119
120

    /**
     * Module TSconfig
     *
     * @var array
     */
121
    protected $modTSconfig = [];
122
123
124
125
126
127

    /**
     * Module shared TSconfig
     *
     * @var array
     */
128
    protected $modSharedTSconfig = [];
129
130
131
132
133

    /**
     * Current ids page record
     *
     * @var array
134
     * @internal
135
136
137
138
139
140
141
142
     */
    public $pageinfo;

    /**
     * "Pseudo" Description -table name
     *
     * @var string
     */
143
    protected $descrTable;
144
145
146
147
148
149

    /**
     * List of column-integers to edit. Is set from TSconfig, default is "1,0,2,3"
     *
     * @var string
     */
150
    protected $colPosList;
151
152
153
154
155
156

    /**
     * Flag: If content can be edited or not.
     *
     * @var bool
     */
157
    protected $EDIT_CONTENT;
158
159
160
161
162
163

    /**
     * Users permissions integer for this page.
     *
     * @var int
     */
164
    protected $CALC_PERMS;
165
166
167
168
169
170

    /**
     * Currently selected language for editing content elements
     *
     * @var int
     */
171
    protected $current_sys_language;
172
173
174
175
176
177

    /**
     * Module configuration
     *
     * @var array
     */
178
    protected $MCONF = [];
179
180
181
182
183
184

    /**
     * Menu configuration
     *
     * @var array
     */
185
    protected $MOD_MENU = [];
186
187
188
189
190

    /**
     * Module settings (session variable)
     *
     * @var array
191
     * @internal
192
     */
193
    public $MOD_SETTINGS = [];
194
195
196
197
198
199

    /**
     * Module output accumulation
     *
     * @var string
     */
200
    protected $content;
201
202
203
204
205
206
207

    /**
     * List of column-integers accessible to the current BE user.
     * Is set from TSconfig, default is $colPosList
     *
     * @var string
     */
208
    protected $activeColPosList;
209
210
211
212
213
214

    /**
     * @var string
     */
    protected $editSelect;

215
216
217
218
219
    /**
     * Caches the available languages in a colPos
     *
     * @var array
     */
220
    protected $languagesInColumnCache = [];
221

222
223
224
225
226
227
228
229
230
231
232
233
    /**
     * @var IconFactory
     */
    protected $iconFactory;

    /**
     * The name of the module
     *
     * @var string
     */
    protected $moduleName = 'web_layout';

234
235
236
237
238
239
240
241
242
243
    /**
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

    /**
     * @var ButtonBar
     */
    protected $buttonBar;

244
245
246
247
248
    /**
     * @var string
     */
    protected $searchContent;

249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
    /**
     * Injects the request object for the current request or subrequest
     * As this controller goes only through the main() method, it is rather simple for now
     *
     * @param ServerRequestInterface $request the current request
     * @return ResponseInterface the response with the content
     */
    public function mainAction(ServerRequestInterface $request): ResponseInterface
    {
        $GLOBALS['SOBE'] = $this;
        $this->init($request);
        $this->main($request);
        return new HtmlResponse($this->moduleTemplate->renderContent());
    }

264
265
    /**
     * Initializing the module
266
     * @param ServerRequestInterface $request
267
     */
268
    protected function init(ServerRequestInterface $request): void
269
    {
270
271
272
273
        // Set the GPvars from outside
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

274
        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
275
        $this->iconFactory = $this->moduleTemplate->getIconFactory();
276
        $this->buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
277
278
279
        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
        // Setting module configuration / page select clause
        $this->MCONF['name'] = $this->moduleName;
280
        $this->perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
281
        // Get session data
282
        $sessionData = $this->getBackendUser()->getSessionData(__CLASS__);
283
        $this->search_field = !empty($sessionData['search_field']) ? $sessionData['search_field'] : '';
284
285
286
287
288
289
290
291
292
293
294

        $this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
        $this->pointer = $parsedBody['pointer'] ?? $queryParams['pointer'] ?? null;
        $this->imagemode = $parsedBody['imagemode'] ?? $queryParams['imagemode'] ?? null;
        $this->popView = $parsedBody['popView'] ?? $queryParams['popView'] ?? null;
        $this->search_field = $parsedBody['search_field'] ?? $queryParams['search_field'] ?? null;
        $this->search_levels = $parsedBody['search_levels'] ?? $queryParams['search_levels'] ?? null;
        $this->showLimit = $parsedBody['showLimit'] ?? $queryParams['showLimit'] ?? null;
        $returnUrl = $parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? null;
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($returnUrl);

295
296
        $sessionData['search_field'] = $this->search_field;
        // Store session data
297
        $this->getBackendUser()->setAndSaveSessionData(__CLASS__, $sessionData);
298
299
300
        // Load page info array:
        $this->pageinfo = BackendUtility::readPageAccess($this->id, $this->perms_clause);
        // Initialize menu
301
        $this->menuConfig($request);
302
303
304
305
306
307
308
309
        // Setting sys language from session var:
        $this->current_sys_language = (int)$this->MOD_SETTINGS['language'];
        // CSH / Descriptions:
        $this->descrTable = '_MOD_' . $this->moduleName;
    }

    /**
     * Initialize menu array
310
     * @param ServerRequestInterface $request
311
     */
312
    protected function menuConfig(ServerRequestInterface $request): void
313
    {
314
315
316
317
        // Set the GPvars from outside
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

318
319
320
321
        /** @var SiteInterface $currentSite */
        $currentSite = $request->getAttribute('site');
        $availableLanguages = $currentSite->getAvailableLanguages($this->getBackendUser(), false, $this->id);

322
323
        $lang = $this->getLanguageService();
        // MENU-ITEMS:
324
        $this->MOD_MENU = [
325
            'tt_content_showHidden' => '',
326
            'function' => [
327
328
                1 => $lang->getLL('m_function_1'),
                2 => $lang->getLL('m_function_2')
329
330
            ],
            'language' => [
331
                0 => $lang->getLL('m_default')
332
333
            ]
        ];
334
        // initialize page/be_user TSconfig settings
335
336
337
        $pageTsConfig = BackendUtility::getPagesTSconfig($this->id);
        $this->modSharedTSconfig['properties'] = $pageTsConfig['mod.']['SHARED.'] ?? [];
        $this->modTSconfig['properties'] = $pageTsConfig['mod.']['web_layout.'] ?? [];
338

339
340
        // First, select all localized page records on the current page.
        // Each represents a possibility for a language on the page. Add these to language selector.
341
        if ($this->id) {
342
343
344
345
346
347
348
349
            // Compile language data for pid != 0 only. The language drop-down is not shown on pid 0
            // since pid 0 can't be localized.
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
            $queryBuilder->getRestrictions()->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
            $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
                ->from('pages')
350
                ->where(
351
                    $queryBuilder->expr()->eq(
352
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
353
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
354
                    )
355
356
357
358
359
360
                )->execute();
            while ($pageTranslation = $statement->fetch()) {
                $languageId = $pageTranslation[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
                if (isset($availableLanguages[$languageId])) {
                    $this->MOD_MENU['language'][$languageId] = $availableLanguages[$languageId]->getTitle();
                }
361
            }
362
363
364
            // Override the label
            if (isset($availableLanguages[0])) {
                $this->MOD_MENU['language'][0] = $availableLanguages[0]->getTitle();
365
366
            }
        }
367
        // Initialize the available actions
368
        $actions = $this->initActions();
369
        // Clean up settings
370
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $parsedBody['SET'] ?? $queryParams['SET'] ?? [], $this->moduleName);
371
        // For all elements to be shown in draft workspaces & to also show hidden elements by default if user hasn't disabled the option
372
373
374
        if ($this->getBackendUser()->workspace != 0
            || !isset($this->MOD_SETTINGS['tt_content_showHidden'])
            || $this->MOD_SETTINGS['tt_content_showHidden'] !== '0'
Pawel Cieslik's avatar
Pawel Cieslik committed
375
        ) {
376
377
            $this->MOD_SETTINGS['tt_content_showHidden'] = 1;
        }
378
379
        // Make action menu from available actions
        $this->makeActionMenu($actions);
380
381
382
    }

    /**
383
     * Initializes the available actions this module provides
384
     *
385
     * @return array the available actions
386
     */
387
    protected function initActions(): array
388
    {
389
        $actions = [
390
391
            1 => $this->getLanguageService()->getLL('m_function_1'),
            2 => $this->getLanguageService()->getLL('m_function_2')
392
        ];
393
        // Find if there are ANY languages at all (and if not, remove the language option from function menu).
394
395
396
397
398
399
400
401
402
403
404
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
        if ($this->getBackendUser()->isAdmin()) {
            $queryBuilder->getRestrictions()->removeAll();
        }

        $count = $queryBuilder
            ->count('uid')
            ->from('sys_language')
            ->execute()
            ->fetchColumn(0);

405
        if (!$count) {
406
            unset($actions['2']);
407
        }
408
409
410
411
412
413
414
        // Page / user TSconfig blinding of menu-items
        $blindActions = $this->modTSconfig['properties']['menu.']['functions.'] ?? [];
        foreach ($blindActions as $key => $value) {
            if (!$value && array_key_exists($key, $actions)) {
                unset($actions[$key]);
            }
        }
415
416
417
418
419
420
421
422
423
424

        return $actions;
    }

    /**
     * This creates the dropdown menu with the different actions this module is able to provide.
     * For now they are Columns, Quick Edit and Languages.
     *
     * @param array $actions array with the available actions
     */
425
    protected function makeActionMenu(array $actions): void
426
    {
427
428
429
430
        $actionMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
        $actionMenu->setIdentifier('actionMenu');
        $actionMenu->setLabel('');

431
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
432

433
        $defaultKey = null;
434
        $foundDefaultKey = false;
435
        foreach ($actions as $key => $action) {
436
437
438
            $menuItem = $actionMenu
                ->makeMenuItem()
                ->setTitle($action)
439
                ->setHref((string)$uriBuilder->buildUriFromRoute($this->moduleName) . '&id=' . $this->id . '&SET[function]=' . $key);
440

441
            if (!$foundDefaultKey) {
442
                $defaultKey = $key;
443
                $foundDefaultKey = true;
444
            }
445
446
            if ((int)$this->MOD_SETTINGS['function'] === $key) {
                $menuItem->setActive(true);
447
                $defaultKey = null;
448
449
            }
            $actionMenu->addMenuItem($menuItem);
450
        }
451
452
453
        if (isset($defaultKey)) {
            $this->MOD_SETTINGS['function'] = $defaultKey;
        }
454
        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu);
455
456
457
458
459
460
461
    }

    /**
     * Generate the flashmessages for current pid
     *
     * @return string HTML content with flashmessages
     */
462
    protected function getHeaderFlashMessagesForCurrentPid(): string
463
464
465
    {
        $content = '';
        $lang = $this->getLanguageService();
466

467
468
469
        $view = GeneralUtility::makeInstance(StandaloneView::class);
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/InfoBox.html'));

470
471
472
473
474
475
476
477
478
        // If page is a folder
        if ($this->pageinfo['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
            $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
            $moduleLoader->load($GLOBALS['TBE_MODULES']);
            $modules = $moduleLoader->modules;
            if (is_array($modules['web']['sub']['list'])) {
                $title = $lang->getLL('goToListModule');
                $message = '<p>' . $lang->getLL('goToListModuleMessage') . '</p>';
                $message .= '<a class="btn btn-info" href="javascript:top.goToModule(\'web_list\',1);">' . $lang->getLL('goToListModule') . '</a>';
479
                $view->assignMultiple([
480
481
482
                    'title' => $title,
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
483
                ]);
484
485
                $content .= $view->render();
            }
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)$this->pageinfo['shortcut_mode'];
            $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
            $targetPage = [];

            if ($this->pageinfo['shortcut'] || $shortcutMode) {
                switch ($shortcutMode) {
                    case PageRepository::SHORTCUT_MODE_NONE:
                        $targetPage = $pageRepository->getPage($this->pageinfo['shortcut']);
                        break;
                    case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
                        $targetPage = reset($pageRepository->getMenu($this->pageinfo['shortcut'] ?: $this->pageinfo['uid']));
                        break;
                    case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
                        $targetPage = $pageRepository->getPage($this->pageinfo['pid']);
                        break;
                }

                $message = '';
                if ($shortcutMode === PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
                    $message .= sprintf($lang->getLL('pageIsRandomInternalLinkMessage'));
                } else {
                    $linkToPid = $this->local_linkThisScript(['id' => $targetPage['uid']]);
                    $path = BackendUtility::getRecordPath($targetPage['uid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 1000);
510
                    $linkedPath = '<a href="' . htmlspecialchars($linkToPid) . '">' . htmlspecialchars($path) . '</a>';
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
                    $message .= sprintf($lang->getLL('pageIsInternalLinkMessage'), $linkedPath);
                }

                $message .= ' (' . htmlspecialchars($lang->sL(BackendUtility::getLabelFromItemlist('pages', 'shortcut_mode', $shortcutMode))) . ')';

                $view->assignMultiple([
                    'title' => $this->pageinfo['title'],
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
                ]);
                $content .= $view->render();
            } else {
                if (empty($targetPage) && $shortcutMode !== PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
                    $view->assignMultiple([
                        'title' => $this->pageinfo['title'],
                        'message' => $lang->getLL('pageIsMisconfiguredInternalLinkMessage'),
                        'state' => InfoboxViewHelper::STATE_ERROR
                    ]);
                    $content .= $view->render();
                }
            }
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_LINK) {
            if (empty($this->pageinfo['url'])) {
                $view->assignMultiple([
                    'title' => $this->pageinfo['title'],
                    'message' => $lang->getLL('pageIsMisconfiguredExternalLinkMessage'),
                    'state' => InfoboxViewHelper::STATE_ERROR
                ]);
                $content .= $view->render();
            } else {
                $externalUrl = htmlspecialchars(GeneralUtility::makeInstance(PageRepository::class)->getExtURL($this->pageinfo));
                if ($externalUrl !== false) {
543
                    $externalUrlHtml = '<a href="' . $externalUrl . '" target="_blank" rel="noopener noreferrer">' . $externalUrl . '</a>';
544
545
546
547
548
549
550
551
                    $view->assignMultiple([
                        'title' => $this->pageinfo['title'],
                        'message' => sprintf($lang->getLL('pageIsExternalLinkMessage'), $externalUrlHtml),
                        'state' => InfoboxViewHelper::STATE_INFO
                    ]);
                    $content .= $view->render();
                }
            }
552
553
554
555
        }
        // If content from different pid is displayed
        if ($this->pageinfo['content_from_pid']) {
            $contentPage = BackendUtility::getRecord('pages', (int)$this->pageinfo['content_from_pid']);
556
            $linkToPid = $this->local_linkThisScript(['id' => $this->pageinfo['content_from_pid']]);
557
            $title = BackendUtility::getRecordTitle('pages', $contentPage);
558
            $link = '<a href="' . htmlspecialchars($linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageinfo['content_from_pid'] . ')</a>';
559
            $message = sprintf($lang->getLL('content_from_pid_title'), $link);
560
            $view->assignMultiple([
561
562
563
                'title' => $title,
                'message' => $message,
                'state' => InfoboxViewHelper::STATE_INFO
564
            ]);
565
            $content .= $view->render();
566
567
568
569
570
571
572
573
574
575
576
        } else {
            $links = $this->getPageLinksWhereContentIsAlsoShownOn($this->pageinfo['uid']);
            if (!empty($links)) {
                $message = sprintf($lang->getLL('content_on_pid_title'), $links);
                $view->assignMultiple([
                    'title' => '',
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
                ]);
                $content .= $view->render();
            }
577
578
579
580
        }
        return $content;
    }

581
582
583
584
585
586
    /**
     * Get all pages with links where the content of a page $pageId is also shown on
     *
     * @param int $pageId
     * @return string
     */
587
    protected function getPageLinksWhereContentIsAlsoShownOn($pageId): string
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
        $queryBuilder->getRestrictions()->removeAll();
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
        $queryBuilder
            ->select('*')
            ->from('pages')
            ->where($queryBuilder->expr()->eq('content_from_pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)));

        $links = [];
        $rows = $queryBuilder->execute()->fetchAll();
        if (!empty($rows)) {
            foreach ($rows as $row) {
                $linkToPid = $this->local_linkThisScript(['id' => $row['uid']]);
                $title = BackendUtility::getRecordTitle('pages', $row);
                $link = '<a href="' . htmlspecialchars($linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')</a>';
                $links[] = $link;
            }
        }
        return implode(', ', $links);
    }

610
611
612
    /**
     * @return string $title
     */
613
    protected function getLocalizedPageTitle(): string
614
615
    {
        if ($this->current_sys_language > 0) {
616
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
617
                ->getQueryBuilderForTable('pages');
618
619
620
621
            $queryBuilder->getRestrictions()
                ->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
622
            $localizedPage = $queryBuilder
623
                ->select('*')
624
                ->from('pages')
625
                ->where(
626
                    $queryBuilder->expr()->eq(
627
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
628
629
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                    ),
630
                    $queryBuilder->expr()->eq(
631
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
632
633
                        $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                    )
634
635
636
637
                )
                ->setMaxResults(1)
                ->execute()
                ->fetch();
638
639
            BackendUtility::workspaceOL('pages', $localizedPage);
            return $localizedPage['title'];
640
        }
641
        return $this->pageinfo['title'];
642
643
644
645
646
    }

    /**
     * Main function.
     * Creates some general objects and calls other functions for the main rendering of module content.
647
648
     *
     * @param ServerRequestInterface $request
649
     */
650
    protected function main(ServerRequestInterface $request): void
651
    {
652
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
653
654
655
        $lang = $this->getLanguageService();
        // Access check...
        // The page will show only if there is a valid page and if this page may be viewed by the user
656
        $access = is_array($this->pageinfo);
657
658
        // Content
        $content = '';
659
660
661
        if ($this->id && $access) {
            // Initialize permission settings:
            $this->CALC_PERMS = $this->getBackendUser()->calcPerms($this->pageinfo);
662
            $this->EDIT_CONTENT = $this->isContentEditable();
663

664
665
            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);

666
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
667

668
669
670
            $this->moduleTemplate->addJavaScriptCode('mainJsFunctions', '
                if (top.fsMod) {
                    top.fsMod.recentIds["web"] = ' . (int)$this->id . ';
671
                    top.fsMod.navFrameHighlightedID["web"] = top.fsMod.currentBank + "_" + ' . (int)$this->id . ';
672
673
674
                }
                ' . ($this->popView ? BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id)) : '') . '
                function deleteRecord(table,id,url) {   //
675
                    window.location.href = ' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('tce_db') . '&cmd[')
676
                                            . ' + table + "][" + id + "][delete]=1&redirect=" + encodeURIComponent(url);
677
678
679
680
                    return false;
                }
            ');

681
            // Find backend layout / columns
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
            $backendLayout = GeneralUtility::callUserFunction(BackendLayoutView::class . '->getSelectedBackendLayout', $this->id, $this);
            if (!empty($backendLayout['__colPosList'])) {
                $this->colPosList = implode(',', $backendLayout['__colPosList']);
            }
            // Removing duplicates, if any
            $this->colPosList = array_unique(GeneralUtility::intExplode(',', $this->colPosList));
            // Accessible columns
            if (isset($this->modSharedTSconfig['properties']['colPos_list']) && trim($this->modSharedTSconfig['properties']['colPos_list']) !== '') {
                $this->activeColPosList = array_unique(GeneralUtility::intExplode(',', trim($this->modSharedTSconfig['properties']['colPos_list'])));
                // Match with the list which is present in the colPosList for the current page
                if (!empty($this->colPosList) && !empty($this->activeColPosList)) {
                    $this->activeColPosList = array_unique(array_intersect(
                        $this->activeColPosList,
                        $this->colPosList
                    ));
                }
            } else {
                $this->activeColPosList = $this->colPosList;
            }
            $this->activeColPosList = implode(',', $this->activeColPosList);
            $this->colPosList = implode(',', $this->colPosList);

704
705
            $content .= $this->getHeaderFlashMessagesForCurrentPid();

706
            // Render the primary module content:
707
            if ($this->MOD_SETTINGS['function'] == 1 || $this->MOD_SETTINGS['function'] == 2) {
708
                $content .= '<form action="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->id, 'imagemode' =>  $this->imagemode])) . '" id="PageLayoutController" method="post">';
709
                // Page title
710
                $content .= '<h1 class="t3js-title-inlineedit">' . htmlspecialchars($this->getLocalizedPageTitle()) . '</h1>';
711
                // All other listings
712
                $content .= $this->renderContent();
713
            }
714
715
            $content .= '</form>';
            $content .= $this->searchContent;
716
            // Setting up the buttons for the docheader
717
718
            $this->makeButtons($request);

719
720
721
722
723
724
725
            // Create LanguageMenu
            $this->makeLanguageMenu();
        } else {
            $this->moduleTemplate->addJavaScriptCode(
                'mainJsFunctions',
                'if (top.fsMod) top.fsMod.recentIds["web"] = ' . (int)$this->id . ';'
            );
726
            $content .= '<h1>' . htmlspecialchars($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']) . '</h1>';
727
728
            $view = GeneralUtility::makeInstance(StandaloneView::class);
            $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/InfoBox.html'));
729
            $view->assignMultiple([
730
731
                'title' => $lang->getLL('clickAPage_header'),
                'message' => $lang->getLL('clickAPage_content'),
732
                'state' => InfoboxViewHelper::STATE_INFO
733
            ]);
734
            $content .= $view->render();
735
        }
736
737
        // Set content
        $this->moduleTemplate->setContent($content);
738
739
740
    }

    /**
741
     * Rendering content
742
743
744
     *
     * @return string
     */
745
    protected function renderContent(): string
746
    {
747
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
748

749
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
750
751
752
753
754
        $dbList = GeneralUtility::makeInstance(PageLayoutView::class);
        $dbList->thumbs = $this->imagemode;
        $dbList->no_noWrap = 1;
        $dbList->descrTable = $this->descrTable;
        $this->pointer = MathUtility::forceIntegerInRange($this->pointer, 0, 100000);
755
        $dbList->script = (string)$uriBuilder->buildUriFromRoute($this->moduleName);
756
757
758
759
        $dbList->showIcon = 0;
        $dbList->setLMargin = 0;
        $dbList->doEdit = $this->EDIT_CONTENT;
        $dbList->ext_CALC_PERMS = $this->CALC_PERMS;
760
        $dbList->agePrefixes = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
761
762
        $dbList->id = $this->id;
        $dbList->nextThree = MathUtility::forceIntegerInRange($this->modTSconfig['properties']['editFieldsAtATime'], 0, 10);
763
764
        $dbList->option_newWizard = empty($this->modTSconfig['properties']['disableNewContentElementWizard']);
        $dbList->defLangBinding = !empty($this->modTSconfig['properties']['defLangBinding']);
765
766
767
768
769
770
771
        if (!$dbList->nextThree) {
            $dbList->nextThree = 1;
        }
        // Create menu for selecting a table to jump to (this is, if more than just pages/tt_content elements are found on the page!)
        // also fills $dbList->activeTables
        $dbList->getTableMenu($this->id);
        // Initialize other variables:
772
773
        $tableOutput = [];
        $tableJSOutput = [];
774
775
776
777
        $CMcounter = 0;
        // Traverse the list of table names which has records on this page (that array is populated
        // by the $dblist object during the function getTableMenu()):
        foreach ($dbList->activeTables as $table => $value) {
778
            $h_func = '';
779
780
781
782
783
784
785
786
            $h_func_b = '';
            if (!isset($dbList->externalTables[$table])) {
                // Boolean: Display up/down arrows and edit icons for tt_content records
                $dbList->tt_contentConfig['showCommands'] = 1;
                // Boolean: Display info-marks or not
                $dbList->tt_contentConfig['showInfo'] = 1;
                // Setting up the tt_content columns to show:
                if (is_array($GLOBALS['TCA']['tt_content']['columns']['colPos']['config']['items'])) {
787
                    $colList = [];
788
789
790
791
792
793
                    $tcaItems = GeneralUtility::callUserFunction(BackendLayoutView::class . '->getColPosListItemsParsed', $this->id, $this);
                    foreach ($tcaItems as $temp) {
                        $colList[] = $temp[1];
                    }
                } else {
                    // ... should be impossible that colPos has no array. But this is the fallback should it make any sense:
794
                    $colList = ['1', '0', '2', '3'];
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
                }
                if ($this->colPosList !== '') {
                    $colList = array_intersect(GeneralUtility::intExplode(',', $this->colPosList), $colList);
                }
                // The order of the rows: Default is left(1), Normal(0), right(2), margin(3)
                $dbList->tt_contentConfig['cols'] = implode(',', $colList);
                $dbList->tt_contentConfig['activeCols'] = $this->activeColPosList;
                $dbList->tt_contentConfig['showHidden'] = $this->MOD_SETTINGS['tt_content_showHidden'];
                $dbList->tt_contentConfig['sys_language_uid'] = (int)$this->current_sys_language;
                // If the function menu is set to "Language":
                if ($this->MOD_SETTINGS['function'] == 2) {
                    $dbList->tt_contentConfig['languageMode'] = 1;
                    $dbList->tt_contentConfig['languageCols'] = $this->MOD_MENU['language'];
                    $dbList->tt_contentConfig['languageColsPointer'] = $this->current_sys_language;
                }
810
811
812
813
814
815
816
817
818
819
820
                // Toggle hidden ContentElements
                $numberOfHiddenElements = $this->getNumberOfHiddenElements($dbList->tt_contentConfig);
                if ($numberOfHiddenElements > 0) {
                    $h_func_b = '
                        <div class="checkbox">
                            <label for="checkTt_content_showHidden">
                                <input type="checkbox" id="checkTt_content_showHidden" class="checkbox" name="SET[tt_content_showHidden]" value="1" ' . ($this->MOD_SETTINGS['tt_content_showHidden'] ? 'checked="checked"' : '') . ' />
                                ' . htmlspecialchars($this->getLanguageService()->getLL('hiddenCE')) . ' (<span class="t3js-hidden-counter">' . $numberOfHiddenElements . '</span>)
                            </label>
                        </div>';
                }
821
822
823
824
825
826
827
828
829
830
831
832
833
            } else {
                if (isset($this->MOD_SETTINGS) && isset($this->MOD_MENU)) {
                    $h_func = BackendUtility::getFuncMenu($this->id, 'SET[' . $table . ']', $this->MOD_SETTINGS[$table], $this->MOD_MENU[$table], '', '');
                }
            }
            // Start the dblist object:
            $dbList->itemsLimitSingleTable = 1000;
            $dbList->start($this->id, $table, $this->pointer, $this->search_field, $this->search_levels, $this->showLimit);
            $dbList->counter = $CMcounter;
            $dbList->ext_function = $this->MOD_SETTINGS['function'];
            // Generate the list of elements here:
            $dbList->generateList();
            // Adding the list content to the tableOutput variable:
834
            $tableOutput[$table] = $h_func . $dbList->HTMLcode . $h_func_b;
835
836
837
838
839
840
841
842
843
844
845
846
847
            // ... and any accumulated JavaScript goes the same way!
            $tableJSOutput[$table] = $dbList->JScode;
            // Increase global counter:
            $CMcounter += $dbList->counter;
            // Reset variables after operation:
            $dbList->HTMLcode = '';
            $dbList->JScode = '';
        }
        // END: traverse tables
        // For Context Sensitive Menus:
        // Init the content
        $content = '';
        // Additional header content
848
849
850
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawHeaderHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
851
852
853
        }
        // Add the content for each table we have rendered (traversing $tableOutput variable)
        foreach ($tableOutput as $table => $output) {
854
            $content .= $output;
855
856
        }
        // Making search form:
857
        if (!$this->modTSconfig['properties']['disableSearchBox'] && ($dbList->counter > 0 || $this->currentPageHasSubPages())) {
858
            $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ToggleSearchToolbox');
859
860
            $toggleSearchFormButton = $this->buttonBar->makeLinkButton()
                ->setClasses('t3js-toggle-search-toolbox')
861
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchIcon'))
862
863
864
                ->setIcon($this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL))
                ->setHref('#');
            $this->buttonBar->addButton($toggleSearchFormButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
865
            $this->searchContent = $dbList->getSearchBox();
866
867
        }
        // Additional footer content
868
869
870
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawFooterHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
871
872
873
874
        }
        return $content;
    }

875
876
877
    /**
     * @return ModuleTemplate
     */
878
    protected function getModuleTemplate(): ModuleTemplate
879
880
881
882
    {
        return $this->moduleTemplate;
    }

883
884
885
886
887
888
    /***************************
     *
     * Sub-content functions, rendering specific parts of the module content.
     *
     ***************************/
    /**
889
     * This creates the buttons for the modules
890
     * @param ServerRequestInterface $request
891
     */
892
    protected function makeButtons(ServerRequestInterface $request): void
893
    {
894
895
896
897
        if ($this->MOD_SETTINGS['function'] == 1 || $this->MOD_SETTINGS['function'] == 2) {
            // Add CSH (Context Sensitive Help) icon to tool bar
            $contextSensitiveHelpButton = $this->buttonBar->makeHelpButton()
                ->setModuleName($this->descrTable)
898
                ->setFieldName('columns_' . $this->MOD_SETTINGS['function']);
899
900
            $this->buttonBar->addButton($contextSensitiveHelpButton);
        }
901
902
903
        $lang = $this->getLanguageService();
        // View page
        if (!VersionState::cast($this->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
904
            $viewButton = $this->buttonBar->makeLinkButton()
905
                ->setOnClick(BackendUtility::viewOnClick($this->pageinfo['uid'], '', BackendUtility::BEgetRootLine($this->pageinfo['uid'])))
906
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
907
                ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
908
909
910
                ->setHref('#');

            $this->buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
911
912
        }
        // Shortcut
913
914
915
916
        $shortcutButton = $this->buttonBar->makeShortcutButton()
            ->setModuleName($this->moduleName)
            ->setGetVariables([
                'id',
917
                'route',
918
919
920
921
922
923
924
925
926
927
                'edit_record',
                'pointer',
                'new_unique_uid',
                'search_field',
                'search_levels',
                'showLimit'
            ])
            ->setSetVariables(array_keys($this->MOD_MENU));
        $this->buttonBar->addButton($shortcutButton);

928
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
929
        // Cache
930
        if (empty($this->modTSconfig['properties']['disableAdvanced'])) {
931
            $clearCacheButton = $this->buttonBar->makeLinkButton()
932
933
934
                ->setHref('#')
                ->setDataAttributes(['id' => $this->pageinfo['uid']])
                ->setClasses('t3js-clear-page-cache')
935
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
936
                ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
937
            $this->buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
938
        }
939
        if (empty($this->modTSconfig['properties']['disableIconToolbar'])) {
940
            // Edit page properties and page language overlay icons
941
942
943
            if ($this->isPageEditable() && $this->getBackendUser()->checkLanguageAccess(0)) {
                /** @var \TYPO3\CMS\Core\Http\NormalizedParams */
                $normalizedParams = $request->getAttribute('normalizedParams');
944
                // Edit localized pages only when one specific language is selected
945
                if ($this->MOD_SETTINGS['function'] == 1 && $this->current_sys_language > 0) {
946
947
                    $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
                    $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
948
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
949
                        ->getQueryBuilderForTable('pages');
950
951
952
953
954
955
                    $queryBuilder->getRestrictions()
                        ->removeAll()
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                        ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
                    $overlayRecord = $queryBuilder
                        ->select('uid')
956
                        ->from('pages')
957
                        ->where(
958
                            $queryBuilder->expr()->eq(
959
                                $localizationParentField,
960
961
962
                                $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                            ),
                            $queryBuilder->expr()->eq(
963
                                $languageField,
964
965
                                $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                            )
966
967
968
969
                        )
                        ->setMaxResults(1)
                        ->execute()
                        ->fetch();
970
971
972
                    // Edit button
                    $urlParameters = [
                        'edit' => [
973
                            'pages' => [
974
975
976
                                $overlayRecord['uid'] => 'edit'
                            ]
                        ],
977
                        'returnUrl' => $normalizedParams->getRequestUri(),
978
                    ];
979
980

                    $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
981
                    $editLanguageButton = $this->buttonBar->makeLinkButton()
982
                        ->setHref($url)
983
                        ->setTitle($lang->getLL('editPageLanguageOverlayProperties'))
984
985
                        ->setIcon($this->iconFactory->getIcon('mimetypes-x-content-page-language-overlay', Icon::SIZE_SMALL));
                    $this->buttonBar->addButton($editLanguageButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
986
                }
987
988
989
990
991
992
                $urlParameters = [
                    'edit' => [
                        'pages' => [
                            $this->id => 'edit'
                        ]
                    ],
993
                    'returnUrl' => $normalizedParams->getRequestUri(),
994
                ];
995
                $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
996
                $editPageButton = $this->buttonBar->makeLinkButton()
997
                    ->setHref($url)
998
                    ->setTitle($lang->getLL('editPageProperties'))
999
1000
                    ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
                $this->buttonBar->addButton($editPageButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
            }
        }
    }

    /*******************************
     *
     * Other functions
     *
     ******************************/
    /**
     * Returns the number of hidden elements (including those hidden by start/end times)
     * on the current page (for the current sys_language)
     *
1014
     * @param array $contentConfig
1015
1016
     * @return int
     */
1017
    protected function getNumberOfHiddenElements(array $contentConfig = []): int
1018
    {
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));

        $queryBuilder
            ->count('uid')
            ->from('tt_content')
            ->where(