PageLayoutController.php 47.4 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\Clipboard\Clipboard;
23
use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
24
use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
25
use TYPO3\CMS\Backend\Module\ModuleLoader;
26
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
27
use TYPO3\CMS\Backend\Routing\UriBuilder;
28
29
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
30
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
Nicole Cordes's avatar
Nicole Cordes committed
31
use TYPO3\CMS\Backend\Utility\BackendUtility;
32
use TYPO3\CMS\Backend\View\BackendLayoutView;
33
use TYPO3\CMS\Backend\View\Drawing\BackendLayoutRenderer;
34
use TYPO3\CMS\Backend\View\PageLayoutContext;
35
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
36
37
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
38
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
39
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
40
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
41
use TYPO3\CMS\Core\Http\HtmlResponse;
42
43
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
44
use TYPO3\CMS\Core\Localization\LanguageService;
45
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
46
use TYPO3\CMS\Core\Page\PageRenderer;
47
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
48
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
49
use TYPO3\CMS\Core\Type\Bitmask\Permission;
Nicole Cordes's avatar
Nicole Cordes committed
50
use TYPO3\CMS\Core\Utility\GeneralUtility;
51
use TYPO3\CMS\Core\Versioning\VersionState;
52
use TYPO3\CMS\Fluid\View\StandaloneView;
53
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
Nicole Cordes's avatar
Nicole Cordes committed
54

55
56
57
/**
 * Script Class for Web > Layout module
 */
58
59
60
61
62
63
class PageLayoutController
{
    /**
     * Page Id for which to make the listing
     *
     * @var int
64
     * @internal
65
66
67
68
69
70
71
72
     */
    public $id;

    /**
     * Module TSconfig
     *
     * @var array
     */
73
    protected $modTSconfig = [];
74
75
76
77
78
79

    /**
     * Module shared TSconfig
     *
     * @var array
     */
80
    protected $modSharedTSconfig = [];
81
82
83
84

    /**
     * Current ids page record
     *
85
     * @var array|bool
86
     * @internal
87
88
89
90
91
92
93
94
     */
    public $pageinfo;

    /**
     * List of column-integers to edit. Is set from TSconfig, default is "1,0,2,3"
     *
     * @var string
     */
95
    protected $colPosList;
96
97
98
99
100
101

    /**
     * Currently selected language for editing content elements
     *
     * @var int
     */
102
    protected $current_sys_language;
103
104
105
106
107
108

    /**
     * Menu configuration
     *
     * @var array
     */
109
    protected $MOD_MENU = [];
110
111
112
113
114

    /**
     * Module settings (session variable)
     *
     * @var array
115
     * @internal
116
     */
117
    public $MOD_SETTINGS = [];
118
119
120
121
122
123
124

    /**
     * List of column-integers accessible to the current BE user.
     * Is set from TSconfig, default is $colPosList
     *
     * @var string
     */
125
    protected $activeColPosList;
126
127
128
129
130
131
132
133

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

134
135
136
137
138
139
140
141
142
143
    /**
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

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

144
145
146
147
148
    /**
     * @var string
     */
    protected $searchContent;

149
150
151
152
153
    /**
     * @var SiteLanguage[]
     */
    protected $availableLanguages;

154
    /**
155
     * @var PageLayoutContext|null
156
     */
157
    protected $context;
158

159
160
161
162
163
    protected IconFactory $iconFactory;
    protected PageRenderer $pageRenderer;
    protected UriBuilder $uriBuilder;
    protected PageRepository $pageRepository;
    protected ModuleTemplateFactory $moduleTemplateFactory;
164
    protected EventDispatcher $eventDispatcher;
165
166
167
168
169
170

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

196
197
        // Load page info array
        $this->pageinfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
198
199
200
201
202
203
204
205
        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)
            );
        }
206

