PageLayoutController.php 45.5 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4

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
namespace TYPO3\CMS\Backend\Controller;

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

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

    /**
     * Module TSconfig
     *
     * @var array
     */
67
    protected $modTSconfig = [];
68
69
70
71
72
73

    /**
     * Module shared TSconfig
     *
     * @var array
     */
74
    protected $modSharedTSconfig = [];
75
76
77
78

    /**
     * Current ids page record
     *
79
     * @var array|bool
80
     * @internal
81
82
83
84
85
86
87
88
     */
    public $pageinfo;

    /**
     * List of column-integers to edit. Is set from TSconfig, default is "1,0,2,3"
     *
     * @var string
     */
89
    protected $colPosList;
90
91
92
93
94
95

    /**
     * Currently selected language for editing content elements
     *
     * @var int
     */
96
    protected $current_sys_language;
97
98
99
100
101
102

    /**
     * Menu configuration
     *
     * @var array
     */
103
    protected $MOD_MENU = [];
104
105
106
107
108

    /**
     * Module settings (session variable)
     *
     * @var array
109
     * @internal
110
     */
111
    public $MOD_SETTINGS = [];
112
113
114
115
116
117
118

    /**
     * List of column-integers accessible to the current BE user.
     * Is set from TSconfig, default is $colPosList
     *
     * @var string
     */
119
    protected $activeColPosList;
120
121
122
123
124
125
126
127

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

128
129
130
131
132
133
134
135
136
137
    /**
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

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

138
139
140
141
142
    /**
     * @var string
     */
    protected $searchContent;

143
144
145
146
147
    /**
     * @var SiteLanguage[]
     */
    protected $availableLanguages;

148
    /**
149
     * @var PageLayoutContext|null
150
     */
151
    protected $context;
152

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    protected IconFactory $iconFactory;
    protected PageRenderer $pageRenderer;
    protected UriBuilder $uriBuilder;
    protected PageRepository $pageRepository;
    protected ModuleTemplateFactory $moduleTemplateFactory;

    public function __construct(
        IconFactory $iconFactory,
        PageRenderer $pageRenderer,
        UriBuilder $uriBuilder,
        PageRepository $pageRepository,
        ModuleTemplateFactory $moduleTemplateFactory
    ) {
        $this->iconFactory = $iconFactory;
        $this->pageRenderer = $pageRenderer;
        $this->uriBuilder = $uriBuilder;
        $this->pageRepository = $pageRepository;
        $this->moduleTemplateFactory = $moduleTemplateFactory;
    }
172
173
174
175
176
177
178
179
180
    /**
     * 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
    {
181
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
182
        $this->buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
183
184
        $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
        // Setting module configuration / page select clause
185
        $this->id = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0);
186

187
188
        // Load page info array
        $this->pageinfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
189
190
191
192
193
194
195
196
        if ($this->pageinfo !== false) {
            // If page info is not resolved, user has no access or the ID parameter was malformed.
            $this->context = GeneralUtility::makeInstance(
                PageLayoutContext::class,
                $this->pageinfo,
                GeneralUtility::makeInstance(BackendLayoutView::class)->getBackendLayoutForPage($this->id)
            );
        }
197

198
199
200
201
202
203
204
        /** @var SiteInterface $currentSite */
        $currentSite = $request->getAttribute('site');
        $this->availableLanguages = $currentSite->getAvailableLanguages($this->getBackendUser(), false, $this->id);
        // initialize page/be_user TSconfig settings
        $pageTsConfig = BackendUtility::getPagesTSconfig($this->id);
        $this->modSharedTSconfig['properties'] = $pageTsConfig['mod.']['SHARED.'] ?? [];
        $this->modTSconfig['properties'] = $pageTsConfig['mod.']['web_layout.'] ?? [];
205

206
        // Initialize menu
207
        $this->menuConfig($request);
208
        // Setting sys language from session var
209
        $this->current_sys_language = (int)$this->MOD_SETTINGS['language'];
210
211
212

        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');

213
214
215
216
217
        $this->moduleTemplate->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab'),
            $this->pageinfo['title'] ?? ''
        );

218
219
        $this->main($request);
        return new HtmlResponse($this->moduleTemplate->renderContent());
220
221
222
223
    }

    /**
     * Initialize menu array
224
     * @param ServerRequestInterface $request
225
     */
226
    protected function menuConfig(ServerRequestInterface $request): void
