[TASK] Use @typo3/icons within PageActions
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / Controller / RecordListController.php
1 <?php
2
3 /*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16 namespace TYPO3\CMS\Recordlist\Controller;
17
18 use Psr\EventDispatcher\EventDispatcherInterface;
19 use Psr\Http\Message\ResponseFactoryInterface;
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use TYPO3\CMS\Backend\Clipboard\Clipboard;
23 use TYPO3\CMS\Backend\Domain\Model\Element\ImmediateActionElement;
24 use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
25 use TYPO3\CMS\Backend\Routing\UriBuilder;
26 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
27 use TYPO3\CMS\Backend\Template\ModuleTemplate;
28 use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
29 use TYPO3\CMS\Backend\Utility\BackendUtility;
30 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
31 use TYPO3\CMS\Core\Database\ConnectionPool;
32 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
33 use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
34 use TYPO3\CMS\Core\DataHandling\DataHandler;
35 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
36 use TYPO3\CMS\Core\Imaging\Icon;
37 use TYPO3\CMS\Core\Imaging\IconFactory;
38 use TYPO3\CMS\Core\Localization\LanguageService;
39 use TYPO3\CMS\Core\Messaging\FlashMessage;
40 use TYPO3\CMS\Core\Messaging\FlashMessageService;
41 use TYPO3\CMS\Core\Page\PageRenderer;
42 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
43 use TYPO3\CMS\Core\Type\Bitmask\Permission;
44 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
45 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
46 use TYPO3\CMS\Core\Utility\GeneralUtility;
47 use TYPO3\CMS\Recordlist\Event\RenderAdditionalContentToRecordListEvent;
48 use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
49 use TYPO3\CMS\Recordlist\View\RecordSearchBoxComponent;
50
51 /**
52 * Script Class for the Web > List module; rendering the listing of records on a page
53 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
54 */
55 class RecordListController
56 {
57 /**
58 * ModuleTemplate object
59 *
60 * @var ModuleTemplate
61 */
62 protected $moduleTemplate;
63
64 /**
65 * @var SiteLanguage[]
66 */
67 protected $siteLanguages = [];
68
69 /**
70 * @var Permission
71 */
72 protected $pagePermissions;
73
74 protected int $id = 0;
75 protected array $pageInfo = [];
76 protected string $returnUrl = '';
77 protected array $modTSconfig = [];
78
79 protected IconFactory $iconFactory;
80 protected PageRenderer $pageRenderer;
81 protected EventDispatcherInterface $eventDispatcher;
82 protected UriBuilder $uriBuilder;
83 protected ModuleTemplateFactory $moduleTemplateFactory;
84 protected ResponseFactoryInterface $responseFactory;
85
86 public function __construct(
87 IconFactory $iconFactory,
88 PageRenderer $pageRenderer,
89 EventDispatcherInterface $eventDispatcher,
90 UriBuilder $uriBuilder,
91 ModuleTemplateFactory $moduleTemplateFactory,
92 ResponseFactoryInterface $responseFactory
93 ) {
94 $this->iconFactory = $iconFactory;
95 $this->pageRenderer = $pageRenderer;
96 $this->eventDispatcher = $eventDispatcher;
97 $this->uriBuilder = $uriBuilder;
98 $this->moduleTemplateFactory = $moduleTemplateFactory;
99 $this->responseFactory = $responseFactory;
100 }
101
102 /**
103 * Injects the request object for the current request or subrequest
104 *
105 * @param ServerRequestInterface $request the current request
106 * @return ResponseInterface the response with the content
107 */
108 public function mainAction(ServerRequestInterface $request): ResponseInterface
109 {
110 $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
111 $this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');
112 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
113 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordDownloadButton');
114 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/ClearCache');
115 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordSearch');
116 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/AjaxDataHandler');
117 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ColumnSelectorButton');
118 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/MultiRecordSelection');
119 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ClipboardPanel');
120 $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');
121
122 BackendUtility::lockRecords();
123 $parsedBody = $request->getParsedBody();
124 $queryParams = $request->getQueryParams();
125 $backendUser = $this->getBackendUserAuthentication();
126 $perms_clause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
127 // GPvars:
128 $this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
129 $pointer = max(0, (int)($parsedBody['pointer'] ?? $queryParams['pointer'] ?? 0));
130 $table = (string)($parsedBody['table'] ?? $queryParams['table'] ?? '');
131 $search_field = (string)($parsedBody['search_field'] ?? $queryParams['search_field'] ?? '');
132 $search_levels = (int)($parsedBody['search_levels'] ?? $queryParams['search_levels'] ?? 0);
133 $this->returnUrl = GeneralUtility::sanitizeLocalUrl((string)($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? ''));
134 $cmd = (string)($parsedBody['cmd'] ?? $queryParams['cmd'] ?? '');
135 // Set site languages
136 $site = $request->getAttribute('site');
137 $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUserAuthentication(), false, $this->id);
138 // Loading module configuration:
139 $this->modTSconfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
140 // Clean up settings:
141 $MOD_SETTINGS = BackendUtility::getModuleData(['clipBoard' => ''], (array)($parsedBody['SET'] ?? $queryParams['SET'] ?? []), 'web_list');
142 // main
143 $lang = $this->getLanguageService();
144 // Loading current page record and checking access:
145 $pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause);
146 $access = is_array($pageinfo);
147 $this->pageInfo = is_array($pageinfo) ? $pageinfo : [];
148
149 $this->pagePermissions = new Permission($backendUser->calcPerms($pageinfo));
150 $userCanEditPage = $this->pagePermissions->editPagePermissionIsGranted() && !empty($this->id) && ($backendUser->isAdmin() || (int)$pageinfo['editlock'] === 0);
151 $pageActionsCallback = null;
152 if ($userCanEditPage) {
153 $pageActionsCallback = 'function(PageActions) {
154 PageActions.setPageId(' . (int)$this->id . ');
155 }';
156 }
157 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback);
158 // Apply predefined values for hidden checkboxes
159 // Set predefined value for Clipboard:
160 if (isset($this->modTSconfig['enableClipBoard'])) {
161 if ($this->modTSconfig['enableClipBoard'] === 'activated') {
162 $MOD_SETTINGS['clipBoard'] = true;
163 } elseif ($this->modTSconfig['enableClipBoard'] === 'deactivated') {
164 $MOD_SETTINGS['clipBoard'] = false;
165 }
166 }
167 if (!isset($MOD_SETTINGS['clipBoard'])) {
168 $MOD_SETTINGS['clipBoard'] = true;
169 }
170 $clipboard = $this->initializeClipboard($request, (bool)$MOD_SETTINGS['clipBoard']);
171 $enableListing = $access || ($this->id === 0 && $search_levels !== 0 && $search_field !== '');
172
173 // Initialize the dblist object:
174 $dblist = GeneralUtility::makeInstance(DatabaseRecordList::class);
175 $dblist->setModuleData($MOD_SETTINGS ?? []);
176 $dblist->calcPerms = $this->pagePermissions;
177 $dblist->returnUrl = $this->returnUrl;
178 $dblist->showClipboardActions = true;
179 $dblist->disableSingleTableView = $this->modTSconfig['disableSingleTableView'] ?? false;
180 $dblist->listOnlyInSingleTableMode = $this->modTSconfig['listOnlyInSingleTableView'] ?? false;
181 $dblist->hideTables = $this->modTSconfig['hideTables'] ?? false;
182 $dblist->hideTranslations = $this->modTSconfig['hideTranslations'] ?? false;
183 $dblist->tableTSconfigOverTCA = $this->modTSconfig['table.'] ?? false;
184 $dblist->allowedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['allowedNewTables'] ?? '', true);
185 $dblist->deniedNewTables = GeneralUtility::trimExplode(',', $this->modTSconfig['deniedNewTables'] ?? '', true);
186 $dblist->pageRow = $this->pageInfo;
187 $dblist->modTSconfig = $this->modTSconfig;
188 $dblist->setLanguagesAllowedForUser($this->siteLanguages);
189 $clickTitleMode = trim($this->modTSconfig['clickTitleMode'] ?? '');
190 $dblist->clickTitleMode = $clickTitleMode === '' ? 'edit' : $clickTitleMode;
191 if (isset($this->modTSconfig['tableDisplayOrder.'])) {
192 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
193 $dblist->setTableDisplayOrder($typoScriptService->convertTypoScriptArrayToPlainArray($this->modTSconfig['tableDisplayOrder.']));
194 }
195
196 $dblist->clipObj = $clipboard;
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 // Deleting records...:
201 // 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 if ($cmd === 'delete' && $request->getMethod() === 'POST') {
203 $items = $clipboard->cleanUpCBC((array)($parsedBody['CBC'] ?? []), (string)($parsedBody['cmd_table'] ?? ''), true);
204 if (!empty($items)) {
205 // Create data handler command array
206 $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 BackendUtility::setUpdateSignal('updatePageTree');
216 }
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/Element/ImmediateActionElement');
228
229 // Setting up the context sensitive menu:
230 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
231 }
232 // access
233 // Begin to compile the whole page, starting out with page header:
234 if (!$this->id) {
235 $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
236 } else {
237 $title = $pageinfo['title'];
238 }
239 $body = ImmediateActionElement::moduleStateUpdate('web', (int)$this->id);
240 $body .= $this->moduleTemplate->header($title);
241
242 // Additional header content
243 /** @var RenderAdditionalContentToRecordListEvent $additionalRecordListEvent */
244 $additionalRecordListEvent = $this->eventDispatcher->dispatch(new RenderAdditionalContentToRecordListEvent($request));
245 $body .= $additionalRecordListEvent->getAdditionalContentAbove();
246 $this->moduleTemplate->setTitle(
247 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:mlang_tabs_tab'),
248 $title
249 );
250
251 $beforeOutput = '';
252 $output = '';
253 // Show the selector to add page translations, but only when in "default" mode.
254 // If not disabled via module TSconfig and the user is allowed, also show the page translations table.
255 if ($this->id && !$search_field && !$cmd && !$table) {
256 $beforeOutput .= $this->languageSelector($request->getAttribute('normalizedParams')->getRequestUri());
257 if ($this->showPageTranslations()) {
258 $pageTranslationsDatabaseRecordList = clone $dblist;
259 $pageTranslationsDatabaseRecordList->listOnlyInSingleTableMode = false;
260 $pageTranslationsDatabaseRecordList->disableSingleTableView = true;
261 $pageTranslationsDatabaseRecordList->deniedNewTables = ['pages'];
262 $pageTranslationsDatabaseRecordList->hideTranslations = '';
263 $pageTranslationsDatabaseRecordList->setLanguagesAllowedForUser($this->siteLanguages);
264 $pageTranslationsDatabaseRecordList->showOnlyTranslatedRecords(true);
265 $output .= $pageTranslationsDatabaseRecordList->getTable('pages', $this->id);
266 }
267 }
268
269 // search box toolbar
270 if (!($this->modTSconfig['disableSearchBox'] ?? false) && ($tableOutput || !empty($search_field))) {
271 $beforeOutput .= $this->renderSearchBox($dblist, $search_field, $search_levels);
272 }
273
274 if (!empty($tableOutput)) {
275 $output .= $tableOutput;
276 } else {
277 if (isset($GLOBALS['TCA'][$table]['ctrl']['title'])) {
278 if (strpos($GLOBALS['TCA'][$table]['ctrl']['title'], 'LLL:') === 0) {
279 $ll = sprintf($lang->getLL('noRecordsOfTypeOnThisPage'), $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title']));
280 } else {
281 $ll = sprintf($lang->getLL('noRecordsOfTypeOnThisPage'), $GLOBALS['TCA'][$table]['ctrl']['title']);
282 }
283 } else {
284 $ll = $lang->getLL('noRecordsOnThisPage');
285 }
286 $flashMessage = GeneralUtility::makeInstance(
287 FlashMessage::class,
288 $ll,
289 '',
290 FlashMessage::INFO
291 );
292 unset($ll);
293 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
294 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
295 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
296 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
297 $defaultFlashMessageQueue->enqueue($flashMessage);
298 }
299
300 if ($beforeOutput) {
301 $body .= '<div class="row">' . $beforeOutput . '</div>';
302 }
303 $body .= $output;
304 // If a listing was produced, create the page footer
305 if ($tableOutput) {
306 // Adding checkbox option for clipboard display
307 $body .= '
308 <div class="mb-3">
309 <form action="" method="post">';
310
311 // Add "clipboard" checkbox:
312 if ($this->modTSconfig['enableClipBoard'] === 'selectable') {
313 $body .= '<div class="form-check form-switch">' .
314 BackendUtility::getFuncCheck($this->id, 'SET[clipBoard]', ($MOD_SETTINGS['clipBoard'] ?? ''), '', $table ? '&table=' . $table : '', 'id="checkShowClipBoard"') .
315 '<label class="form-check-label" for="checkShowClipBoard">' .
316 BackendUtility::wrapInHelp('xMOD_csh_corebe', 'list_options', htmlspecialchars($lang->getLL('showClipBoard'))) .
317 '</label>' .
318 '</div>';
319 }
320
321 $body .= '
322 </form>
323 </div>';
324 }
325 // Printing clipboard if enabled
326 if ($MOD_SETTINGS['clipBoard'] && ($tableOutput || $clipboard->hasElements())) {
327 $body .= '<typo3-backend-clipboard-panel return-url="' . htmlspecialchars($dblist->listURL()) . '"></typo3-backend-clipboard-panel>';
328 }
329 // Additional footer content
330 $body .= $additionalRecordListEvent->getAdditionalContentBelow();
331 // Setting up the buttons for docheader
332 $this->getDocHeaderButtons(
333 $clipboard,
334 $queryParams,
335 $table,
336 $dblist->listURL(),
337 $MOD_SETTINGS
338 );
339
340 if ($pageinfo) {
341 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageinfo);
342 }
343
344 $this->moduleTemplate->setContent($body);
345 return $this->htmlResponse($this->moduleTemplate->renderContent());
346 }
347
348 /**
349 * Processing incoming data and configures the clipboard.
350 *
351 * @param ServerRequestInterface $request
352 * @param bool $isClipboardShown
353 * @return Clipboard
354 */
355 protected function initializeClipboard(ServerRequestInterface $request, bool $isClipboardShown): Clipboard
356 {
357 $clipboard = GeneralUtility::makeInstance(Clipboard::class);
358 $cmd = (string)($request->getParsedBody()['cmd'] ?? $request->getQueryParams()['cmd'] ?? '');
359 // Initialize - reads the clipboard content from the user session
360 $clipboard->initializeClipboard($request);
361 // Clipboard actions are handled:
362 // CB is the clipboard command array
363 $CB = array_replace_recursive($request->getQueryParams()['CB'] ?? [], $request->getParsedBody()['CB'] ?? []);
364 if ($cmd === 'copyMarked' || $cmd === 'removeMarked') {
365 // Get CBC from request, and map the element values (true => copy, false => remove)
366 $CBC = array_map(static fn () => ($cmd === 'copyMarked'), (array)($request->getParsedBody()['CBC'] ?? []));
367 $cmd_table = (string)($request->getParsedBody()['cmd_table'] ?? $request->getQueryParams()['cmd_table'] ?? '');
368 // Cleanup CBC
369 $CB['el'] = $clipboard->cleanUpCBC($CBC, $cmd_table);
370 }
371 if (!$isClipboardShown) {
372 // If the clipboard is NOT shown, set the pad to 'normal'.
373 $CB['setP'] = 'normal';
374 }
375 // Execute commands.
376 $clipboard->setCmd($CB);
377 // Clean up pad
378 $clipboard->cleanCurrent();
379 // Save the clipboard content
380 $clipboard->endClipboard();
381 return $clipboard;
382 }
383
384 protected function renderSearchBox(DatabaseRecordList $dblist, string $searchWord, int $searchLevels): string
385 {
386 $searchBoxVisible = !empty($dblist->searchString);
387 $searchBox = GeneralUtility::makeInstance(RecordSearchBoxComponent::class)
388 ->setAllowedSearchLevels((array)($this->modTSconfig['searchLevel.']['items.'] ?? []))
389 ->setSearchWord($searchWord)
390 ->setSearchLevel($searchLevels)
391 ->render($dblist->listURL('', '-1', 'pointer,search_field'));
392
393 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
394 $searchButton = $buttonBar->makeLinkButton();
395 $searchButton
396 ->setHref('#')
397 ->setClasses('t3js-toggle-search-toolbox')
398 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchIcon'))
399 ->setIcon($this->iconFactory->getIcon('actions-search', Icon::SIZE_SMALL));
400 $buttonBar->addButton(
401 $searchButton,
402 ButtonBar::BUTTON_POSITION_LEFT,
403 90
404 );
405 return '<div class="col-6" style="' . ($searchBoxVisible ?: 'display: none') . '" id="db_list-searchbox-toolbar">' . $searchBox . '</div>';
406 }
407
408 /**
409 * Create the panel of buttons for submitting the form or otherwise perform
410 * operations.
411 *
412 * @param Clipboard $clipboard
413 * @param array $queryParams
414 * @param string $table
415 * @param string $listUrl
416 * @param array $moduleSettings
417 */
418 protected function getDocHeaderButtons(Clipboard $clipboard, array $queryParams, string $table, string $listUrl, array $moduleSettings): void
419 {
420 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
421 $backendUser = $this->getBackendUserAuthentication();
422 $lang = $this->getLanguageService();
423 // CSH
424 if (!$this->id) {
425 $fieldName = 'list_module_root';
426 } else {
427 $fieldName = 'list_module';
428 }
429 $cshButton = $buttonBar->makeHelpButton()
430 ->setModuleName('xMOD_csh_corebe')
431 ->setFieldName($fieldName);
432 $buttonBar->addButton($cshButton);
433 // New record on pages that are not locked by editlock
434 if (!($this->modTSconfig['noCreateRecordsLink'] ?? false) && $this->editLockPermissions()) {
435 $newRecordButton = $buttonBar->makeLinkButton()
436 ->setHref((string)$this->uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id, 'returnUrl' => $listUrl]))
437 ->setTitle($lang->getLL('newRecordGeneral'))
438 ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
439 $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
440 }
441
442 if ($this->id !== 0) {
443 if ($this->canCreatePreviewLink()) {
444 $previewDataAttributes = PreviewUriBuilder::create((int)$this->id)
445 ->withRootLine(BackendUtility::BEgetRootLine($this->id))
446 ->buildDispatcherDataAttributes();
447 $viewButton = $buttonBar->makeLinkButton()
448 ->setHref('#')
449 ->setDataAttributes($previewDataAttributes ?? [])
450 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
451 ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL));
452 $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
453 }
454 // If edit permissions are set, see
455 // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
456 if ($this->pagePermissions->editPagePermissionIsGranted() && $this->editLockPermissions() && $backendUser->checkLanguageAccess(0)) {
457 // Edit
458 $editLink = $this->uriBuilder->buildUriFromRoute('record_edit', [
459 'edit' => [
460 'pages' => [
461 $this->id => 'edit',
462 ],
463 ],
464 'returnUrl' => $listUrl,
465 ]);
466 $editButton = $buttonBar->makeLinkButton()
467 ->setHref($editLink)
468 ->setTitle($lang->getLL('editPage'))
469 ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
470 $buttonBar->addButton($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
471 }
472 }
473
474 // Paste
475 if (($this->pagePermissions->createPagePermissionIsGranted() || $this->pagePermissions->editContentPermissionIsGranted()) && $this->editLockPermissions()) {
476 $elFromTable = $clipboard->elFromTable();
477 if (!empty($elFromTable)) {
478 $confirmMessage = $clipboard->confirmMsgText('pages', $this->pageInfo, 'into', $elFromTable);
479 $pasteButton = $buttonBar->makeLinkButton()
480 ->setHref($clipboard->pasteUrl('', $this->id))
481 ->setTitle($lang->getLL('clip_paste'))
482 ->setClasses('t3js-modal-trigger')
483 ->setDataAttributes([
484 'severity' => 'warning',
485 'bs-content' => $confirmMessage,
486 'title' => $lang->getLL('clip_paste'),
487 ])
488 ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL));
489 $buttonBar->addButton($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
490 }
491 }
492 // Cache
493 if ($this->id !== 0) {
494 $clearCacheButton = $buttonBar->makeLinkButton()
495 ->setHref('#')
496 ->setDataAttributes(['id' => $this->id])
497 ->setClasses('t3js-clear-page-cache')
498 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
499 ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
500 $buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT);
501 }
502 if ($table && (!isset($this->modTSconfig['noExportRecordsLinks'])
503 || (isset($this->modTSconfig['noExportRecordsLinks'])
504 && !$this->modTSconfig['noExportRecordsLinks']))
505 ) {
506 // Export
507 if (ExtensionManagementUtility::isLoaded('impexp')) {
508 $url = (string)$this->uriBuilder->buildUriFromRoute('tx_impexp_export', ['tx_impexp' => ['list' => [$table . ':' . $this->id]]]);
509 $exportButton = $buttonBar->makeLinkButton()
510 ->setHref($url)
511 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export'))
512 ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL))
513 ->setShowLabelText(true);
514 $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
515 }
516 }
517 // Reload
518 $reloadButton = $buttonBar->makeLinkButton()
519 ->setHref($listUrl)
520 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
521 ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
522 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
523
524 // Shortcut
525 $shortCutButton = $buttonBar->makeShortcutButton()->setRouteIdentifier('web_list');
526 $arguments = [
527 'id' => $this->id,
528 ];
529 $potentialArguments = [
530 'pointer',
531 'table',
532 'search_field',
533 'search_levels',
534 'sortField',
535 'sortRev',
536 ];
537 foreach ($potentialArguments as $argument) {
538 if (!empty($queryParams[$argument])) {
539 $arguments[$argument] = $queryParams[$argument];
540 }
541 }
542 foreach ($moduleSettings as $moduleSettingKey => $moduleSettingValue) {
543 $arguments['GET'][$moduleSettingKey] = $moduleSettingValue;
544 }
545 $shortCutButton->setArguments($arguments);
546 $shortCutButton->setDisplayName($this->getShortcutTitle($arguments));
547 $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
548
549 // Back
550 if ($this->returnUrl) {
551 $backButton = $buttonBar->makeLinkButton()
552 ->setHref($this->returnUrl)
553 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
554 ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
555 $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT);
556 }
557 }
558
559 /**
560 * Make selector box for creating new translation in a language
561 * Displays only languages which are not yet present for the current page and
562 * that are not disabled with page TS.
563 *
564 * @param string $requestUri
565 * @return string HTML <select> element (if there were items for the box anyways...)
566 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
567 */
568 protected function languageSelector(string $requestUri): string
569 {
570 if (!$this->getBackendUserAuthentication()->check('tables_modify', 'pages')) {
571 return '';
572 }
573 $availableTranslations = [];
574 foreach ($this->siteLanguages as $siteLanguage) {
575 if ($siteLanguage->getLanguageId() === 0) {
576 continue;
577 }
578 $availableTranslations[$siteLanguage->getLanguageId()] = $siteLanguage->getTitle();
579 }
580 // Then, subtract the languages which are already on the page:
581 $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
582 $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
583 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
584 $queryBuilder->getRestrictions()->removeAll()
585 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
586 ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUserAuthentication()->workspace));
587 $statement = $queryBuilder->select('uid', $languageField)
588 ->from('pages')
589 ->where(
590 $queryBuilder->expr()->eq(
591 $localizationParentField,
592 $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
593 )
594 )
595 ->execute();
596 while ($pageTranslation = $statement->fetchAssociative()) {
597 unset($availableTranslations[(int)$pageTranslation[$languageField]]);
598 }
599 // If any languages are left, make selector:
600 if (!empty($availableTranslations)) {
601 $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:new_language')) . '</option>';
602 foreach ($availableTranslations as $languageUid => $languageTitle) {
603 // Build localize command URL to DataHandler (tce_db)
604 // which redirects to FormEngine (record_edit)
605 // which, when finished editing should return back to the current page (returnUrl)
606 $parameters = [
607 'justLocalized' => 'pages:' . $this->id . ':' . $languageUid,
608 'returnUrl' => $requestUri,
609 ];
610 $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters);
611 $params = [];
612 $params['redirect'] = $redirectUrl;
613 $params['cmd']['pages'][$this->id]['localize'] = $languageUid;
614 $targetUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
615 $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
616 }
617
618 return '<div class="col-auto">'
619 . '<select class="form-select" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">'
620 . $output
621 . '</select></div>';
622 }
623 return '';
624 }
625
626 /**
627 * Returns the configuration of mod.web_list.noViewWithDokTypes or the
628 * default value 254 (Sys Folders) and 255 (Recycler), if not set.
629 */
630 protected function canCreatePreviewLink(): bool
631 {
632 if (isset($this->modTSconfig['noViewWithDokTypes'])) {
633 $noViewDokTypes = GeneralUtility::trimExplode(',', $this->modTSconfig['noViewWithDokTypes'], true);
634 } else {
635 $noViewDokTypes = [
636 PageRepository::DOKTYPE_SYSFOLDER,
637 PageRepository::DOKTYPE_RECYCLER,
638 ];
639 }
640
641 return !in_array($this->pageInfo['doktype'] ?? 0, $noViewDokTypes);
642 }
643
644 /**
645 * Check whether or not the current backend user is an admin or the current page is
646 * locked by editlock.
647 *
648 * @return bool
649 */
650 protected function editLockPermissions(): bool
651 {
652 return $this->getBackendUserAuthentication()->isAdmin() || !($this->pageInfo['editlock'] ?? false);
653 }
654
655 /**
656 * Returns the shortcut title for the current page
657 *
658 * @param array $arguments
659 * @return string
660 */
661 protected function getShortcutTitle(array $arguments): string
662 {
663 $pageTitle = '';
664 $tableTitle = '';
665 $languageService = $this->getLanguageService();
666
667 if (isset($arguments['table'])) {
668 $tableTitle = ': ' . (isset($GLOBALS['TCA'][$arguments['table']]['ctrl']['title']) ? $languageService->sL($GLOBALS['TCA'][$arguments['table']]['ctrl']['title']) : $arguments['table']);
669 }
670
671 if ($this->pageInfo !== []) {
672 $pageTitle = BackendUtility::getRecordTitle('pages', $this->pageInfo);
673 }
674
675 return trim(sprintf(
676 $languageService->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:shortcut.title'),
677 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:mlang_tabs_tab'),
678 $tableTitle,
679 $pageTitle,
680 $this->id
681 ));
682 }
683
684 protected function showPageTranslations(): bool
685 {
686 if (!$this->getBackendUserAuthentication()->check('tables_select', 'pages')) {
687 return false;
688 }
689
690 if (isset($this->modTSconfig['table.']['pages.']['hideTable'])) {
691 return !$this->modTSconfig['table.']['pages.']['hideTable'];
692 }
693
694 $hideTables = $this->modTSconfig['hideTables'] ?? '';
695 return !($GLOBALS['TCA']['pages']['ctrl']['hideTable'] ?? false)
696 && $hideTables !== '*'
697 && !in_array('pages', GeneralUtility::trimExplode(',', $hideTables), true);
698 }
699
700 protected function htmlResponse(string $html): ResponseInterface
701 {
702 $response = $this->responseFactory->createResponse()
703 ->withHeader('Content-Type', 'text/html; charset=utf-8');
704 $response->getBody()->write($html);
705 return $response;
706 }
707
708 /**
709 * @return BackendUserAuthentication
710 */
711 protected function getBackendUserAuthentication(): BackendUserAuthentication
712 {
713 return $GLOBALS['BE_USER'];
714 }
715
716 /**
717 * @return LanguageService
718 */
719 protected function getLanguageService(): LanguageService
720 {
721 return $GLOBALS['LANG'];
722 }
723 }