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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

250
251
252
253
254
    /**
     * @var SiteLanguage[]
     */
    protected $availableLanguages;

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

270
271
    /**
     * Initializing the module
272
     * @param ServerRequestInterface $request
273
     */
274
    protected function init(ServerRequestInterface $request): void
275
    {
276
277
278
279
        // Set the GPvars from outside
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

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

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

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

    /**
     * Initialize menu array
316
     * @param ServerRequestInterface $request
317
     */
318
    protected function menuConfig(ServerRequestInterface $request): void
319
    {
320
321
322
323
        // Set the GPvars from outside
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

324
325
        /** @var SiteInterface $currentSite */
        $currentSite = $request->getAttribute('site');
326
        $this->availableLanguages = $currentSite->getAvailableLanguages($this->getBackendUser(), false, $this->id);
327

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

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

    /**
389
     * Initializes the available actions this module provides
390
     *
391
     * @return array the available actions
392
     */
393
    protected function initActions(): array
394
    {
395
        $actions = [
396
            1 => $this->getLanguageService()->getLL('m_function_1')
397
        ];
398
399
400
        // 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');
401
        }
402
        $this->makeLanguageMenu();
403
404
405
406
407
408
409
        // 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]);
            }
        }
410
411
412
413
414
415
416
417
418
419

        return $actions;
    }

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

426
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
427

428
        $defaultKey = null;
429
        $foundDefaultKey = false;
430
        foreach ($actions as $key => $action) {
431
432
433
            $menuItem = $actionMenu
                ->makeMenuItem()
                ->setTitle($action)
434
                ->setHref((string)$uriBuilder->buildUriFromRoute($this->moduleName) . '&id=' . $this->id . '&SET[function]=' . $key);
435

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

    /**
     * Generate the flashmessages for current pid
     *
     * @return string HTML content with flashmessages
     */
457
    protected function getHeaderFlashMessagesForCurrentPid(): string
458
459
460
    {
        $content = '';
        $lang = $this->getLanguageService();
461

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

465
466
467
468
469
470
471
472
473
        // If page is a folder
        if ($this->pageinfo['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
            $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
            $moduleLoader->load($GLOBALS['TBE_MODULES']);
            $modules = $moduleLoader->modules;
            if (is_array($modules['web']['sub']['list'])) {
                $title = $lang->getLL('goToListModule');
                $message = '<p>' . $lang->getLL('goToListModuleMessage') . '</p>';
                $message .= '<a class="btn btn-info" href="javascript:top.goToModule(\'web_list\',1);">' . $lang->getLL('goToListModule') . '</a>';
474
                $view->assignMultiple([
475
476
477
                    'title' => $title,
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
478
                ]);
479
480
                $content .= $view->render();
            }
481
482
483
484
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)$this->pageinfo['shortcut_mode'];
            $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
            $targetPage = [];
485
486
            $message = '';
            $state = InfoboxViewHelper::STATE_ERROR;
487

488
            if ($shortcutMode || $this->pageinfo['shortcut']) {
489
490
                switch ($shortcutMode) {
                    case PageRepository::SHORTCUT_MODE_NONE:
491
492
                        $targetPage = $this->getTargetPageIfVisible($pageRepository->getPage($this->pageinfo['shortcut']));
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : '';
493
494
                        break;
                    case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
495
496
497
                        $menuOfPages = $pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
                        $targetPage = reset($menuOfPages) ?: [];
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredFirstSubpageMessage') : '';
498
499
                        break;
                    case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
500
501
502
503
504
505
506
507
508
509
510
                        $targetPage = $this->getTargetPageIfVisible($pageRepository->getPage($this->pageinfo['pid']));
                        $message .= $targetPage === [] ? $lang->getLL('pageIsMisconfiguredParentPageMessage') : '';
                        break;
                    case PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE:
                        $possibleTargetPages = $pageRepository->getMenu($this->pageinfo['uid'], '*', 'sorting', 'AND hidden = 0');
                        if ($possibleTargetPages === []) {
                            $message .= $lang->getLL('pageIsMisconfiguredOrNotAccessibleRandomInternalLinkMessage');
                            break;
                        }
                        $message = $lang->getLL('pageIsRandomInternalLinkMessage');
                        $state = InfoboxViewHelper::STATE_INFO;
511
512
                        break;
                }
513
514
                $message = htmlspecialchars($message);
                if ($targetPage !== [] && $shortcutMode !== PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE) {
515
516
                    $linkToPid = $this->local_linkThisScript(['id' => $targetPage['uid']]);
                    $path = BackendUtility::getRecordPath($targetPage['uid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 1000);
517
                    $linkedPath = '<a href="' . htmlspecialchars($linkToPid) . '">' . htmlspecialchars($path) . '</a>';
518
519
520
                    $message .= sprintf(htmlspecialchars($lang->getLL('pageIsInternalLinkMessage')), $linkedPath);
                    $message .= ' (' . htmlspecialchars($lang->sL(BackendUtility::getLabelFromItemlist('pages', 'shortcut_mode', $shortcutMode))) . ')';
                    $state = InfoboxViewHelper::STATE_INFO;
521
522
                }
            } else {
523
524
                $message = htmlspecialchars($lang->getLL('pageIsMisconfiguredInternalLinkMessage'));
                $state = InfoboxViewHelper::STATE_ERROR;
525
            }
526
527
528
529
530
531
532

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $this->buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
921
922
        }
        // Shortcut
923
924
925
926
        $shortcutButton = $this->buttonBar->makeShortcutButton()
            ->setModuleName($this->moduleName)
            ->setGetVariables([
                'id',
927
                'route',
928
929
930
931
932
933
934
935
936
937
                'edit_record',
                'pointer',
                'new_unique_uid',
                'search_field',
                'search_levels',
                'showLimit'
            ])
            ->setSetVariables(array_keys($this->MOD_MENU));
        $this->buttonBar->addButton($shortcutButton);

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

                    $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
991
                    $editLanguageButton = $this->buttonBar->makeLinkButton()
992
                        ->setHref($url)
993
                        ->setTitle($lang->getLL('editPageLanguageOverlayProperties'))
994
995
                        ->setIcon($this->iconFactory