227
228
    {
        // MENU-ITEMS:
229
        $this->MOD_MENU = [
230
            'tt_content_showHidden' => '',
231
            'function' => [
232
233
                1 => $this->getLanguageService()->getLL('m_function_1'),
                2 => $this->getLanguageService()->getLL('m_function_2')
234
235
            ],
            'language' => [
236
                0 => $this->getLanguageService()->getLL('m_default')
237
238
            ]
        ];
239

240
241
        // 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.
242
        if ($this->id) {
243
244
245
246
247
            // 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))
248
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
249
250
            $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
                ->from('pages')
251
                ->where(
252
                    $queryBuilder->expr()->eq(
253
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
254
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
255
                    )
256
                )->execute();
257
            while ($pageTranslation = $statement->fetchAssociative()) {
258
                $languageId = $pageTranslation[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
259
260
                if (isset($this->availableLanguages[$languageId])) {
                    $this->MOD_MENU['language'][$languageId] = $this->availableLanguages[$languageId]->getTitle();
261
                }
262
            }
263
            // Override the label
264
265
            if (isset($this->availableLanguages[0])) {
                $this->MOD_MENU['language'][0] = $this->availableLanguages[0]->getTitle();
266
            }
267
268
269
270
271
272
273
            // We need to add -1 (all) here so a possible -1 value in &SET['language'] will be respected
            // by BackendUtility::getModuleData. Actually, this is only relevant if we are dealing with the
            // "languages" mode, which however can only be determined, after the MOD_SETTINGS have been calculated
            // by BackendUtility::getModuleData => chicken and egg problem. We therefore remove the -1 item from
            // the menu again, as soon as we are able to determine the requested mode.
            // @todo Replace the whole "mode" handling with some more robust solution
            $this->MOD_MENU['language'][-1] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:multipleLanguages');
274
        }
275
        // Initialize the available actions
276
        $actions = $this->initActions();
277
        // Clean up settings
278
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $request->getParsedBody()['SET'] ?? $request->getQueryParams()['SET'] ?? [], $this->moduleName);
279
        // For all elements to be shown in draft workspaces & to also show hidden elements by default if user hasn't disabled the option
280
281
282
        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
283
        ) {
284
285
            $this->MOD_SETTINGS['tt_content_showHidden'] = 1;
        }
286
287
288
289
        if ((int)$this->MOD_SETTINGS['function'] !== 2) {
            // Remove -1 (all) from the module menu if not "languages" mode
            unset($this->MOD_MENU['language'][-1]);
        }
290
291
        // Make action menu from available actions
        $this->makeActionMenu($actions);
292
293
294
    }

    /**
295
     * Initializes the available actions this module provides
296
     *
297
     * @return array the available actions
298
     */
299
    protected function initActions(): array
300
    {
301
        $actions = [
302
            1 => $this->getLanguageService()->getLL('m_function_1')
303
        ];
304
305
306
        // Find if there are ANY languages at all (and if not, do not show the language option from function menu).
        if (count($this->availableLanguages) > 1) {
            $actions[2] = $this->getLanguageService()->getLL('m_function_2');
307
        }
308
        $this->makeLanguageMenu();
309
310
311
312
313
314
315
        // 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]);
            }
        }
316
317
318
319
320
321

        return $actions;
    }

    /**
     * This creates the dropdown menu with the different actions this module is able to provide.
322
     * For now they are Columns and Languages.
323
324
325
     *
     * @param array $actions array with the available actions
     */
326
    protected function makeActionMenu(array $actions): void
327
    {
328
329
330
331
        $actionMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
        $actionMenu->setIdentifier('actionMenu');
        $actionMenu->setLabel('');

332
        $defaultKey = null;
333
        $foundDefaultKey = false;
334
        foreach ($actions as $key => $action) {
335
336
337
            $menuItem = $actionMenu
                ->makeMenuItem()
                ->setTitle($action)
338
                ->setHref((string)$this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->id, 'SET' => ['function' => $key]]));
339

340
            if (!$foundDefaultKey) {
341
                $defaultKey = $key;
342
                $foundDefaultKey = true;
343
            }
344
345
            if ((int)$this->MOD_SETTINGS['function'] === $key) {
                $menuItem->setActive(true);
346
                $defaultKey = null;
347
348
            }
            $actionMenu->addMenuItem($menuItem);
349
        }
