RecordListController.php 34.7 KB
Newer Older
1
<?php
2

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

16
17
namespace TYPO3\CMS\Recordlist\Controller;

18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Http\Message\ResponseFactoryInterface;
20
21
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Markus Klein's avatar
Markus Klein committed
22
use TYPO3\CMS\Backend\Clipboard\Clipboard;
23
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
24
use TYPO3\CMS\Backend\Routing\UriBuilder;
25
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
26
use TYPO3\CMS\Backend\Template\ModuleTemplate;
27
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
28
use TYPO3\CMS\Backend\Utility\BackendUtility;
Markus Klein's avatar
Markus Klein committed
29
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
30
use TYPO3\CMS\Core\Database\ConnectionPool;
31
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
32
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
Markus Klein's avatar
Markus Klein committed
33
use TYPO3\CMS\Core\DataHandling\DataHandler;
34
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
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\Messaging\FlashMessage;
39
use TYPO3\CMS\Core\Messaging\FlashMessageService;
40
use TYPO3\CMS\Core\Page\PageRenderer;
41
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
42
use TYPO3\CMS\Core\Type\Bitmask\Permission;
43
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
44
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
45
use TYPO3\CMS\Core\Utility\GeneralUtility;
46
use TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent;
47
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
48
use TYPO3\CMS\Recordlist\View\RecordSearchBoxComponent;
49

50
51
/**
 * Script Class for the Web > List module; rendering the listing of records on a page
52
 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
53
 */
54
class RecordListController
55
{
56
57
58
59
60
61
62
    /**
     * ModuleTemplate object
     *
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

63
64
65
66
67
    /**
     * @var SiteLanguage[]
     */
    protected $siteLanguages = [];

68
69
70
71
72
73
74
75
76
77
    /**
     * @var Permission
     */
    protected $pagePermissions;

    protected int $id = 0;
    protected array $pageInfo = [];
    protected string $returnUrl = '';
    protected array $modTSconfig = [];

78
79
80
81
82
    protected IconFactory $iconFactory;
    protected PageRenderer $pageRenderer;
    protected EventDispatcherInterface $eventDispatcher;
    protected UriBuilder $uriBuilder;
    protected ModuleTemplateFactory $moduleTemplateFactory;
83
    protected ResponseFactoryInterface $responseFactory;
84

85
86
87
88
89
    public function __construct(
        IconFactory $iconFactory,
        PageRenderer $pageRenderer,
        EventDispatcherInterface $eventDispatcher,
        UriBuilder $uriBuilder,
90
91
        ModuleTemplateFactory $moduleTemplateFactory,
        ResponseFactoryInterface $responseFactory
92
    ) {
93
        $this->iconFactory = $iconFactory;
94
        $this->pageRenderer = $pageRenderer;
95
        $this->eventDispatcher = $eventDispatcher;
96
        $this->uriBuilder = $uriBuilder;
97
        $this->moduleTemplateFactory = $moduleTemplateFactory;
98
        $this->responseFactory = $responseFactory;
99
100
101
    }

    /**
102
     * Injects the request object for the current request or subrequest
103
     *
104
105
     * @param ServerRequestInterface $request the current request
     * @return ResponseInterface the response with the content
106
     */
107
    public function mainAction(ServerRequestInterface $request): ResponseInterface
108
    {
109
110
111
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
        $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
112
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordDownloadButton');
113
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ColumnSelectorButton');
114
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
115
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordSearch');
116
117
118
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/AjaxDataHandler');
        $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');

119
        BackendUtility::lockRecords();
120
121
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();
122
        $backendUser = $this->getBackendUserAuthentication();
123
        $perms_clause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
124
        // GPvars:
125
        $this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
126
        $pointer = max(0, (int)($parsedBody['pointer'] ?? $queryParams['pointer'] ?? 0));
127
128
129
        $table = (string)($parsedBody['table'] ?? $queryParams['table'] ?? '');
        $search_field = (string)($parsedBody['search_field'] ?? $queryParams['search_field'] ?? '');
        $search_levels = (int)($parsedBody['search_levels'] ?? $queryParams['search_levels'] ?? 0);
130
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl((string)($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? ''));
131
132
133
        $cmd = (string)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? '');
        // Set site languages
        $site = $request->getAttribute('site');
134
        $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id);