207
208
209
210
211
212
213
        /** @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.'] ?? [];
214

215
        // Initialize menu
216
        $this->menuConfig($request);
217
        // Setting sys language from session var
218
        $this->current_sys_language = (int)$this->MOD_SETTINGS['language'];
219
220
221
222
        // Create LanguageMenu
        $this->makeLanguageMenu();
        // Make action menu from available actions
        $this->makeActionMenu();
223
224

        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
225
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/NewContentElementWizardButton');
226

227
228
229
230
231
        $this->moduleTemplate->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab'),
            $this->pageinfo['title'] ?? ''
        );

232
233
        $this->main($request);
        return new HtmlResponse($this->moduleTemplate->renderContent());
234
235
236
237
    }

    /**
     * Initialize menu array
238
     * @param ServerRequestInterface $request
239
     */
240
    protected function menuConfig(ServerRequestInterface $request): void
241
242
    {
        // MENU-ITEMS:
243
        $this->MOD_MENU = [
244
            'tt_content_showHidden' => '',
245
            'function' => [
246
                1 => $this->getLanguageService()->getLL('m_function_1'),
247
                2 => $this->getLanguageService()->getLL('m_function_2'),
248
249
            ],
            'language' => [
250
251
                0 => $this->getLanguageService()->getLL('m_default'),
            ],
252
        ];
253

254
255
        // 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.
256
        if ($this->id) {
257
258
259
260
261
            // 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))
262
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
263
264
            $statement = $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
                ->from('pages')
265
                ->where(
266
                    $queryBuilder->expr()->eq(
267
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
268
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
269
                    )
270
                )->execute();
271
            while ($pageTranslation = $statement->fetchAssociative()) {
272
                $languageId = $pageTranslation[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
273
274
                if (isset($this->availableLanguages[$languageId])) {
                    $this->MOD_MENU['language'][$languageId] = $this->availableLanguages[$languageId]->getTitle();
275
                }
276
            }
277
            // Override the label
278
279
            if (isset($this->availableLanguages[0])) {
                $this->MOD_MENU['language'][0] = $this->availableLanguages[0]->getTitle();
280
            }
281

282
283
            // Add special "-1" in case translations of the current page exist
            if (count($this->MOD_MENU['language']) > 1) {
284
285
286
287
288
289
290
291
                // 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');
            }
292
        }
293
        // Clean up settings
294
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $request->getParsedBody()['SET'] ?? $request->getQueryParams()['SET'] ?? [], $this->moduleName);
295
        // For all elements to be shown in draft workspaces & to also show hidden elements by default if user hasn't disabled the option
296
297
298
        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
299
        ) {
300
301
            $this->MOD_SETTINGS['tt_content_showHidden'] = 1;
        }
302
303
304
        if ((int)$this->MOD_SETTINGS['function'] !== 2) {
            // Remove -1 (all) from the module menu if not "languages" mode
            unset($this->MOD_MENU['language'][-1]);
305
306
307
308
309
            // In case -1 (all) is still set as language, but we are no longer in
            // "languages" mode, we fall back to the default, preventing an empty grid.
            if ((int)$this->MOD_SETTINGS['language'] === -1) {
                $this->MOD_SETTINGS['language'] = 0;
            }
310
        }
311
312
313
    }

    /**
314
     * Initializes the available actions this module provides
315
     *
316
     * @return array the available actions
317
     */
318
    protected function initActions(): array
319
    {
320
        $actions = [
321
            1 => $this->getLanguageService()->getLL('m_function_1'),
322
        ];
323
        // Find if there are ANY languages at all (and if not, do not show the language option from function menu).
324
325
        // The second check is for an edge case: Only two languages in the site and the default is not allowed.
        if (count($this->availableLanguages) > 1 || (int)array_key_first($this->availableLanguages) > 0) {
326
            $actions[2] = $this->getLanguageService()->getLL('m_function_2');
327
        }
328
329
330
331
332
333
334
        // 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]);
            }
        }
335
336
337
338
339
340

        return $actions;
    }

    /**
     * This creates the dropdown menu with the different actions this module is able to provide.
341
     * For now they are Columns and Languages.
342
     */
343
    protected function makeActionMenu(): void