350
351
352
        if (isset($defaultKey)) {
            $this->MOD_SETTINGS['function'] = $defaultKey;
        }
353
        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu);
354
355
356
357
358
359
360
    }

    /**
     * Generate the flashmessages for current pid
     *
     * @return string HTML content with flashmessages
     */
361
    protected function getHeaderFlashMessagesForCurrentPid(): string
362
363
364
    {
        $content = '';
        $lang = $this->getLanguageService();
365

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

369
370
371
372
373
374
375
376
        // 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>';
377
378
                $message .= '<a class="btn btn-info" data-dispatch-action="TYPO3.ModuleMenu.showModule" data-dispatch-args-list="web_list">'
                    . $lang->getLL('goToListModule') . '</a>';
379
                $view->assignMultiple([
380
381
382
                    'title' => $title,
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
383
                ]);
384
385
                $content .= $view->render();
            }
386
387
388
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)$this->pageinfo['shortcut_mode'];
            $targetPage = [];
389
390
            $message = '';
            $state = InfoboxViewHelper::STATE_ERROR;
391

392
            if ($shortcutMode || $this->pageinfo['shortcut']) {
393
394
                switch ($shortcutMode) {
                    case PageRepository::SHORTCUT_MODE_NONE:
395
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage($this->pageinfo['shortcut']));
396
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : '';
397
398
                        break;
                    case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
399
                        $menuOfPages = $this->pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
400
401
                        $targetPage = reset($menuOfPages) ?: [];
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredFirstSubpageMessage') : '';
402
403
                        break;
                    case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
404
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage($this->pageinfo['pid']));
405
406
407
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredParentPageMessage') : '';
                        break;
                    case PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE:
408
                        $possibleTargetPages = $this->pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
409
410
411
412
413
414
                        if ($possibleTargetPages === []) {
                            $message .= $lang->getLL('pageIsMisconfiguredOrNotAccessibleRandomInternalLinkMessage');
                            break;
                        }
                        $message = $lang->getLL('pageIsRandomInternalLinkMessage');
                        $state = InfoboxViewHelper::STATE_INFO;
415
416
                        break;
                }
417
418
                $message = htmlspecialchars($message);
                if ($targetPage !== [] && $shortcutMode !== PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
419
                    $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $targetPage['id']]);
420
                    $path = BackendUtility::getRecordPath($targetPage['uid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 1000);
421
                    $linkedPath = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($path) . '</a>';
422
                    $message .= sprintf(htmlspecialchars($lang->getLL('pageIsInternalLinkMessage')), $linkedPath);
423
                    $message .= ' (' . htmlspecialchars($lang->sL(BackendUtility::getLabelFromItemlist('pages', 'shortcut_mode', (string)$shortcutMode))) . ')';
424
                    $state = InfoboxViewHelper::STATE_INFO;
425
426
                }
            } else {
427
428
                $message = htmlspecialchars($lang->getLL('pageIsMisconfiguredInternalLinkMessage'));
                $state = InfoboxViewHelper::STATE_ERROR;
429
            }
430
431
432
433
434
435
436

            $view->assignMultiple([
                'title' => $this->pageinfo['title'],
                'message' => $message,
                'state' => $state
            ]);
            $content .= $view->render();
437
438
439
440
441
442
443
444
445
        } 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 {
446
                $externalUrl = $this->pageRepository->getExtURL($this->pageinfo);
447
448
                if (is_string($externalUrl)) {
                    $externalUrl = htmlspecialchars($externalUrl);
449
                    $externalUrlHtml = '<a href="' . $externalUrl . '" target="_blank" rel="noreferrer">' . $externalUrl . '</a>';
450
451
452
453
454
455
456
457
                    $view->assignMultiple([
                        'title' => $this->pageinfo['title'],
                        'message' => sprintf($lang->getLL('pageIsExternalLinkMessage'), $externalUrlHtml),
                        'state' => InfoboxViewHelper::STATE_INFO
                    ]);
                    $content .= $view->render();
                }
            }
458
459
460
        }
        // If content from different pid is displayed
        if ($this->pageinfo['content_from_pid']) {
461
            $contentPage = (array)BackendUtility::getRecord('pages', (int)$this->pageinfo['content_from_pid']);
462
            $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->pageinfo['content_from_pid']]);
463
            $title = BackendUtility::getRecordTitle('pages', $contentPage);