135
        // Loading module configuration:
136
        $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
137
        // Clean up settings:
138
        $MOD_SETTINGS = BackendUtility::getModuleData(['clipBoard' => ''], (array)($parsedBody['SET'] ?? $queryParams['SET'] ?? []), 'web_list');
139
        // main
140
141
        $lang = $this->getLanguageService();
        // Loading current page record and checking access:
142
        $pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause);
143
        $access = is_array($pageinfo);
144
        $this->pageInfo = is_array($pageinfo) ? $pageinfo : [];
145

146
147
        $this->pagePermissions = new Permission($backendUser->calcPerms($pageinfo));
        $userCanEditPage = $this->pagePermissions->editPagePermissionIsGranted() && !empty($this->id) && ($backendUser->isAdmin() || (int)$pageinfo['editlock'] === 0);
148
        $pageActionsCallback = null;
149
        if ($userCanEditPage) {
150
            $pageActionsCallback = 'function(PageActions) {
151
                PageActions.setPageId(' . (int)$this->id . ');
152
            }';
153
        }
154
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback);
155
156
        // Apply predefined values for hidden checkboxes
        // Set predefined value for Clipboard:
157
158
159
160
161
162
163
164
165
166
167
        if (isset($this->modTSconfig['enableClipBoard'])) {
            if ($this->modTSconfig['enableClipBoard'] === 'activated') {
                $MOD_SETTINGS['clipBoard'] = true;
            } elseif ($this->modTSconfig['enableClipBoard'] === 'deactivated') {
                $MOD_SETTINGS['clipBoard'] = false;
            }
        }
        if (!isset($MOD_SETTINGS['clipBoard'])) {
            $MOD_SETTINGS['clipBoard'] = true;
        }
        $clipboard = $this->initializeClipboard($request, (bool)$MOD_SETTINGS['clipBoard']);
168
169
        $enableListing = $access || ($this->id === 0 && $search_levels !== 0 && $search_field !== '');

170
        // Initialize the dblist object:
171
        $dblist = GeneralUtility::makeInstance(DatabaseRecordList::class);
172
        $dblist->setModuleData($MOD_SETTINGS ?? []);
173
174
        $dblist->calcPerms = $this->pagePermissions;
        $dblist->returnUrl = $this->returnUrl;
175
        $dblist->showClipboard = true;
176
177
178
179
180
181
182
        $dblist->disableSingleTableView = $this->modTSconfig['disableSingleTableView'] ?? false;
        $dblist->listOnlyInSingleTableMode = $this->modTSconfig['listOnlyInSingleTableView'] ?? false;
        $dblist->hideTables = $this->modTSconfig['hideTables'] ?? false;
        $dblist->hideTranslations = $this->modTSconfig['hideTranslations'] ?? false;
        $dblist->tableTSconfigOverTCA = $this->modTSconfig['table.'] ?? false;
        $dblist->allowedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['allowedNewTables'] ?? '', true);
        $dblist->deniedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['deniedNewTables'] ?? '', true);
183
        $dblist->pageRow = $this->pageInfo;
184
        $dblist->modTSconfig = $this->modTSconfig;
185
        $dblist->setLanguagesAllowedForUser($this->siteLanguages);
186
        $clickTitleMode = trim($this->modTSconfig['clickTitleMode'] ?? '');
187
        $dblist->clickTitleMode = $clickTitleMode === '' ? 'edit' : $clickTitleMode;
188
        if (isset($this->modTSconfig['tableDisplayOrder.'])) {
189
            $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
190
            $dblist->setTableDisplayOrder($typoScriptService->convertTypoScriptArrayToPlainArray($this->modTSconfig['tableDisplayOrder.']));
191
        }
192

193
        $dblist->clipObj = $clipboard;
194
195
        // This flag will prevent the clipboard panel in being shown.
        // It is set, if the clickmenu-layer is active AND the extended view is not enabled.
196
        $dblist->dontShowClipControlPanels = ($clipboard->current === 'normal' && !($this->modTSconfig['showClipControlPanelsDespiteOfCMlayers'] ?? false));
197
        // If there is access to the page or root page is used for searching, then render the list contents and set up the document template object:
198
        $tableOutput = '';