344
    {
345
        $actions = $this->initActions();
346
347
348
349
        $actionMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
        $actionMenu->setIdentifier('actionMenu');
        $actionMenu->setLabel('');

350
        $defaultKey = null;
351
        $foundDefaultKey = false;
352
        foreach ($actions as $key => $action) {
353
354
355
            $menuItem = $actionMenu
                ->makeMenuItem()
                ->setTitle($action)
356
                ->setHref((string)$this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->id, 'SET' => ['function' => $key]]));
357

358
            if (!$foundDefaultKey) {
359
                $defaultKey = $key;
360
                $foundDefaultKey = true;
361
            }
362
363
            if ((int)$this->MOD_SETTINGS['function'] === $key) {
                $menuItem->setActive(true);
364
                $defaultKey = null;
365
366
            }
            $actionMenu->addMenuItem($menuItem);
367
        }
368
369
370
        if (isset($defaultKey)) {
            $this->MOD_SETTINGS['function'] = $defaultKey;
        }
371
        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu);
372
373
374
    }

    /**
375
     * Generate various messages (rendered as callouts) for the current page record (such as if the page has a special doktype).
376
     *
377
     * @return string HTML content with messages
378
     */
379
    protected function generateMessagesForCurrentPage(): string
380
381
382
    {
        $content = '';
        $lang = $this->getLanguageService();
383

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

387
388
389
390
        // If page is a folder
        if ($this->pageinfo['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
            $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
            $moduleLoader->load($GLOBALS['TBE_MODULES']);
391
392
            $modules = $moduleLoader->getModules();
            if (is_array($modules['web']['sub']['list'] ?? null)) {
393
394
                $title = $lang->getLL('goToListModule');
                $message = '<p>' . $lang->getLL('goToListModuleMessage') . '</p>';
395
396
                $message .= '<a class="btn btn-info" data-dispatch-action="TYPO3.ModuleMenu.showModule" data-dispatch-args-list="web_list">'
                    . $lang->getLL('goToListModule') . '</a>';
397
                $view->assignMultiple([
398
399
                    'title' => $title,
                    'message' => $message,
400
                    'state' => InfoboxViewHelper::STATE_INFO,
401
                ]);
402
403
                $content .= $view->render();
            }
404
405
406
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)$this->pageinfo['shortcut_mode'];
            $targetPage = [];
407
408
            $message = '';
            $state = InfoboxViewHelper::STATE_ERROR;
409

410
            if ($shortcutMode || $this->pageinfo['shortcut']) {
411
412
413
414
415
416
                // Store the current group access clause and unset it afterwards since it should
                // not be used while searching for configured shortcut pages. Actually ->getPage()
                // would allow to disable it via an argument. However, getMenu() currently does not.
                // @todo Refactor as soon as ->getMenu() allows to dynamically disable group access check
                $tempGroupAccess = $this->pageRepository->where_groupAccess;
                $this->pageRepository->where_groupAccess = '';
417
418
                switch ($shortcutMode) {
                    case PageRepository::SHORTCUT_MODE_NONE:
419
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage($this->pageinfo['shortcut']));
420
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : '';
421
422
                        break;
                    case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
423
                        $menuOfPages = $this->pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
424
425
                        $targetPage = reset($menuOfPages) ?: [];
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredFirstSubpageMessage') : '';
426
427
                        break;
                    case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
428
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage($this->pageinfo['pid']));
429
430
431
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredParentPageMessage') : '';
                        break;
                    case PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE:
432
                        $possibleTargetPages = $this->pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
433
434
435
436
437
438
                        if ($possibleTargetPages === []) {
                            $message .= $lang->getLL('pageIsMisconfiguredOrNotAccessibleRandomInternalLinkMessage');
                            break;
                        }
                        $message = $lang->getLL('pageIsRandomInternalLinkMessage');
                        $state = InfoboxViewHelper::STATE_INFO;
439
440
                        break;
                }
441
                $this->pageRepository->where_groupAccess = $tempGroupAccess;
442
443
                $message = htmlspecialchars($message);
                if ($targetPage !== [] && $shortcutMode !== PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
444
                    $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $targetPage['uid']]);