464
            $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageinfo['content_from_pid'] . ')</a>';
465
            $message = sprintf($lang->getLL('content_from_pid_title'), $link);
466
            $view->assignMultiple([
467
468
469
                'title' => $title,
                'message' => $message,
                'state' => InfoboxViewHelper::STATE_INFO
470
            ]);
471
            $content .= $view->render();
472
473
474
475
476
477
478
479
480
481
482
        } 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();
            }
483
484
485
486
        }
        return $content;
    }

487
488
489
490
491
492
    /**
     * Get all pages with links where the content of a page $pageId is also shown on
     *
     * @param int $pageId
     * @return string
     */
493
    protected function getPageLinksWhereContentIsAlsoShownOn($pageId): string
494
495
496
497
498
499
500
501
502
503
    {
        $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 = [];
504
        $rows = $queryBuilder->execute()->fetchAllAssociative();
505
506
        if (!empty($rows)) {
            foreach ($rows as $row) {
507
                $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' =>  $row['uid']]);
508
                $title = BackendUtility::getRecordTitle('pages', $row);
509
                $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')</a>';
510
511
512
513
514
515
                $links[] = $link;
            }
        }
        return implode(', ', $links);
    }

516
517
518
    /**
     * @return string $title
     */
519
    protected function getLocalizedPageTitle(): string
520
521
    {
        if ($this->current_sys_language > 0) {
522
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
523
                ->getQueryBuilderForTable('pages');
524
525
526
            $queryBuilder->getRestrictions()
                ->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
527
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
528
            $localizedPage = $queryBuilder
529
                ->select('*')
530
                ->from('pages')
531
                ->where(
532
                    $queryBuilder->expr()->eq(
533
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
534
535
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                    ),
536
                    $queryBuilder->expr()->eq(
537
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
538
539
                        $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                    )
540
541
542
                )
                ->setMaxResults(1)
                ->execute()
543
                ->fetchAssociative();
544
545
            BackendUtility::workspaceOL('pages', $localizedPage);
            return $localizedPage['title'];
546
        }
547
        return $this->pageinfo['title'];
548
549
550
551
552
    }

    /**
     * Main function.
     * Creates some general objects and calls other functions for the main rendering of module content.
553
554
     *
     * @param ServerRequestInterface $request
555
     */
556
    protected function main(ServerRequestInterface $request): void