199
        if ($enableListing) {
200
201
            // Deleting records...:
            // Has not to do with the clipboard but is simply the delete action. The clipboard object is used to clean up the submitted entries to only the selected table.
202
203
            if ($cmd === 'delete' && $request->getMethod() === 'POST') {
                $items = $clipboard->cleanUpCBC($parsedBody['CBC'] ?? [], $parsedBody['cmd_table'] ?? '', 1);
204
                if (!empty($items)) {
205
206
                    // Create data handler command array
                    $dataHandlerCmd = [];
207
                    foreach ($items as $iK => $value) {
208
                        $iKParts = explode('|', (string)$iK);
209
                        $dataHandlerCmd[$iKParts[0]][$iKParts[1]]['delete'] = 1;
210
211
                    }
                    $tce = GeneralUtility::makeInstance(DataHandler::class);
212
                    $tce->start([], $dataHandlerCmd);
213
                    $tce->process_cmdmap();
214
                    if (isset($dataHandlerCmd['pages'])) {
215
216
                        BackendUtility::setUpdateSignal('updatePageTree');
                    }
217
                    $tce->printLogErrorMessages();
218
219
220
                }
            }
            // Initialize the listing object, dblist, for rendering the list:
221
            $dblist->start($this->id, $table, $pointer, $search_field, $search_levels);
222
            $dblist->setDispFields();
223
            // Render the list of tables:
224
            $tableOutput = $dblist->generateList();
225

226
            // Add JavaScript functions to the page:
227
            $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ClipboardComponent');
228
229
230
231

            $this->moduleTemplate->addJavaScriptCode(
                'RecordListInlineJS',
                '
232
233
234
				function setHighlight(id) {
					top.fsMod.recentIds["web"] = id;
					top.fsMod.navFrameHighlightedID["web"] = top.fsMod.currentBank + "_" + id; // For highlighting
235

236
237
					if (top.nav_frame && top.nav_frame.refresh_nav) {
						top.nav_frame.refresh_nav();
238
239
					}
				}
240
				function editRecords(table,idList,addParams,CBflag) {
241
					window.location.href="' . (string)$this->uriBuilder->buildUriFromRoute('record_edit', ['returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()]) . '&edit["+table+"]["+idList+"]=edit"+addParams;
242
243
				}

244
				if (top.fsMod) top.fsMod.recentIds["web"] = ' . (int)$this->id . ';
245
246
247
			'
            );

248
            // Setting up the context sensitive menu:
249
            $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
250
251
252
        }
        // access
        // Begin to compile the whole page, starting out with page header:
253
        if (!$this->id) {
254
            $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
255
        } else {
256
            $title = $pageinfo['title'];
257
        }
258
        $body = $this->moduleTemplate->header($title);
259
260

        // Additional header content
261
262
        /** @var RenderAdditionalContentToRecordListEvent $additionalRecordListEvent */
        $additionalRecordListEvent = $this->eventDispatcher->dispatch(new RenderAdditionalContentToRecordListEvent($request));
263
        $body .= $additionalRecordListEvent->getAdditionalContentAbove();
264
265
266
267
        $this->moduleTemplate->setTitle(
            $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:mlang_tabs_tab'),
            $title
        );
268

269
        $beforeOutput = '';
270
        $output = '';
271
272
        // Show the selector to add page translations, but only when in "default" mode.
        // If not disabled via module TSconfig and the user is allowed, also show the page translations table.
273
        if ($this->id && !$search_field && !$cmd && !$table) {
274
            $beforeOutput .= $this->languageSelector($request->getAttribute('normalizedParams')->getRequestUri());
275
276
277
278
279
280
281
282
283
284
            if ($this->showPageTranslations()) {
                $pageTranslationsDatabaseRecordList = clone $dblist;
                $pageTranslationsDatabaseRecordList->listOnlyInSingleTableMode = false;
                $pageTranslationsDatabaseRecordList->disableSingleTableView = true;
                $pageTranslationsDatabaseRecordList->deniedNewTables = ['pages'];
                $pageTranslationsDatabaseRecordList->hideTranslations = '';
                $pageTranslationsDatabaseRecordList->setLanguagesAllowedForUser($this->siteLanguages);
                $pageTranslationsDatabaseRecordList->showOnlyTranslatedRecords(true);
                $output .= $pageTranslationsDatabaseRecordList->getTable('pages', $this->id);
            }
285
286
        }

