PageLayoutController.php 50.1 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\Http\HtmlResponse;
33
34
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
35
use TYPO3\CMS\Core\Localization\LanguageService;
36
use TYPO3\CMS\Core\Page\PageRenderer;
37
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
38
use TYPO3\CMS\Core\Type\Bitmask\Permission;
Nicole Cordes's avatar
Nicole Cordes committed
39
40
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
41
use TYPO3\CMS\Core\Versioning\VersionState;
42
use TYPO3\CMS\Fluid\View\StandaloneView;
43
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
44
use TYPO3\CMS\Frontend\Page\PageRepository;
Nicole Cordes's avatar
Nicole Cordes committed
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
    /**
     * 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());
    }

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

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

        $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);

294
295
        $sessionData['search_field'] = $this->search_field;
        // Store session data
296
        $this->getBackendUser()->setAndSaveSessionData(__CLASS__, $sessionData);
297
298
299
        // Load page info array:
        $this->pageinfo = BackendUtility::readPageAccess($this->id, $this->perms_clause);
        // Initialize menu
300
        $this->menuConfig($request);
301
302
303
304
305
306
307
308
        // 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
309
     * @param ServerRequestInterface $request
310
     */
311
    protected function menuConfig(ServerRequestInterface $request): void
312
    {
313
314
315
316
        // Set the GPvars from outside
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

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

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

338
339
        // 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.
340
        if ($this->id) {
341
342
343
344
345
346
347
348
            // 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')
349
                ->where(
350
                    $queryBuilder->expr()->eq(
351
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
352
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
353
                    )
354
355
356
357
358
359
                )->execute();
            while ($pageTranslation = $statement->fetch()) {
                $languageId = $pageTranslation[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
                if (isset($availableLanguages[$languageId])) {
                    $this->MOD_MENU['language'][$languageId] = $availableLanguages[$languageId]->getTitle();
                }
360
            }
361
362
363
            // Override the label
            if (isset($availableLanguages[0])) {
                $this->MOD_MENU['language'][0] = $availableLanguages[0]->getTitle();
364
365
            }
        }
366
        // Initialize the available actions
367
        $actions = $this->initActions();
368
        // Clean up settings
369
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $parsedBody['SET'] ?? $queryParams['SET'] ?? [], $this->moduleName);
370
        // For all elements to be shown in draft workspaces & to also show hidden elements by default if user hasn't disabled the option
371
372
373
        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
374
        ) {
375
376
            $this->MOD_SETTINGS['tt_content_showHidden'] = 1;
        }
377
378
        // Make action menu from available actions
        $this->makeActionMenu($actions);
379
380
381
    }

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

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

404
        if (!$count) {
405
            unset($actions['2']);
406
        }
407
408
409
410
411
412
413
        // 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]);
            }
        }
414
415
416
417
418
419
420
421
422
423

        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
     */
424
    protected function makeActionMenu(array $actions): void
425
    {
426
427
428
429
        $actionMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
        $actionMenu->setIdentifier('actionMenu');
        $actionMenu->setLabel('');

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

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

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

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

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

469
470
471
472
473
474
475
476
477
        // 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>';
478
                $view->assignMultiple([
479
480
481
                    'title' => $title,
                    'message' => $message,
                    'state' => InfoboxViewHelper::STATE_INFO
482
                ]);
483
484
                $content .= $view->render();
            }
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
        } elseif ($this->pageinfo['doktype'] === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)$this->pageinfo['shortcut_mode'];
            $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
            $targetPage = [];

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

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

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

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

580
581
582
583
584
585
    /**
     * Get all pages with links where the content of a page $pageId is also shown on
     *
     * @param int $pageId
     * @return string
     */
586
    protected function getPageLinksWhereContentIsAlsoShownOn($pageId): string
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
    {
        $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);
    }

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

    /**
     * Main function.
     * Creates some general objects and calls other functions for the main rendering of module content.
646
647
     *
     * @param ServerRequestInterface $request
648
     */
649
    protected function main(ServerRequestInterface $request): void
650
    {
651
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
652
653
654
        $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
655
        $access = is_array($this->pageinfo);
656
657
        // Content
        $content = '';
658
659
660
        if ($this->id && $access) {
            // Initialize permission settings:
            $this->CALC_PERMS = $this->getBackendUser()->calcPerms($this->pageinfo);
661
            $this->EDIT_CONTENT = $this->isContentEditable();
662

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

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

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

680
            // Find backend layout / columns
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
            $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);

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

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

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

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

748
        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
749
750
751
752
753
        $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);
754
        $dbList->script = (string)$uriBuilder->buildUriFromRoute($this->moduleName);
755
756
757
758
        $dbList->showIcon = 0;
        $dbList->setLMargin = 0;
        $dbList->doEdit = $this->EDIT_CONTENT;
        $dbList->ext_CALC_PERMS = $this->CALC_PERMS;
759
        $dbList->agePrefixes = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
760
761
        $dbList->id = $this->id;
        $dbList->nextThree = MathUtility::forceIntegerInRange($this->modTSconfig['properties']['editFieldsAtATime'], 0, 10);
762
763
        $dbList->option_newWizard = empty($this->modTSconfig['properties']['disableNewContentElementWizard']);
        $dbList->defLangBinding = !empty($this->modTSconfig['properties']['defLangBinding']);
764
765
766
767
768
769
770
        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:
771
772
        $tableOutput = [];
        $tableJSOutput = [];
773
774
775
776
        $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) {
777
            $h_func = '';
778
779
780
781
782
783
784
785
            $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'])) {
786
                    $colList = [];
787
788
789
790
791
792
                    $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:
793
                    $colList = ['1', '0', '2', '3'];
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
                }
                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;
                }
809
810
811
812
813
814
815
816
817
818
819
                // 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>';
                }
820
821
822
823
824
825
826
827
828
829
830
831
832
            } 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:
833
            $tableOutput[$table] = $h_func . $dbList->HTMLcode . $h_func_b;
834
835
836
837
838
839
840
841
842
843
844
845
846
            // ... 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
847
848
849
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawHeaderHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
850
851
852
        }
        // Add the content for each table we have rendered (traversing $tableOutput variable)
        foreach ($tableOutput as $table => $output) {
853
            $content .= $output;
854
855
        }
        // Making search form:
856
        if (!$this->modTSconfig['properties']['disableSearchBox'] && ($dbList->counter > 0 || $this->currentPageHasSubPages())) {
857
            $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ToggleSearchToolbox');
858
859
            $toggleSearchFormButton = $this->buttonBar->makeLinkButton()
                ->setClasses('t3js-toggle-search-toolbox')
860
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchIcon'))
861
862
863
                ->setIcon($this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL))
                ->setHref('#');
            $this->buttonBar->addButton($toggleSearchFormButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
864
            $this->searchContent = $dbList->getSearchBox();
865
866
        }
        // Additional footer content
867
868
869
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/db_layout.php']['drawFooterHook'] ?? [] as $hook) {
            $params = [];
            $content .= GeneralUtility::callUserFunction($hook, $params, $this);
870
871
872
873
        }
        return $content;
    }

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

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

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

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

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

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

        $queryBuilder
            ->count('uid')
            ->from('tt_content')
            ->where(
1028
1029
1030
                $queryBuilder->expr()->eq(
                    'pid',
                    $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043