557
    {
558
        $content = '';
559
560
        // Access check...
        // The page will show only if there is a valid page and if this page may be viewed by the user
561
        if ($this->id && is_array($this->pageinfo)) {
562
563
            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);

564
565
566
            $this->moduleTemplate->addJavaScriptCode('mainJsFunctions', '
                if (top.fsMod) {
                    top.fsMod.recentIds["web"] = ' . (int)$this->id . ';
567
                    top.fsMod.navFrameHighlightedID["web"] = top.fsMod.currentBank + "_" + ' . (int)$this->id . ';
568
569
                }
                function deleteRecord(table,id,url) {   //
570
                    window.location.href = ' . GeneralUtility::quoteJSvalue((string)$this->uriBuilder->buildUriFromRoute('tce_db') . '&cmd[')
571
                                            . ' + table + "][" + id + "][delete]=1&redirect=" + encodeURIComponent(url);
572
573
574
575
                    return false;
                }
            ');

576
577
            if ($this->context instanceof PageLayoutContext) {
                $backendLayout = $this->context->getBackendLayout();
578

579
580
581
                // Find backend layout / columns
                if (!empty($backendLayout->getColumnPositionNumbers())) {
                    $this->colPosList = implode(',', $backendLayout->getColumnPositionNumbers());
582
                }
583
                // Removing duplicates, if any
584
                $colPosArray = array_unique(GeneralUtility::intExplode(',', $this->colPosList));
585
586
                // Accessible columns
                if (isset($this->modSharedTSconfig['properties']['colPos_list']) && trim($this->modSharedTSconfig['properties']['colPos_list']) !== '') {
587
                    $activeColPosArray = array_unique(GeneralUtility::intExplode(',', trim($this->modSharedTSconfig['properties']['colPos_list'])));
588
                    // Match with the list which is present in the colPosList for the current page
589
590
591
592
                    if (!empty($colPosArray) && !empty($activeColPosArray)) {
                        $activeColPosArray = array_unique(array_intersect(
                            $activeColPosArray,
                            $colPosArray
593
594
595
                        ));
                    }
                } else {
596
                    $activeColPosArray = $colPosArray;
597
                }
598
599
                $this->activeColPosList = implode(',', $activeColPosArray);
                $this->colPosList = implode(',', $colPosArray);
600
601
            }

602
603
            $content .= $this->getHeaderFlashMessagesForCurrentPid();

604
            // Render the primary module content:
605
            $content .= '<form action="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->id])) . '" id="PageLayoutController" method="post">';
606
607
608
609
            // Page title
            $content .= '<h1 class="' . ($this->isPageEditable($this->current_sys_language) ? 't3js-title-inlineedit' : '') . '">' . htmlspecialchars($this->getLocalizedPageTitle()) . '</h1>';
            // All other listings
            $content .= $this->renderContent();
610
            $content .= '</form>';
611
            // Setting up the buttons for the docheader
612
613
            $this->makeButtons($request);

614
615
616
617
618
619
620
            // Create LanguageMenu
            $this->makeLanguageMenu();
        } else {
            $this->moduleTemplate->addJavaScriptCode(
                'mainJsFunctions',
                'if (top.fsMod) top.fsMod.recentIds["web"] = ' . (int)$this->id . ';'
            );
621
            $content .= '<h1>' . htmlspecialchars($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']) . '</h1>';
622
623
            $view = GeneralUtility::makeInstance(StandaloneView::class);
            $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/InfoBox.html'));
624
            $view->assignMultiple([
625
626
                'title' => $this->getLanguageService()->getLL('clickAPage_header'),
                'message' => $this->getLanguageService()->getLL('clickAPage_content'),
627
                'state' => InfoboxViewHelper::STATE_INFO
628
            ]);
629
            $content .= $view->render();
630
        }
631
632
        // Set content
        $this->moduleTemplate->setContent($content);
633
634
635
    }

    /**
636
     * Rendering content
637
638
639
     *
     * @return string
     */
640
    protected function renderContent(): string
641
    {
642
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
643
644
645
646
647
648
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization');
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/DragDrop');
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste');
        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
649

650
651
        $tableOutput = '';
        $numberOfHiddenElements = 0;
652

653
654
655
656
657
658
659
660
661
662
663
664
665
666
        if ($this->context instanceof PageLayoutContext) {
            // Context may not be set, which happens if the page module is viewed by a user with no access to the
            // current page, or if the ID parameter is malformed. In this case we do not resolve any backend layout
            // or other page structure information and we do not render any "table output" for the module.
            $configuration = $this->context->getDrawingConfiguration();
            $configuration->setDefaultLanguageBinding(!empty($this->modTSconfig['properties']['defLangBinding']));
            $configuration->setActiveColumns(GeneralUtility::trimExplode(',', $this->activeColPosList));
            $configuration->setShowHidden((bool)$this->MOD_SETTINGS['tt_content_showHidden']);
            $configuration->setLanguageColumns($this->MOD_MENU['language']);
            $configuration->setShowNewContentWizard(empty($this->modTSconfig['properties']['disableNewContentElementWizard']));
            $configuration->setSelectedLanguageId((int)$this->MOD_SETTINGS['language']);
            if ($this->MOD_SETTINGS['function'] == 2) {
                $configuration->setLanguageMode(true);
            }
667

668
669
            $numberOfHiddenElements = $this->getNumberOfHiddenElements($configuration->getLanguageColumns());

670
            $pageLayoutDrawer = $this->context->getBackendLayoutRenderer();
671

672
673
674
675
676
677
            $pageActionsCallback = null;
            if ($this->context->isPageEditable()) {
                $languageOverlayId = 0;
                $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->current_sys_language);
                if (is_array($pageLocalizationRecord)) {
                    $pageLocalizationRecord = reset($pageLocalizationRecord);
678
                }
679
680
                if (!empty($pageLocalizationRecord['uid'])) {
                    $languageOverlayId = $pageLocalizationRecord['uid'];
681
                }
682
683
684
685
                $pageActionsCallback = 'function(PageActions) {
                    PageActions.setPageId(' . (int)$this->id . ');
                    PageActions.setLanguageOverlayId(' . $languageOverlayId . ');
                }';
686
            }
687
688
            $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback);
            $tableOutput = $pageLayoutDrawer->drawContent();
689
690
        }