445
                    $path = BackendUtility::getRecordPath($targetPage['uid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 1000);
446
                    $linkedPath = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($path) . '</a>';
447
                    $message .= sprintf(htmlspecialchars($lang->getLL('pageIsInternalLinkMessage')), $linkedPath);
448
                    $message .= ' (' . htmlspecialchars($lang->sL(BackendUtility::getLabelFromItemlist('pages', 'shortcut_mode', (string)$shortcutMode))) . ')';
449
                    $state = InfoboxViewHelper::STATE_INFO;
450
451
                }
            } else {
452
453
                $message = htmlspecialchars($lang->getLL('pageIsMisconfiguredInternalLinkMessage'));
                $state = InfoboxViewHelper::STATE_ERROR;
454
            }
455
456
457
458

            $view->assignMultiple([
                'title' => $this->pageinfo['title'],
                'message' => $message,
459
                'state' => $state,
460
461
            ]);
            $content .= $view->render();
462
463
464
465
466
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_LINK) {
            if (empty($this->pageinfo['url'])) {
                $view->assignMultiple([
                    'title' => $this->pageinfo['title'],
                    'message' => $lang->getLL('pageIsMisconfiguredExternalLinkMessage'),
467
                    'state' => InfoboxViewHelper::STATE_ERROR,
468
469
470
                ]);
                $content .= $view->render();
            } else {
471
                $externalUrl = $this->pageRepository->getExtURL($this->pageinfo);
472
473
                if (is_string($externalUrl)) {
                    $externalUrl = htmlspecialchars($externalUrl);
474
                    $externalUrlHtml = '<a href="' . $externalUrl . '" target="_blank" rel="noreferrer">' . $externalUrl . '</a>';
475
476
477
                    $view->assignMultiple([
                        'title' => $this->pageinfo['title'],
                        'message' => sprintf($lang->getLL('pageIsExternalLinkMessage'), $externalUrlHtml),
478
                        'state' => InfoboxViewHelper::STATE_INFO,
479
480
481
482
                    ]);
                    $content .= $view->render();
                }
            }
483
484
485
        }
        // If content from different pid is displayed
        if ($this->pageinfo['content_from_pid']) {
486
            $contentPage = (array)BackendUtility::getRecord('pages', (int)$this->pageinfo['content_from_pid']);
487
            $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->pageinfo['content_from_pid']]);
488
            $title = BackendUtility::getRecordTitle('pages', $contentPage);
489
            $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageinfo['content_from_pid'] . ')</a>';
490
            $message = sprintf($lang->getLL('content_from_pid_title'), $link);
491
            $view->assignMultiple([
492
493
                'title' => $title,
                'message' => $message,
494
                'state' => InfoboxViewHelper::STATE_INFO,
495
            ]);
496
            $content .= $view->render();
497
498
499
500
501
502
503
        } else {
            $links = $this->getPageLinksWhereContentIsAlsoShownOn($this->pageinfo['uid']);
            if (!empty($links)) {
                $message = sprintf($lang->getLL('content_on_pid_title'), $links);
                $view->assignMultiple([
                    'title' => '',
                    'message' => $message,
504
                    'state' => InfoboxViewHelper::STATE_INFO,
505
506
507
                ]);
                $content .= $view->render();
            }
508
509
510
511
        }
        return $content;
    }

512
513
514
515
516
517
    /**
     * Get all pages with links where the content of a page $pageId is also shown on
     *
     * @param int $pageId
     * @return string
     */
518
    protected function getPageLinksWhereContentIsAlsoShownOn($pageId): string
