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