287
288
289
290
291
        // search box toolbar
        if (!($this->modTSconfig['disableSearchBox'] ?? false) && ($tableOutput || !empty($search_field))) {
            $beforeOutput .= $this->renderSearchBox($dblist, $search_field, $search_levels);
        }

292
293
        if (!empty($tableOutput)) {
            $output .= $tableOutput;
294
        } else {
295
296
297
            if (isset($GLOBALS['TCA'][$table]['ctrl']['title'])) {
                if (strpos($GLOBALS['TCA'][$table]['ctrl']['title'], 'LLL:') === 0) {
                    $ll = sprintf($lang->getLL('noRecordsOfTypeOnThisPage'), $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title']));
298
                } else {
299
                    $ll = sprintf($lang->getLL('noRecordsOfTypeOnThisPage'), $GLOBALS['TCA'][$table]['ctrl']['title']);
300
301
302
303
                }
            } else {
                $ll = $lang->getLL('noRecordsOnThisPage');
            }
304
            $flashMessage = GeneralUtility::makeInstance(
305
                FlashMessage::class,
306
                $ll,
307
308
                '',
                FlashMessage::INFO
309
            );
310
            unset($ll);
311
            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
312
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
313
            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
314
315
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
316
317
        }

318
319
320
        if ($beforeOutput) {
            $body .= '<div class="row">' . $beforeOutput . '</div>';
        }
321
        $body .= $output;
322
        // If a listing was produced, create the page footer
323
        if ($tableOutput) {
324
            // Adding checkbox option for clipboard display
325
            $body .= '
326
					<div class="mb-3">
327
						<form action="" method="post">';
328

329
            // Add "clipboard" checkbox:
330
            if ($this->modTSconfig['enableClipBoard'] === 'selectable') {
331
                $body .= '<div class="form-check form-switch">' .
332
                    BackendUtility::getFuncCheck($this->id, 'SET[clipBoard]', ($MOD_SETTINGS['clipBoard'] ?? ''), '', $table ? '&table=' . $table : '', 'id="checkShowClipBoard"') .
333
334
335
336
                    '<label class="form-check-label" for="checkShowClipBoard">' .
                    BackendUtility::wrapInHelp('xMOD_csh_corebe', 'list_options', htmlspecialchars($lang->getLL('showClipBoard'))) .
                    '</label>' .
                    '</div>';
337
338
            }

339
            $body .= '
340
341
						</form>
					</div>';
342
343
        }
        // Printing clipboard if enabled
344
345
        if ($MOD_SETTINGS['clipBoard'] && ($tableOutput || $clipboard->hasElements())) {
            $body .= $clipboard->printClipboard();
346
347
        }
        // Additional footer content
348
        $body .= $additionalRecordListEvent->getAdditionalContentBelow();
349
        // Setting up the buttons for docheader
350
351
352
353
354
355
356
        $this->getDocHeaderButtons(
            $clipboard,
            $queryParams,
            $table,
            $dblist->listURL(),
            $MOD_SETTINGS
        );
357

358
359
        if ($pageinfo) {
            $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageinfo);
360
361
        }

362
        $this->moduleTemplate->setContent($body);
363
        return $this->htmlResponse($this->moduleTemplate->renderContent());
364
365
    }

366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
    /**
     * Processing incoming data and configures the clipboard.
     *
     * @param ServerRequestInterface $request
     * @param bool $isClipboardShown
     * @return Clipboard
     */
    protected function initializeClipboard(ServerRequestInterface $request, bool $isClipboardShown): Clipboard
    {
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        $cmd = (string)($request->getParsedBody()['cmd'] ?? $request->getQueryParams()['cmd'] ?? '');
        // Initialize - reads the clipboard content from the user session
        $clipboard->initializeClipboard();
        // Clipboard actions are handled:
        // CB is the clipboard command array
Jochen Roth's avatar
Jochen Roth committed
381
        $CB = array_replace_recursive($request->getQueryParams()['CB'] ?? [], $request->getParsedBody()['CB'] ?? []);
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
        if ($cmd === 'setCB') {
            // CBH is all the fields selected for the clipboard, CBC is the checkbox fields which were checked.
            // By merging we get a full array of checked/unchecked elements
            // This is set to the 'el' array of the CB after being parsed so only the table in question is registered.
            $cmd_table = (string)($request->getParsedBody()['cmd_table'] ?? $request->getQueryParams()['cmd_table'] ?? '');
            $CB['el'] = $clipboard->cleanUpCBC(array_merge($request->getParsedBody()['CBH'] ?? [], (array)($request->getParsedBody()['CBC'] ?? [])), $cmd_table);
        }
        if (!$isClipboardShown) {
            // If the clipboard is NOT shown, set the pad to 'normal'.
            $CB['setP'] = 'normal';
        }
        // Execute commands.
        $clipboard->setCmd($CB);
        // Clean up pad
        $clipboard->cleanCurrent();
        // Save the clipboard content
        $clipboard->endClipboard();
        return $clipboard;
    }