691
        if ($this->getBackendUser()->check('tables_select', 'tt_content') && $numberOfHiddenElements > 0) {
692
            // Toggle hidden ContentElements
693
            $tableOutput .= '
694
695
696
                <div class="form-check">
                    <input type="checkbox" id="checkTt_content_showHidden" class="form-check-input" name="SET[tt_content_showHidden]" value="1" ' . ($this->MOD_SETTINGS['tt_content_showHidden'] ? 'checked="checked"' : '') . ' />
                    <label class="form-check-label" for="checkTt_content_showHidden">
697
698
699
                        ' . htmlspecialchars($this->getLanguageService()->getLL('hiddenCE')) . ' (<span class="t3js-hidden-counter">' . $numberOfHiddenElements . '</span>)
                    </label>
                </div>';
700
        }
701

702
703
704
        // Init the content
        $content = '';
        // Additional header content
705
706
707
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawHeaderHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
708
        }
709
        $content .= $tableOutput;
710

711
        // Additional footer content
712
713
714
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawFooterHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
715
716
717
718
        }
        return $content;
    }

719
720
721
722
723
724
725
726
727
728
    /**
     * Make the ModuleTemplate public accessible for the use in hooks.
     *
     * @return ModuleTemplate
     */
    public function getModuleTemplate(): ModuleTemplate
    {
        return $this->moduleTemplate;
    }

729
730
731
732
733
734
    /***************************
     *
     * Sub-content functions, rendering specific parts of the module content.
     *
     ***************************/
    /**
735
     * This creates the buttons for the modules
736
     * @param ServerRequestInterface $request
737
     */
738
    protected function makeButtons(ServerRequestInterface $request): void