519
520
521
522
523
524
525
526
527
528
    {
        $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 = [];
529
        $rows = $queryBuilder->execute()->fetchAllAssociative();
530
531
        if (!empty($rows)) {
            foreach ($rows as $row) {
532
                $linkToPid = $this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' =>  $row['uid']]);
533
                $title = BackendUtility::getRecordTitle('pages', $row);
534
                $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')</a>';
535
536
537
538
539
540
                $links[] = $link;
            }
        }
        return implode(', ', $links);
    }

541
542
543
    /**
     * @return string $title
     */
544
    protected function getLocalizedPageTitle(): string
545
546
    {
        if ($this->current_sys_language > 0) {
547
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
548
                ->getQueryBuilderForTable('pages');
549
550
551
            $queryBuilder->getRestrictions()
                ->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
552
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
553
            $localizedPage = $queryBuilder
554
                ->select('*')
555
                ->from('pages')
556
                ->where(
557
                    $queryBuilder->expr()->eq(
558
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
559
560
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                    ),
561
                    $queryBuilder->expr()->eq(
562
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
563
564
                        $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                    )
565
566
567
                )
                ->setMaxResults(1)
                ->execute()
568
                ->fetchAssociative();
569
570
            BackendUtility::workspaceOL('pages', $localizedPage);
            return $localizedPage['title'];
571
        }
572
        return $this->pageinfo['title'];
573
574
575
576
577
    }

    /**
     * Main function.
     * Creates some general objects and calls other functions for the main rendering of module content.
578
579
     *
     * @param ServerRequestInterface $request
580
     */
581
    protected function main(ServerRequestInterface $request): void
582
    {
583
        $content = '';
584
585
        // Access check...
        // The page will show only if there is a valid page and if this page may be viewed by the user
586
        if ($this->id && is_array($this->pageinfo)) {
587
            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
588
            $content .= ImmediateActionElement::moduleStateUpdateWithCurrentMount('web', (int)$this->id, true);
589
590
            if ($this->context instanceof PageLayoutContext) {
                $backendLayout = $this->context->getBackendLayout();
591

592
593
594
                // Find backend layout / columns
                if (!empty($backendLayout->getColumnPositionNumbers())) {
                    $this->colPosList = implode(',', $backendLayout->getColumnPositionNumbers());
595
                }
596
                // Removing duplicates, if any
597
                $colPosArray = array_unique(GeneralUtility::intExplode(',', $this->colPosList));
598
599
                // Accessible columns
                if (isset($this->modSharedTSconfig['properties']['colPos_list']) && trim($this->modSharedTSconfig['properties']['colPos_list']) !== '') {
600
                    $activeColPosArray = array_unique(GeneralUtility::intExplode(',', trim($this->modSharedTSconfig['properties']['colPos_list'])));
601
                    // Match with the list which is present in the colPosList for the current page
602
603
604
605
                    if (!empty($colPosArray) && !empty($activeColPosArray)) {
                        $activeColPosArray = array_unique(array_intersect(
                            $activeColPosArray,
                            $colPosArray
606
607
608
                        ));
                    }
                } else {
609
                    $activeColPosArray = $colPosArray;
610
                }
611
612
                $this->activeColPosList = implode(',', $activeColPosArray);
                $this->colPosList = implode(',', $colPosArray);
613
614
            }

615
            $content .= $this->generateMessagesForCurrentPage();
616

617
            // Render the primary module content:
618
            $content .= '<form action="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute($this->moduleName, ['id' => $this->id])) . '" id="PageLayoutController" method="post">';
619
620
621
            // Page title
            $content .= '<h1 class="' . ($this->isPageEditable($this->current_sys_language) ? 't3js-title-inlineedit' : '') . '">' . htmlspecialchars($this->getLocalizedPageTitle()) . '</h1>';
            // All other listings
622
            $content .= $this->renderContent($request);
623
            $content .= '</form>';
624
            // Setting up the buttons for the docheader
625
            $this->makeButtons($request);
626
            $this->initializeClipboard($request);
627
        } else {
628
            $content .= ImmediateActionElement::moduleStateUpdate('web', (int)$this->id);
629
            $content .= '<h1>' . htmlspecialchars($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']) . '</h1>';
630
631
            $view = GeneralUtility::makeInstance(StandaloneView::class);
            $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/InfoBox.html'));
632
            $view->assignMultiple([
633
634
                'title' => $this->getLanguageService()->getLL('clickAPage_header'),
                'message' => $this->getLanguageService()->getLL('clickAPage_content'),
635
                'state' => InfoboxViewHelper::STATE_INFO,
636
            ]);
637
            $content .= $view->render();
638
        }
639
640
        // Set content
        $this->moduleTemplate->setContent($content);
641
642
    }

643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
    /**
     * Initializes the clipboard for generating paste links dynamically via JavaScript after each "+ Content" symbol
     */
    protected function initializeClipboard(ServerRequestInterface $request): void
    {
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        $clipboard->initializeClipboard($request);
        $clipboard->lockToNormal();
        $clipboard->cleanCurrent();
        $clipboard->endClipboard();
        $elFromTable = $clipboard->elFromTable('tt_content');
        if (!empty($elFromTable) && $this->isContentEditable($this->current_sys_language)) {
            $pasteItem = (int)substr((string)key($elFromTable), 11);
            $pasteRecord = BackendUtility::getRecordWSOL('tt_content', $pasteItem);
            $pasteTitle = BackendUtility::getRecordTitle('tt_content', $pasteRecord, false, true);
            $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste', '
            function(Paste) {
                Paste.itemOnClipboardUid = ' . $pasteItem . ';
                Paste.itemOnClipboardTitle = ' . GeneralUtility::quoteJSvalue($pasteTitle) . ';
662
                Paste.copyMode = ' . GeneralUtility::quoteJSvalue($clipboard->clipData['normal']['mode'] ?? '') . ';
663
664
665
666
            }');
        }
    }

667
    /**
668
     * Rendering content
669
670
671
     *
     * @return string
     */
672
    protected function renderContent(ServerRequestInterface $request): string
673
    {
674
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
675
676
677
678
        $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');
679
        $this->pageRenderer->loadRequireJsModule(ImmediateActionElement::MODULE_NAME);
680
        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
681

682
683
        $tableOutput = '';
        $numberOfHiddenElements = 0;
684

685
686
687
688
689
690
691
692
693
694
695
696
697
698
        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);
            }
