1<?php
2namespace TYPO3\CMS\Recordlist\RecordList;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
18use TYPO3\CMS\Backend\Module\BaseScriptClass;
19use TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface;
20use TYPO3\CMS\Backend\Routing\UriBuilder;
21use TYPO3\CMS\Backend\Template\Components\ButtonBar;
22use TYPO3\CMS\Backend\Template\DocumentTemplate;
23use TYPO3\CMS\Backend\Template\ModuleTemplate;
24use TYPO3\CMS\Backend\Tree\View\PageTreeView;
25use TYPO3\CMS\Backend\Utility\BackendUtility;
26use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27use TYPO3\CMS\Core\Cache\CacheManager;
28use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
29use TYPO3\CMS\Core\Database\Connection;
30use TYPO3\CMS\Core\Database\ConnectionPool;
31use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32use TYPO3\CMS\Core\Database\Query\QueryHelper;
33use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
34use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
35use TYPO3\CMS\Core\Database\ReferenceIndex;
36use TYPO3\CMS\Core\Imaging\Icon;
37use TYPO3\CMS\Core\Imaging\IconFactory;
38use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
39use TYPO3\CMS\Core\LinkHandling\LinkService;
40use TYPO3\CMS\Core\Localization\LanguageService;
41use TYPO3\CMS\Core\Log\LogManager;
42use TYPO3\CMS\Core\Messaging\FlashMessage;
43use TYPO3\CMS\Core\Messaging\FlashMessageService;
44use TYPO3\CMS\Core\Service\DependencyOrderingService;
45use TYPO3\CMS\Core\Type\Bitmask\Permission;
46use TYPO3\CMS\Core\Utility\CsvUtility;
47use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
48use TYPO3\CMS\Core\Utility\GeneralUtility;
49use TYPO3\CMS\Core\Utility\HttpUtility;
50use TYPO3\CMS\Core\Utility\MathUtility;
51use TYPO3\CMS\Core\Versioning\VersionState;
52use TYPO3\CMS\Frontend\Page\PageRepository;
53
54/**
55 * Class for rendering of Web>List module
56 * @internal This class is a specific TYPO3 Backend implementation and is not part of the TYPO3's Core API.
57 */
58class DatabaseRecordList
59{
60    use PublicPropertyDeprecationTrait;
61
62    /**
63     * @var array
64     */
65    protected $deprecatedPublicProperties = [
66        'newWizards' => 'Using $newWizards of class DatabaseRecordList from outside is discouraged, property will be removed in TYPO3 v10.0.',
67    ];
68
69    // *********
70    // External:
71    // *********
72
73    /**
74     * Used to indicate which tables (values in the array) that can have a
75     * create-new-record link. If the array is empty, all tables are allowed.
76     *
77     * @var string[]
78     */
79    public $allowedNewTables = [];
80
81    /**
82     * Used to indicate which tables (values in the array) that cannot have a
83     * create-new-record link. If the array is empty, all tables are allowed.
84     *
85     * @var string[]
86     */
87    public $deniedNewTables = [];
88
89    /**
90     * If TRUE, the control panel will contain links to the create-new wizards for
91     * pages and tt_content elements (normally, the link goes to just creating a new
92     * element without the wizards!).
93     *
94     * @var bool
95     * @deprecated and unused since TYPO3 v9, will be removed in TYPO3 v10.0
96     */
97    public $newWizards = false;
98
99    /**
100     * If TRUE, will disable the rendering of clipboard + control panels.
101     *
102     * @var bool
103     */
104    public $dontShowClipControlPanels = false;
105
106    /**
107     * If TRUE, will show the clipboard in the field list.
108     *
109     * @var bool
110     */
111    public $showClipboard = false;
112
113    /**
114     * If TRUE, will DISABLE all control panels in lists. (Takes precedence)
115     *
116     * @var bool
117     */
118    public $noControlPanels = false;
119
120    /**
121     * If TRUE, clickmenus will be rendered
122     *
123     * @var bool
124     */
125    public $clickMenuEnabled = true;
126
127    /**
128     * Count of record rows in view
129     *
130     * @var int
131     */
132    public $totalRowCount;
133
134    /**
135     * Space icon used for alignment
136     *
137     * @var string
138     */
139    public $spaceIcon;
140
141    /**
142     * Disable single table view
143     *
144     * @var bool
145     */
146    public $disableSingleTableView = false;
147
148    // *********
149    // Internal:
150    // *********
151
152    /**
153     * Set to the page record (see writeTop())
154     *
155     * @var string[]
156     */
157    public $pageRow = [];
158
159    /**
160     * Shared module configuration, used by localization features
161     *
162     * @var array
163     */
164    public $modSharedTSconfig = [];
165
166    /**
167     * Contains page translation languages
168     *
169     * @var array
170     */
171    public $pageOverlays = [];
172
173    /**
174     * Contains sys language icons and titles
175     *
176     * @var array
177     */
178    public $languageIconTitles = [];
179
180    /**
181     * Tables which should not list their translations
182     *
183     * @var string
184     */
185    public $hideTranslations = '';
186
187    /**
188     * If set, the listing is returned as CSV instead.
189     *
190     * @var bool
191     */
192    public $csvOutput = false;
193
194    /**
195     * Cache for record path
196     *
197     * @var mixed[]
198     */
199    public $recPath_cache = [];
200
201    /**
202     * Field, to sort list by
203     *
204     * @var string
205     */
206    public $sortField;
207
208    /**
209     * default Max items shown per table in "multi-table mode", may be overridden by tables.php
210     *
211     * @var int
212     */
213    public $itemsLimitPerTable = 20;
214
215    /**
216     * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes;
217     *
218     * @var array
219     */
220    public $addElement_tdParams = [];
221
222    /**
223     * Page id
224     *
225     * @var int
226     */
227    public $id;
228
229    /**
230     * @var int
231     */
232    public $no_noWrap = 0;
233
234    /**
235     * Set to zero, if you don't want a left-margin with addElement function
236     *
237     * @var int
238     */
239    public $setLMargin = 1;
240
241    /**
242     * Used for tracking duplicate values of fields
243     *
244     * @var string[]
245     */
246    public $duplicateStack = [];
247
248    /**
249     * Current script name
250     *
251     * @var string
252     */
253    public $script = 'index.php';
254
255    /**
256     * If TRUE, records are listed only if a specific table is selected.
257     *
258     * @var bool
259     */
260    public $listOnlyInSingleTableMode = false;
261
262    /**
263     * Script URL
264     *
265     * @var string
266     */
267    public $thisScript = '';
268
269    /**
270     * JavaScript code accumulation
271     *
272     * @var string
273     */
274    public $JScode = '';
275
276    /**
277     * @var TranslationConfigurationProvider
278     */
279    public $translateTools;
280
281    /**
282     * default Max items shown per table in "single-table mode", may be overridden by tables.php
283     *
284     * @var int
285     */
286    public $itemsLimitSingleTable = 100;
287
288    /**
289     * Array of collapsed / uncollapsed tables in multi table view
290     *
291     * @var int[][]
292     */
293    public $tablesCollapsed = [];
294
295    /**
296     * @var array[] Module configuration
297     */
298    public $modTSconfig;
299
300    /**
301     * String with accumulated HTML content
302     *
303     * @var string
304     */
305    public $HTMLcode = '';
306
307    /**
308     * Keys are fieldnames and values are td-css-classes to add in addElement();
309     *
310     * @var array
311     */
312    public $addElement_tdCssClass = [];
313
314    /**
315     * Thumbnails on records containing files (pictures)
316     *
317     * @var bool
318     */
319    public $thumbs = 0;
320
321    /**
322     * Used for tracking next/prev uids
323     *
324     * @var int[][]
325     */
326    public $currentTable = [];
327
328    /**
329     * Indicates if all available fields for a user should be selected or not.
330     *
331     * @var int
332     */
333    public $allFields = 0;
334
335    /**
336     * Number of records to show
337     *
338     * @var int
339     */
340    public $showLimit = 0;
341
342    /**
343     * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
344     *
345     * @var array
346     */
347    public $fieldArray = [];
348
349    /**
350     * Tables which should not get listed
351     *
352     * @var string
353     */
354    public $hideTables = '';
355
356    /**
357     * Containing which fields to display in extended mode
358     *
359     * @var string[]
360     */
361    public $displayFields;
362
363    /**
364     * If set this is <td> CSS-classname for odd columns in addElement. Used with db_layout / pages section
365     *
366     * @var string
367     */
368    public $oddColumnsCssClass = '';
369
370    /**
371     * Not used in this class - but maybe extension classes...
372     * Max length of strings
373     *
374     * @var int
375     */
376    public $fixedL = 30;
377
378    /**
379     * Page select permissions
380     *
381     * @var string
382     */
383    public $perms_clause = '';
384
385    /**
386     * Return URL
387     *
388     * @var string
389     */
390    public $returnUrl = '';
391
392    /**
393     * Tablename if single-table mode
394     *
395     * @var string
396     */
397    public $table = '';
398
399    /**
400     * Some permissions...
401     *
402     * @var int
403     */
404    public $calcPerms = 0;
405
406    /**
407     * Mode for what happens when a user clicks the title of a record.
408     *
409     * @var string
410     */
411    public $clickTitleMode = '';
412
413    /**
414     * @var int
415     */
416    public $showIcon = 1;
417
418    /**
419     * Levels to search down.
420     *
421     * @var int
422     */
423    public $searchLevels = '';
424
425    /**
426     * "LIMIT " in SQL...
427     *
428     * @var int
429     */
430    public $iLimit = 0;
431
432    /**
433     * Set to the total number of items for a table when selecting.
434     *
435     * @var string
436     */
437    public $totalItems = '';
438
439    /**
440     * OBSOLETE - NOT USED ANYMORE. leftMargin
441     *
442     * @var int
443     */
444    public $leftMargin = 0;
445
446    /**
447     * TSconfig which overwrites TCA-Settings
448     *
449     * @var mixed[][]
450     */
451    public $tableTSconfigOverTCA = [];
452
453    /**
454     * Loaded with page record with version overlay if any.
455     *
456     * @var string[]
457     */
458    public $pageRecord = [];
459
460    /**
461     * Fields to display for the current table
462     *
463     * @var string[]
464     */
465    public $setFields = [];
466
467    /**
468     * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard
469     *
470     * @var int
471     */
472    public $counter = 0;
473
474    /**
475     * Pointer for browsing list
476     *
477     * @var int
478     */
479    public $firstElementNumber = 0;
480
481    /**
482     * Counting the elements no matter what...
483     *
484     * @var int
485     */
486    public $eCounter = 0;
487
488    /**
489     * Search string
490     *
491     * @var string
492     */
493    public $searchString = '';
494
495    /**
496     * Field, indicating to sort in reverse order.
497     *
498     * @var bool
499     */
500    public $sortRev;
501
502    /**
503     * String, can contain the field name from a table which must have duplicate values marked.
504     *
505     * @var string
506     */
507    public $duplicateField;
508
509    /**
510     * Specify a list of tables which are the only ones allowed to be displayed.
511     *
512     * @var string
513     */
514    public $tableList = '';
515
516    /**
517     * Used to accumulate CSV lines for CSV export.
518     *
519     * @var string[]
520     */
521    protected $csvLines = [];
522
523    /**
524     * Clipboard object
525     *
526     * @var \TYPO3\CMS\Backend\Clipboard\Clipboard
527     */
528    public $clipObj;
529
530    /**
531     * Tracking names of elements (for clipboard use)
532     *
533     * @var string[]
534     */
535    public $CBnames = [];
536
537    /**
538     * [$tablename][$uid] = number of references to this record
539     *
540     * @var int[][]
541     */
542    protected $referenceCount = [];
543
544    /**
545     * Translations of the current record
546     *
547     * @var string[]
548     */
549    public $translations;
550
551    /**
552     * select fields for the query which fetches the translations of the current
553     * record
554     *
555     * @var string
556     */
557    public $selFieldList;
558
559    /**
560     * @var mixed[]
561     */
562    public $pageinfo;
563
564    /**
565     * Injected by RecordList
566     *
567     * @var string[]
568     */
569    public $MOD_MENU;
570
571    /**
572     * If defined the records are editable
573     *
574     * @var bool
575     */
576    protected $editable = true;
577
578    /**
579     * @var IconFactory
580     */
581    protected $iconFactory;
582
583    /**
584     * Array with before/after setting for tables
585     * Structure:
586     * 'tableName' => [
587     *    'before' => ['A', ...]
588     *    'after' => []
589     *  ]
590     *
591     * @var array[]
592     */
593    protected $tableDisplayOrder = [];
594
595    /**
596     * Override the page ids taken into account by getPageIdConstraint()
597     *
598     * @var array
599     */
600    protected $overridePageIdList = [];
601
602    /**
603     * Override/add urlparameters in listUrl() method
604     * @var mixed[]
605     */
606    protected $overrideUrlParameters = [];
607
608    /**
609     * Current link: array with table names and uid
610     *
611     * @var array
612     */
613    protected $currentLink = [];
614
615    /**
616     * Only used to render translated records, used in list module to show page translations
617     *
618     * @var bool
619     */
620    protected $showOnlyTranslatedRecords = false;
621
622    /**
623     * All languages that are included in the site configuration
624     * for the current page. New records can only be created in those
625     * languages.
626     *
627     * @var array
628     */
629    protected $systemLanguagesOnPage;
630
631    /**
632     * All languages that are allowed by the user
633     *
634     * @var array
635     */
636    protected $languagesAllowedForUser = [];
637
638    /**
639     * Constructor
640     */
641    public function __construct()
642    {
643        if (isset($GLOBALS['BE_USER']->uc['titleLen']) && $GLOBALS['BE_USER']->uc['titleLen'] > 0) {
644            $this->fixedL = $GLOBALS['BE_USER']->uc['titleLen'];
645        }
646        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
647        $this->getTranslateTools();
648        $this->determineScriptUrl();
649
650        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
651    }
652
653    /**
654     * Create the panel of buttons for submitting the form or otherwise perform
655     * operations.
656     *
657     * @return string[] All available buttons as an assoc. array
658     */
659    public function getButtons()
660    {
661        $module = $this->getModule();
662        $backendUser = $this->getBackendUserAuthentication();
663        $lang = $this->getLanguageService();
664        $buttons = [
665            'csh' => '',
666            'view' => '',
667            'edit' => '',
668            'hide_unhide' => '',
669            'move' => '',
670            'new_record' => '',
671            'paste' => '',
672            'level_up' => '',
673            'cache' => '',
674            'reload' => '',
675            'shortcut' => '',
676            'back' => '',
677            'csv' => '',
678            'export' => ''
679        ];
680        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
681        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
682        // Get users permissions for this page record:
683        $localCalcPerms = $backendUser->calcPerms($this->pageRow);
684        // CSH
685        if ((string)$this->id === '') {
686            $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_noId');
687        } elseif (!$this->id) {
688            $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_root');
689        } else {
690            $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module');
691        }
692        if (isset($this->id)) {
693            // View Exclude doktypes 254,255 Configuration:
694            // mod.web_list.noViewWithDokTypes = 254,255
695            if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) {
696                $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true);
697            } else {
698                //default exclusion: doktype 254 (folder), 255 (recycler)
699                $noViewDokTypes = [
700                    PageRepository::DOKTYPE_SYSFOLDER,
701                    PageRepository::DOKTYPE_RECYCLER
702                ];
703            }
704            if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) {
705                $onClick = htmlspecialchars(BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id)));
706                $buttons['view'] = '<a href="#" onclick="' . $onClick . '" title="'
707                    . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">'
708                    . $this->iconFactory->getIcon('actions-document-view', Icon::SIZE_SMALL)->render() . '</a>';
709            }
710            // New record on pages that are not locked by editlock
711            if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) {
712                $onClick = htmlspecialchars('return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');');
713                $buttons['new_record'] = '<a href="#" onclick="' . $onClick . '" title="'
714                    . htmlspecialchars($lang->getLL('newRecordGeneral')) . '">'
715                    . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</a>';
716            }
717            // If edit permissions are set, see
718            // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
719            if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions() && $this->getBackendUserAuthentication()->checkLanguageAccess(0)) {
720                // Edit
721                $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit';
722                $onClick = htmlspecialchars(BackendUtility::editOnClick($params, '', -1));
723                $buttons['edit'] = '<a href="#" onclick="' . $onClick . '" title="' . htmlspecialchars($lang->getLL('editPage')) . '">'
724                    . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render()
725                    . '</a>';
726            }
727            // Paste
728            if (($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) {
729                $elFromTable = $this->clipObj->elFromTable('');
730                if (!empty($elFromTable)) {
731                    $confirmText = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
732                    $buttons['paste'] = '<a'
733                        . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $this->id)) . '"'
734                        . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
735                        . ' class="t3js-modal-trigger"'
736                        . ' data-severity="warning"'
737                        . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
738                        . ' data-content="' . htmlspecialchars($confirmText) . '"'
739                        . '>'
740                        . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
741                        . '</a>';
742                }
743            }
744            // Cache
745            $buttons['cache'] = '<a href="' . htmlspecialchars($this->listURL() . '&clear_cache=1') . '" title="'
746                . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache')) . '">'
747                . $this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL)->render() . '</a>';
748            if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks'])
749                || (isset($module->modTSconfig['properties']['noExportRecordsLinks'])
750                    && !$module->modTSconfig['properties']['noExportRecordsLinks']))
751            ) {
752                // CSV
753                $buttons['csv'] = '<a href="' . htmlspecialchars($this->listURL() . '&csv=1') . '" title="'
754                    . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv')) . '">'
755                    . $this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL)->render() . '</a>';
756                // Export
757                if (ExtensionManagementUtility::isLoaded('impexp')) {
758                    $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']);
759                    $buttons['export'] = '<a href="' . htmlspecialchars($url . '&tx_impexp[list][]='
760                            . rawurlencode($this->table . ':' . $this->id)) . '" title="'
761                        . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export')) . '">'
762                        . $this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL)->render() . '</a>';
763                }
764            }
765            // Reload
766            $buttons['reload'] = '<a href="' . htmlspecialchars($this->listURL()) . '" title="'
767                . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload')) . '">'
768                . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a>';
769            // Shortcut
770            if ($backendUser->mayMakeShortcut()) {
771                $buttons['shortcut'] = $this->getDocumentTemplate()->makeShortcutIcon(
772                    'id, M, imagemode, pointer, table, search_field, search_levels, showLimit, sortField, sortRev',
773                    implode(',', array_keys($this->MOD_MENU)),
774                    'web_list'
775                );
776            }
777            // Back
778            if ($this->returnUrl) {
779                $href = htmlspecialchars(GeneralUtility::linkThisUrl($this->returnUrl, ['id' => $this->id]));
780                $buttons['back'] = '<a href="' . $href . '" class="typo3-goBack" title="'
781                    . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack')) . '">'
782                    . $this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL)->render() . '</a>';
783            }
784        }
785        return $buttons;
786    }
787
788    /**
789     * Create the panel of buttons for submitting the form or otherwise perform
790     * operations.
791     *
792     * @param ModuleTemplate $moduleTemplate
793     */
794    public function getDocHeaderButtons(ModuleTemplate $moduleTemplate)
795    {
796        $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
797        $module = $this->getModule();
798        $backendUser = $this->getBackendUserAuthentication();
799        $lang = $this->getLanguageService();
800        // Get users permissions for this page record:
801        $localCalcPerms = $backendUser->calcPerms($this->pageRow);
802        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
803        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
804        // CSH
805        if ((string)$this->id === '') {
806            $fieldName = 'list_module_noId';
807        } elseif (!$this->id) {
808            $fieldName = 'list_module_root';
809        } else {
810            $fieldName = 'list_module';
811        }
812        $cshButton = $buttonBar->makeHelpButton()
813            ->setModuleName('xMOD_csh_corebe')
814            ->setFieldName($fieldName);
815        $buttonBar->addButton($cshButton);
816        if (isset($this->id)) {
817            // View Exclude doktypes 254,255 Configuration:
818            // mod.web_list.noViewWithDokTypes = 254,255
819            if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) {
820                $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true);
821            } else {
822                //default exclusion: doktype 254 (folder), 255 (recycler)
823                $noViewDokTypes = [
824                    PageRepository::DOKTYPE_SYSFOLDER,
825                    PageRepository::DOKTYPE_RECYCLER
826                ];
827            }
828            // New record on pages that are not locked by editlock
829            if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) {
830                $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');';
831                $newRecordButton = $buttonBar->makeLinkButton()
832                    ->setHref('#')
833                    ->setOnClick($onClick)
834                    ->setTitle($lang->getLL('newRecordGeneral'))
835                    ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
836                $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
837            }
838            if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) {
839                $onClick = BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id));
840                $viewButton = $buttonBar->makeLinkButton()
841                    ->setHref('#')
842                    ->setOnClick($onClick)
843                    ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
844                    ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL));
845                $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
846            }
847            // If edit permissions are set, see
848            // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
849            if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions() && $backendUser->checkLanguageAccess(0)) {
850                // Edit
851                $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit';
852                $onClick = BackendUtility::editOnClick($params, '', -1);
853                $editButton = $buttonBar->makeLinkButton()
854                    ->setHref('#')
855                    ->setOnClick($onClick)
856                    ->setTitle($lang->getLL('editPage'))
857                    ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
858                $buttonBar->addButton($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
859            }
860            // Paste
861            if ($this->showClipboard && ($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) {
862                $elFromTable = $this->clipObj->elFromTable('');
863                if (!empty($elFromTable)) {
864                    $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
865                    $pasteButton = $buttonBar->makeLinkButton()
866                        ->setHref($this->clipObj->pasteUrl('', $this->id))
867                        ->setTitle($lang->getLL('clip_paste'))
868                        ->setClasses('t3js-modal-trigger')
869                        ->setDataAttributes([
870                            'severity' => 'warning',
871                            'content' => $confirmMessage,
872                            'title' => $lang->getLL('clip_paste')
873                        ])
874                        ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL));
875                    $buttonBar->addButton($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
876                }
877            }
878            // Cache
879            $clearCacheButton = $buttonBar->makeLinkButton()
880                ->setHref($this->listURL() . '&clear_cache=1')
881                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
882                ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
883            $buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT);
884            if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks'])
885                || (isset($module->modTSconfig['properties']['noExportRecordsLinks'])
886                    && !$module->modTSconfig['properties']['noExportRecordsLinks']))
887            ) {
888                // CSV
889                $csvButton = $buttonBar->makeLinkButton()
890                    ->setHref($this->listURL() . '&csv=1')
891                    ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv'))
892                    ->setIcon($this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL))
893                    ->setShowLabelText(true);
894                $buttonBar->addButton($csvButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
895                // Export
896                if (ExtensionManagementUtility::isLoaded('impexp')) {
897                    $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']);
898                    $exportButton = $buttonBar->makeLinkButton()
899                        ->setHref($url . '&tx_impexp[list][]=' . rawurlencode($this->table . ':' . $this->id))
900                        ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export'))
901                        ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL))
902                        ->setShowLabelText(true);
903                    $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
904                }
905            }
906            // Reload
907            $reloadButton = $buttonBar->makeLinkButton()
908                ->setHref($this->listURL())
909                ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
910                ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
911            $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
912            // Shortcut
913            if ($backendUser->mayMakeShortcut()) {
914                $shortCutButton = $buttonBar->makeShortcutButton()
915                    ->setModuleName('web_list')
916                    ->setGetVariables([
917                        'id',
918                        'route',
919                        'imagemode',
920                        'pointer',
921                        'table',
922                        'search_field',
923                        'search_levels',
924                        'showLimit',
925                        'sortField',
926                        'sortRev'
927                    ])
928                    ->setSetVariables(array_keys($this->MOD_MENU));
929                $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
930            }
931            // Back
932            if ($this->returnUrl) {
933                $backButton = $buttonBar->makeLinkButton()
934                    ->setHref($this->returnUrl)
935                    ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
936                    ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
937                $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT);
938            }
939        }
940    }
941
942    /**
943     * Creates the listing of records from a single table
944     *
945     * @param string $table Table name
946     * @param int $id Page id
947     * @param string $rowList List of fields to show in the listing. Pseudo fields will be added including the record header.
948     * @throws \UnexpectedValueException
949     * @return string HTML table with the listing for the record.
950     */
951    public function getTable($table, $id, $rowList = '')
952    {
953        $rowListArray = GeneralUtility::trimExplode(',', $rowList, true);
954        // if no columns have been specified, show description (if configured)
955        if (!empty($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']) && empty($rowListArray)) {
956            $rowListArray[] = $GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'];
957        }
958        $backendUser = $this->getBackendUserAuthentication();
959        $lang = $this->getLanguageService();
960        // Init
961        $addWhere = '';
962        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
963        $titleCol = $GLOBALS['TCA'][$table]['ctrl']['label'];
964        $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
965        $l10nEnabled = $GLOBALS['TCA'][$table]['ctrl']['languageField']
966                     && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
967        $tableCollapsed = (bool)$this->tablesCollapsed[$table];
968        // prepare space icon
969        $this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
970        // Cleaning rowlist for duplicates and place the $titleCol as the first column always!
971        $this->fieldArray = [];
972        // title Column
973        // Add title column
974        $this->fieldArray[] = $titleCol;
975        // Control-Panel
976        if (!GeneralUtility::inList($rowList, '_CONTROL_')) {
977            $this->fieldArray[] = '_CONTROL_';
978        }
979        // Clipboard
980        if ($this->showClipboard) {
981            $this->fieldArray[] = '_CLIPBOARD_';
982        }
983        // Ref
984        if (!$this->dontShowClipControlPanels) {
985            $this->fieldArray[] = '_REF_';
986        }
987        // Path
988        if ($this->searchLevels) {
989            $this->fieldArray[] = '_PATH_';
990        }
991        // Localization
992        if ($l10nEnabled) {
993            $this->fieldArray[] = '_LOCALIZATION_';
994            // Do not show the "Localize to:" field when only translated records should be shown
995            if (!$this->showOnlyTranslatedRecords) {
996                $this->fieldArray[] = '_LOCALIZATION_b';
997            }
998            // Only restrict to the default language if no search request is in place
999            // And if only translations should be shown
1000            if ($this->searchString === '' && !$this->showOnlyTranslatedRecords) {
1001                $addWhere = (string)$queryBuilder->expr()->orX(
1002                    $queryBuilder->expr()->lte($GLOBALS['TCA'][$table]['ctrl']['languageField'], 0),
1003                    $queryBuilder->expr()->eq($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 0)
1004                );
1005            }
1006        }
1007        // Cleaning up:
1008        $this->fieldArray = array_unique(array_merge($this->fieldArray, $rowListArray));
1009        if ($this->noControlPanels) {
1010            $tempArray = array_flip($this->fieldArray);
1011            unset($tempArray['_CONTROL_']);
1012            unset($tempArray['_CLIPBOARD_']);
1013            $this->fieldArray = array_keys($tempArray);
1014        }
1015        // Creating the list of fields to include in the SQL query:
1016        $selectFields = $this->fieldArray;
1017        $selectFields[] = 'uid';
1018        $selectFields[] = 'pid';
1019        // adding column for thumbnails
1020        if ($thumbsCol) {
1021            $selectFields[] = $thumbsCol;
1022        }
1023        if ($table === 'pages') {
1024            $selectFields[] = 'module';
1025            $selectFields[] = 'extendToSubpages';
1026            $selectFields[] = 'nav_hide';
1027            $selectFields[] = 'doktype';
1028            $selectFields[] = 'shortcut';
1029            $selectFields[] = 'shortcut_mode';
1030            $selectFields[] = 'mount_pid';
1031        }
1032        if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
1033            $selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']);
1034        }
1035        foreach (['type', 'typeicon_column', 'editlock'] as $field) {
1036            if ($GLOBALS['TCA'][$table]['ctrl'][$field]) {
1037                $selectFields[] = $GLOBALS['TCA'][$table]['ctrl'][$field];
1038            }
1039        }
1040        if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1041            $selectFields[] = 't3ver_id';
1042            $selectFields[] = 't3ver_state';
1043            $selectFields[] = 't3ver_wsid';
1044        }
1045        if ($l10nEnabled) {
1046            $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1047            $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1048        }
1049        if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1050            $selectFields = array_merge(
1051                $selectFields,
1052                GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true)
1053            );
1054        }
1055        // Unique list!
1056        $selectFields = array_unique($selectFields);
1057        $fieldListFields = $this->makeFieldList($table, 1);
1058        if (empty($fieldListFields) && $GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) {
1059            $message = sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessage'), $table, $table);
1060            $messageTitle = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessageTitle');
1061            /** @var FlashMessage $flashMessage */
1062            $flashMessage = GeneralUtility::makeInstance(
1063                FlashMessage::class,
1064                $message,
1065                $messageTitle,
1066                FlashMessage::WARNING,
1067                true
1068            );
1069            /** @var FlashMessageService $flashMessageService */
1070            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1071            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
1072            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1073            $defaultFlashMessageQueue->enqueue($flashMessage);
1074        }
1075        // Making sure that the fields in the field-list ARE in the field-list from TCA!
1076        $selectFields = array_intersect($selectFields, $fieldListFields);
1077        // Implode it into a list of fields for the SQL-statement.
1078        $selFieldList = implode(',', $selectFields);
1079        $this->selFieldList = $selFieldList;
1080        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable'] ?? [] as $className) {
1081            $hookObject = GeneralUtility::makeInstance($className);
1082            if (!$hookObject instanceof RecordListGetTableHookInterface) {
1083                throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListGetTableHookInterface::class, 1195114460);
1084            }
1085            $hookObject->getDBlistQuery($table, $id, $addWhere, $selFieldList, $this);
1086        }
1087
1088        if ($table == 'pages' && $this->showOnlyTranslatedRecords) {
1089            $addWhere .= ' AND ' . $GLOBALS['TCA']['pages']['ctrl']['languageField'] . ' IN(' . implode(',', array_keys($this->languagesAllowedForUser)) . ')';
1090        }
1091
1092        $additionalConstraints = empty($addWhere) ? [] : [QueryHelper::stripLogicalOperatorPrefix($addWhere)];
1093        $selFieldList = GeneralUtility::trimExplode(',', $selFieldList, true);
1094
1095        // Create the SQL query for selecting the elements in the listing:
1096        // do not do paging when outputting as CSV
1097        if ($this->csvOutput) {
1098            $this->iLimit = 0;
1099        }
1100        if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
1101            // Get the two previous rows for sorting if displaying page > 1
1102            $this->firstElementNumber -= 2;
1103            $this->iLimit += 2;
1104            // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1105            $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints);
1106            $this->firstElementNumber += 2;
1107            $this->iLimit -= 2;
1108        } else {
1109            // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1110            $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints);
1111        }
1112
1113        // Finding the total amount of records on the page
1114        // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1115        $this->setTotalItems($table, $id, $additionalConstraints);
1116
1117        // Init:
1118        $queryResult = $queryBuilder->execute();
1119        $dbCount = 0;
1120        $out = '';
1121        $tableHeader = '';
1122        $listOnlyInSingleTableMode = $this->listOnlyInSingleTableMode && !$this->table;
1123        // If the count query returned any number of records, we perform the real query,
1124        // selecting records.
1125        if ($this->totalItems) {
1126            // Fetch records only if not in single table mode
1127            if ($listOnlyInSingleTableMode) {
1128                $dbCount = $this->totalItems;
1129            } else {
1130                // Set the showLimit to the number of records when outputting as CSV
1131                if ($this->csvOutput) {
1132                    $this->showLimit = $this->totalItems;
1133                    $this->iLimit = $this->totalItems;
1134                    $dbCount = $this->totalItems;
1135                } else {
1136                    if ($this->firstElementNumber + $this->showLimit <= $this->totalItems) {
1137                        $dbCount = $this->showLimit + 2;
1138                    } else {
1139                        $dbCount = $this->totalItems - $this->firstElementNumber + 2;
1140                    }
1141                }
1142            }
1143        }
1144        // If any records was selected, render the list:
1145        if ($dbCount) {
1146            $tableIdentifier = $table;
1147            // Use a custom table title for translated pages
1148            if ($table == 'pages' && $this->showOnlyTranslatedRecords) {
1149                // pages records in list module are split into two own sections, one for pages with
1150                // sys_language_uid = 0 "Page" and an own section for sys_language_uid > 0 "Page Translation".
1151                // This if sets the different title for the page translation case and a unique table identifier
1152                // which is used in DOM as id.
1153                $tableTitle = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:pageTranslation'));
1154                $tableIdentifier = 'pages_translated';
1155            } else {
1156                $tableTitle = htmlspecialchars($lang->sL($GLOBALS['TCA'][$table]['ctrl']['title']));
1157                if ($tableTitle === '') {
1158                    $tableTitle = $table;
1159                }
1160            }
1161            // Header line is drawn
1162            $theData = [];
1163            if ($this->disableSingleTableView) {
1164                $theData[$titleCol] = '<span class="c-table">' . BackendUtility::wrapInHelp($table, '', $tableTitle)
1165                    . '</span> (<span class="t3js-table-total-items">' . $this->totalItems . '</span>)';
1166            } else {
1167                $icon = $this->table
1168                    ? '<span title="' . htmlspecialchars($lang->getLL('contractView')) . '">' . $this->iconFactory->getIcon('actions-view-table-collapse', Icon::SIZE_SMALL)->render() . '</span>'
1169                    : '<span title="' . htmlspecialchars($lang->getLL('expandView')) . '">' . $this->iconFactory->getIcon('actions-view-table-expand', Icon::SIZE_SMALL)->render() . '</span>';
1170                $theData[$titleCol] = $this->linkWrapTable($table, $tableTitle . ' (<span class="t3js-table-total-items">' . $this->totalItems . '</span>) ' . $icon);
1171            }
1172            if ($listOnlyInSingleTableMode) {
1173                $tableHeader .= BackendUtility::wrapInHelp($table, '', $theData[$titleCol]);
1174            } else {
1175                // Render collapse button if in multi table mode
1176                $collapseIcon = '';
1177                if (!$this->table) {
1178                    $href = htmlspecialchars($this->listURL() . '&collapse[' . $table . ']=' . ($tableCollapsed ? '0' : '1'));
1179                    $title = $tableCollapsed
1180                        ? htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.expandTable'))
1181                        : htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.collapseTable'));
1182                    $icon = '<span class="collapseIcon">' . $this->iconFactory->getIcon(($tableCollapsed ? 'actions-view-list-expand' : 'actions-view-list-collapse'), Icon::SIZE_SMALL)->render() . '</span>';
1183                    $collapseIcon = '<a href="' . $href . '" title="' . $title . '" class="pull-right t3js-toggle-recordlist" data-table="' . htmlspecialchars($tableIdentifier) . '" data-toggle="collapse" data-target="#recordlist-' . htmlspecialchars($tableIdentifier) . '">' . $icon . '</a>';
1184                }
1185                $tableHeader .= $theData[$titleCol] . $collapseIcon;
1186            }
1187            // Render table rows only if in multi table view or if in single table view
1188            $rowOutput = '';
1189            if (!$listOnlyInSingleTableMode || $this->table) {
1190                // Fixing an order table for sortby tables
1191                $this->currentTable = [];
1192                $currentIdList = [];
1193                $doSort = $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField;
1194                $prevUid = 0;
1195                $prevPrevUid = 0;
1196                // Get first two rows and initialize prevPrevUid and prevUid if on page > 1
1197                if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
1198                    $row = $queryResult->fetch();
1199                    $prevPrevUid = -((int)$row['uid']);
1200                    $row = $queryResult->fetch();
1201                    $prevUid = $row['uid'];
1202                }
1203                $accRows = [];
1204                // Accumulate rows here
1205                while ($row = $queryResult->fetch()) {
1206                    if (!$this->isRowListingConditionFulfilled($table, $row)) {
1207                        continue;
1208                    }
1209                    // In offline workspace, look for alternative record:
1210                    BackendUtility::workspaceOL($table, $row, $backendUser->workspace, true);
1211                    if (is_array($row)) {
1212                        $accRows[] = $row;
1213                        $currentIdList[] = $row['uid'];
1214                        if ($doSort) {
1215                            if ($prevUid) {
1216                                $this->currentTable['prev'][$row['uid']] = $prevPrevUid;
1217                                $this->currentTable['next'][$prevUid] = '-' . $row['uid'];
1218                                $this->currentTable['prevUid'][$row['uid']] = $prevUid;
1219                            }
1220                            $prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid'];
1221                            $prevUid = $row['uid'];
1222                        }
1223                    }
1224                }
1225                $this->totalRowCount = count($accRows);
1226                // CSV initiated
1227                if ($this->csvOutput) {
1228                    $this->initCSV();
1229                }
1230                // Render items:
1231                $this->CBnames = [];
1232                $this->duplicateStack = [];
1233                $this->eCounter = $this->firstElementNumber;
1234                $cc = 0;
1235                foreach ($accRows as $row) {
1236                    // Render item row if counter < limit
1237                    if ($cc < $this->iLimit) {
1238                        $cc++;
1239                        $this->translations = false;
1240                        $rowOutput .= $this->renderListRow($table, $row, $cc, $titleCol, $thumbsCol);
1241                        // If no search happened it means that the selected
1242                        // records are either default or All language and here we will not select translations
1243                        // which point to the main record:
1244                        if ($l10nEnabled && $this->searchString === '' && !($this->hideTranslations === '*' || GeneralUtility::inList($this->hideTranslations, $table))) {
1245                            // For each available translation, render the record:
1246                            if (is_array($this->translations)) {
1247                                foreach ($this->translations as $lRow) {
1248                                    // $lRow isn't always what we want - if record was moved we've to work with the
1249                                    // placeholder records otherwise the list is messed up a bit
1250                                    if ($row['_MOVE_PLH_uid'] && $row['_MOVE_PLH_pid']) {
1251                                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1252                                            ->getQueryBuilderForTable($table);
1253                                        $queryBuilder->getRestrictions()
1254                                            ->removeAll()
1255                                            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1256                                        $predicates = [
1257                                            $queryBuilder->expr()->eq(
1258                                                't3ver_move_id',
1259                                                $queryBuilder->createNamedParameter((int)$lRow['uid'], \PDO::PARAM_INT)
1260                                            ),
1261                                            $queryBuilder->expr()->eq(
1262                                                'pid',
1263                                                $queryBuilder->createNamedParameter((int)$row['_MOVE_PLH_pid'], \PDO::PARAM_INT)
1264                                            ),
1265                                            $queryBuilder->expr()->eq(
1266                                                't3ver_wsid',
1267                                                $queryBuilder->createNamedParameter((int)$row['t3ver_wsid'], \PDO::PARAM_INT)
1268                                            ),
1269                                        ];
1270
1271                                        $tmpRow = $queryBuilder
1272                                            ->select(...$selFieldList)
1273                                            ->from($table)
1274                                            ->andWhere(...$predicates)
1275                                            ->execute()
1276                                            ->fetch();
1277
1278                                        $lRow = is_array($tmpRow) ? $tmpRow : $lRow;
1279                                    }
1280                                    if (!$this->isRowListingConditionFulfilled($table, $lRow)) {
1281                                        continue;
1282                                    }
1283                                    // In offline workspace, look for alternative record:
1284                                    BackendUtility::workspaceOL($table, $lRow, $backendUser->workspace, true);
1285                                    if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
1286                                        $currentIdList[] = $lRow['uid'];
1287                                        $rowOutput .= $this->renderListRow($table, $lRow, $cc, $titleCol, $thumbsCol, 18);
1288                                    }
1289                                }
1290                            }
1291                        }
1292                    }
1293                    // Counter of total rows incremented:
1294                    $this->eCounter++;
1295                }
1296                // Record navigation is added to the beginning and end of the table if in single
1297                // table mode
1298                if ($this->table) {
1299                    $rowOutput = $this->renderListNavigation('top') . $rowOutput . $this->renderListNavigation('bottom');
1300                } else {
1301                    // Show that there are more records than shown
1302                    if ($this->totalItems > $this->itemsLimitPerTable) {
1303                        $countOnFirstPage = $this->totalItems > $this->itemsLimitSingleTable ? $this->itemsLimitSingleTable : $this->totalItems;
1304                        $hasMore = $this->totalItems > $this->itemsLimitSingleTable;
1305                        $colspan = $this->showIcon ? count($this->fieldArray) + 1 : count($this->fieldArray);
1306                        $rowOutput .= '<tr><td colspan="' . $colspan . '">
1307								<a href="' . htmlspecialchars($this->listURL() . '&table=' . rawurlencode($tableIdentifier)) . '" class="btn btn-default">'
1308                            . '<span class="t3-icon fa fa-chevron-down"></span> <i>[1 - ' . $countOnFirstPage . ($hasMore ? '+' : '') . ']</i></a>
1309								</td></tr>';
1310                    }
1311                }
1312                // The header row for the table is now created:
1313                $out .= $this->renderListHeader($table, $currentIdList);
1314            }
1315
1316            $collapseClass = $tableCollapsed && !$this->table ? 'collapse' : 'collapse in';
1317            $dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded';
1318
1319            // The list of records is added after the header:
1320            $out .= $rowOutput;
1321            // ... and it is all wrapped in a table:
1322            $out = '
1323
1324
1325
1326			<!--
1327				DB listing of elements:	"' . htmlspecialchars($tableIdentifier) . '"
1328			-->
1329				<div class="panel panel-space panel-default recordlist">
1330					<div class="panel-heading">
1331					' . $tableHeader . '
1332					</div>
1333					<div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($tableIdentifier) . '">
1334						<div class="table-fit">
1335							<table data-table="' . htmlspecialchars($tableIdentifier) . '" class="table table-striped table-hover' . ($listOnlyInSingleTableMode ? ' typo3-dblist-overview' : '') . '">
1336								' . $out . '
1337							</table>
1338						</div>
1339					</div>
1340				</div>
1341			';
1342            // Output csv if...
1343            // This ends the page with exit.
1344            if ($this->csvOutput) {
1345                $this->outputCSV($table);
1346            }
1347        }
1348        // Return content:
1349        return $out;
1350    }
1351
1352    /**
1353     * Get viewOnClick link for pages or tt_content records
1354     *
1355     * @param string $table
1356     * @param array $row
1357     *
1358     * @return string
1359     */
1360    protected function getOnClickForRow(string $table, array $row): string
1361    {
1362        if ($table === 'tt_content') {
1363            // Link to a content element, possibly translated and with anchor
1364            $additionalParams = '';
1365            $language = (int)$row[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']];
1366            if ($language > 0) {
1367                $additionalParams = '&L=' . $language;
1368            }
1369            $onClick = BackendUtility::viewOnClick(
1370                $this->id,
1371                '',
1372                null,
1373                '#c' . $row['uid'],
1374                '',
1375                $additionalParams
1376            );
1377        } elseif ($table === 'pages' && $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] > 0) {
1378            // Link to a page translation needs uid of default language page as id
1379            $onClick = BackendUtility::viewOnClick(
1380                $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']],
1381                '',
1382                null,
1383                '',
1384                '',
1385                '&L=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]
1386            );
1387        } else {
1388            // Link to a page in the default language
1389            $onClick = BackendUtility::viewOnClick($row['uid']);
1390        }
1391        return $onClick;
1392    }
1393
1394    /**
1395     * Check if all row listing conditions are fulfilled.
1396     *
1397     * This function serves as a dummy method to be overridden in extending classes.
1398     *
1399     * @param string $table Table name
1400     * @param string[] $row Record
1401     * @return bool True, if all conditions are fulfilled.
1402     */
1403    protected function isRowListingConditionFulfilled($table, $row)
1404    {
1405        return true;
1406    }
1407
1408    /**
1409     * Rendering a single row for the list
1410     *
1411     * @param string $table Table name
1412     * @param mixed[] $row Current record
1413     * @param int $cc Counter, counting for each time an element is rendered (used for alternating colors)
1414     * @param string $titleCol Table field (column) where header value is found
1415     * @param string $thumbsCol Table field (column) where (possible) thumbnails can be found
1416     * @param int $indent Indent from left.
1417     * @return string Table row for the element
1418     * @internal
1419     * @see getTable()
1420     */
1421    public function renderListRow($table, $row, $cc, $titleCol, $thumbsCol, $indent = 0)
1422    {
1423        if (!is_array($row)) {
1424            return '';
1425        }
1426        $rowOutput = '';
1427        $id_orig = null;
1428        // If in search mode, make sure the preview will show the correct page
1429        if ((string)$this->searchString !== '') {
1430            $id_orig = $this->id;
1431            $this->id = $row['pid'];
1432        }
1433
1434        $tagAttributes = [
1435            'class' => [],
1436            'data-table' => $table,
1437            'title' => 'id=' . $row['uid'],
1438        ];
1439
1440        // Add active class to record of current link
1441        if (
1442            isset($this->currentLink['tableNames'])
1443            && (int)$this->currentLink['uid'] === (int)$row['uid']
1444            && GeneralUtility::inList($this->currentLink['tableNames'], $table)
1445        ) {
1446            $tagAttributes['class'][] = 'active';
1447        }
1448        // Add special classes for first and last row
1449        if ($cc == 1 && $indent == 0) {
1450            $tagAttributes['class'][] = 'firstcol';
1451        }
1452        if ($cc == $this->totalRowCount || $cc == $this->iLimit) {
1453            $tagAttributes['class'][] = 'lastcol';
1454        }
1455        // Overriding with versions background color if any:
1456        if (!empty($row['_CSSCLASS'])) {
1457            $tagAttributes['class'] = [$row['_CSSCLASS']];
1458        }
1459
1460        $tagAttributes['class'][] = 't3js-entity';
1461
1462        // Incr. counter.
1463        $this->counter++;
1464        // The icon with link
1465        $toolTip = BackendUtility::getRecordToolTip($row, $table);
1466        $additionalStyle = $indent ? ' style="margin-left: ' . $indent . 'px;"' : '';
1467        $iconImg = '<span ' . $toolTip . ' ' . $additionalStyle . '>'
1468            . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render()
1469            . '</span>';
1470        $theIcon = $this->clickMenuEnabled ? BackendUtility::wrapClickMenuOnIcon($iconImg, $table, $row['uid']) : $iconImg;
1471        // Preparing and getting the data-array
1472        $theData = [];
1473        $localizationMarkerClass = '';
1474        foreach ($this->fieldArray as $fCol) {
1475            if ($fCol == $titleCol) {
1476                $recTitle = BackendUtility::getRecordTitle($table, $row, false, true);
1477                $warning = '';
1478                // If the record is edit-locked	by another user, we will show a little warning sign:
1479                $lockInfo = BackendUtility::isRecordLocked($table, $row['uid']);
1480                if ($lockInfo) {
1481                    $warning = '<span data-toggle="tooltip" data-placement="right" data-title="' . htmlspecialchars($lockInfo['msg']) . '">'
1482                        . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>';
1483                }
1484                $theData[$fCol] = $theData['__label'] = $warning . $this->linkWrapItems($table, $row['uid'], $recTitle, $row);
1485                // Render thumbnails, if:
1486                // - a thumbnail column exists
1487                // - there is content in it
1488                // - the thumbnail column is visible for the current type
1489                $type = 0;
1490                if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
1491                    $typeColumn = $GLOBALS['TCA'][$table]['ctrl']['type'];
1492                    $type = $row[$typeColumn];
1493                }
1494                // If current type doesn't exist, set it to 0 (or to 1 for historical reasons,
1495                // if 0 doesn't exist)
1496                if (!isset($GLOBALS['TCA'][$table]['types'][$type])) {
1497                    $type = isset($GLOBALS['TCA'][$table]['types'][0]) ? 0 : 1;
1498                }
1499
1500                $visibleColumns = $this->getVisibleColumns($GLOBALS['TCA'][$table], $type);
1501
1502                if ($this->thumbs &&
1503                    trim($row[$thumbsCol]) &&
1504                    preg_match('/(^|(.*(;|,)?))' . $thumbsCol . '(((;|,).*)|$)/', $visibleColumns) === 1
1505                ) {
1506                    $thumbCode = '<br />' . $this->thumbCode($row, $table, $thumbsCol);
1507                    $theData[$fCol] .= $thumbCode;
1508                    $theData['__label'] .= $thumbCode;
1509                }
1510                if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])
1511                    && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] != 0
1512                    && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0
1513                ) {
1514                    // It's a translated record with a language parent
1515                    $localizationMarkerClass = ' localization';
1516                }
1517            } elseif ($fCol === 'pid') {
1518                $theData[$fCol] = $row[$fCol];
1519            } elseif ($fCol === '_PATH_') {
1520                $theData[$fCol] = $this->recPath($row['pid']);
1521            } elseif ($fCol === '_REF_') {
1522                $theData[$fCol] = $this->createReferenceHtml($table, $row['uid']);
1523            } elseif ($fCol === '_CONTROL_') {
1524                $theData[$fCol] = $this->makeControl($table, $row);
1525            } elseif ($fCol === '_CLIPBOARD_') {
1526                $theData[$fCol] = $this->makeClip($table, $row);
1527            } elseif ($fCol === '_LOCALIZATION_') {
1528                [$lC1, $lC2] = $this->makeLocalizationPanel($table, $row);
1529                $theData[$fCol] = $lC1;
1530                $theData[$fCol . 'b'] = '<div class="btn-group">' . $lC2 . '</div>';
1531            } elseif ($fCol === '_LOCALIZATION_b') {
1532                // deliberately empty
1533            } else {
1534                $pageId = $table === 'pages' ? $row['uid'] : $row['pid'];
1535                $tmpProc = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 100, $row['uid'], true, $pageId);
1536                $theData[$fCol] = $this->linkUrlMail(htmlspecialchars($tmpProc), $row[$fCol]);
1537                if ($this->csvOutput) {
1538                    $row[$fCol] = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 0, $row['uid']);
1539                }
1540            }
1541        }
1542        // Reset the ID if it was overwritten
1543        if ((string)$this->searchString !== '') {
1544            $this->id = $id_orig;
1545        }
1546        // Add row to CSV list:
1547        if ($this->csvOutput) {
1548            $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['customizeCsvRow'] ?? [];
1549            if (!empty($hooks)) {
1550                $hookParameters = [
1551                    'databaseRow' => &$row,
1552                    'tableName' => $table,
1553                    'pageId' => $this->id
1554                ];
1555                foreach ($hooks as $hookFunction) {
1556                    GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
1557                }
1558            }
1559            $this->addToCSV($row);
1560        }
1561        // Add classes to table cells
1562        $this->addElement_tdCssClass[$titleCol] = 'col-title col-responsive' . $localizationMarkerClass;
1563        $this->addElement_tdCssClass['__label'] = $this->addElement_tdCssClass[$titleCol];
1564        $this->addElement_tdCssClass['_CONTROL_'] = 'col-control';
1565        if ($this->getModule()->MOD_SETTINGS['clipBoard']) {
1566            $this->addElement_tdCssClass['_CLIPBOARD_'] = 'col-clipboard';
1567        }
1568        $this->addElement_tdCssClass['_PATH_'] = 'col-path';
1569        $this->addElement_tdCssClass['_LOCALIZATION_'] = 'col-localizationa';
1570        $this->addElement_tdCssClass['_LOCALIZATION_b'] = 'col-localizationb';
1571        // Create element in table cells:
1572        $theData['uid'] = $row['uid'];
1573        if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])
1574            && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])
1575        ) {
1576            $theData['_l10nparent_'] = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1577        }
1578
1579        $tagAttributes = array_map(
1580            function ($attributeValue) {
1581                if (is_array($attributeValue)) {
1582                    return implode(' ', $attributeValue);
1583                }
1584                return $attributeValue;
1585            },
1586            $tagAttributes
1587        );
1588
1589        $rowOutput .= $this->addElement(1, $theIcon, $theData, GeneralUtility::implodeAttributes($tagAttributes, true));
1590        // Finally, return table row element:
1591        return $rowOutput;
1592    }
1593
1594    /**
1595     * Gets the number of records referencing the record with the UID $uid in
1596     * the table $tableName.
1597     *
1598     * @param string $tableName
1599     * @param int $uid
1600     * @return int The number of references to record $uid in table
1601     */
1602    protected function getReferenceCount($tableName, $uid)
1603    {
1604        if (!isset($this->referenceCount[$tableName][$uid])) {
1605            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1606            $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid);
1607            $this->referenceCount[$tableName][$uid] = $numberOfReferences;
1608        }
1609        return $this->referenceCount[$tableName][$uid];
1610    }
1611
1612    /**
1613     * Rendering the header row for a table
1614     *
1615     * @param string $table Table name
1616     * @param int[] $currentIdList Array of the currently displayed uids of the table
1617     * @throws \UnexpectedValueException
1618     * @return string Header table row
1619     * @internal
1620     * @see getTable()
1621     */
1622    public function renderListHeader($table, $currentIdList)
1623    {
1624        $tsConfig = BackendUtility::getPagesTSconfig($this->id);
1625        $tsConfigOfTable = is_array($tsConfig['TCEFORM.'][$table . '.']) ? $tsConfig['TCEFORM.'][$table . '.'] : null;
1626
1627        $lang = $this->getLanguageService();
1628        // Init:
1629        $theData = [];
1630        $icon = '';
1631        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1632        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1633        // Traverse the fields:
1634        foreach ($this->fieldArray as $fCol) {
1635            // Calculate users permissions to edit records in the table:
1636            $permsEdit = $this->calcPerms & ($table === 'pages' ? 2 : 16) && $this->overlayEditLockPermissions($table);
1637            switch ((string)$fCol) {
1638                case '_PATH_':
1639                    // Path
1640                    $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_')) . ']</i>';
1641                    break;
1642                case '_REF_':
1643                    // References
1644                    $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c__REF_')) . ']</i>';
1645                    break;
1646                case '_LOCALIZATION_':
1647                    // Path
1648                    $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._LOCALIZATION_')) . ']</i>';
1649                    break;
1650                case '_LOCALIZATION_b':
1651                    // Path
1652                    $theData[$fCol] = htmlspecialchars($lang->getLL('Localize'));
1653                    break;
1654                case '_CLIPBOARD_':
1655                    if (!$this->getModule()->MOD_SETTINGS['clipBoard']) {
1656                        break;
1657                    }
1658                    // Clipboard:
1659                    $cells = [];
1660                    // If there are elements on the clipboard for this table, and the parent page is not locked by editlock
1661                    // then display the "paste into" icon:
1662                    $elFromTable = $this->clipObj->elFromTable($table);
1663                    if (!empty($elFromTable) && $this->overlayEditLockPermissions($table)) {
1664                        $href = htmlspecialchars($this->clipObj->pasteUrl($table, $this->id));
1665                        $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
1666                        $cells['pasteAfter'] = '<a class="btn btn-default t3js-modal-trigger"'
1667                            . ' href="' . $href . '"'
1668                            . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
1669                            . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
1670                            . ' data-content="' . htmlspecialchars($confirmMessage) . '"'
1671                            . ' data-severity="warning">'
1672                            . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
1673                            . '</a>';
1674                    }
1675                    // If the numeric clipboard pads are enabled, display the control icons for that:
1676                    if ($this->clipObj->current !== 'normal') {
1677                        // The "select" link:
1678                        $spriteIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render();
1679                        $cells['copyMarked'] = $this->linkClipboardHeaderIcon($spriteIcon, $table, 'setCB', '', $lang->getLL('clip_selectMarked'));
1680                        // The "edit marked" link:
1681                        $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1682                            . '&edit[' . $table . '][{entityIdentifiers:editList}]=edit'
1683                            . '&returnUrl={T3_THIS_LOCATION}';
1684                        $cells['edit'] = '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1685                            . ' data-uri="' . htmlspecialchars($editUri) . '"'
1686                            . ' title="' . htmlspecialchars($lang->getLL('clip_editMarked')) . '">'
1687                            . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1688                        // The "Delete marked" link:
1689                        $cells['delete'] = $this->linkClipboardHeaderIcon(
1690                            $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(),
1691                            $table,
1692                            'delete',
1693                            sprintf($lang->getLL('clip_deleteMarkedWarning'), $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
1694                            $lang->getLL('clip_deleteMarked')
1695                        );
1696                        // The "Select all" link:
1697                        $onClick = htmlspecialchars('checkOffCB(' . GeneralUtility::quoteJSvalue(implode(',', $this->CBnames)) . ', this); return false;');
1698                        $cells['markAll'] = '<a class="btn btn-default" rel="" href="#" onclick="' . $onClick . '" title="'
1699                            . htmlspecialchars($lang->getLL('clip_markRecords')) . '">'
1700                            . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
1701                    } else {
1702                        $cells['empty'] = '';
1703                    }
1704                    /*
1705                     * hook:  renderListHeaderActions: Allows to change the clipboard icons of the Web>List table headers
1706                     * usage: Above each listed table in Web>List a header row is shown.
1707                     *        This hook allows to modify the icons responsible for the clipboard functions
1708                     *        (shown above the clipboard checkboxes when a clipboard other than "Normal" is selected),
1709                     *        or other "Action" functions which perform operations on the listed records.
1710                     */
1711                    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
1712                        $hookObject = GeneralUtility::makeInstance($className);
1713                        if (!$hookObject instanceof RecordListHookInterface) {
1714                            throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567850);
1715                        }
1716                        $cells = $hookObject->renderListHeaderActions($table, $currentIdList, $cells, $this);
1717                    }
1718                    $theData[$fCol] = '';
1719                    if (isset($cells['edit']) && isset($cells['delete'])) {
1720                        $theData[$fCol] .= '<div class="btn-group" role="group">' . $cells['edit'] . $cells['delete'] . '</div>';
1721                        unset($cells['edit'], $cells['delete']);
1722                    }
1723                    $theData[$fCol] .= '<div class="btn-group" role="group">' . implode('', $cells) . '</div>';
1724                    break;
1725                case '_CONTROL_':
1726                    // Control panel:
1727                    if ($this->isEditable($table)) {
1728                        // If new records can be created on this page, add links:
1729                        $permsAdditional = ($table === 'pages' ? 8 : 16);
1730                        if ($this->calcPerms & $permsAdditional && $this->showNewRecLink($table)) {
1731                            $spriteIcon = $table === 'pages'
1732                                ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL)
1733                                : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL);
1734                            if ($table === 'tt_content') {
1735                                // If mod.newContentElementWizard.override is set, use that extension's create new content wizard instead:
1736                                $newContentElementWizard = BackendUtility::getPagesTSconfig($this->pageinfo['uid'])['mod.']['newContentElementWizard.']['override']
1737                                    ?? 'new_content_element_wizard';
1738                                $url = (string)$uriBuilder->buildUriFromRoute(
1739                                    $newContentElementWizard,
1740                                    [
1741                                        'id' => $this->id,
1742                                        'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
1743                                    ]
1744                                );
1745                                $icon = '<a href="' . htmlspecialchars($url) . '" '
1746                                    . 'data-title="' . htmlspecialchars($lang->getLL('new')) . '"'
1747                                    . 'class="btn btn-default t3js-toggle-new-content-element-wizard disabled"">'
1748                                    . $spriteIcon->render()
1749                                    . '</a>';
1750                            } elseif ($table === 'pages') {
1751                                $parameters = ['id' => $this->id, 'pagesOnly' => 1, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')];
1752                                $href = (string)$uriBuilder->buildUriFromRoute('db_new', $parameters);
1753                                $icon = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="' . htmlspecialchars($lang->getLL('new')) . '">'
1754                                    . $spriteIcon->render() . '</a>';
1755                            } else {
1756                                $params = '&edit[' . $table . '][' . $this->id . ']=new';
1757                                if ($table === 'pages') {
1758                                    $params .= '&overrideVals[pages][doktype]=' . (int)$this->pageRow['doktype'];
1759                                }
1760                                $icon = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
1761                                    . '" title="' . htmlspecialchars($lang->getLL('new')) . '">' . $spriteIcon->render() . '</a>';
1762                            }
1763                        }
1764                        // If the table can be edited, add link for editing ALL SHOWN fields for all listed records:
1765                        if ($permsEdit && $this->table && is_array($currentIdList)) {
1766                            $entityIdentifiers = 'entityIdentifiers';
1767                            if ($this->clipNumPane()) {
1768                                $entityIdentifiers .= ':editList';
1769                            }
1770                            $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1771                                . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit'
1772                                . '&columnsOnly=' . implode(',', $this->fieldArray)
1773                                . '&returnUrl={T3_THIS_LOCATION}';
1774                            $icon .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1775                                . ' data-uri="' . htmlspecialchars($editUri) . '"'
1776                                . ' title="' . htmlspecialchars($lang->getLL('editShownColumns')) . '">'
1777                                . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1778                            $icon = '<div class="btn-group" role="group">' . $icon . '</div>';
1779                        }
1780                        // Add an empty entry, so column count fits again after moving this into $icon
1781                        $theData[$fCol] = '&nbsp;';
1782                    } else {
1783                        $icon = $this->spaceIcon;
1784                    }
1785                    break;
1786                default:
1787                    // Regular fields header:
1788                    $theData[$fCol] = '';
1789
1790                    // Check if $fCol is really a field and get the label and remove the colons
1791                    // at the end
1792                    $sortLabel = BackendUtility::getItemLabel($table, $fCol);
1793                    if ($sortLabel !== null) {
1794                        $sortLabel = rtrim(trim($lang->sL($sortLabel)), ':');
1795
1796                        // Field label
1797                        $fieldTSConfig = [];
1798                        if (isset($tsConfigOfTable[$fCol . '.'])
1799                            && is_array($tsConfigOfTable[$fCol . '.'])
1800                        ) {
1801                            $fieldTSConfig = $tsConfigOfTable[$fCol . '.'];
1802                        }
1803                        if (!empty($fieldTSConfig['label'])) {
1804                            $sortLabel = $lang->sL($fieldTSConfig['label']);
1805                        }
1806                        if (!empty($fieldTSConfig['label.'][$lang->lang])) {
1807                            $sortLabel = $lang->sL($fieldTSConfig['label.'][$lang->lang]);
1808                        }
1809                        $sortLabel = htmlspecialchars($sortLabel);
1810                    } else {
1811                        // No TCA field, only output the $fCol variable with square brackets []
1812                        $sortLabel = htmlspecialchars($fCol);
1813                        $sortLabel = '<i>[' . rtrim(trim($sortLabel), ':') . ']</i>';
1814                    }
1815
1816                    if ($this->table && is_array($currentIdList)) {
1817                        // If the numeric clipboard pads are selected, show duplicate sorting link:
1818                        if ($this->clipNumPane()) {
1819                            $theData[$fCol] .= '<a class="btn btn-default" href="' . htmlspecialchars($this->listURL('', '-1') . '&duplicateField=' . $fCol)
1820                                . '" title="' . htmlspecialchars($lang->getLL('clip_duplicates')) . '">'
1821                                . $this->iconFactory->getIcon('actions-document-duplicates-select', Icon::SIZE_SMALL)->render() . '</a>';
1822                        }
1823                        // If the table can be edited, add link for editing THIS field for all
1824                        // listed records:
1825                        if ($this->isEditable($table) && $permsEdit && $GLOBALS['TCA'][$table]['columns'][$fCol]) {
1826                            $entityIdentifiers = 'entityIdentifiers';
1827                            if ($this->clipNumPane()) {
1828                                $entityIdentifiers .= ':editList';
1829                            }
1830                            $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1831                                . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit'
1832                                . '&columnsOnly=' . $fCol
1833                                . '&returnUrl={T3_THIS_LOCATION}';
1834                            $iTitle = sprintf($lang->getLL('editThisColumn'), $sortLabel);
1835                            $theData[$fCol] .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1836                                . ' data-uri="' . htmlspecialchars($editUri) . '"'
1837                                . ' title="' . htmlspecialchars($iTitle) . '">'
1838                                . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1839                        }
1840                        if (strlen($theData[$fCol]) > 0) {
1841                            $theData[$fCol] = '<div class="btn-group" role="group">' . $theData[$fCol] . '</div> ';
1842                        }
1843                    }
1844                    $theData[$fCol] .= $this->addSortLink($sortLabel, $fCol, $table);
1845            }
1846        }
1847        /*
1848         * hook:  renderListHeader: Allows to change the contents of columns/cells of the Web>List table headers
1849         * usage: Above each listed table in Web>List a header row is shown.
1850         *        Containing the labels of all shown fields and additional icons to create new records for this
1851         *        table or perform special clipboard tasks like mark and copy all listed records to clipboard, etc.
1852         */
1853        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
1854            $hookObject = GeneralUtility::makeInstance($className);
1855            if (!$hookObject instanceof RecordListHookInterface) {
1856                throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567855);
1857            }
1858            $theData = $hookObject->renderListHeader($table, $currentIdList, $theData, $this);
1859        }
1860
1861        // Create and return header table row:
1862        return '<thead>' . $this->addElement(1, $icon, $theData, '', '', '', 'th') . '</thead>';
1863    }
1864
1865    /**
1866     * Get pointer for first element on the page
1867     *
1868     * @param int $page Page number starting with 1
1869     * @return int Pointer to first element on the page (starting with 0)
1870     */
1871    protected function getPointerForPage($page)
1872    {
1873        return ($page - 1) * $this->iLimit;
1874    }
1875
1876    /**
1877     * Creates a page browser for tables with many records
1878     *
1879     * @param string $renderPart Distinguish between 'top' and 'bottom' part of the navigation (above or below the records)
1880     * @return string Navigation HTML
1881     */
1882    protected function renderListNavigation($renderPart = 'top')
1883    {
1884        $totalPages = ceil($this->totalItems / $this->iLimit);
1885        // Show page selector if not all records fit into one page
1886        if ($totalPages <= 1) {
1887            return '';
1888        }
1889        $content = '';
1890        $listURL = $this->listURL('', $this->table, 'firstElementNumber');
1891        // 1 = first page
1892        // 0 = first element
1893        $currentPage = floor($this->firstElementNumber / $this->iLimit) + 1;
1894        // Compile first, previous, next, last and refresh buttons
1895        if ($currentPage > 1) {
1896            $labelFirst = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:first'));
1897            $labelPrevious = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:previous'));
1898            $first = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage(1) . '" title="' . $labelFirst . '">'
1899                . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</a></li>';
1900            $previous = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage - 1) . '" title="' . $labelPrevious . '">'
1901                . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</a></li>';
1902        } else {
1903            $first = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</span></li>';
1904            $previous = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</span></li>';
1905        }
1906        if ($currentPage < $totalPages) {
1907            $labelNext = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:next'));
1908            $labelLast = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:last'));
1909            $next = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage + 1) . '" title="' . $labelNext . '">'
1910                . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</a></li>';
1911            $last = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($totalPages) . '" title="' . $labelLast . '">'
1912                . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</a></li>';
1913        } else {
1914            $next = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</span></li>';
1915            $last = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</span></li>';
1916        }
1917        $reload = '<li><a href="#" onclick="document.dblistForm.action=' . GeneralUtility::quoteJSvalue($listURL
1918            . '&pointer=') . '+calculatePointer(document.getElementById(' . GeneralUtility::quoteJSvalue('jumpPage-' . $renderPart)
1919            . ').value); document.dblistForm.submit(); return true;" title="'
1920            . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:reload')) . '">'
1921            . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a></li>';
1922        if ($renderPart === 'top') {
1923            // Add js to traverse a page select input to a pointer value
1924            $content = '
1925<script type="text/javascript">
1926/*<![CDATA[*/
1927	function calculatePointer(page) {
1928		if (page > ' . $totalPages . ') {
1929			page = ' . $totalPages . ';
1930		}
1931		if (page < 1) {
1932			page = 1;
1933		}
1934		return (page - 1) * ' . $this->iLimit . ';
1935	}
1936/*]]>*/
1937</script>
1938';
1939        }
1940        $pageNumberInput = '
1941			<input type="number" min="1" max="' . $totalPages . '" value="' . $currentPage . '" size="3" class="form-control input-sm paginator-input" id="jumpPage-' . $renderPart . '" name="jumpPage-'
1942            . $renderPart . '" onkeyup="if (event.keyCode == 13) { document.dblistForm.action=' . htmlspecialchars(GeneralUtility::quoteJSvalue($listURL . '&pointer='))
1943            . '+calculatePointer(this.value); document.dblistForm.submit(); } return true;" />
1944			';
1945        $pageIndicatorText = sprintf(
1946            $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:pageIndicator'),
1947            $pageNumberInput,
1948            $totalPages
1949        );
1950        $pageIndicator = '<li><span>' . $pageIndicatorText . '</span></li>';
1951        if ($this->totalItems > $this->firstElementNumber + $this->iLimit) {
1952            $lastElementNumber = $this->firstElementNumber + $this->iLimit;
1953        } else {
1954            $lastElementNumber = $this->totalItems;
1955        }
1956        $rangeIndicator = '<li><span>' . sprintf(
1957            $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:rangeIndicator'),
1958            $this->firstElementNumber + 1,
1959            $lastElementNumber
1960        ) . '</span></li>';
1961
1962        $titleColumn = $this->fieldArray[0];
1963        $data = [
1964            $titleColumn => $content . '
1965				<nav class="pagination-wrap">
1966					<ul class="pagination pagination-block">
1967						' . $first . '
1968						' . $previous . '
1969						' . $rangeIndicator . '
1970						' . $pageIndicator . '
1971						' . $next . '
1972						' . $last . '
1973						' . $reload . '
1974					</ul>
1975				</nav>
1976			'
1977        ];
1978        return $this->addElement(1, '', $data);
1979    }
1980
1981    /*********************************
1982     *
1983     * Rendering of various elements
1984     *
1985     *********************************/
1986
1987    /**
1988     * Creates the control panel for a single record in the listing.
1989     *
1990     * @param string $table The table
1991     * @param mixed[] $row The record for which to make the control panel.
1992     * @throws \UnexpectedValueException
1993     * @return string HTML table with the control panel (unless disabled)
1994     */
1995    public function makeControl($table, $row)
1996    {
1997        $backendUser = $this->getBackendUserAuthentication();
1998        $userTsConfig = $backendUser->getTSConfig();
1999        $module = $this->getModule();
2000        $rowUid = $row['uid'];
2001        if (ExtensionManagementUtility::isLoaded('workspaces') && isset($row['_ORIG_uid'])) {
2002            $rowUid = $row['_ORIG_uid'];
2003        }
2004        $cells = [
2005            'primary' => [],
2006            'secondary' => []
2007        ];
2008        // Enables to hide the move elements for localized records - doesn't make much sense to perform these options for them
2009        // For page translations these icons should never be shown
2010        $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0;
2011        if ($table === 'pages') {
2012            // If the listed table is 'pages' we have to request the permission settings for each page.
2013            $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
2014        } else {
2015            // If the listed table is not 'pages' we have to request the permission settings from the parent page
2016            $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $row['pid']));
2017        }
2018        $permsEdit = $table === 'pages'
2019                     && $backendUser->checkLanguageAccess((int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']])
2020                     && $localCalcPerms & Permission::PAGE_EDIT
2021                     || $table !== 'pages'
2022                        && $localCalcPerms & Permission::CONTENT_EDIT
2023                        && $backendUser->recordEditAccessInternals($table, $row);
2024        $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit);
2025        // "Show" link (only pages and tt_content elements)
2026        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
2027        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
2028
2029        if ($table === 'pages' || $table === 'tt_content') {
2030            $onClick = $this->getOnClickForRow($table, $row);
2031            $viewAction = '<a class="btn btn-default" href="#" onclick="'
2032                            . htmlspecialchars(
2033                                $onClick
2034                            ) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">';
2035            if ($table === 'pages') {
2036                $viewAction .= $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render();
2037            } else {
2038                $viewAction .= $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render();
2039            }
2040            $viewAction .= '</a>';
2041            $this->addActionToCellGroup($cells, $viewAction, 'view');
2042        }
2043        // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
2044        if ($permsEdit && $this->isEditable($table)) {
2045            $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit';
2046            $iconIdentifier = 'actions-open';
2047            if ($table === 'pages') {
2048                // Disallow manual adjustment of the language field for pages
2049                $params .= '&overrideVals[pages][sys_language_uid]=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
2050                $iconIdentifier = 'actions-page-open';
2051            }
2052            $editAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
2053                . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render() . '</a>';
2054        } else {
2055            $editAction = $this->spaceIcon;
2056        }
2057        $this->addActionToCellGroup($cells, $editAction, 'edit');
2058        // "Info": (All records)
2059        $onClick = 'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;';
2060        $viewBigAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('showInfo')) . '">'
2061            . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>';
2062        $this->addActionToCellGroup($cells, $viewBigAction, 'viewBig');
2063        // "Move" wizard link for pages/tt_content elements:
2064        if ($permsEdit && ($table === 'tt_content' || $table === 'pages') && $this->isEditable($table)) {
2065            if ($isL10nOverlay) {
2066                $moveAction = $this->spaceIcon;
2067            } else {
2068                $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('move_element') . '&table=' . $table . '&uid=' . $row['uid']) . ');';
2069                $linkTitleLL = htmlspecialchars($this->getLanguageService()->getLL('move_' . ($table === 'tt_content' ? 'record' : 'page')));
2070                $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-move', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-document-move', Icon::SIZE_SMALL));
2071                $moveAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $linkTitleLL . '">' . $icon->render() . '</a>';
2072            }
2073            $this->addActionToCellGroup($cells, $moveAction, 'move');
2074        }
2075        // If the table is NOT a read-only table, then show these links:
2076        if ($this->isEditable($table)) {
2077            // "Revert" link (history/undo)
2078            if ((bool)\trim($userTsConfig['options.']['showHistory.'][$table] ?? $userTsConfig['options.']['showHistory'] ?? '1')) {
2079                $moduleUrl = (string)$uriBuilder->buildUriFromRoute('record_history', ['element' => $table . ':' . $row['uid']]);
2080                $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue($moduleUrl) . ',\'#latest\');';
2081                $historyAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="'
2082                    . htmlspecialchars($this->getLanguageService()->getLL('history')) . '">'
2083                    . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render() . '</a>';
2084                $this->addActionToCellGroup($cells, $historyAction, 'history');
2085            }
2086            // "Edit Perms" link:
2087            if ($table === 'pages' && $backendUser->check('modules', 'system_BeuserTxPermission') && ExtensionManagementUtility::isLoaded('beuser')) {
2088                if ($isL10nOverlay) {
2089                    $permsAction = $this->spaceIcon;
2090                } else {
2091                    $href = (string)$uriBuilder->buildUriFromRoute('system_BeuserTxPermission') . '&id=' . $row['uid'] . '&tx_beuser_system_beusertxpermission[action]=edit' . $this->makeReturnUrl();
2092                    $permsAction = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="'
2093                        . htmlspecialchars($this->getLanguageService()->getLL('permissions')) . '">'
2094                        . $this->iconFactory->getIcon('actions-lock', Icon::SIZE_SMALL)->render() . '</a>';
2095                }
2096                $this->addActionToCellGroup($cells, $permsAction, 'perms');
2097            }
2098            // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row
2099            // or if default values can depend on previous record):
2100            if (($GLOBALS['TCA'][$table]['ctrl']['sortby'] || $GLOBALS['TCA'][$table]['ctrl']['useColumnsForDefaultValues']) && $permsEdit) {
2101                if ($table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT || $table === 'pages' && $this->calcPerms & Permission::PAGE_NEW) {
2102                    if ($table === 'pages' && $isL10nOverlay) {
2103                        $this->addActionToCellGroup($cells, $this->spaceIcon, 'new');
2104                    } elseif ($this->showNewRecLink($table)) {
2105                        $params = '&edit[' . $table . '][' . -($row['_MOVE_PLH'] ? $row['_MOVE_PLH_uid'] : $row['uid']) . ']=new';
2106                        $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
2107                        $titleLabel = 'new';
2108                        if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
2109                            $titleLabel .= ($table === 'pages' ? 'Page' : 'Record');
2110                        }
2111                        $newAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
2112                            . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($titleLabel)) . '">'
2113                            . $icon->render() . '</a>';
2114                        $this->addActionToCellGroup($cells, $newAction, 'new');
2115                    }
2116                }
2117            }
2118            // "Up/Down" links
2119            if ($permsEdit && $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField && !$this->searchLevels) {
2120                if (!$isL10nOverlay && isset($this->currentTable['prev'][$row['uid']])) {
2121                    // Up
2122                    $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prev'][$row['uid']];
2123                    $moveUpAction = '<a class="btn btn-default" href="#" onclick="'
2124                        . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2125                        . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
2126                        . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
2127                } else {
2128                    $moveUpAction = $this->spaceIcon;
2129                }
2130                $this->addActionToCellGroup($cells, $moveUpAction, 'moveUp');
2131
2132                if (!$isL10nOverlay && $this->currentTable['next'][$row['uid']]) {
2133                    // Down
2134                    $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['next'][$row['uid']];
2135                    $moveDownAction = '<a class="btn btn-default" href="#" onclick="'
2136                        . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2137                        . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
2138                        . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
2139                } else {
2140                    $moveDownAction = $this->spaceIcon;
2141                }
2142                $this->addActionToCellGroup($cells, $moveDownAction, 'moveDown');
2143            }
2144            // "Hide/Unhide" links:
2145            $hiddenField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2146
2147            if (
2148                !empty($GLOBALS['TCA'][$table]['columns'][$hiddenField])
2149                && (empty($GLOBALS['TCA'][$table]['columns'][$hiddenField]['exclude'])
2150                    || $backendUser->check('non_exclude_fields', $table . ':' . $hiddenField))
2151            ) {
2152                if (!$permsEdit || $this->isRecordCurrentBackendUser($table, $row)) {
2153                    $hideAction = $this->spaceIcon;
2154                } else {
2155                    $hideTitle = htmlspecialchars($this->getLanguageService()->getLL('hide' . ($table === 'pages' ? 'Page' : '')));
2156                    $unhideTitle = htmlspecialchars($this->getLanguageService()->getLL('unHide' . ($table === 'pages' ? 'Page' : '')));
2157                    if ($row[$hiddenField]) {
2158                        $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=0';
2159                        $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="hidden" href="#"'
2160                                        . ' data-params="' . htmlspecialchars($params) . '"'
2161                                        . ' title="' . $unhideTitle . '"'
2162                                        . ' data-toggle-title="' . $hideTitle . '">'
2163                                        . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '</a>';
2164                    } else {
2165                        $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=1';
2166                        $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="visible" href="#"'
2167                                        . ' data-params="' . htmlspecialchars($params) . '"'
2168                                        . ' title="' . $hideTitle . '"'
2169                                        . ' data-toggle-title="' . $unhideTitle . '">'
2170                                        . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '</a>';
2171                    }
2172                }
2173                $this->addActionToCellGroup($cells, $hideAction, 'hide');
2174            }
2175            // "Delete" link:
2176            $disableDelete = (bool)\trim($userTsConfig['options.']['disableDelete.'][$table] ?? $userTsConfig['options.']['disableDelete'] ?? '0');
2177            if ($permsEdit && !$disableDelete && ($table === 'pages' && $localCalcPerms & Permission::PAGE_DELETE || $table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT)) {
2178                // Check if the record version is in "deleted" state, because that will switch the action to "restore"
2179                if ($backendUser->workspace > 0 && isset($row['t3ver_state']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
2180                    $actionName = 'restore';
2181                    $refCountMsg = '';
2182                } else {
2183                    $actionName = 'delete';
2184                    $refCountMsg = BackendUtility::referenceCount(
2185                        $table,
2186                        $row['uid'],
2187                        ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
2188                        $this->getReferenceCount($table, $row['uid'])
2189                    ) . BackendUtility::translationCount(
2190                        $table,
2191                        $row['uid'],
2192                        ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
2193                    );
2194                }
2195
2196                if ($this->isRecordCurrentBackendUser($table, $row)) {
2197                    $deleteAction = $this->spaceIcon;
2198                } else {
2199                    $title = BackendUtility::getRecordTitle($table, $row);
2200                    $warningText = $this->getLanguageService()->getLL($actionName . 'Warning') . ' "' . $title . '" ' . '[' . $table . ':' . $row['uid'] . ']' . $refCountMsg;
2201
2202                    $params = 'cmd[' . $table . '][' . $row['uid'] . '][delete]=1';
2203                    $icon = $this->iconFactory->getIcon('actions-edit-' . $actionName, Icon::SIZE_SMALL)->render();
2204                    $linkTitle = htmlspecialchars($this->getLanguageService()->getLL($actionName));
2205                    $l10nParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
2206                    $deleteAction = '<a class="btn btn-default t3js-record-delete" href="#" '
2207                                    . ' data-button-ok-text="' . htmlspecialchars($linkTitle) . '"'
2208                                    . ' data-l10parent="' . ($l10nParentField ? htmlspecialchars($row[$l10nParentField]) : '') . '"'
2209                                    . ' data-params="' . htmlspecialchars($params) . '" data-title="' . htmlspecialchars($title) . '"'
2210                                    . ' data-message="' . htmlspecialchars($warningText) . '" title="' . $linkTitle . '"'
2211                                    . '>' . $icon . '</a>';
2212                }
2213            } else {
2214                $deleteAction = $this->spaceIcon;
2215            }
2216            $this->addActionToCellGroup($cells, $deleteAction, 'delete');
2217            // "Levels" links: Moving pages into new levels...
2218            if ($permsEdit && $table === 'pages' && !$this->searchLevels) {
2219                // Up (Paste as the page right after the current parent page)
2220                if ($this->calcPerms & Permission::PAGE_NEW) {
2221                    $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . -$this->id;
2222                    $moveLeftAction = '<a class="btn btn-default" href="#" onclick="'
2223                        . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2224                        . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('prevLevel')) . '">'
2225                        . $this->iconFactory->getIcon('actions-move-left', Icon::SIZE_SMALL)->render() . '</a>';
2226                    $this->addActionToCellGroup($cells, $isL10nOverlay ? $this->spaceIcon : $moveLeftAction, 'moveLeft');
2227                }
2228                // Down (Paste as subpage to the page right above)
2229                if (!$isL10nOverlay && $this->currentTable['prevUid'][$row['uid']]) {
2230                    $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $this->currentTable['prevUid'][$row['uid']]));
2231                    if ($localCalcPerms & Permission::PAGE_NEW) {
2232                        $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prevUid'][$row['uid']];
2233                        $moveRightAction = '<a class="btn btn-default" href="#" onclick="'
2234                            . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2235                            . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('nextLevel')) . '">'
2236                            . $this->iconFactory->getIcon('actions-move-right', Icon::SIZE_SMALL)->render() . '</a>';
2237                    } else {
2238                        $moveRightAction = $this->spaceIcon;
2239                    }
2240                } else {
2241                    $moveRightAction = $this->spaceIcon;
2242                }
2243                $this->addActionToCellGroup($cells, $moveRightAction, 'moveRight');
2244            }
2245        }
2246        /*
2247         * hook: recStatInfoHooks: Allows to insert HTML before record icons on various places
2248         */
2249        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [];
2250        if (!empty($hooks)) {
2251            $stat = '';
2252            $_params = [$table, $row['uid']];
2253            foreach ($hooks as $_funcRef) {
2254                $stat .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2255            }
2256            $this->addActionToCellGroup($cells, $stat, 'stat');
2257        }
2258        /*
2259         * hook:  makeControl: Allows to change control icons of records in list-module
2260         * usage: This hook method gets passed the current $cells array as third parameter.
2261         *        This array contains values for the icons/actions generated for each record in Web>List.
2262         *        Each array entry is accessible by an index-key.
2263         *        The order of the icons is depending on the order of those array entries.
2264         */
2265        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? false)) {
2266            // for compatibility reason, we move all icons to the rootlevel
2267            // before calling the hooks
2268            foreach ($cells as $section => $actions) {
2269                foreach ($actions as $actionKey => $action) {
2270                    $cells[$actionKey] = $action;
2271                }
2272            }
2273            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] as $className) {
2274                $hookObject = GeneralUtility::makeInstance($className);
2275                if (!$hookObject instanceof RecordListHookInterface) {
2276                    throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567840);
2277                }
2278                $cells = $hookObject->makeControl($table, $row, $cells, $this);
2279            }
2280            // now sort icons again into primary and secondary sections
2281            // after all hooks are processed
2282            $hookCells = $cells;
2283            foreach ($hookCells as $key => $value) {
2284                if ($key === 'primary' || $key === 'secondary') {
2285                    continue;
2286                }
2287                $this->addActionToCellGroup($cells, $value, $key);
2288            }
2289        }
2290        $output = '<!-- CONTROL PANEL: ' . $table . ':' . $row['uid'] . ' -->';
2291        foreach ($cells as $classification => $actions) {
2292            $visibilityClass = ($classification !== 'primary' && !$module->MOD_SETTINGS['bigControlPanel'] ? 'collapsed' : 'expanded');
2293            if ($visibilityClass === 'collapsed') {
2294                $cellOutput = '';
2295                foreach ($actions as $action) {
2296                    $cellOutput .= $action;
2297                }
2298                $output .= ' <div class="btn-group">' .
2299                    '<span id="actions_' . $table . '_' . $row['uid'] . '" class="btn-group collapse collapse-horizontal width">' . $cellOutput . '</span>' .
2300                    '<a href="#actions_' . $table . '_' . $row['uid'] . '" class="btn btn-default collapsed" data-toggle="collapse" aria-expanded="false"><span class="t3-icon fa fa-ellipsis-h"></span></a>' .
2301                    '</div>';
2302            } else {
2303                $output .= ' <div class="btn-group" role="group">' . implode('', $actions) . '</div>';
2304            }
2305        }
2306        return $output;
2307    }
2308
2309    /**
2310     * Creates the clipboard panel for a single record in the listing.
2311     *
2312     * @param string $table The table
2313     * @param mixed[] $row The record for which to make the clipboard panel.
2314     * @throws \UnexpectedValueException
2315     * @return string HTML table with the clipboard panel (unless disabled)
2316     */
2317    public function makeClip($table, $row)
2318    {
2319        // Return blank, if disabled:
2320        if (!$this->getModule()->MOD_SETTINGS['clipBoard']) {
2321            return '';
2322        }
2323        if (!$this->isEditable($table)) {
2324            return '';
2325        }
2326        $cells = [];
2327        $cells['pasteAfter'] = ($cells['pasteInto'] = $this->spaceIcon);
2328        // Enables to hide the copy, cut and paste icons for localized records - doesn't make much sense to perform these options for them
2329        // For page translations these icons should never be shown
2330        $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0;
2331        // Return blank, if disabled:
2332        // Whether a numeric clipboard pad is active or the normal pad we will see different content of the panel:
2333        // For the "Normal" pad:
2334        if ($this->clipObj->current === 'normal') {
2335            // Show copy/cut icons:
2336            $isSel = (string)$this->clipObj->isSelected($table, $row['uid']);
2337            if ($isL10nOverlay || !$this->overlayEditLockPermissions($table, $row)) {
2338                $cells['copy'] = $this->spaceIcon;
2339                $cells['cut'] = $this->spaceIcon;
2340            } else {
2341                $copyIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL);
2342                $cutIcon = $this->iconFactory->getIcon('actions-edit-cut', Icon::SIZE_SMALL);
2343
2344                if ($isSel === 'copy') {
2345                    $copyIcon = $this->iconFactory->getIcon('actions-edit-copy-release', Icon::SIZE_SMALL);
2346                } elseif ($isSel === 'cut') {
2347                    $cutIcon = $this->iconFactory->getIcon('actions-edit-cut-release', Icon::SIZE_SMALL);
2348                }
2349
2350                $cells['copy'] = '<a class="btn btn-default" href="#" onclick="'
2351                    . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2352                        $table,
2353                        $row['uid'],
2354                        1,
2355                        $isSel === 'copy',
2356                        ['returnUrl' => '']
2357                    )) . ');')
2358                    . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy')) . '">'
2359                    . $copyIcon->render() . '</a>';
2360
2361                // Check permission to cut page or content
2362                if ($table === 'pages') {
2363                    $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
2364                    $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
2365                } else {
2366                    $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT;
2367                }
2368                $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit);
2369
2370                // If the listed table is 'pages' we have to request the permission settings for each page:
2371                if ($table === 'pages') {
2372                    if ($permsEdit) {
2373                        $cells['cut'] = '<a class="btn btn-default" href="#" onclick="'
2374                        . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2375                            $table,
2376                            $row['uid'],
2377                            0,
2378                            $isSel === 'cut',
2379                            ['returnUrl' => '']
2380                        )) . ');')
2381                        . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">'
2382                        . $cutIcon->render() . '</a>';
2383                    } else {
2384                        $cells['cut'] = $this->spaceIcon;
2385                    }
2386                } else {
2387                    if ($this->calcPerms & Permission::CONTENT_EDIT) {
2388                        $cells['cut'] = '<a class="btn btn-default" href="#" onclick="'
2389                        . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2390                            $table,
2391                            $row['uid'],
2392                            0,
2393                            $isSel === 'cut',
2394                            ['returnUrl' => '']
2395                        )) . ');')
2396                        . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">'
2397                        . $cutIcon->render() . '</a>';
2398                    } else {
2399                        $cells['cut'] = $this->spaceIcon;
2400                    }
2401                }
2402            }
2403        } else {
2404            // For the numeric clipboard pads (showing checkboxes where one can select elements on/off)
2405            // Setting name of the element in ->CBnames array:
2406            $n = $table . '|' . $row['uid'];
2407            $this->CBnames[] = $n;
2408            // Check if the current element is selected and if so, prepare to set the checkbox as selected:
2409            $checked = $this->clipObj->isSelected($table, $row['uid']) ? 'checked="checked" ' : '';
2410            // If the "duplicateField" value is set then select all elements which are duplicates...
2411            if ($this->duplicateField && isset($row[$this->duplicateField])) {
2412                $checked = '';
2413                if (in_array($row[$this->duplicateField], $this->duplicateStack)) {
2414                    $checked = 'checked="checked" ';
2415                }
2416                $this->duplicateStack[] = $row[$this->duplicateField];
2417            }
2418            // Adding the checkbox to the panel:
2419            $cells['select'] = $isL10nOverlay
2420                ? $this->spaceIcon
2421                : '<input type="hidden" name="CBH[' . $n . ']" value="0" /><label class="btn btn-default btn-checkbox"><input type="checkbox"'
2422                    . ' name="CBC[' . $n . ']" value="1" ' . $checked . '/><span class="t3-icon fa"></span></label>';
2423        }
2424        // Now, looking for selected elements from the current table:
2425        $elFromTable = $this->clipObj->elFromTable($table);
2426        if (!empty($elFromTable) && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
2427            // IF elements are found, they can be individually ordered and are not locked by editlock, then add a "paste after" icon:
2428            $cells['pasteAfter'] = $isL10nOverlay || !$this->overlayEditLockPermissions($table, $row)
2429                ? $this->spaceIcon
2430                : '<a class="btn btn-default t3js-modal-trigger"'
2431                    . ' href="' . htmlspecialchars($this->clipObj->pasteUrl($table, -$row['uid'])) . '"'
2432                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"'
2433                    . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"'
2434                    . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'after', $elFromTable)) . '"'
2435                    . ' data-severity="warning">'
2436                    . $this->iconFactory->getIcon('actions-document-paste-after', Icon::SIZE_SMALL)->render() . '</a>';
2437        }
2438        // Now, looking for elements in general:
2439        $elFromTable = $this->clipObj->elFromTable('');
2440        if ($table === 'pages' && !$isL10nOverlay && !empty($elFromTable)) {
2441            $cells['pasteInto'] = '<a class="btn btn-default t3js-modal-trigger"'
2442                . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $row['uid'])) . '"'
2443                . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
2444                . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
2445                . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'into', $elFromTable)) . '"'
2446                . ' data-severity="warning">'
2447                . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '</a>';
2448        }
2449        /*
2450         * hook:  makeClip: Allows to change clip-icons of records in list-module
2451         * usage: This hook method gets passed the current $cells array as third parameter.
2452         *        This array contains values for the clipboard icons generated for each record in Web>List.
2453         *        Each array entry is accessible by an index-key.
2454         *        The order of the icons is depending on the order of those array entries.
2455         */
2456        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
2457            $hookObject = GeneralUtility::makeInstance($className);
2458            if (!$hookObject instanceof RecordListHookInterface) {
2459                throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567845);
2460            }
2461            $cells = $hookObject->makeClip($table, $row, $cells, $this);
2462        }
2463        return '<div class="btn-group" role="group">' . implode('', $cells) . '</div>';
2464    }
2465
2466    /**
2467     * Creates the HTML for a reference count for the record with the UID $uid
2468     * in the table $tableName.
2469     *
2470     * @param string $tableName
2471     * @param int $uid
2472     * @return string HTML of reference a link, will be empty if there are no
2473     */
2474    protected function createReferenceHtml($tableName, $uid)
2475    {
2476        $referenceCount = GeneralUtility::makeInstance(ConnectionPool::class)
2477            ->getConnectionForTable('sys_refindex')
2478            ->count(
2479                '*',
2480                'sys_refindex',
2481                [
2482                    'ref_table' => $tableName,
2483                    'ref_uid' => (int)$uid,
2484                    'deleted' => 0,
2485                ]
2486            );
2487
2488        return $this->generateReferenceToolTip(
2489            $referenceCount,
2490            GeneralUtility::quoteJSvalue($tableName) . ', ' . GeneralUtility::quoteJSvalue($uid)
2491        );
2492    }
2493
2494    /**
2495     * Creates the localization panel
2496     *
2497     * @param string $table The table
2498     * @param mixed[] $row The record for which to make the localization panel.
2499     * @return string[] Array with key 0/1 with content for column 1 and 2
2500     */
2501    public function makeLocalizationPanel($table, $row)
2502    {
2503        $out = [
2504            0 => '',
2505            1 => ''
2506        ];
2507        // Reset translations
2508        $this->translations = [];
2509
2510        // Language title and icon:
2511        $out[0] = $this->languageFlag($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
2512        // Guard clause so we can quickly return if a record is localized to "all languages"
2513        // It should only be possible to localize a record off default (uid 0)
2514        // Reasoning: The Parent is for ALL languages... why overlay with a localization?
2515        if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === -1) {
2516            return $out;
2517        }
2518
2519        $translations = $this->translateTools->translationInfo($table, $row['uid'], 0, $row, $this->selFieldList);
2520        if (is_array($translations)) {
2521            $this->translations = $translations['translations'];
2522            // Traverse page translations and add icon for each language that does NOT yet exist and is included in site configuration:
2523            $lNew = '';
2524            foreach ($this->pageOverlays as $lUid_OnPage => $lsysRec) {
2525                if (isset($this->systemLanguagesOnPage[$lUid_OnPage]) && $this->isEditable($table) && !isset($translations['translations'][$lUid_OnPage]) && $this->getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)) {
2526                    $url = $this->listURL();
2527                    $href = BackendUtility::getLinkToDataHandlerAction(
2528                        '&cmd[' . $table . '][' . $row['uid'] . '][localize]=' . $lUid_OnPage,
2529                        $url . '&justLocalized=' . rawurlencode($table . ':' . $row['uid'] . ':' . $lUid_OnPage)
2530                    );
2531                    $language = BackendUtility::getRecord('sys_language', $lUid_OnPage, 'title');
2532                    if ($this->languageIconTitles[$lUid_OnPage]['flagIcon']) {
2533                        $lC = $this->iconFactory->getIcon($this->languageIconTitles[$lUid_OnPage]['flagIcon'], Icon::SIZE_SMALL)->render();
2534                    } else {
2535                        $lC = $this->languageIconTitles[$lUid_OnPage]['title'];
2536                    }
2537                    $lC = '<a href="' . htmlspecialchars($href) . '" title="'
2538                        . htmlspecialchars($language['title']) . '" class="btn btn-default t3js-action-localize">'
2539                        . $lC . '</a> ';
2540                    $lNew .= $lC;
2541                }
2542            }
2543            if ($lNew) {
2544                $out[1] .= $lNew;
2545            }
2546        } elseif ($row['l18n_parent']) {
2547            $out[0] = '&nbsp;&nbsp;&nbsp;&nbsp;' . $out[0];
2548        }
2549        return $out;
2550    }
2551
2552    /**
2553     * Creates a checkbox list for selecting fields to display from a table:
2554     *
2555     * @param string $table Table name
2556     * @param bool $formFields If TRUE, form-fields will be wrapped around the table.
2557     * @return string HTML table with the selector check box (name: displayFields['.$table.'][])
2558     */
2559    public function fieldSelectBox($table, $formFields = true)
2560    {
2561        $lang = $this->getLanguageService();
2562        // Init:
2563        $formElements = ['', ''];
2564        if ($formFields) {
2565            $formElements = ['<form action="' . htmlspecialchars($this->listURL()) . '" method="post" name="fieldSelectBox">', '</form>'];
2566        }
2567        // Load already selected fields, if any:
2568        $setFields = is_array($this->setFields[$table]) ? $this->setFields[$table] : [];
2569        // Request fields from table:
2570        $fields = $this->makeFieldList($table, false, true);
2571        // Add pseudo "control" fields
2572        $fields[] = '_PATH_';
2573        $fields[] = '_REF_';
2574        $fields[] = '_LOCALIZATION_';
2575        $fields[] = '_CONTROL_';
2576        $fields[] = '_CLIPBOARD_';
2577        // Create a checkbox for each field:
2578        $checkboxes = [];
2579        $checkAllChecked = true;
2580        $tsConfig = BackendUtility::getPagesTSconfig($this->id);
2581        $tsConfigOfTable = is_array($tsConfig['TCEFORM.'][$table . '.']) ? $tsConfig['TCEFORM.'][$table . '.'] : null;
2582        foreach ($fields as $fieldName) {
2583            // Hide field if hidden
2584            if ($tsConfigOfTable && is_array($tsConfigOfTable[$fieldName . '.']) && isset($tsConfigOfTable[$fieldName . '.']['disabled']) && (int)$tsConfigOfTable[$fieldName . '.']['disabled'] === 1) {
2585                continue;
2586            }
2587            // Determine, if checkbox should be checked
2588            if (in_array($fieldName, $setFields, true) || $fieldName === $this->fieldArray[0]) {
2589                $checked = ' checked="checked"';
2590            } else {
2591                $checkAllChecked = false;
2592                $checked = '';
2593            }
2594
2595            // Field label
2596            $fieldTSConfig = [];
2597            $fieldLabel = '';
2598            if (isset($tsConfigOfTable[$fieldName . '.'])
2599                && is_array($tsConfigOfTable[$fieldName . '.'])
2600            ) {
2601                $fieldTSConfig = $tsConfigOfTable[$fieldName . '.'];
2602            }
2603            if (!empty($fieldTSConfig['label'])) {
2604                $fieldLabel = $fieldTSConfig['label'];
2605            }
2606            if (!empty($fieldTSConfig['label.'][$lang->lang])) {
2607                $fieldLabel = $fieldTSConfig['label.'][$lang->lang];
2608            }
2609
2610            $fieldLabel = $fieldLabel ?: BackendUtility::getItemLabel($table, $fieldName);
2611
2612            $checkboxes[] = '<tr><td class="col-checkbox"><input type="checkbox" id="check-' . $fieldName . '" name="displayFields['
2613                . $table . '][]" value="' . $fieldName . '" ' . $checked
2614                . ($fieldName === $this->fieldArray[0] ? ' disabled="disabled"' : '') . '></td><td class="col-title">'
2615                . '<label class="label-block" for="check-' . $fieldName . '">' . htmlspecialchars($lang->sL($fieldLabel)) . ' <span class="text-muted text-monospace">[' . htmlspecialchars($fieldName) . ']</span></label></td></tr>';
2616        }
2617        // Table with the field selector::
2618        $content = $formElements[0] . '
2619			<input type="hidden" name="displayFields[' . $table . '][]" value="">
2620			<div class="table-fit table-scrollable">
2621				<table border="0" cellpadding="0" cellspacing="0" class="table table-transparent table-hover">
2622					<thead>
2623						<tr>
2624							<th class="col-checkbox checkbox" colspan="2">
2625								<label><input type="checkbox" class="checkbox checkAll" ' . ($checkAllChecked ? ' checked="checked"' : '') . '>
2626								' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall')) . '</label>
2627							</th>
2628						</tr>
2629					</thead>
2630					<tbody>
2631					' . implode('', $checkboxes) . '
2632					</tbody>
2633				</table>
2634			</div>
2635			<input type="submit" name="search" class="btn btn-default" value="'
2636            . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.setFields')) . '"/>
2637			' . $formElements[1];
2638        return '<div class="fieldSelectBox">' . $content . '</div>';
2639    }
2640
2641    /*********************************
2642     *
2643     * Helper functions
2644     *
2645     *********************************/
2646    /**
2647     * Creates a link around $string. The link contains an onclick action
2648     * which submits the script with some clipboard action.
2649     * Currently, this is used for setting elements / delete elements.
2650     *
2651     * @param string $string The HTML content to link (image/text)
2652     * @param string $table Table name
2653     * @param string $cmd Clipboard command (eg. "setCB" or "delete")
2654     * @param string $warning Warning text, if any ("delete" uses this for confirmation
2655     * @param string $title title attribute for the anchor
2656     * @return string <a> tag wrapped link.
2657     */
2658    public function linkClipboardHeaderIcon($string, $table, $cmd, $warning = '', $title = '')
2659    {
2660        $jsCode = 'document.dblistForm.cmd.value=' . GeneralUtility::quoteJSvalue($cmd)
2661            . ';document.dblistForm.cmd_table.value='
2662            . GeneralUtility::quoteJSvalue($table)
2663            . ';document.dblistForm.submit();';
2664
2665        $attributes = [];
2666        if ($title !== '') {
2667            $attributes['title'] = $title;
2668        }
2669        if ($warning) {
2670            $attributes['class'] = 'btn btn-default t3js-modal-trigger';
2671            $attributes['data-href'] = 'javascript:' . $jsCode;
2672            $attributes['data-severity'] = 'warning';
2673            $attributes['data-title'] = $title;
2674            $attributes['data-content'] = $warning;
2675        } else {
2676            $attributes['class'] = 'btn btn-default';
2677            $attributes['onclick'] = $jsCode . 'return false;';
2678        }
2679
2680        $attributesString = '';
2681        foreach ($attributes as $key => $value) {
2682            $attributesString .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
2683        }
2684        return '<a href="#" ' . $attributesString . '>' . $string . '</a>';
2685    }
2686
2687    /**
2688     * Returns TRUE if a numeric clipboard pad is selected/active
2689     *
2690     * @return bool
2691     */
2692    public function clipNumPane()
2693    {
2694        return in_array('_CLIPBOARD_', $this->fieldArray) && $this->clipObj->current !== 'normal';
2695    }
2696
2697    /**
2698     * Creates a sort-by link on the input string ($code).
2699     * It will automatically detect if sorting should be ascending or descending depending on $this->sortRev.
2700     * Also some fields will not be possible to sort (including if single-table-view is disabled).
2701     *
2702     * @param string $code The string to link (text)
2703     * @param string $field The fieldname represented by the title ($code)
2704     * @param string $table Table name
2705     * @return string Linked $code variable
2706     */
2707    public function addSortLink($code, $field, $table)
2708    {
2709        // Certain circumstances just return string right away (no links):
2710        if ($field === '_CONTROL_' || $field === '_LOCALIZATION_' || $field === '_CLIPBOARD_' || $field === '_REF_' || $this->disableSingleTableView) {
2711            return $code;
2712        }
2713        // If "_PATH_" (showing record path) is selected, force sorting by pid field (will at least group the records!)
2714        if ($field === '_PATH_') {
2715            $field = 'pid';
2716        }
2717        //	 Create the sort link:
2718        $sortUrl = $this->listURL('', '-1', 'sortField,sortRev,table,firstElementNumber') . '&table=' . $table
2719            . '&sortField=' . $field . '&sortRev=' . ($this->sortRev || $this->sortField != $field ? 0 : 1);
2720        $sortArrow = $this->sortField === $field
2721            ? $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render()
2722            : '';
2723        // Return linked field:
2724        return '<a href="' . htmlspecialchars($sortUrl) . '">' . $code . $sortArrow . '</a>';
2725    }
2726
2727    /**
2728     * Returns the path for a certain pid
2729     * The result is cached internally for the session, thus you can call
2730     * this function as much as you like without performance problems.
2731     *
2732     * @param int $pid The page id for which to get the path
2733     * @return mixed[] The path.
2734     */
2735    public function recPath($pid)
2736    {
2737        if (!isset($this->recPath_cache[$pid])) {
2738            $this->recPath_cache[$pid] = BackendUtility::getRecordPath($pid, $this->perms_clause, 20);
2739        }
2740        return $this->recPath_cache[$pid];
2741    }
2742
2743    /**
2744     * Returns TRUE if a link for creating new records should be displayed for $table
2745     *
2746     * @param string $table Table name
2747     * @return bool Returns TRUE if a link for creating new records should be displayed for $table
2748     * @see \TYPO3\CMS\Backend\Controller\NewRecordController::showNewRecLink
2749     */
2750    public function showNewRecLink($table)
2751    {
2752        // No deny/allow tables are set:
2753        if (empty($this->allowedNewTables) && empty($this->deniedNewTables)) {
2754            return true;
2755        }
2756        return !in_array($table, $this->deniedNewTables)
2757            && (empty($this->allowedNewTables) || in_array($table, $this->allowedNewTables));
2758    }
2759
2760    /**
2761     * Creates the "&returnUrl" parameter for links - this is used when the script links
2762     * to other scripts and passes its own URL with the link so other scripts can return to the listing again.
2763     * Uses REQUEST_URI as value.
2764     *
2765     * @return string
2766     */
2767    public function makeReturnUrl()
2768    {
2769        return '&returnUrl=' . rawurlencode(GeneralUtility::getIndpEnv('REQUEST_URI'));
2770    }
2771
2772    /************************************
2773     *
2774     * CSV related functions
2775     *
2776     ************************************/
2777    /**
2778     * Initializes internal csvLines array with the header of field names
2779     */
2780    protected function initCSV()
2781    {
2782        $this->addHeaderRowToCSV();
2783    }
2784
2785    /**
2786     * Add header line with field names as CSV line
2787     */
2788    protected function addHeaderRowToCSV()
2789    {
2790        $fieldArray = array_combine($this->fieldArray, $this->fieldArray);
2791        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['customizeCsvHeader'] ?? [];
2792        if (!empty($hooks)) {
2793            $hookParameters = [
2794                'fields' => &$fieldArray
2795            ];
2796            foreach ($hooks as $hookFunction) {
2797                GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
2798            }
2799        }
2800        // Add header row, control fields will be reduced inside addToCSV()
2801        $this->addToCSV($fieldArray);
2802    }
2803
2804    /**
2805     * Adds selected columns of one table row as CSV line.
2806     *
2807     * @param mixed[] $row Record array, from which the values of fields found in $this->fieldArray will be listed in the CSV output.
2808     */
2809    protected function addToCSV(array $row = [])
2810    {
2811        $rowReducedByControlFields = self::removeControlFieldsFromFieldRow($row);
2812        // Get an field array without control fields but in the expected order
2813        $fieldArray = array_intersect_key(array_flip($this->fieldArray), $rowReducedByControlFields);
2814        // Overwrite fieldArray to keep the order with an array of needed fields
2815        $rowReducedToSelectedColumns = array_replace($fieldArray, array_intersect_key($rowReducedByControlFields, $fieldArray));
2816        $this->setCsvRow($rowReducedToSelectedColumns);
2817    }
2818
2819    /**
2820     * Remove control fields from row for CSV export
2821     *
2822     * @param mixed[] $row fieldNames => fieldValues
2823     * @return mixed[] Input array reduces by control fields
2824     */
2825    protected static function removeControlFieldsFromFieldRow(array $row = [])
2826    {
2827        // Possible control fields in a list row
2828        $controlFields = [
2829            '_PATH_',
2830            '_REF_',
2831            '_CONTROL_',
2832            '_CLIPBOARD_',
2833            '_LOCALIZATION_',
2834            '_LOCALIZATION_b'
2835        ];
2836        return array_diff_key($row, array_flip($controlFields));
2837    }
2838
2839    /**
2840     * Adds input row of values to the internal csvLines array as a CSV formatted line
2841     *
2842     * @param mixed[] $csvRow Array with values to be listed.
2843     */
2844    public function setCsvRow($csvRow)
2845    {
2846        $csvDelimiter = $this->modTSconfig['properties']['csvDelimiter'] ?? ',';
2847        $csvQuote = $this->modTSconfig['properties']['csvQuote'] ?? '"';
2848
2849        $this->csvLines[] = CsvUtility::csvValues($csvRow, $csvDelimiter, $csvQuote);
2850    }
2851
2852    /**
2853     * Compiles the internal csvLines array to a csv-string and outputs it to the browser.
2854     * This function exits!
2855     *
2856     * @param string $prefix Filename prefix:
2857     */
2858    public function outputCSV($prefix)
2859    {
2860        // Setting filename:
2861        $filename = $prefix . '_' . date('dmy-Hi') . '.csv';
2862        // Creating output header:
2863        header('Content-Type: application/octet-stream');
2864        header('Content-Disposition: attachment; filename=' . $filename);
2865        // Cache-Control header is needed here to solve an issue with browser IE and
2866        // versions lower than 9. See for more information: http://support.microsoft.com/kb/323308
2867        header("Cache-Control: ''");
2868        // Printing the content of the CSV lines:
2869        echo implode(CRLF, $this->csvLines);
2870        // Exits:
2871        die;
2872    }
2873
2874    /**
2875     * add action into correct section
2876     *
2877     * @param array $cells
2878     * @param string $action
2879     * @param string $actionKey
2880     */
2881    public function addActionToCellGroup(&$cells, $action, $actionKey)
2882    {
2883        $cellsMap = [
2884            'primary' => [
2885                'view', 'edit', 'hide', 'delete', 'stat'
2886            ],
2887            'secondary' => [
2888                'viewBig', 'history', 'perms', 'new', 'move', 'moveUp', 'moveDown', 'moveLeft', 'moveRight', 'version'
2889            ]
2890        ];
2891        $classification = in_array($actionKey, $cellsMap['primary']) ? 'primary' : 'secondary';
2892        $cells[$classification][$actionKey] = $action;
2893        unset($cells[$actionKey]);
2894    }
2895
2896    /**
2897     * Check if the record represents the current backend user
2898     *
2899     * @param string $table
2900     * @param array $row
2901     * @return bool
2902     */
2903    protected function isRecordCurrentBackendUser($table, $row)
2904    {
2905        return $table === 'be_users' && (int)$row['uid'] === $this->getBackendUserAuthentication()->user['uid'];
2906    }
2907
2908    /**
2909     * @param bool $isEditable
2910     */
2911    public function setIsEditable($isEditable)
2912    {
2913        $this->editable = $isEditable;
2914    }
2915
2916    /**
2917     * Check if the table is readonly or editable
2918     * @param string $table
2919     * @return bool
2920     */
2921    public function isEditable($table)
2922    {
2923        $backendUser = $this->getBackendUserAuthentication();
2924        return !$GLOBALS['TCA'][$table]['ctrl']['readOnly']
2925            && $this->editable
2926            && ($backendUser->isAdmin() || $backendUser->check('tables_modify', $table));
2927    }
2928
2929    /**
2930     * Check if the current record is locked by editlock. Pages are locked if their editlock flag is set,
2931     * records are if they are locked themselves or if the page they are on is locked (a page’s editlock
2932     * is transitive for its content elements).
2933     *
2934     * @param string $table
2935     * @param array $row
2936     * @param bool $editPermission
2937     * @return bool
2938     */
2939    protected function overlayEditLockPermissions($table, $row = [], $editPermission = true)
2940    {
2941        if ($editPermission && !$this->getBackendUserAuthentication()->isAdmin()) {
2942            // If no $row is submitted we only check for general edit lock of current page (except for table "pages")
2943            if (empty($row)) {
2944                return $table === 'pages' ? true : !$this->pageRow['editlock'];
2945            }
2946            if (($table === 'pages' && $row['editlock']) || ($table !== 'pages' && $this->pageRow['editlock'])) {
2947                $editPermission = false;
2948            } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['editlock']) && $row[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
2949                $editPermission = false;
2950            }
2951        }
2952        return $editPermission;
2953    }
2954
2955    /**
2956     * Check whether or not the current backend user is an admin or the current page is
2957     * locked by editlock.
2958     *
2959     * @return bool
2960     */
2961    protected function editLockPermissions()
2962    {
2963        return $this->getBackendUserAuthentication()->isAdmin() || !$this->pageRow['editlock'];
2964    }
2965
2966    /**
2967     * @return BaseScriptClass
2968     */
2969    protected function getModule()
2970    {
2971        return $GLOBALS['SOBE'];
2972    }
2973
2974    /**
2975     * @return DocumentTemplate
2976     */
2977    protected function getDocumentTemplate()
2978    {
2979        return $GLOBALS['TBE_TEMPLATE'];
2980    }
2981
2982    /**
2983     * Initializes the list generation
2984     *
2985     * @param int $id Page id for which the list is rendered. Must be >= 0
2986     * @param string $table Tablename - if extended mode where only one table is listed at a time.
2987     * @param int $pointer Browsing pointer.
2988     * @param string $search Search word, if any
2989     * @param int $levels Number of levels to search down the page tree
2990     * @param int $showLimit Limit of records to be listed.
2991     */
2992    public function start($id, $table, $pointer, $search = '', $levels = 0, $showLimit = 0)
2993    {
2994        $backendUser = $this->getBackendUserAuthentication();
2995        // Setting internal variables:
2996        // sets the parent id
2997        $this->id = (int)$id;
2998        // Store languages that are included in the site configuration for the current page.
2999        $this->systemLanguagesOnPage = $this->translateTools->getSystemLanguages($this->id);
3000        if ($GLOBALS['TCA'][$table]) {
3001            // Setting single table mode, if table exists:
3002            $this->table = $table;
3003        }
3004        $this->firstElementNumber = $pointer;
3005        $this->searchString = trim($search);
3006        $this->searchLevels = (int)$levels;
3007        $this->showLimit = MathUtility::forceIntegerInRange($showLimit, 0, 10000);
3008        // Setting GPvars:
3009        $this->csvOutput = (bool)GeneralUtility::_GP('csv');
3010        $this->sortField = GeneralUtility::_GP('sortField');
3011        $this->sortRev = GeneralUtility::_GP('sortRev');
3012        $this->displayFields = GeneralUtility::_GP('displayFields');
3013        $this->duplicateField = GeneralUtility::_GP('duplicateField');
3014        if (GeneralUtility::_GP('justLocalized')) {
3015            $this->localizationRedirect(GeneralUtility::_GP('justLocalized'));
3016        }
3017        // Init dynamic vars:
3018        $this->counter = 0;
3019        $this->JScode = '';
3020        $this->HTMLcode = '';
3021        // Limits
3022        if (isset($this->modTSconfig['properties']['itemsLimitPerTable'])) {
3023            $this->itemsLimitPerTable = MathUtility::forceIntegerInRange(
3024                (int)$this->modTSconfig['properties']['itemsLimitPerTable'],
3025                1,
3026                10000
3027            );
3028        }
3029        if (isset($this->modTSconfig['properties']['itemsLimitSingleTable'])) {
3030            $this->itemsLimitSingleTable = MathUtility::forceIntegerInRange(
3031                (int)$this->modTSconfig['properties']['itemsLimitSingleTable'],
3032                1,
3033                10000
3034            );
3035        }
3036
3037        // If there is a current link to a record, set the current link uid and get the table name from the link handler configuration
3038        $currentLinkValue = isset($this->overrideUrlParameters['P']['currentValue']) ? trim($this->overrideUrlParameters['P']['currentValue']) : '';
3039        if ($currentLinkValue) {
3040            $linkService = GeneralUtility::makeInstance(LinkService::class);
3041            try {
3042                $currentLinkParts = $linkService->resolve($currentLinkValue);
3043                if ($currentLinkParts['type'] === 'record' && isset($currentLinkParts['identifier'])) {
3044                    $this->currentLink['tableNames'] = $this->tableList;
3045                    $this->currentLink['uid'] = (int)$currentLinkParts['uid'];
3046                }
3047            } catch (UnknownLinkHandlerException $e) {
3048            }
3049        }
3050
3051        // $table might be NULL at this point in the code. As the expressionBuilder
3052        // is used to limit returned records based on the page permissions and the
3053        // uid field of the pages it can hardcoded to work on the pages table.
3054        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3055            ->getQueryBuilderForTable('pages')
3056            ->expr();
3057        $permsClause = $expressionBuilder->andX($backendUser->getPagePermsClause(Permission::PAGE_SHOW));
3058        // This will hide records from display - it has nothing to do with user rights!!
3059        $pidList = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '', true);
3060        if (!empty($pidList)) {
3061            $permsClause->add($expressionBuilder->notIn('pages.uid', $pidList));
3062        }
3063        $this->perms_clause = (string)$permsClause;
3064
3065        // Get configuration of collapsed tables from user uc and merge with sanitized GP vars
3066        $this->tablesCollapsed = is_array($backendUser->uc['moduleData']['list'])
3067            ? $backendUser->uc['moduleData']['list']
3068            : [];
3069        $collapseOverride = GeneralUtility::_GP('collapse');
3070        if (is_array($collapseOverride)) {
3071            foreach ($collapseOverride as $collapseTable => $collapseValue) {
3072                if (is_array($GLOBALS['TCA'][$collapseTable]) && ($collapseValue == 0 || $collapseValue == 1)) {
3073                    $this->tablesCollapsed[$collapseTable] = $collapseValue;
3074                }
3075            }
3076            // Save modified user uc
3077            $backendUser->uc['moduleData']['list'] = $this->tablesCollapsed;
3078            $backendUser->writeUC($backendUser->uc);
3079            $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
3080            if ($returnUrl !== '') {
3081                HttpUtility::redirect($returnUrl);
3082            }
3083        }
3084        $this->initializeLanguages();
3085    }
3086
3087    /**
3088     * Traverses the table(s) to be listed and renders the output code for each:
3089     * The HTML is accumulated in $this->HTMLcode
3090     * Finishes off with a stopper-gif
3091     */
3092    public function generateList()
3093    {
3094        // Set page record in header
3095        $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id);
3096        $hideTablesArray = GeneralUtility::trimExplode(',', $this->hideTables);
3097
3098        $backendUser = $this->getBackendUserAuthentication();
3099
3100        // pre-process tables and add sorting instructions
3101        $tableNames = array_flip(array_keys($GLOBALS['TCA']));
3102        foreach ($tableNames as $tableName => &$config) {
3103            $hideTable = false;
3104
3105            // Checking if the table should be rendered:
3106            // Checks that we see only permitted/requested tables:
3107            if ($this->table && $tableName !== $this->table
3108                || $this->tableList && !GeneralUtility::inList($this->tableList, $tableName)
3109                || !$backendUser->check('tables_select', $tableName)
3110            ) {
3111                $hideTable = true;
3112            }
3113
3114            if (!$hideTable) {
3115                // Don't show table if hidden by TCA ctrl section
3116                // Don't show table if hidden by pageTSconfig mod.web_list.hideTables
3117                $hideTable = $hideTable
3118                    || !empty($GLOBALS['TCA'][$tableName]['ctrl']['hideTable'])
3119                    || in_array($tableName, $hideTablesArray, true)
3120                    || in_array('*', $hideTablesArray, true);
3121                // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table
3122                if (isset($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'])) {
3123                    $hideTable = (bool)$this->tableTSconfigOverTCA[$tableName . '.']['hideTable'];
3124                }
3125            }
3126            if ($hideTable) {
3127                unset($tableNames[$tableName]);
3128            } else {
3129                if (isset($this->tableDisplayOrder[$tableName])) {
3130                    // Copy display order information
3131                    $tableNames[$tableName] = $this->tableDisplayOrder[$tableName];
3132                } else {
3133                    $tableNames[$tableName] = [];
3134                }
3135            }
3136        }
3137        unset($config);
3138
3139        $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class)
3140            ->orderByDependencies($tableNames);
3141
3142        foreach ($orderedTableNames as $tableName => $_) {
3143            // check if we are in single- or multi-table mode
3144            if ($this->table) {
3145                $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems'])
3146                    ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems']
3147                    : $this->itemsLimitSingleTable;
3148            } else {
3149                // if there are no records in table continue current foreach
3150                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3151                    ->getQueryBuilderForTable($tableName);
3152                $queryBuilder->getRestrictions()
3153                    ->removeAll()
3154                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3155                    ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
3156                $queryBuilder = $this->addPageIdConstraint($tableName, $queryBuilder);
3157                $firstRow = $queryBuilder->select('uid')
3158                    ->from($tableName)
3159                    ->setMaxResults(1)
3160                    ->execute()
3161                    ->fetch();
3162                if (!is_array($firstRow)) {
3163                    continue;
3164                }
3165                $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxDBListItems'])
3166                    ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxDBListItems']
3167                    : $this->itemsLimitPerTable;
3168            }
3169            if ($this->showLimit) {
3170                $this->iLimit = $this->showLimit;
3171            }
3172            // Setting fields to select:
3173            if ($this->allFields) {
3174                $fields = $this->makeFieldList($tableName);
3175                $fields[] = 'tstamp';
3176                $fields[] = 'crdate';
3177                $fields[] = '_PATH_';
3178                $fields[] = '_CONTROL_';
3179                if (is_array($this->setFields[$tableName])) {
3180                    $fields = array_intersect($fields, $this->setFields[$tableName]);
3181                } else {
3182                    $fields = [];
3183                }
3184            } else {
3185                $fields = [];
3186            }
3187
3188            // Finally, render the list:
3189            $this->HTMLcode .= $this->getTable($tableName, $this->id, implode(',', $fields));
3190        }
3191    }
3192
3193    /**
3194     * Creates the search box
3195     *
3196     * @param bool $formFields If TRUE, the search box is wrapped in its own form-tags
3197     * @return string HTML for the search box
3198     */
3199    public function getSearchBox($formFields = true)
3200    {
3201        /** @var IconFactory $iconFactory */
3202        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
3203        $lang = $this->getLanguageService();
3204        // Setting form-elements, if applicable:
3205        $formElements = ['', ''];
3206        if ($formFields) {
3207            $formElements = [
3208                '<form action="' . htmlspecialchars(
3209                    $this->listURL('', '-1', 'firstElementNumber,search_field')
3210                ) . '" method="post">',
3211                '</form>'
3212            ];
3213        }
3214        // Make level selector:
3215        $opt = [];
3216
3217        // "New" generation of search levels ... based on TS config
3218        $config = BackendUtility::getPagesTSconfig($this->id);
3219        $searchLevelsFromTSconfig = $config['mod.']['web_list.']['searchLevel.']['items.'];
3220        $searchLevelItems = [];
3221
3222        // get translated labels for search levels from pagets
3223        foreach ($searchLevelsFromTSconfig as $keySearchLevel => $labelConfigured) {
3224            $label = $lang->sL('LLL:' . $labelConfigured);
3225            if ($label === '') {
3226                $label = $labelConfigured;
3227            }
3228            $searchLevelItems[$keySearchLevel] = $label;
3229        }
3230
3231        foreach ($searchLevelItems as $kv => $label) {
3232            $opt[] = '<option value="' . $kv . '"' . ($kv === $this->searchLevels ? ' selected="selected"' : '') . '>' . htmlspecialchars(
3233                $label
3234            ) . '</option>';
3235        }
3236        $lMenu = '<select class="form-control" name="search_levels" title="' . htmlspecialchars(
3237            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search_levels')
3238        ) . '" id="search_levels">' . implode('', $opt) . '</select>';
3239        // Table with the search box:
3240        $content = '<div class="db_list-searchbox-form db_list-searchbox-toolbar module-docheader-bar module-docheader-bar-search t3js-module-docheader-bar t3js-module-docheader-bar-search" id="db_list-searchbox-toolbar" style="display: ' . ($this->searchString == '' ? 'none' : 'block') . ';">
3241			' . $formElements[0] . '
3242                <div id="typo3-dblist-search">
3243                    <div class="panel panel-default">
3244                        <div class="panel-body">
3245                            <div class="row">
3246                                <div class="col-sm-6 col-xs-12">
3247                                    <label for="search_field">' . htmlspecialchars(
3248            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.searchString')
3249        ) . '</label>
3250									<input class="form-control" type="search" placeholder="' . htmlspecialchars(
3251            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enterSearchString')
3252        ) . '" title="' . htmlspecialchars(
3253            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchString')
3254        ) . '" name="search_field" id="search_field" value="' . htmlspecialchars($this->searchString) . '" />
3255                                </div>
3256                                <div class="col-xs-12 col-sm-3">
3257									<label for="search_levels">' . htmlspecialchars(
3258            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.search_levels')
3259        ) . '</label>
3260									' . $lMenu . '
3261                                </div>
3262                                <div class="col-xs-12 col-sm-3">
3263									<label for="showLimit">' . htmlspecialchars(
3264            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.limit')
3265        ) . '</label>
3266									<input class="form-control" type="number" min="0" max="10000" placeholder="10" title="' . htmlspecialchars(
3267            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.limit')
3268        ) . '" name="showLimit" id="showLimit" value="' . htmlspecialchars(
3269            ($this->showLimit ? $this->showLimit : '')
3270        ) . '" />
3271                                </div>
3272                                <div class="col-xs-12">
3273                                    <div class="form-control-wrap">
3274                                        <button type="submit" class="btn btn-default" name="search" title="' . htmlspecialchars(
3275            $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search')
3276        ) . '">
3277                                            ' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render(
3278            ) . ' ' . htmlspecialchars(
3279                $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.search')
3280            ) . '
3281                                        </button>
3282                                    </div>
3283                                </div>
3284                            </div>
3285                        </div>
3286                    </div>
3287                </div>
3288			' . $formElements[1] . '</div>';
3289        return $content;
3290    }
3291
3292    /**
3293     * Setting the field names to display in extended list.
3294     * Sets the internal variable $this->setFields
3295     */
3296    public function setDispFields()
3297    {
3298        $backendUser = $this->getBackendUserAuthentication();
3299        // Getting from session:
3300        $dispFields = $backendUser->getModuleData('list/displayFields');
3301        // If fields has been inputted, then set those as the value and push it to session variable:
3302        if (is_array($this->displayFields)) {
3303            reset($this->displayFields);
3304            $tKey = key($this->displayFields);
3305            $dispFields[$tKey] = $this->displayFields[$tKey];
3306            $backendUser->pushModuleData('list/displayFields', $dispFields);
3307        }
3308        // Setting result:
3309        $this->setFields = $dispFields;
3310    }
3311
3312    /**
3313     * Create thumbnail code for record/field
3314     *
3315     * @param mixed[] $row Record array
3316     * @param string $table Table (record is from)
3317     * @param string $field Field name for which thumbnail are to be rendered.
3318     * @return string HTML for thumbnails, if any.
3319     */
3320    public function thumbCode($row, $table, $field)
3321    {
3322        return BackendUtility::thumbCode($row, $table, $field);
3323    }
3324
3325    /**
3326     * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted
3327     * depending on the current searchlevel setting.
3328     *
3329     * @param string $table Table name
3330     * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions
3331     * @param string[] $additionalConstraints Additional part for where clause
3332     * @param string[] $fields Field list to select, * for all
3333     * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder
3334     */
3335    public function getQueryBuilder(
3336        string $table,
3337        int $pageId,
3338        array $additionalConstraints = [],
3339        array $fields = ['*']
3340    ): QueryBuilder {
3341        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3342            ->getQueryBuilderForTable($table);
3343        $queryBuilder->getRestrictions()
3344            ->removeAll()
3345            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3346            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
3347        $queryBuilder
3348            ->select(...$fields)
3349            ->from($table);
3350
3351        if (!empty($additionalConstraints)) {
3352            $queryBuilder->andWhere(...$additionalConstraints);
3353        }
3354
3355        $queryBuilder = $this->prepareQueryBuilder($table, $pageId, $fields, $additionalConstraints, $queryBuilder);
3356
3357        return $queryBuilder;
3358    }
3359
3360    /**
3361     * Return the modified QueryBuilder object ($queryBuilder) which will be
3362     * used to select the records from a table $table with pid = $this->pidList
3363     *
3364     * @param string $table Table name
3365     * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions
3366     * @param string[] $fieldList List of fields to select from the table
3367     * @param string[] $additionalConstraints Additional part for where clause
3368     * @param QueryBuilder $queryBuilder
3369     * @param bool $addSorting
3370     * @return QueryBuilder
3371     */
3372    protected function prepareQueryBuilder(
3373        string $table,
3374        int $pageId,
3375        array $fieldList = ['*'],
3376        array $additionalConstraints = [],
3377        QueryBuilder $queryBuilder,
3378        bool $addSorting = true
3379    ): QueryBuilder {
3380        $parameters = [
3381            'table' => $table,
3382            'fields' => $fieldList,
3383            'groupBy' => null,
3384            'orderBy' => null,
3385            'firstResult' => $this->firstElementNumber ?: null,
3386            'maxResults' => $this->iLimit ?: null
3387        ];
3388
3389        if ($this->iLimit > 0) {
3390            $queryBuilder->setMaxResults($this->iLimit);
3391        }
3392
3393        if ($this->firstElementNumber > 0) {
3394            $queryBuilder->setFirstResult($this->firstElementNumber);
3395        }
3396
3397        if ($addSorting) {
3398            if ($this->sortField && in_array($this->sortField, $this->makeFieldList($table, 1))) {
3399                $queryBuilder->orderBy($this->sortField, $this->sortRev ? 'DESC' : 'ASC');
3400            } else {
3401                $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby'];
3402                $orderBys = QueryHelper::parseOrderBy((string)$orderBy);
3403                foreach ($orderBys as $orderBy) {
3404                    $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]);
3405                }
3406            }
3407        }
3408
3409        // Build the query constraints
3410        $queryBuilder = $this->addPageIdConstraint($table, $queryBuilder);
3411        $searchWhere = $this->makeSearchString($table, $pageId);
3412        if (!empty($searchWhere)) {
3413            $queryBuilder->andWhere($searchWhere);
3414        }
3415
3416        // Filtering on displayable pages (permissions):
3417        if ($table === 'pages' && $this->perms_clause) {
3418            $queryBuilder->andWhere($this->perms_clause);
3419        }
3420
3421        // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set
3422        if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])
3423            && (GeneralUtility::inList($this->hideTranslations, $table) || $this->hideTranslations === '*')
3424        ) {
3425            $queryBuilder->andWhere(
3426                $queryBuilder->expr()->eq(
3427                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3428                    0
3429                )
3430            );
3431        } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) && $this->showOnlyTranslatedRecords) {
3432            // When only translated records should be shown, it is necessary to use l10n_parent=pageId, instead of
3433            // a check to the PID
3434            $queryBuilder->andWhere(
3435                $queryBuilder->expr()->eq(
3436                    $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3437                    $queryBuilder->createNamedParameter(
3438                        $this->id,
3439                        \PDO::PARAM_INT
3440                    )
3441                )
3442            );
3443        }
3444
3445        $hookName = DatabaseRecordList::class;
3446        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'])) {
3447            // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead.
3448            trigger_error('The hook ($GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][' . $hookName . '][\'buildQueryParameters\']) will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead.', E_USER_DEPRECATED);
3449            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'] as $className) {
3450                $hookObject = GeneralUtility::makeInstance($className);
3451                if (method_exists($hookObject, 'buildQueryParametersPostProcess')) {
3452                    $hookObject->buildQueryParametersPostProcess(
3453                        $parameters,
3454                        $table,
3455                        $pageId,
3456                        $additionalConstraints,
3457                        $fieldList,
3458                        $this,
3459                        $queryBuilder
3460                    );
3461                }
3462            }
3463        }
3464        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['modifyQuery'] ?? [] as $className) {
3465            $hookObject = GeneralUtility::makeInstance($className);
3466            if (method_exists($hookObject, 'modifyQuery')) {
3467                $hookObject->modifyQuery(
3468                    $parameters,
3469                    $table,
3470                    $pageId,
3471                    $additionalConstraints,
3472                    $fieldList,
3473                    $queryBuilder
3474                );
3475            }
3476        }
3477
3478        // array_unique / array_filter used to eliminate empty and duplicate constraints
3479        // the array keys are eliminated by this as well to facilitate argument unpacking
3480        // when used with the querybuilder.
3481        // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
3482        if (!empty($parameters['where'])) {
3483            $parameters['where'] = array_unique(array_filter(array_values($parameters['where'])));
3484        }
3485        if (!empty($parameters['where'])) {
3486            $this->logDeprecation('where');
3487            $queryBuilder->where(...$parameters['where']);
3488        }
3489        if (!empty($parameters['orderBy'])) {
3490            $this->logDeprecation('orderBy');
3491            foreach ($parameters['orderBy'] as $fieldNameAndSorting) {
3492                [$fieldName, $sorting] = $fieldNameAndSorting;
3493                $queryBuilder->addOrderBy($fieldName, $sorting);
3494            }
3495        }
3496        if (!empty($parameters['firstResult']) && $parameters['firstResult'] !== $this->firstElementNumber) {
3497            $this->logDeprecation('firstResult');
3498            $queryBuilder->setFirstResult((int)$parameters['firstResult']);
3499        }
3500        if (!empty($parameters['maxResults']) && $parameters['maxResults'] !== $this->iLimit) {
3501            $this->logDeprecation('maxResults');
3502            $queryBuilder->setMaxResults((int)$parameters['maxResults']);
3503        }
3504        if (!empty($parameters['groupBy'])) {
3505            $this->logDeprecation('groupBy');
3506            $queryBuilder->groupBy($parameters['groupBy']);
3507        }
3508
3509        return $queryBuilder;
3510    }
3511
3512    /**
3513     * Executed a query to set $this->totalItems to the number of total
3514     * items, eg. for pagination
3515     *
3516     * @param string $table Table name
3517     * @param int $pageId Only used to build the search constraints, $this->pidList is used for restrictions
3518     * @param array $constraints Additional constraints for where clause
3519     */
3520    public function setTotalItems(string $table, int $pageId, array $constraints)
3521    {
3522        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3523            ->getQueryBuilderForTable($table);
3524
3525        $queryBuilder->getRestrictions()
3526            ->removeAll()
3527            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3528            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
3529        $queryBuilder
3530            ->from($table);
3531
3532        if (!empty($constraints)) {
3533            $queryBuilder->andWhere(...$constraints);
3534        }
3535
3536        $queryBuilder = $this->prepareQueryBuilder($table, $pageId, ['*'], $constraints, $queryBuilder, false);
3537        // Reset limit and offset for full count query
3538        $queryBuilder->setFirstResult(0);
3539        $queryBuilder->setMaxResults(1);
3540
3541        $this->totalItems = (int)$queryBuilder->count('*')
3542            ->execute()
3543            ->fetchColumn();
3544    }
3545
3546    /**
3547     * Creates part of query for searching after a word ($this->searchString)
3548     * fields in input table.
3549     *
3550     * @param string $table Table, in which the fields are being searched.
3551     * @param int $currentPid Page id for the possible search limit. -1 only if called from an old XCLASS.
3552     * @return string Returns part of WHERE-clause for searching, if applicable.
3553     */
3554    public function makeSearchString($table, $currentPid = -1)
3555    {
3556        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3557        $expressionBuilder = $queryBuilder->expr();
3558        $constraints = [];
3559        $currentPid = (int)$currentPid;
3560        $tablePidField = $table === 'pages' ? 'uid' : 'pid';
3561        // Make query only if table is valid and a search string is actually defined
3562        if (empty($this->searchString)) {
3563            return '';
3564        }
3565
3566        $searchableFields = $this->getSearchFields($table);
3567        if (MathUtility::canBeInterpretedAsInteger($this->searchString)) {
3568            $constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString);
3569            foreach ($searchableFields as $fieldName) {
3570                if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
3571                    continue;
3572                }
3573                $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
3574                $fieldType = $fieldConfig['type'];
3575                $evalRules = $fieldConfig['eval'] ?: '';
3576                if ($fieldType === 'input' && $evalRules && GeneralUtility::inList($evalRules, 'int')) {
3577                    if (!isset($fieldConfig['search']['pidonly'])
3578                        || ($fieldConfig['search']['pidonly'] && $currentPid > 0)
3579                    ) {
3580                        $constraints[] = $expressionBuilder->andX(
3581                            $expressionBuilder->eq($fieldName, (int)$this->searchString),
3582                            $expressionBuilder->eq($tablePidField, (int)$currentPid)
3583                        );
3584                    }
3585                } elseif ($fieldType === 'text'
3586                    || $fieldType === 'flex'
3587                    || ($fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules)))
3588                ) {
3589                    $constraints[] = $expressionBuilder->like(
3590                        $fieldName,
3591                        $queryBuilder->quote('%' . (int)$this->searchString . '%')
3592                    );
3593                }
3594            }
3595        } elseif (!empty($searchableFields)) {
3596            $like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%');
3597            foreach ($searchableFields as $fieldName) {
3598                if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) {
3599                    continue;
3600                }
3601                $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config'];
3602                $fieldType = $fieldConfig['type'];
3603                $evalRules = $fieldConfig['eval'] ?: '';
3604                $searchConstraint = $expressionBuilder->andX(
3605                    $expressionBuilder->comparison(
3606                        'LOWER(' . $queryBuilder->castFieldToTextType($fieldName) . ')',
3607                        'LIKE',
3608                        'LOWER(' . $like . ')'
3609                    )
3610                );
3611                if (is_array($fieldConfig['search'])) {
3612                    $searchConfig = $fieldConfig['search'];
3613                    if (in_array('case', $searchConfig)) {
3614                        // Replace case insensitive default constraint
3615                        $searchConstraint = $expressionBuilder->andX($expressionBuilder->like($fieldName, $like));
3616                    }
3617                    if (in_array('pidonly', $searchConfig) && $currentPid > 0) {
3618                        $searchConstraint->add($expressionBuilder->eq($tablePidField, (int)$currentPid));
3619                    }
3620                    if ($searchConfig['andWhere']) {
3621                        $searchConstraint->add(
3622                            QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere'])
3623                        );
3624                    }
3625                }
3626                if ($fieldType === 'text'
3627                    || $fieldType === 'flex'
3628                    || $fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules))
3629                ) {
3630                    if ($searchConstraint->count() !== 0) {
3631                        $constraints[] = $searchConstraint;
3632                    }
3633                }
3634            }
3635        }
3636        // Call hook to add or change the list
3637        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][DatabaseRecordList::class]['makeSearchStringConstraints'] ?? [] as $className) {
3638            $hookObject = GeneralUtility::makeInstance($className);
3639            if (method_exists($hookObject, 'makeSearchStringConstraints')) {
3640                $constraints = $hookObject->makeSearchStringConstraints(
3641                    $queryBuilder,
3642                    $constraints,
3643                    $this->searchString,
3644                    $table,
3645                    $currentPid
3646                );
3647            }
3648        }
3649        // If no search field conditions have been built ensure no results are returned
3650        if (empty($constraints)) {
3651            return '0=1';
3652        }
3653
3654        return $expressionBuilder->orX(...$constraints);
3655    }
3656
3657    /**
3658     * Fetches a list of fields to use in the Backend search for the given table.
3659     *
3660     * @param string $tableName
3661     * @return string[]
3662     */
3663    protected function getSearchFields($tableName)
3664    {
3665        $fieldArray = [];
3666        $fieldListWasSet = false;
3667        // Get fields from ctrl section of TCA first
3668        if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) {
3669            $fieldArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true);
3670            $fieldListWasSet = true;
3671        }
3672        // Call hook to add or change the list
3673        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'])) {
3674            $hookParameters = [
3675                'tableHasSearchConfiguration' => $fieldListWasSet,
3676                'tableName' => $tableName,
3677                'searchFields' => &$fieldArray,
3678                'searchString' => $this->searchString
3679            ];
3680            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'] as $hookFunction) {
3681                GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
3682            }
3683        }
3684        return $fieldArray;
3685    }
3686
3687    /**
3688     * Returns the title (based on $code) of a table ($table) with the proper link around. For headers over tables.
3689     * The link will cause the display of all extended mode or not for the table.
3690     *
3691     * @param string $table Table name
3692     * @param string $code Table label
3693     * @return string The linked table label
3694     */
3695    public function linkWrapTable($table, $code)
3696    {
3697        if ($this->table !== $table) {
3698            return '<a href="' . htmlspecialchars(
3699                $this->listURL('', $table, 'firstElementNumber')
3700            ) . '">' . $code . '</a>';
3701        }
3702        return '<a href="' . htmlspecialchars(
3703            $this->listURL('', '', 'sortField,sortRev,table,firstElementNumber')
3704        ) . '">' . $code . '</a>';
3705    }
3706
3707    /**
3708     * Returns the title (based on $code) of a record (from table $table) with the proper link around (that is for 'pages'-records a link to the level of that record...)
3709     *
3710     * @param string $table Table name
3711     * @param int $uid Item uid
3712     * @param string $code Item title (not htmlspecialchars()'ed yet)
3713     * @param mixed[] $row Item row
3714     * @return string The item title. Ready for HTML output (is htmlspecialchars()'ed)
3715     */
3716    public function linkWrapItems($table, $uid, $code, $row)
3717    {
3718        $lang = $this->getLanguageService();
3719        $origCode = $code;
3720        // If the title is blank, make a "no title" label:
3721        if ((string)$code === '') {
3722            $code = '<i>[' . htmlspecialchars(
3723                $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')
3724            ) . ']</i> - '
3725                . htmlspecialchars(BackendUtility::getRecordTitle($table, $row));
3726        } else {
3727            $code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8', false);
3728            if ($code != htmlspecialchars($origCode)) {
3729                $code = '<span title="' . htmlspecialchars(
3730                    $origCode,
3731                    ENT_QUOTES,
3732                    'UTF-8',
3733                    false
3734                ) . '">' . $code . '</span>';
3735            } else {
3736                $code = '<span title="' . $code . '">' . $code . '</span>';
3737            }
3738        }
3739        switch ((string)$this->clickTitleMode) {
3740            case 'edit':
3741                // If the listed table is 'pages' we have to request the permission settings for each page:
3742                if ($table === 'pages') {
3743                    $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(
3744                        BackendUtility::getRecord('pages', $row['uid'])
3745                    );
3746                    $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
3747                } else {
3748                    $backendUser = $this->getBackendUserAuthentication();
3749                    $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT && $backendUser->recordEditAccessInternals($table, $row);
3750                }
3751                // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
3752                if ($permsEdit && $this->isEditable($table)) {
3753                    $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit';
3754                    $code = '<a href="#" onclick="' . htmlspecialchars(
3755                        BackendUtility::editOnClick($params, '', -1)
3756                    ) . '" title="' . htmlspecialchars($lang->getLL('edit')) . '">' . $code . '</a>';
3757                }
3758                break;
3759            case 'show':
3760                // "Show" link (only pages and tt_content elements)
3761                if ($table === 'pages' || $table === 'tt_content') {
3762                    $onClick = $this->getOnClickForRow($table, $row);
3763                    $code = '<a href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars(
3764                        $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')
3765                    ) . '">' . $code . '</a>';
3766                }
3767                break;
3768            case 'info':
3769                // "Info": (All records)
3770                $code = '<a href="#" onclick="' . htmlspecialchars(
3771                    'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;'
3772                ) . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>';
3773                break;
3774            default:
3775                // Output the label now:
3776                if ($table === 'pages') {
3777                    $code = '<a href="' . htmlspecialchars(
3778                        $this->listURL($uid, '', 'firstElementNumber')
3779                    ) . '" onclick="setHighlight(' . (int)$uid . ')">' . $code . '</a>';
3780                } else {
3781                    $code = $this->linkUrlMail($code, $origCode);
3782                }
3783        }
3784        return $code;
3785    }
3786
3787    /**
3788     * Wrapping input code in link to URL or email if $testString is either.
3789     *
3790     * @param string $code code to wrap
3791     * @param string $testString String which is tested for being a URL or email and which will be used for the link if so.
3792     * @return string Link-Wrapped $code value, if $testString was URL or email.
3793     */
3794    public function linkUrlMail($code, $testString)
3795    {
3796        // Check for URL:
3797        $scheme = parse_url($testString, PHP_URL_SCHEME);
3798        if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') {
3799            return '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
3800        }
3801        // Check for email:
3802        if (GeneralUtility::validEmail($testString)) {
3803            return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
3804        }
3805        // Return if nothing else...
3806        return $code;
3807    }
3808
3809    /**
3810     * Creates the URL to this script, including all relevant GPvars
3811     * Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit
3812     * The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $exclList variable.
3813     *
3814     * @param string $altId Alternative id value. Enter blank string for the current id ($this->id)
3815     * @param string $table Table name to display. Enter "-1" for the current table.
3816     * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber")
3817     * @return string URL
3818     */
3819    public function listURL($altId = '', $table = '-1', $exclList = '')
3820    {
3821        $urlParameters = [];
3822        if ((string)$altId !== '') {
3823            $urlParameters['id'] = $altId;
3824        } else {
3825            $urlParameters['id'] = $this->id;
3826        }
3827        if ($table === '-1') {
3828            $urlParameters['table'] = $this->table;
3829        } else {
3830            $urlParameters['table'] = $table;
3831        }
3832        if ($this->thumbs) {
3833            $urlParameters['imagemode'] = $this->thumbs;
3834        }
3835        if ($this->returnUrl) {
3836            $urlParameters['returnUrl'] = $this->returnUrl;
3837        }
3838        if ((!$exclList || !GeneralUtility::inList($exclList, 'search_field')) && $this->searchString) {
3839            $urlParameters['search_field'] = $this->searchString;
3840        }
3841        if ($this->searchLevels) {
3842            $urlParameters['search_levels'] = $this->searchLevels;
3843        }
3844        if ($this->showLimit) {
3845            $urlParameters['showLimit'] = $this->showLimit;
3846        }
3847        if ((!$exclList || !GeneralUtility::inList($exclList, 'firstElementNumber')) && $this->firstElementNumber) {
3848            $urlParameters['pointer'] = $this->firstElementNumber;
3849        }
3850        if ((!$exclList || !GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) {
3851            $urlParameters['sortField'] = $this->sortField;
3852        }
3853        if ((!$exclList || !GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) {
3854            $urlParameters['sortRev'] = $this->sortRev;
3855        }
3856
3857        $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters);
3858
3859        if ($routePath = GeneralUtility::_GP('route')) {
3860            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
3861            $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
3862        } else {
3863            $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?');
3864        }
3865        return $url;
3866    }
3867
3868    /**
3869     * Returns "requestUri" - which is basically listURL
3870     * @return string Content of ->listURL()
3871     */
3872    public function requestUri()
3873    {
3874        return $this->listURL();
3875    }
3876
3877    /**
3878     * Makes the list of fields to select for a table
3879     *
3880     * @param string $table Table name
3881     * @param bool $dontCheckUser If set, users access to the field (non-exclude-fields) is NOT checked.
3882     * @param bool $addDateFields If set, also adds crdate and tstamp fields (note: they will also be added if user is admin or dontCheckUser is set)
3883     * @return string[] Array, where values are fieldnames to include in query
3884     */
3885    public function makeFieldList($table, $dontCheckUser = false, $addDateFields = false)
3886    {
3887        $backendUser = $this->getBackendUserAuthentication();
3888        // Init fieldlist array:
3889        $fieldListArr = [];
3890        // Check table:
3891        if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['columns']) && is_array(
3892            $GLOBALS['TCA'][$table]['columns']
3893        )) {
3894            if (isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) {
3895                // Traverse configured columns and add them to field array, if available for user.
3896                foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fieldValue) {
3897                    if ($fieldValue['config']['type'] === 'none') {
3898                        // Never render or fetch type=none fields from db
3899                        continue;
3900                    }
3901                    if ($dontCheckUser
3902                        || (!$fieldValue['exclude'] || $backendUser->check('non_exclude_fields', $table . ':' . $fN)) && $fieldValue['config']['type'] !== 'passthrough'
3903                    ) {
3904                        $fieldListArr[] = $fN;
3905                    }
3906                }
3907
3908                $fieldListArr[] = 'uid';
3909                $fieldListArr[] = 'pid';
3910
3911                // Add date fields
3912                if ($dontCheckUser || $backendUser->isAdmin() || $addDateFields) {
3913                    if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
3914                        $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp'];
3915                    }
3916                    if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
3917                        $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['crdate'];
3918                    }
3919                }
3920                // Add more special fields:
3921                if ($dontCheckUser || $backendUser->isAdmin()) {
3922                    if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
3923                        $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id'];
3924                    }
3925                    if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
3926                        $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
3927                    }
3928                    if (ExtensionManagementUtility::isLoaded(
3929                        'workspaces'
3930                    ) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
3931                        $fieldListArr[] = 't3ver_id';
3932                        $fieldListArr[] = 't3ver_state';
3933                        $fieldListArr[] = 't3ver_wsid';
3934                    }
3935                }
3936            } else {
3937                GeneralUtility::makeInstance(LogManager::class)
3938                    ->getLogger(__CLASS__)
3939                    ->error('TCA is broken for the table "' . $table . '": no required "columns" entry in TCA.');
3940            }
3941        }
3942        return $fieldListArr;
3943    }
3944
3945    /**
3946     * Redirects to FormEngine if a record is just localized.
3947     *
3948     * @param string $justLocalized String with table, orig uid and language separated by ":
3949     */
3950    public function localizationRedirect($justLocalized)
3951    {
3952        [$table, $orig_uid, $language] = explode(':', $justLocalized);
3953        if ($GLOBALS['TCA'][$table]
3954            && $GLOBALS['TCA'][$table]['ctrl']['languageField']
3955            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
3956        ) {
3957            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
3958            $queryBuilder->getRestrictions()
3959                ->removeAll()
3960                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
3961                ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
3962
3963            $localizedRecordUid = $queryBuilder->select('uid')
3964                ->from($table)
3965                ->where(
3966                    $queryBuilder->expr()->eq(
3967                        $GLOBALS['TCA'][$table]['ctrl']['languageField'],
3968                        $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)
3969                    ),
3970                    $queryBuilder->expr()->eq(
3971                        $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
3972                        $queryBuilder->createNamedParameter($orig_uid, \PDO::PARAM_INT)
3973                    )
3974                )
3975                ->setMaxResults(1)
3976                ->execute()
3977                ->fetchColumn();
3978
3979            if ($localizedRecordUid !== false) {
3980                // Create parameters and finally run the classic page module for creating a new page translation
3981                $url = $this->listURL();
3982                /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
3983                $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
3984
3985                $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute(
3986                    'record_edit',
3987                    [
3988                        'edit[' . $table . '][' . $localizedRecordUid . ']' => 'edit',
3989                        'returnUrl' => $url
3990                    ]
3991                );
3992                HttpUtility::redirect($editUserAccountUrl);
3993            }
3994        }
3995    }
3996
3997    /**
3998     * Set URL parameters to override or add in the listUrl() method.
3999     *
4000     * @param string[] $urlParameters
4001     */
4002    public function setOverrideUrlParameters(array $urlParameters)
4003    {
4004        $currentUrlParameter = GeneralUtility::_GP('curUrl');
4005        if (isset($currentUrlParameter['url'])) {
4006            $urlParameters['P']['currentValue'] = $currentUrlParameter['url'];
4007        }
4008        $this->overrideUrlParameters = $urlParameters;
4009    }
4010
4011    /**
4012     * Set table display order information
4013     *
4014     * Structure of $orderInformation:
4015     *   'tableName' => [
4016     *      'before' => // comma-separated string list or array of table names
4017     *      'after' => // comma-separated string list or array of table names
4018     * ]
4019     *
4020     * @param array $orderInformation
4021     * @throws \UnexpectedValueException
4022     */
4023    public function setTableDisplayOrder(array $orderInformation)
4024    {
4025        foreach ($orderInformation as $tableName => &$configuration) {
4026            if (isset($configuration['before'])) {
4027                if (is_string($configuration['before'])) {
4028                    $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true);
4029                } elseif (!is_array($configuration['before'])) {
4030                    throw new \UnexpectedValueException(
4031                        'The specified "before" order configuration for table "' . $tableName . '" is invalid.',
4032                        1504793406
4033                    );
4034                }
4035            }
4036            if (isset($configuration['after'])) {
4037                if (is_string($configuration['after'])) {
4038                    $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true);
4039                } elseif (!is_array($configuration['after'])) {
4040                    throw new \UnexpectedValueException(
4041                        'The specified "after" order configuration for table "' . $tableName . '" is invalid.',
4042                        1504793407
4043                    );
4044                }
4045            }
4046        }
4047        $this->tableDisplayOrder = $orderInformation;
4048    }
4049
4050    /**
4051     * @return array
4052     */
4053    public function getOverridePageIdList(): array
4054    {
4055        return $this->overridePageIdList;
4056    }
4057
4058    /**
4059     * @param int[]|array $overridePageIdList
4060     */
4061    public function setOverridePageIdList(array $overridePageIdList)
4062    {
4063        $this->overridePageIdList = array_map('intval', $overridePageIdList);
4064    }
4065
4066    /**
4067     * Get all allowed mount pages to be searched in.
4068     *
4069     * @param int $id Page id
4070     * @param int $depth Depth to go down
4071     * @param string $perms_clause select clause
4072     * @return int[]
4073     */
4074    protected function getSearchableWebmounts($id, $depth, $perms_clause)
4075    {
4076        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
4077        $hash = 'webmounts_list' . md5($id . '-' . $depth . '-' . $perms_clause);
4078        if ($runtimeCache->has($hash)) {
4079            $idList = $runtimeCache->get($hash);
4080        } else {
4081            $backendUser = $this->getBackendUserAuthentication();
4082            /** @var PageTreeView $tree */
4083            $tree = GeneralUtility::makeInstance(PageTreeView::class);
4084            $tree->init('AND ' . $perms_clause);
4085            $tree->makeHTML = 0;
4086            $tree->fieldArray = ['uid', 'php_tree_stop'];
4087            $idList = [];
4088
4089            $allowedMounts = !$backendUser->isAdmin() && $id === 0
4090                ? $backendUser->returnWebmounts()
4091                : [$id];
4092
4093            foreach ($allowedMounts as $allowedMount) {
4094                $idList[] = $allowedMount;
4095                if ($depth) {
4096                    $tree->getTree($allowedMount, $depth, '');
4097                }
4098                $idList = array_merge($idList, $tree->ids);
4099            }
4100            $runtimeCache->set($hash, $idList);
4101        }
4102
4103        return $idList;
4104    }
4105
4106    /**
4107     * Add conditions to the QueryBuilder object ($queryBuilder) to limit a
4108     * query to a list of page IDs based on the current search level setting.
4109     *
4110     * @param string $tableName
4111     * @param QueryBuilder $queryBuilder
4112     * @return QueryBuilder Modified QueryBuilder object
4113     */
4114    protected function addPageIdConstraint(string $tableName, QueryBuilder $queryBuilder): QueryBuilder
4115    {
4116        // Set search levels:
4117        $searchLevels = $this->searchLevels;
4118
4119        // Set search levels to 999 instead of -1 as the following methods
4120        // do not support -1 as valid value for infinite search.
4121        if ($searchLevels === -1) {
4122            $searchLevels = 999;
4123        }
4124
4125        // When querying translated pages, the PID of the translated pages should be the same as the
4126        // the PID of the current page
4127        if ($tableName === 'pages' && $this->showOnlyTranslatedRecords) {
4128            $queryBuilder->andWhere(
4129                $queryBuilder->expr()->eq(
4130                    $tableName . '.pid',
4131                    $queryBuilder->createNamedParameter($this->pageRecord['pid'], \PDO::PARAM_INT)
4132                )
4133            );
4134        } elseif ($searchLevels === 0) {
4135            $queryBuilder->andWhere(
4136                $queryBuilder->expr()->eq(
4137                    $tableName . '.pid',
4138                    $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
4139                )
4140            );
4141        } elseif ($searchLevels > 0) {
4142            $allowedMounts = $this->getSearchableWebmounts($this->id, $searchLevels, $this->perms_clause);
4143            $queryBuilder->andWhere(
4144                $queryBuilder->expr()->in(
4145                    $tableName . '.pid',
4146                    $queryBuilder->createNamedParameter($allowedMounts, Connection::PARAM_INT_ARRAY)
4147                )
4148            );
4149        }
4150
4151        if (!empty($this->getOverridePageIdList())) {
4152            $queryBuilder->andWhere(
4153                $queryBuilder->expr()->in(
4154                    $tableName . '.pid',
4155                    $queryBuilder->createNamedParameter($this->getOverridePageIdList(), Connection::PARAM_INT_ARRAY)
4156                )
4157            );
4158        }
4159
4160        return $queryBuilder;
4161    }
4162
4163    /**
4164     * Method used to log deprecated usage of old buildQueryParametersPostProcess hook arguments
4165     *
4166     * @param string $index
4167     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 - see method usages
4168     */
4169    protected function logDeprecation(string $index)
4170    {
4171        trigger_error('[index: ' . $index . '] $parameters in "buildQueryParameters"-Hook will be removed in TYPO3 v10.0, use $queryBuilder instead.', E_USER_DEPRECATED);
4172    }
4173
4174    /**
4175     * @return BackendUserAuthentication
4176     */
4177    protected function getBackendUserAuthentication()
4178    {
4179        return $GLOBALS['BE_USER'];
4180    }
4181
4182    /**
4183     * Returns a table-row with the content from the fields in the input data array.
4184     * OBS: $this->fieldArray MUST be set! (represents the list of fields to display)
4185     *
4186     * @param int $h Is an integer >=0 and denotes how tall an element is. Set to '0' makes a halv line, -1 = full line, set to 1 makes a 'join' and above makes 'line'
4187     * @param string $icon Is the <img>+<a> of the record. If not supplied the first 'join'-icon will be a 'line' instead
4188     * @param array $data Is the dataarray, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags
4189     * @param string $rowParams Is insert in the <tr>-tags. Must carry a ' ' as first character
4190     * @param string $_ OBSOLETE - NOT USED ANYMORE. $lMargin is the leftMargin (int)
4191     * @param string $_2 OBSOLETE - NOT USED ANYMORE. Is the HTML <img>-tag for an alternative 'gfx/ol/line.gif'-icon (used in the top)
4192     * @param string $colType Defines the tag being used for the columns. Default is td.
4193     *
4194     * @return string HTML content for the table row
4195     */
4196    public function addElement($h, $icon, $data, $rowParams = '', $_ = '', $_2 = '', $colType = 'td')
4197    {
4198        $colType = ($colType === 'th') ? 'th' : 'td';
4199        $noWrap = $this->no_noWrap ? '' : ' nowrap';
4200        // Start up:
4201        $l10nParent = isset($data['_l10nparent_']) ? (int)$data['_l10nparent_'] : 0;
4202        $out = '
4203		<!-- Element, begin: -->
4204		<tr ' . $rowParams . ' data-uid="' . (int)$data['uid'] . '" data-l10nparent="' . $l10nParent . '">';
4205        // Show icon and lines
4206        if ($this->showIcon) {
4207            $out .= '
4208			<' . $colType . ' class="col-icon nowrap">';
4209            if (!$h) {
4210                $out .= '&nbsp;';
4211            } else {
4212                for ($a = 0; $a < $h; $a++) {
4213                    if (!$a) {
4214                        if ($icon) {
4215                            $out .= $icon;
4216                        }
4217                    }
4218                }
4219            }
4220            $out .= '</' . $colType . '>
4221			';
4222        }
4223        // Init rendering.
4224        $colsp = '';
4225        $lastKey = '';
4226        $c = 0;
4227        $ccount = 0;
4228        // __label is used as the label key to circumvent problems with uid used as label (see #67756)
4229        // as it was introduced later on, check if it really exists before using it
4230        $fields = $this->fieldArray;
4231        if ($colType === 'td' && array_key_exists('__label', $data)) {
4232            $fields[0] = '__label';
4233        }
4234        // Traverse field array which contains the data to present:
4235        foreach ($fields as $vKey) {
4236            if (isset($data[$vKey])) {
4237                if ($lastKey) {
4238                    $cssClass = $this->addElement_tdCssClass[$lastKey];
4239                    if ($this->oddColumnsCssClass && $ccount % 2 == 0) {
4240                        $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]);
4241                    }
4242                    $out .= '
4243						<' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . '</' . $colType . '>';
4244                }
4245                $lastKey = $vKey;
4246                $c = 1;
4247                $ccount++;
4248            } else {
4249                if (!$lastKey) {
4250                    $lastKey = $vKey;
4251                }
4252                $c++;
4253            }
4254            if ($c > 1) {
4255                $colsp = ' colspan="' . $c . '"';
4256            } else {
4257                $colsp = '';
4258            }
4259        }
4260        if ($lastKey) {
4261            $cssClass = $this->addElement_tdCssClass[$lastKey];
4262            if ($this->oddColumnsCssClass) {
4263                $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]);
4264            }
4265            $out .= '
4266				<' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . '</' . $colType . '>';
4267        }
4268        // End row
4269        $out .= '
4270		</tr>';
4271        // Return row.
4272        return $out;
4273    }
4274
4275    /**
4276     * Dummy function, used to write the top of a table listing.
4277     */
4278    public function writeTop()
4279    {
4280    }
4281
4282    /**
4283     * Creates a forward/reverse button based on the status of ->eCounter, ->firstElementNumber, ->iLimit
4284     *
4285     * @param string $table Table name
4286     * @return array array([boolean], [HTML]) where [boolean] is 1 for reverse element, [HTML] is the table-row code for the element
4287     */
4288    public function fwd_rwd_nav($table = '')
4289    {
4290        $code = '';
4291        if ($this->eCounter >= $this->firstElementNumber && $this->eCounter < $this->firstElementNumber + $this->iLimit) {
4292            if ($this->firstElementNumber && $this->eCounter == $this->firstElementNumber) {
4293                // 	Reverse
4294                $theData = [];
4295                $titleCol = $this->fieldArray[0];
4296                $theData[$titleCol] = $this->fwd_rwd_HTML('fwd', $this->eCounter, $table);
4297                $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"');
4298            }
4299            return [1, $code];
4300        }
4301        if ($this->eCounter == $this->firstElementNumber + $this->iLimit) {
4302            // 	Forward
4303            $theData = [];
4304            $titleCol = $this->fieldArray[0];
4305            $theData[$titleCol] = $this->fwd_rwd_HTML('rwd', $this->eCounter, $table);
4306            $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"');
4307        }
4308        return [0, $code];
4309    }
4310
4311    /**
4312     * Creates the button with link to either forward or reverse
4313     *
4314     * @param string $type Type: "fwd" or "rwd
4315     * @param int $pointer Pointer
4316     * @param string $table Table name
4317     * @return string
4318     * @internal
4319     */
4320    public function fwd_rwd_HTML($type, $pointer, $table = '')
4321    {
4322        $content = '';
4323        $tParam = $table ? '&table=' . rawurlencode($table) : '';
4324        switch ($type) {
4325            case 'fwd':
4326                $href = $this->listURL() . '&pointer=' . ($pointer - $this->iLimit) . $tParam;
4327                $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
4328                    'actions-move-up',
4329                    Icon::SIZE_SMALL
4330                )->render() . '</a> <i>[' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']</i>';
4331                break;
4332            case 'rwd':
4333                $href = $this->listURL() . '&pointer=' . $pointer . $tParam;
4334                $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon(
4335                    'actions-move-down',
4336                    Icon::SIZE_SMALL
4337                )->render() . '</a> <i>[' . ($pointer + 1) . ' - ' . $this->totalItems . ']</i>';
4338                break;
4339        }
4340        return $content;
4341    }
4342
4343    /**
4344     * Returning JavaScript for ClipBoard functionality.
4345     *
4346     * @return string
4347     */
4348    public function CBfunctions()
4349    {
4350        return '
4351		// checkOffCB()
4352	function checkOffCB(listOfCBnames, link) {	//
4353		var checkBoxes, flag, i;
4354		var checkBoxes = listOfCBnames.split(",");
4355		if (link.rel === "") {
4356			link.rel = "allChecked";
4357			flag = true;
4358		} else {
4359			link.rel = "";
4360			flag = false;
4361		}
4362		for (i = 0; i < checkBoxes.length; i++) {
4363			setcbValue(checkBoxes[i], flag);
4364		}
4365	}
4366		// cbValue()
4367	function cbValue(CBname) {	//
4368		var CBfullName = "CBC["+CBname+"]";
4369		return (document.dblistForm[CBfullName] && document.dblistForm[CBfullName].checked ? 1 : 0);
4370	}
4371		// setcbValue()
4372	function setcbValue(CBname,flag) {	//
4373		CBfullName = "CBC["+CBname+"]";
4374		if(document.dblistForm[CBfullName]) {
4375			document.dblistForm[CBfullName].checked = flag ? "on" : 0;
4376		}
4377	}
4378
4379		';
4380    }
4381
4382    /**
4383     * Initializes page languages and icons
4384     */
4385    public function initializeLanguages()
4386    {
4387        // Look up page overlays:
4388        $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
4389        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'];
4390        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
4391            ->getQueryBuilderForTable('pages');
4392        $queryBuilder->getRestrictions()
4393            ->removeAll()
4394            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
4395            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
4396        $result = $queryBuilder
4397            ->select('*')
4398            ->from('pages')
4399            ->where(
4400                $queryBuilder->expr()->andX(
4401                    $queryBuilder->expr()->eq($localizationParentField, $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
4402                    $queryBuilder->expr()->gt(
4403                        $languageField,
4404                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
4405                    )
4406                )
4407            )
4408            ->execute();
4409
4410        $this->pageOverlays = [];
4411        while ($row = $result->fetch()) {
4412            $this->pageOverlays[$row[$languageField]] = $row;
4413        }
4414
4415        $this->languageIconTitles = $this->getTranslateTools()->getSystemLanguages($this->id);
4416    }
4417
4418    /**
4419     * Return the icon for the language
4420     *
4421     * @param int $sys_language_uid Sys language uid
4422     * @param bool $addAsAdditionalText If set to true, only the flag is returned
4423     * @return string Language icon
4424     */
4425    public function languageFlag($sys_language_uid, $addAsAdditionalText = true)
4426    {
4427        $out = '';
4428        $title = htmlspecialchars($this->languageIconTitles[$sys_language_uid]['title']);
4429        if ($this->languageIconTitles[$sys_language_uid]['flagIcon']) {
4430            $out .= '<span title="' . $title . '">' . $this->iconFactory->getIcon(
4431                $this->languageIconTitles[$sys_language_uid]['flagIcon'],
4432                Icon::SIZE_SMALL
4433            )->render() . '</span>';
4434            if (!$addAsAdditionalText) {
4435                return $out;
4436            }
4437            $out .= '&nbsp;';
4438        }
4439        $out .= $title;
4440        return $out;
4441    }
4442
4443    /**
4444     * Sets the script url depending on being a module or script request
4445     */
4446    protected function determineScriptUrl()
4447    {
4448        if ($routePath = GeneralUtility::_GP('route')) {
4449            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
4450            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
4451        } else {
4452            $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
4453        }
4454    }
4455
4456    /**
4457     * @return string
4458     */
4459    protected function getThisScript()
4460    {
4461        return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
4462    }
4463
4464    /**
4465     * Gets an instance of TranslationConfigurationProvider
4466     *
4467     * @return TranslationConfigurationProvider
4468     */
4469    protected function getTranslateTools()
4470    {
4471        if (!isset($this->translateTools)) {
4472            $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
4473        }
4474        return $this->translateTools;
4475    }
4476
4477    /**
4478     * Generates HTML code for a Reference tooltip out of
4479     * sys_refindex records you hand over
4480     *
4481     * @param int $references number of records from sys_refindex table
4482     * @param string $launchViewParameter JavaScript String, which will be passed as parameters to top.TYPO3.InfoWindow.showItem
4483     * @return string
4484     */
4485    protected function generateReferenceToolTip($references, $launchViewParameter = '')
4486    {
4487        if (!$references) {
4488            $htmlCode = '-';
4489        } else {
4490            $htmlCode = '<a href="#"';
4491            if ($launchViewParameter !== '') {
4492                $htmlCode .= ' onclick="' . htmlspecialchars(
4493                    'top.TYPO3.InfoWindow.showItem(' . $launchViewParameter . '); return false;'
4494                ) . '"';
4495            }
4496            $htmlCode .= ' title="' . htmlspecialchars(
4497                $this->getLanguageService()->sL(
4498                    'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references'
4499                ) . ' (' . $references . ')'
4500            ) . '">';
4501            $htmlCode .= $references;
4502            $htmlCode .= '</a>';
4503        }
4504        return $htmlCode;
4505    }
4506
4507    /**
4508     * If enabled, only translations are shown (= only with l10n_parent)
4509     * See the use case in RecordList class, where a list of page translations is rendered before.
4510     *
4511     * @param bool $showOnlyTranslatedRecords
4512     */
4513    public function showOnlyTranslatedRecords(bool $showOnlyTranslatedRecords)
4514    {
4515        $this->showOnlyTranslatedRecords = $showOnlyTranslatedRecords;
4516    }
4517
4518    /**
4519     * Flatten palettes into types showitem
4520     *
4521     * By replacing the palettes in showitem of a TCA type with each palette content, the fields within a palette
4522     * can be considered as visible database columns for a backend form.
4523     *
4524     * @param array $tableTCA
4525     * @param string $type
4526     * @return string
4527     */
4528    protected function getVisibleColumns(array $tableTCA, string $type)
4529    {
4530        $visibleColumns = $tableTCA['types'][$type]['showitem'] ?? '';
4531
4532        if (strpos($visibleColumns, '--palette--') !== false) {
4533            $matches = [];
4534            preg_match_all('/--palette--\s*;[^;]*;\s*(\w+)/', $visibleColumns, $matches, PREG_SET_ORDER);
4535            if (!empty($matches)) {
4536                foreach ($matches as $palette) {
4537                    $paletteColumns = $tableTCA['palettes'][$palette[1]]['showitem'] ?? '';
4538                    $paletteColumns = rtrim($paletteColumns, ", \t\r\n");
4539                    $visibleColumns = str_replace($palette[0], $paletteColumns, $visibleColumns);
4540                }
4541            }
4542        }
4543
4544        return $visibleColumns;
4545    }
4546
4547    /**
4548     * Returns the language service
4549     * @return LanguageService
4550     */
4551    protected function getLanguageService()
4552    {
4553        return $GLOBALS['LANG'];
4554    }
4555
4556    /**
4557     * @param array $languagesAllowedForUser
4558     * @return DatabaseRecordList
4559     */
4560    public function setLanguagesAllowedForUser(array $languagesAllowedForUser): DatabaseRecordList
4561    {
4562        $this->languagesAllowedForUser = $languagesAllowedForUser;
4563        return $this;
4564    }
4565}
4566