739
    {
740
741
742
743
744
        // Add CSH (Context Sensitive Help) icon to tool bar
        $contextSensitiveHelpButton = $this->buttonBar->makeHelpButton()
            ->setModuleName('_MOD_' . $this->moduleName)
            ->setFieldName('columns_' . $this->MOD_SETTINGS['function']);
        $this->buttonBar->addButton($contextSensitiveHelpButton);
745
746
        $lang = $this->getLanguageService();
        // View page
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
        $pageTsConfig = BackendUtility::getPagesTSconfig($this->id);
        // Exclude sysfolders, spacers and recycler by default
        $excludeDokTypes = [
            PageRepository::DOKTYPE_RECYCLER,
            PageRepository::DOKTYPE_SYSFOLDER,
            PageRepository::DOKTYPE_SPACER
        ];
        // Custom override of values
        if (isset($pageTsConfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
            $excludeDokTypes = GeneralUtility::intExplode(
                ',',
                $pageTsConfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
                true
            );
        }

        if (
            !in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)
            && !VersionState::cast($this->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
        ) {
767
            $languageParameter = $this->current_sys_language ? ('&L=' . $this->current_sys_language) : '';
768
            $previewDataAttributes = PreviewUriBuilder::create((int)$this->pageinfo['uid'])
769
770
771
                ->withRootLine(BackendUtility::BEgetRootLine($this->pageinfo['uid']))
                ->withAdditionalQueryParameters($languageParameter)
                ->buildDispatcherDataAttributes();
772
            $viewButton = $this->buttonBar->makeLinkButton()
773
                ->setDataAttributes($previewDataAttributes ?? [])
774
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
775
                ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
776
777
778
                ->setHref('#');

            $this->buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
779
780
        }
        // Shortcut
781
        $shortcutButton = $this->buttonBar->makeShortcutButton()
782
            ->setRouteIdentifier($this->moduleName)
783
            ->setDisplayName($this->getShortcutTitle())
784
785
786
787
788
789
790
791
            ->setArguments([
                'id' => (int)$this->id,
                'SET' => [
                    'tt_content_showHidden' => (bool)$this->MOD_SETTINGS['tt_content_showHidden'],
                    'function' => (int)$this->MOD_SETTINGS['function'],
                    'language' => (int)$this->current_sys_language,
                ]
            ]);
792
793
        $this->buttonBar->addButton($shortcutButton);

794
        // Cache
795
796
797
798
799
800
801
802
        $clearCacheButton = $this->buttonBar->makeLinkButton()
            ->setHref('#')
            ->setDataAttributes(['id' => $this->pageinfo['uid']])
            ->setClasses('t3js-clear-page-cache')
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
            ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
        $this->buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);

803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
        // Edit page properties and page language overlay icons
        if ($this->isPageEditable(0)) {
            /** @var \TYPO3\CMS\Core\Http\NormalizedParams */
            $normalizedParams = $request->getAttribute('normalizedParams');
            // Edit localized pages only when one specific language is selected
            if ($this->MOD_SETTINGS['function'] == 1 && $this->current_sys_language > 0) {
                $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
                $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                    ->getQueryBuilderForTable('pages');
                $queryBuilder->getRestrictions()
                    ->removeAll()
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                    ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
                $overlayRecord = $queryBuilder
                    ->select('uid')
                    ->from('pages')
                    ->where(
                        $queryBuilder->expr()->eq(
                            $localizationParentField,
                            $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                        ),
                        $queryBuilder->expr()->eq(
                            $languageField,
                            $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
828
                        )
829
830
831
                    )
                    ->setMaxResults(1)
                    ->execute()
832
                    ->fetchAssociative();
833
834
                BackendUtility::workspaceOL('pages', $overlayRecord, (int)$this->getBackendUser()->workspace);
                // Edit button
835
836
837
                $urlParameters = [
                    'edit' => [
                        'pages' => [
838
                            $overlayRecord['uid'] => 'edit'
839
840
                        ]
                    ],
841
                    'returnUrl' => $normalizedParams->getRequestUri(),
842
                ];
843

844
                $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
845
                $editLanguageButton = $this->buttonBar->makeLinkButton()
846
                    ->setHref($url)
847
848
849
                    ->setTitle($lang->getLL('editPageLanguageOverlayProperties'))
                    ->setIcon($this->iconFactory->getIcon('mimetypes-x-content-page-language-overlay', Icon::SIZE_SMALL));
                $this->buttonBar->addButton($editLanguageButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
850
            }
851
852
853
854
855
856
857
858
859
860
861
862
863
864
            $urlParameters = [
                'edit' => [
                    'pages' => [
                        $this->id => 'edit'
                    ]
                ],
                'returnUrl' => $normalizedParams->getRequestUri(),
            ];
            $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
            $editPageButton = $this->buttonBar->makeLinkButton()
                ->setHref($url)
                ->setTitle($lang->getLL('editPageProperties'))
                ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
            $this->buttonBar->addButton($editPageButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
865
866
867
868
869
870
871
872
873
874
875
876
        }
    }

    /*******************************
     *
     * Other functions
     *
     ******************************/
    /**
     * Returns the number of hidden elements (including those hidden by start/end times)
     * on the current page (for the current sys_language)
     *
877
     * @param array $languageColumns
878
879
     * @return int
     */
880
    protected function getNumberOfHiddenElements(array $languageColumns): int
881
    {
882
        $andWhere = [];
883
884
885
886
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
887
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
888
889
890
891
892

        $queryBuilder
            ->count('uid')
            ->from('tt_content')
            ->where(
893
894
895
                $queryBuilder->expr()->eq(
                    'pid',
                    $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
896
897
898
                )
            );

899
        if (!empty($languageColumns)) {
900
901
902
903
904
905
906
907
908
909
910
            // Multi-language view is active
            if ($this->current_sys_language > 0) {
                $queryBuilder->andWhere(
                    $queryBuilder->expr()->in(
                        'sys_language_uid',
                        [0, $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)]
                    )
                );
            }
        } else {
            $queryBuilder->andWhere(
911
912
913
914
                $queryBuilder->expr()->eq(
                    'sys_language_uid',
                    $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                )
915
            );
916
        }
917
918

        if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled'])) {
919
920
921
922
            $andWhere[] = $queryBuilder->expr()->neq(
                'hidden',
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
            );
923
924
925
926
        }

        if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['starttime'])) {
            $andWhere[] = $queryBuilder->expr()->andX(
927
928
929
930
931
932
933
934
                $queryBuilder->expr()->neq(
                    'starttime',
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
                ),
                $queryBuilder->expr()->gt(
                    'starttime',
                    $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], \PDO::PARAM_INT)
                )
935
936
937
938
939
            );
        }

        if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['endtime'])) {
            $andWhere[] = $queryBuilder->expr()->andX(
940
941
942
943
944
945
946
947
                $queryBuilder->expr()->neq(
                    'endtime',
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
                ),
                $queryBuilder->expr()->lte(
                    'endtime',
                    $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], \PDO::PARAM_INT)
                )
948
949
950
951
952
953
954
955
956
957