699

700
701
            $numberOfHiddenElements = $this->getNumberOfHiddenElements($configuration->getLanguageColumns());

702
            $pageActionsInstruction = JavaScriptModuleInstruction::forRequireJS('TYPO3/CMS/Backend/PageActions');
703
704
705
706
707
            if ($this->context->isPageEditable()) {
                $languageOverlayId = 0;
                $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->current_sys_language);
                if (is_array($pageLocalizationRecord)) {
                    $pageLocalizationRecord = reset($pageLocalizationRecord);
708
                }
709
710
                if (!empty($pageLocalizationRecord['uid'])) {
                    $languageOverlayId = $pageLocalizationRecord['uid'];
711
                }
712
713
714
                $pageActionsInstruction
                    ->invoke('setPageId', (int)$this->id)
                    ->invoke('setLanguageOverlayId', $languageOverlayId);
715
            }
716
            $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($pageActionsInstruction);
717
            $tableOutput = GeneralUtility::makeInstance(BackendLayoutRenderer::class, $this->context)->drawContent();
718
719
        }

720
        if ($this->getBackendUser()->check('tables_select', 'tt_content') && $numberOfHiddenElements > 0) {
721
            // Toggle hidden ContentElements
722
            $tableOutput .= '
723
724
725
                <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">
726
727
728
                        ' . htmlspecialchars($this->getLanguageService()->getLL('hiddenCE')) . ' (<span class="t3js-hidden-counter">' . $numberOfHiddenElements . '</span>)
                    </label>
                </div>';
729
        }
730

731
732
        $event = $this->eventDispatcher->dispatch(new ModifyPageLayoutContentEvent($request, $this->moduleTemplate));
        return $event->getHeaderContent() . $tableOutput . $event->getFooterContent();