402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
    protected function renderSearchBox(DatabaseRecordList $dblist, string $searchWord, int $searchLevels): string
    {
        $searchBoxVisible = !empty($dblist->searchString);
        $searchBox = GeneralUtility::makeInstance(RecordSearchBoxComponent::class)
            ->setAllowedSearchLevels((array)($this->modTSconfig['searchLevel.']['items.'] ?? []))
            ->setSearchWord($searchWord)
            ->setSearchLevel($searchLevels)
            ->render($dblist->listURL('', '-1', 'pointer,search_field'));

        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
        $searchButton = $buttonBar->makeLinkButton();
        $searchButton
            ->setHref('#')
            ->setClasses('t3js-toggle-search-toolbox')
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchIcon'))
            ->setIcon($this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL));
        $buttonBar->addButton(
            $searchButton,
            ButtonBar::BUTTON_POSITION_LEFT,
            90
        );
        return '<div class="col-6" style="' . ($searchBoxVisible ?: 'display: none') . '" id="db_list-searchbox-toolbar">' . $searchBox . '</div>';
    }

426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
    /**
     * Create the panel of buttons for submitting the form or otherwise perform
     * operations.
     *
     * @param Clipboard $clipboard
     * @param array $queryParams
     * @param string $table
     * @param string $listUrl
     * @param array $moduleSettings
     */
    protected function getDocHeaderButtons(Clipboard $clipboard, array $queryParams, string $table, string $listUrl, array $moduleSettings): void
    {
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
        $backendUser = $this->getBackendUserAuthentication();
        $lang = $this->getLanguageService();
        // CSH
        if (!$this->id) {
            $fieldName = 'list_module_root';
        } else {
            $fieldName = 'list_module';
        }
        $cshButton = $buttonBar->makeHelpButton()
            ->setModuleName('xMOD_csh_corebe')
            ->setFieldName($fieldName);
        $buttonBar->addButton($cshButton);
        // New record on pages that are not locked by editlock
        if (!($this->modTSconfig['noCreateRecordsLink'] ?? false) && $this->editLockPermissions()) {
            $newRecordButton = $buttonBar->makeLinkButton()
                ->setHref((string)$this->uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id, 'returnUrl' => $listUrl]))
                ->setTitle($lang->getLL('newRecordGeneral'))
                ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
            $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
        }

        if ($this->id !== 0) {
            if ($this->canCreatePreviewLink()) {
                $previewDataAttributes = PreviewUriBuilder::create((int)$this->id)
                    ->withRootLine(BackendUtility::BEgetRootLine($this->id))
                    ->buildDispatcherDataAttributes();
                $viewButton = $buttonBar->makeLinkButton()
                    ->setHref('#')
                    ->setDataAttributes($previewDataAttributes ?? [])
                    ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
                    ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL));
                $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
            }
            // If edit permissions are set, see
            // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
            if ($this->pagePermissions->editPagePermissionIsGranted() && $this->editLockPermissions() && $backendUser->checkLanguageAccess(0)) {
                // Edit
                $editLink = $this->uriBuilder->buildUriFromRoute('record_edit', [
                    'edit' => [
                        'pages' => [
                            $this->id => 'edit'
                        ]
                    ],
                    'returnUrl' => $listUrl
                ]);
                $editButton = $buttonBar->makeLinkButton()
                    ->setHref($editLink)
                    ->setTitle($lang->getLL('editPage'))
                    ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
                $buttonBar->addButton($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
            }
        }

        // Paste
        if (($this->pagePermissions->createPagePermissionIsGranted() || $this->pagePermissions->editContentPermissionIsGranted()) && $this->editLockPermissions()) {
            $elFromTable = $clipboard->elFromTable();
            if (!empty($elFromTable)) {
                $confirmMessage = $clipboard->confirmMsgText('pages', $this->pageInfo, 'into', $elFromTable);
                $pasteButton = $buttonBar->makeLinkButton()
                    ->setHref($clipboard->pasteUrl('', $this->id))
                    ->setTitle($lang->getLL('clip_paste'))
                    ->setClasses('t3js-modal-trigger')
                    ->setDataAttributes([
                        'severity' => 'warning',
503
                        'bs-content' => $confirmMessage,
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
                        'title' => $lang->getLL('clip_paste')
                    ])
                    ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL));
                $buttonBar->addButton($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
            }
        }
        // Cache
        if ($this->id !== 0) {
            $clearCacheButton = $buttonBar->makeLinkButton()
                ->setHref('#')
                ->setDataAttributes(['id' => $this->id])
                ->setClasses('t3js-clear-page-cache')
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
                ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
            $buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT);
        }
        if ($table && (!isset($this->modTSconfig['noExportRecordsLinks'])
                || (isset($this->modTSconfig['noExportRecordsLinks'])
                    && !$this->modTSconfig['noExportRecordsLinks']))
        ) {
            // Export
            if (ExtensionManagementUtility::isLoaded('impexp')) {
526
                $url = (string)$this->uriBuilder->buildUriFromRoute('tx_impexp_export', ['tx_impexp' => ['list' => [$table . ':' . $this->id]]]);
527
                $exportButton = $buttonBar->makeLinkButton()
528
                    ->setHref($url)
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
                    ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export'))
                    ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL))
                    ->setShowLabelText(true);
                $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
            }
        }
        // Reload
        $reloadButton = $buttonBar->makeLinkButton()
            ->setHref($listUrl)
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
            ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
        $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);

        // Shortcut
        $shortCutButton = $buttonBar->makeShortcutButton()->setRouteIdentifier('web_list');
        $arguments = [
            'id' => $this->id
        ];
        $potentialArguments = [
            'pointer',
            'table',
            'search_field',
            'search_levels',
            'sortField',
            'sortRev'
        ];
        foreach ($potentialArguments as $argument) {
            if (!empty($queryParams[$argument])) {
                $arguments[$argument] = $queryParams[$argument];
            }
        }
        foreach ($moduleSettings as $moduleSettingKey => $moduleSettingValue) {
            $arguments['GET'][$moduleSettingKey] = $moduleSettingValue;
        }
        $shortCutButton->setArguments($arguments);
        $shortCutButton->setDisplayName($this->getShortcutTitle($arguments));
        $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);

        // Back
        if ($this->returnUrl) {
            $backButton = $buttonBar->makeLinkButton()
                ->setHref($this->returnUrl)
                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
                ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
            $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT);
        }
    }