733
734
    }

735
736
737
738
739
740
    /***************************
     *
     * Sub-content functions, rendering specific parts of the module content.
     *
     ***************************/
    /**
741
     * This creates the buttons for the modules
742
     * @param ServerRequestInterface $request
743
     */
744
    protected function makeButtons(ServerRequestInterface $request): void
745
    {
746
747
748
749
750
        // 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);
751
752
        $lang = $this->getLanguageService();
        // View page
753
754
755
756
757
        $pageTsConfig = BackendUtility::getPagesTSconfig($this->id);
        // Exclude sysfolders, spacers and recycler by default
        $excludeDokTypes = [
            PageRepository::DOKTYPE_RECYCLER,
            PageRepository::DOKTYPE_SYSFOLDER,
758
            PageRepository::DOKTYPE_SPACER,
759
760
761
762
763
764
765
766
767
768
769
770
771
772
        ];
        // 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)
        ) {
773
            $languageParameter = $this->current_sys_language ? ('&L=' . $this->current_sys_language) : '';
774
            $previewDataAttributes = PreviewUriBuilder::create((int)$this->pageinfo['uid'])
775
776
777
                ->withRootLine(BackendUtility::BEgetRootLine($this->pageinfo['uid']))
                ->withAdditionalQueryParameters($languageParameter)
                ->buildDispatcherDataAttributes();
778
            $viewButton = $this->buttonBar->makeLinkButton()
779
                ->setDataAttributes($previewDataAttributes ?? [])
780
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
781
                ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
782
783
784
                ->setHref('#');

            $this->buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
785
786
        }
        // Shortcut
787
        $shortcutButton = $this->buttonBar->makeShortcutButton()
788
            ->setRouteIdentifier($this->moduleName)
789
            ->setDisplayName($this->getShortcutTitle())
790
791
792
793
794
795
            ->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,
796
                ],
797
            ]);
798
799
        $this->buttonBar->addButton($shortcutButton);

800
        // Cache
801
802
803
804
805
806
807
808
        $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);

809
        // Edit page properties
810
        if ($this->isPageEditable(0)) {
811
812
813
            $url = (string)$this->uriBuilder->buildUriFromRoute(
                'record_edit',
                [
814
815
                    'edit' => [
                        'pages' => [
816
                            $this->id => 'edit',
817
                        ],
818
                    ],
819
820
821
                    'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
                ]
            );
822
823
824
825
826
            $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);
827
        }
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872

        // Edit page properties of page language overlay (Only when one specific language is selected)
        if ((int)$this->MOD_SETTINGS['function'] === 1
            && $this->current_sys_language > 0
            && $this->isPageEditable($this->current_sys_language)
        ) {
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
            $queryBuilder->getRestrictions()
                ->removeAll()
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
            $overlayRecord = $queryBuilder
                ->select('uid')
                ->from('pages')
                ->where(
                    $queryBuilder->expr()->eq(
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
                    ),
                    $queryBuilder->expr()->eq(
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
                        $queryBuilder->createNamedParameter($this->current_sys_language, \PDO::PARAM_INT)
                    )
                )
                ->setMaxResults(1)
                ->execute()
                ->fetchAssociative();
            BackendUtility::workspaceOL('pages', $overlayRecord, $this->getBackendUser()->workspace);
            $url = (string)$this->uriBuilder->buildUriFromRoute(
                'record_edit',
                [
                    'edit' => [
                        'pages' => [
                            $overlayRecord['uid'] => 'edit',
                        ],
                    ],
                    'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
                ]
            );
            $editLanguageButton = $this->buttonBar->makeLinkButton()
                ->setHref($url)
                ->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);
        }
873
874
875
876
877
878
879
880
881
    }

    /*******************************
     *
     * Other functions
     *
     ******************************/
    /**
     * Returns the number of hidden elements (including those hidden by start/end times)
882
     * on the current page (for the current site language)
883
     *
884
     * @param array $languageColumns
885
886
     * @return int
     */