577
578
579
580
581
    /**
     * Make selector box for creating new translation in a language
     * Displays only languages which are not yet present for the current page and
     * that are not disabled with page TS.
     *
582
     * @param string $requestUri
583
     * @return string HTML <select> element (if there were items for the box anyways...)
584
     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
585
     */
586
    protected function languageSelector(string $requestUri): string
587
    {
588
589
590
591
592
593
594
        if (!$this->getBackendUserAuthentication()->check('tables_modify', 'pages')) {
            return '';
        }
        $availableTranslations = [];
        foreach ($this->siteLanguages as $siteLanguage) {
            if ($siteLanguage->getLanguageId() === 0) {
                continue;
595
            }
596
597
598
599
600
601
602
603
            $availableTranslations[$siteLanguage->getLanguageId()] = $siteLanguage->getTitle();
        }
        // Then, subtract the languages which are already on the page:
        $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
        $queryBuilder->getRestrictions()->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
604
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUserAuthentication()->workspace));
605
606
607
608
609
        $statement = $queryBuilder->select('uid', $languageField)
            ->from('pages')
            ->where(
                $queryBuilder->expr()->eq(
                    $localizationParentField,
610
                    $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
611
                )
612
613
            )
            ->execute();
614
        while ($pageTranslation = $statement->fetchAssociative()) {
615
616
617
618
619
620
621
622
623
624
            unset($availableTranslations[(int)$pageTranslation[$languageField]]);
        }
        // If any languages are left, make selector:
        if (!empty($availableTranslations)) {
            $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:new_language')) . '</option>';
            foreach ($availableTranslations as $languageUid => $languageTitle) {
                // Build localize command URL to DataHandler (tce_db)
                // which redirects to FormEngine (record_edit)
                // which, when finished editing should return back to the current page (returnUrl)
                $parameters = [
625
                    'justLocalized' => 'pages:' . $this->id . ':' . $languageUid,
626
                    'returnUrl' => $requestUri
627
                ];
628
                $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters);
629
630
                $params = [];
                $params['redirect'] = $redirectUrl;
631
                $params['cmd']['pages'][$this->id]['localize'] = $languageUid;
632
                $targetUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
633
                $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
634
            }
635

636
            return '<div class="col-auto">'
637
                . '<select class="form-select" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">'
638
                . $output
639
                . '</select></div>';
640
641
642
643
        }
        return '';
    }

644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
    /**
     * Returns the configuration of mod.web_list.noViewWithDokTypes or the
     * default value 254 (Sys Folders) and 255 (Recycler), if not set.
     */
    protected function canCreatePreviewLink(): bool
    {
        if (isset($this->modTSconfig['noViewWithDokTypes'])) {
            $noViewDokTypes = GeneralUtility::trimExplode(',', $this->modTSconfig['noViewWithDokTypes'], true);
        } else {
            $noViewDokTypes = [
                PageRepository::DOKTYPE_SYSFOLDER,
                PageRepository::DOKTYPE_RECYCLER
            ];
        }

        return !in_array($this->pageInfo['doktype'] ?? 0, $noViewDokTypes);
    }

    /**
     * Check whether or not the current backend user is an admin or the current page is
     * locked by editlock.
     *
     * @return bool
     */
    protected function editLockPermissions(): bool
    {
670
        return $this->getBackendUserAuthentication()->isAdmin() || !($this->pageInfo['editlock'] ?? false);
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
    }

    /**
     * Returns the shortcut title for the current page
     *
     * @param array $arguments
     * @return string
     */
    protected function getShortcutTitle(array $arguments): string
    {
        $pageTitle = '';
        $tableTitle = '';
        $languageService = $this->getLanguageService();

        if (isset($arguments['table'])) {
            $tableTitle = ': ' . $languageService->sL($GLOBALS['TCA'][$arguments['table']]['ctrl']['title'] ?? '') ?: $arguments['table'];
        }

        if ($this->pageInfo !== []) {
            $pageTitle = BackendUtility::getRecordTitle('pages', $this->pageInfo);
        }

        return trim(sprintf(
            $languageService->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:shortcut.title'),
            $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:mlang_tabs_tab'),
            $tableTitle,
            $pageTitle,
            $this->id
        ));
    }

702
703
704
705
706
707
    protected function showPageTranslations(): bool
    {
        if (!$this->getBackendUserAuthentication()->check('tables_select', 'pages')) {
            return false;
        }

708
709
        if (isset($this->modTSconfig['table.']['pages.']['hideTable'])) {
            return !$this->modTSconfig['table.']['pages.']['hideTable'];
710
711
712
        }

        $hideTables = $this->modTSconfig['hideTables'] ?? '';
713
        return !($GLOBALS['TCA']['pages']['ctrl']['hideTable'] ?? false)
714
715
716
717
            && $hideTables !== '*'
            && !in_array('pages', GeneralUtility::trimExplode(',', $hideTables), true);
    }

718
719
720
721
722
723
724
725
    protected function htmlResponse(string $html): ResponseInterface
    {
        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8');
        $response->getBody()->write($html);
        return $response;
    }

726
727
728
    /**
     * @return BackendUserAuthentication
     */
729
    protected function getBackendUserAuthentication(): BackendUserAuthentication
730
731
732
733
734
735
736
    {
        return $GLOBALS['BE_USER'];
    }

    /**
     * @return LanguageService
     */
737
    protected function getLanguageService(): LanguageService
738
739
740
    {
        return $GLOBALS['LANG'];
    }
741
}