PageLayoutView::tt_content_drawItem()   B
last analyzed

Complexity

Conditions 7
Paths 25

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 25
nop 1
dl 0
loc 35
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\View;
17
18
use Doctrine\DBAL\ForwardCompatibility\Result;
19
use Doctrine\DBAL\Statement;
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerAwareTrait;
23
use TYPO3\CMS\Backend\Clipboard\Clipboard;
24
use TYPO3\CMS\Backend\Controller\Page\LocalizationController;
25
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
26
use TYPO3\CMS\Backend\Routing\UriBuilder;
27
use TYPO3\CMS\Backend\Utility\BackendUtility;
28
use TYPO3\CMS\Backend\View\Event\AfterSectionMarkupGeneratedEvent;
29
use TYPO3\CMS\Backend\View\Event\BeforeSectionMarkupGeneratedEvent;
30
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
31
use TYPO3\CMS\Core\Core\Environment;
32
use TYPO3\CMS\Core\Database\Connection;
33
use TYPO3\CMS\Core\Database\ConnectionPool;
34
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
35
use TYPO3\CMS\Core\Database\Query\QueryHelper;
36
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
37
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
38
use TYPO3\CMS\Core\Database\ReferenceIndex;
39
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
40
use TYPO3\CMS\Core\Imaging\Icon;
41
use TYPO3\CMS\Core\Imaging\IconFactory;
42
use TYPO3\CMS\Core\Localization\LanguageService;
43
use TYPO3\CMS\Core\Messaging\FlashMessage;
44
use TYPO3\CMS\Core\Messaging\FlashMessageService;
45
use TYPO3\CMS\Core\Page\PageRenderer;
46
use TYPO3\CMS\Core\Service\FlexFormService;
47
use TYPO3\CMS\Core\Site\Entity\NullSite;
48
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
49
use TYPO3\CMS\Core\Site\SiteFinder;
50
use TYPO3\CMS\Core\Type\Bitmask\Permission;
51
use TYPO3\CMS\Core\Utility\GeneralUtility;
52
use TYPO3\CMS\Core\Utility\StringUtility;
53
use TYPO3\CMS\Core\Versioning\VersionState;
54
use TYPO3\CMS\Fluid\View\StandaloneView;
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
 * @deprecated Will be removed in TYPO3 11
60
 */
61
class PageLayoutView implements LoggerAwareInterface
62
{
63
    use LoggerAwareTrait;
64
65
    /**
66
     * If TRUE, new-wizards are linked to rather than the regular new-element list.
67
     *
68
     * @var bool
69
     */
70
    public $option_newWizard = true;
71
72
    /**
73
     * If TRUE, elements will have edit icons (probably this is whether the user has permission to edit the page content). Set externally.
74
     *
75
     * @var bool
76
     */
77
    public $doEdit = true;
78
79
    /**
80
     * If set TRUE, the language mode of tt_content elements will be rendered with hard binding between
81
     * default language content elements and their translations!
82
     *
83
     * @var bool
84
     */
85
    public $defLangBinding = false;
86
87
    /**
88
     * External, static: Configuration of tt_content element display:
89
     *
90
     * @var array
91
     */
92
    public $tt_contentConfig = [
93
        'languageCols' => 0,
94
        'languageMode' => 0,
95
        'languageColsPointer' => 0,
96
        // Displays hidden records as well
97
        'showHidden' => 1,
98
        // Which language
99
        'sys_language_uid' => 0,
100
        'cols' => '1,0,2,3',
101
        // Which columns can be accessed by current BE user
102
        'activeCols' => '1,0,2,3'
103
    ];
104
105
    /**
106
     * Used to move content up / down
107
     * @var array
108
     */
109
    public $tt_contentData = [
110
        'prev' => [],
111
        'next' => []
112
    ];
113
114
    /**
115
     * Used to store labels for CTypes for tt_content elements
116
     *
117
     * @var array
118
     */
119
    public $CType_labels = [];
120
121
    /**
122
     * Used to store labels for the various fields in tt_content elements
123
     *
124
     * @var array
125
     */
126
    public $itemLabels = [];
127
128
    /**
129
     * Page id
130
     *
131
     * @var int
132
     */
133
    public $id;
134
135
    /**
136
     * Loaded with page record with version overlay if any.
137
     *
138
     * @var string[]
139
     */
140
    public $pageRecord = [];
141
142
    /**
143
     * Contains site languages for this page ID
144
     *
145
     * @var SiteLanguage[]
146
     */
147
    protected $siteLanguages = [];
148
149
    /**
150
     * @var Clipboard
151
     */
152
    protected $clipboard;
153
154
    /**
155
     * Current ids page record
156
     *
157
     * @var array
158
     */
159
    protected $pageinfo;
160
161
    /**
162
     * Caches the amount of content elements as a matrix
163
     *
164
     * @var array
165
     * @internal
166
     */
167
    protected $contentElementCache = [];
168
169
    /**
170
     * @var IconFactory
171
     */
172
    protected $iconFactory;
173
174
    /**
175
     * Stores whether a certain language has translations in it
176
     *
177
     * @var array
178
     */
179
    protected $languageHasTranslationsCache = [];
180
181
    /**
182
     * @var LocalizationController
183
     */
184
    protected $localizationController;
185
186
    /**
187
     * Cache the number of references to a record
188
     *
189
     * @var array
190
     */
191
    protected $referenceCount = [];
192
193
    /**
194
     * @var EventDispatcherInterface
195
     */
196
    protected $eventDispatcher;
197
198
    /**
199
     * @var UriBuilder
200
     */
201
    protected $uriBuilder;
202
203
    public function __construct(EventDispatcherInterface $eventDispatcher)
204
    {
205
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
206
        $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
207
        $this->localizationController = GeneralUtility::makeInstance(LocalizationController::class);
208
        $this->eventDispatcher = $eventDispatcher;
209
    }
210
211
    /**
212
     * @param PageLayoutContext $context
213
     * @return PageLayoutView
214
     * @internal
215
     */
216
    public static function createFromPageLayoutContext(PageLayoutContext $context): PageLayoutView
217
    {
218
        $drawingConfiguration = $context->getDrawingConfiguration();
219
        $languageId = $drawingConfiguration->getSelectedLanguageId();
220
        /** @var PageLayoutView $pageLayoutView */
221
        $pageLayoutView = GeneralUtility::makeInstance(self::class);
222
        $pageLayoutView->id = $context->getPageId();
223
        $pageLayoutView->pageinfo = BackendUtility::readPageAccess($pageLayoutView->id, '') ?: [];
224
        $pageLayoutView->pageRecord = $context->getPageRecord();
225
        $pageLayoutView->option_newWizard = $drawingConfiguration->getShowNewContentWizard();
226
        $pageLayoutView->defLangBinding = $drawingConfiguration->getDefaultLanguageBinding();
227
        $pageLayoutView->tt_contentConfig['cols'] = implode(',', $drawingConfiguration->getActiveColumns());
228
        $pageLayoutView->tt_contentConfig['activeCols'] = implode(',', $drawingConfiguration->getActiveColumns());
229
        $pageLayoutView->tt_contentConfig['showHidden'] = $drawingConfiguration->getShowHidden();
230
        $pageLayoutView->tt_contentConfig['sys_language_uid'] = $languageId;
231
        if ($drawingConfiguration->getLanguageMode()) {
232
            $pageLayoutView->tt_contentConfig['languageMode'] = 1;
233
            $pageLayoutView->tt_contentConfig['languageCols'] = $drawingConfiguration->getLanguageColumns();
234
            $pageLayoutView->tt_contentConfig['languageColsPointer'] = $languageId;
235
        }
236
        $pageLayoutView->doEdit = $pageLayoutView->isContentEditable($languageId);
237
        $pageLayoutView->CType_labels = $context->getContentTypeLabels();
238
        $pageLayoutView->itemLabels = $context->getItemLabels();
239
        return $pageLayoutView;
240
    }
241
242
    protected function initialize()
243
    {
244
        $this->resolveSiteLanguages($this->id);
245
        $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id);
246
        $this->initializeClipboard();
247
        $this->pageinfo = BackendUtility::readPageAccess($this->id, '') ?: [];
248
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
249
        $pageActionsCallback = null;
250
        if ($this->isPageEditable()) {
251
            $languageOverlayId = 0;
252
            $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->tt_contentConfig['sys_language_uid']);
253
            if (is_array($pageLocalizationRecord)) {
254
                $pageLocalizationRecord = reset($pageLocalizationRecord);
255
            }
256
            if (!empty($pageLocalizationRecord['uid'])) {
257
                $languageOverlayId = $pageLocalizationRecord['uid'];
258
            }
259
            $pageActionsCallback = 'function(PageActions) {
260
                PageActions.setPageId(' . (int)$this->id . ');
261
                PageActions.setLanguageOverlayId(' . $languageOverlayId . ');
262
            }';
263
        }
264
        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback);
265
        // Get labels for CTypes and tt_content element fields in general:
266
        $this->CType_labels = [];
267
        foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $val) {
268
            $this->CType_labels[$val[1]] = $this->getLanguageService()->sL($val[0]);
269
        }
270
271
        $this->itemLabels = [];
272
        foreach ($GLOBALS['TCA']['tt_content']['columns'] as $name => $val) {
273
            $this->itemLabels[$name] = $this->getLanguageService()->sL($val['label']);
274
        }
275
    }
276
277
    /**
278
     * Build a list of language IDs that should be rendered in this view
279
     * @return int[]
280
     */
281
    protected function getSelectedLanguages(): array
282
    {
283
        $langList = $this->tt_contentConfig['sys_language_uid'];
284
        if ($this->tt_contentConfig['languageMode']) {
285
            if ($this->tt_contentConfig['languageColsPointer']) {
286
                $langList = '0,' . $this->tt_contentConfig['languageColsPointer'];
287
            } else {
288
                $langList = implode(',', array_keys($this->tt_contentConfig['languageCols']));
289
            }
290
        }
291
        return GeneralUtility::intExplode(',', $langList);
292
    }
293
294
    /**
295
     * Renders Content Elements from the tt_content table from page id
296
     *
297
     * @param int $id Page id
298
     * @return string HTML for the listing
299
     */
300
    public function getTable_tt_content($id)
301
    {
302
        $this->id = (int)$id;
303
        $this->initialize();
304
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
305
            ->getConnectionForTable('tt_content')
306
            ->getExpressionBuilder();
307
308
        $languageColumn = [];
309
        $out = '';
310
        $tcaItems = GeneralUtility::makeInstance(BackendLayoutView::class)->getColPosListItemsParsed($this->id);
311
        $languageIds = $this->getSelectedLanguages();
312
        $defaultLanguageElementsByColumn = [];
313
        $defLangBinding = [];
314
        // For each languages...
315
        // If not languageMode, then we'll only be through this once.
316
        foreach ($languageIds as $lP) {
317
            if (!isset($this->contentElementCache[$lP])) {
318
                $this->contentElementCache[$lP] = [];
319
            }
320
321
            if (count($languageIds) === 1 || $lP === 0) {
322
                $showLanguage = $expressionBuilder->in('sys_language_uid', [$lP, -1]);
323
            } else {
324
                $showLanguage = $expressionBuilder->eq('sys_language_uid', $lP);
325
            }
326
            $content = [];
327
            $head = [];
328
329
            $backendLayout = $this->getBackendLayoutView()->getSelectedBackendLayout($this->id);
330
            $columns = $backendLayout['__colPosList'];
331
            // Select content records per column
332
            $contentRecordsPerColumn = $this->getContentRecordsPerColumn('tt_content', $id, $columns, $showLanguage);
333
            $cList = array_keys($contentRecordsPerColumn);
334
            // For each column, render the content into a variable:
335
            foreach ($cList as $columnId) {
336
                if (!isset($this->contentElementCache[$lP])) {
337
                    $this->contentElementCache[$lP] = [];
338
                }
339
340
                if (!$lP) {
341
                    $defaultLanguageElementsByColumn[$columnId] = [];
342
                }
343
344
                // Start wrapping div
345
                $content[$columnId] .= '<div data-colpos="' . $columnId . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper';
346
                if (empty($contentRecordsPerColumn[$columnId])) {
347
                    $content[$columnId] .= ' t3-page-ce-empty';
348
                }
349
                $content[$columnId] .= '">';
350
                // Add new content at the top most position
351
                $link = '';
352
                if ($this->isContentEditable()
353
                    && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
354
                ) {
355
                    if ($this->option_newWizard) {
356
                        $urlParameters = [
357
                            'id' => $id,
358
                            'sys_language_uid' => $lP,
359
                            'colPos' => $columnId,
360
                            'uid_pid' => $id,
361
                            'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
362
                        ];
363
                        $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override']
364
                            ?? 'new_content_element_wizard';
365
                        $url = (string)$this->uriBuilder->buildUriFromRoute($routeName, $urlParameters);
366
                    } else {
367
                        $urlParameters = [
368
                            'edit' => [
369
                                'tt_content' => [
370
                                    $id => 'new'
371
                                ]
372
                            ],
373
                            'defVals' => [
374
                                'tt_content' => [
375
                                    'colPos' => $columnId,
376
                                    'sys_language_uid' => $lP
377
                                ]
378
                            ],
379
                            'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
380
                        ];
381
                        $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
382
                    }
383
                    $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement'));
384
                    $link = '<a href="' . htmlspecialchars($url) . '" '
385
                        . 'title="' . $title . '"'
386
                        . 'data-title="' . $title . '"'
387
                        . 'class="btn btn-default btn-sm ' . ($this->option_newWizard ? 't3js-toggle-new-content-element-wizard disabled' : '') . '">'
388
                        . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
389
                        . ' '
390
                        . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
391
                }
392
                if ($this->getBackendUser()->checkLanguageAccess($lP) && $columnId !== 'unused') {
393
                    $content[$columnId] .= '
394
                    <div class="t3-page-ce t3js-page-ce" data-page="' . (int)$id . '" id="' . StringUtility::getUniqueId() . '">
395
                        <div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-page-' . $id . '-' . StringUtility::getUniqueId() . '">'
396
                            . $link
397
                            . '</div>
398
                        <div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div>
399
                    </div>
400
                    ';
401
                }
402
                $editUidList = '';
403
                if (!isset($contentRecordsPerColumn[$columnId]) || !is_array($contentRecordsPerColumn[$columnId])) {
404
                    $message = GeneralUtility::makeInstance(
405
                        FlashMessage::class,
406
                        $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:error.invalidBackendLayout'),
407
                        '',
408
                        FlashMessage::WARNING
409
                    );
410
                    $service = GeneralUtility::makeInstance(FlashMessageService::class);
411
                    $queue = $service->getMessageQueueByIdentifier();
412
                    $queue->addMessage($message);
413
                } else {
414
                    $rowArr = $contentRecordsPerColumn[$columnId];
415
                    $this->generateTtContentDataArray($rowArr);
416
417
                    foreach ((array)$rowArr as $rKey => $row) {
418
                        $this->contentElementCache[$lP][$columnId][$row['uid']] = $row;
419
                        if ($this->tt_contentConfig['languageMode']) {
420
                            $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
421
                        }
422
                        if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
423
                            $singleElementHTML = '<div class="t3-page-ce-dragitem" id="' . StringUtility::getUniqueId() . '">';
424
                            if (!$lP && ($this->defLangBinding || $row['sys_language_uid'] != -1)) {
425
                                $defaultLanguageElementsByColumn[$columnId][] = ($row['_ORIG_uid'] ?? $row['uid']);
426
                            }
427
                            $editUidList .= $row['uid'] . ',';
428
                            $disableMoveAndNewButtons = $this->defLangBinding && $lP > 0 && $this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP);
429
                            $singleElementHTML .= $this->tt_content_drawHeader(
430
                                $row,
431
                                0,
432
                                $disableMoveAndNewButtons,
433
                                true,
434
                                $this->hasContentModificationAndAccessPermissions()
435
                            );
436
                            $innerContent = '<div ' . ($row['_ORIG_uid'] ? ' class="ver-element"' : '') . '>'
437
                                . $this->tt_content_drawItem($row) . '</div>';
438
                            $singleElementHTML .= '<div class="t3-page-ce-body-inner">' . $innerContent . '</div></div>'
439
                                . $this->tt_content_drawFooter($row);
440
                            $isDisabled = $this->isDisabled('tt_content', $row);
441
                            $statusHidden = $isDisabled ? ' t3-page-ce-hidden t3js-hidden-record' : '';
442
                            $displayNone = !$this->tt_contentConfig['showHidden'] && $isDisabled ? ' style="display: none;"' : '';
443
                            $highlightHeader = '';
444
                            if ($this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid']) && (int)$row['l18n_parent'] === 0) {
445
                                $highlightHeader = ' t3-page-ce-danger';
446
                            } elseif ($columnId === 'unused') {
447
                                $highlightHeader = ' t3-page-ce-warning';
448
                            }
449
                            $singleElementHTML = '<div class="t3-page-ce' . $highlightHeader . ' t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
450
                                . $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '" data-language-uid="'
451
                                . $row['sys_language_uid'] . '"' . $displayNone . '>' . $singleElementHTML . '</div>';
452
453
                            $singleElementHTML .= '<div class="t3-page-ce" data-colpos="' . $columnId . '">';
454
                            $singleElementHTML .= '<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-page-' . $id .
455
                                '-' . StringUtility::getUniqueId() . '">';
456
                            // Add icon "new content element below"
457
                            if (!$disableMoveAndNewButtons
458
                                && $this->isContentEditable($lP)
459
                                && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
460
                                && $columnId !== 'unused'
461
                            ) {
462
                                // New content element:
463
                                if ($this->option_newWizard) {
464
                                    $urlParameters = [
465
                                        'id' => $row['pid'],
466
                                        'sys_language_uid' => $row['sys_language_uid'],
467
                                        'colPos' => $row['colPos'],
468
                                        'uid_pid' => -$row['uid'],
469
                                        'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
470
                                    ];
471
                                    $routeName = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['newContentElementWizard.']['override']
472
                                        ?? 'new_content_element_wizard';
473
                                    $url = (string)$this->uriBuilder->buildUriFromRoute($routeName, $urlParameters);
474
                                } else {
475
                                    $urlParameters = [
476
                                        'edit' => [
477
                                            'tt_content' => [
478
                                                -$row['uid'] => 'new'
479
                                            ]
480
                                        ],
481
                                        'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
482
                                    ];
483
                                    $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
484
                                }
485
                                $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement'));
486
                                $singleElementHTML .= '<a href="' . htmlspecialchars($url) . '" '
487
                                    . 'title="' . $title . '"'
488
                                    . 'data-title="' . $title . '"'
489
                                    . 'class="btn btn-default btn-sm ' . ($this->option_newWizard ? 't3js-toggle-new-content-element-wizard disabled' : '') . '">'
490
                                    . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
491
                                    . ' '
492
                                    . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
493
                            }
494
                            $singleElementHTML .= '</div></div><div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div></div>';
495
                            if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) {
496
                                $defLangBinding[$columnId][$lP][$row[$lP ? 'l18n_parent' : 'uid'] ?: $row['uid']] = $singleElementHTML;
497
                            } else {
498
                                $content[$columnId] .= $singleElementHTML;
499
                            }
500
                        } else {
501
                            unset($rowArr[$rKey]);
502
                        }
503
                    }
504
                    $content[$columnId] .= '</div>';
505
                    if ($columnId === 'unused') {
506
                        if (empty($unusedElementsMessage)) {
507
                            $unusedElementsMessage = GeneralUtility::makeInstance(
508
                                FlashMessage::class,
509
                                $this->getLanguageService()->getLL('staleUnusedElementsWarning'),
510
                                $this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'),
511
                                FlashMessage::WARNING
512
                            );
513
                            $service = GeneralUtility::makeInstance(FlashMessageService::class);
514
                            $queue = $service->getMessageQueueByIdentifier();
515
                            $queue->addMessage($unusedElementsMessage);
516
                        }
517
                        $colTitle = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:unusedColPos');
518
                        $editParam = '';
519
                    } else {
520
                        $colTitle = '';
521
                        foreach ($tcaItems as $item) {
522
                            if ($item[1] == $columnId) {
523
                                $colTitle = $this->getLanguageService()->sL($item[0]);
524
                            }
525
                        }
526
                        if (empty($colTitle)) {
527
                            $colTitle = BackendUtility::getProcessedValue('tt_content', 'colPos', (string)$columnId) ?? '';
528
                        }
529
                        $editParam = $this->doEdit && !empty($rowArr)
530
                            ? '&edit[tt_content][' . $editUidList . ']=edit&recTitle=' . rawurlencode(BackendUtility::getRecordTitle('pages', $this->pageRecord, true))
531
                            : '';
532
                    }
533
                    $head[$columnId] .= $this->tt_content_drawColHeader($colTitle, $editParam);
534
                }
535
            }
536
            // For each column, fit the rendered content into a table cell:
537
            $out = '';
538
            if ($this->tt_contentConfig['languageMode']) {
539
                // in language mode process the content elements, but only fill $languageColumn. output will be generated later
540
                $sortedLanguageColumn = [];
541
                foreach ($cList as $columnId) {
542
                    if (GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnId) || $columnId === 'unused') {
543
                        $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
544
545
                        // We sort $languageColumn again according to $cList as it may contain data already from above.
546
                        $sortedLanguageColumn[$columnId] = $languageColumn[$columnId];
547
                    }
548
                }
549
                if (!empty($languageColumn['unused'])) {
550
                    $sortedLanguageColumn['unused'] = $languageColumn['unused'];
551
                }
552
                $languageColumn = $sortedLanguageColumn;
553
            } else {
554
                // GRID VIEW:
555
                $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">';
556
                // Add colgroups
557
                $colCount = (int)$backendLayout['__config']['backend_layout.']['colCount'];
558
                $rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount'];
559
                $colSpan = 0;
560
                $rowSpan = 0;
561
                $grid .= '<colgroup>';
562
                for ($i = 0; $i < $colCount; $i++) {
563
                    $grid .= '<col />';
564
                }
565
                $grid .= '</colgroup>';
566
567
                // Check how to handle restricted columns
568
                $hideRestrictedCols = (bool)(BackendUtility::getPagesTSconfig($id)['mod.']['web_layout.']['hideRestrictedCols'] ?? false);
569
570
                // Cycle through rows
571
                for ($row = 1; $row <= $rowCount; $row++) {
572
                    $rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.'];
573
                    if (!isset($rowConfig)) {
574
                        continue;
575
                    }
576
                    $grid .= '<tr>';
577
                    for ($col = 1; $col <= $colCount; $col++) {
578
                        $columnConfig = $rowConfig['columns.'][$col . '.'];
579
                        if (!isset($columnConfig)) {
580
                            continue;
581
                        }
582
                        // Which tt_content colPos should be displayed inside this cell
583
                        $columnKey = (int)$columnConfig['colPos'];
584
                        // Render the grid cell
585
                        $colSpan = (int)$columnConfig['colspan'];
586
                        $rowSpan = (int)$columnConfig['rowspan'];
587
                        $grid .= '<td valign="top"' .
588
                            ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') .
589
                            ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') .
590
                            ' 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 .
591
                            ((!isset($columnConfig['colPos']) || $columnConfig['colPos'] === '') ? ' t3-grid-cell-unassigned' : '') .
592
                            ((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') : '') .
593
                            ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') .
594
                            ($rowSpan > 0 ? ' t3-gridCell-height' . $rowSpan : '') . '">';
595
596
                        // Draw the pre-generated header with edit and new buttons if a colPos is assigned.
597
                        // If not, a new header without any buttons will be generated.
598
                        if (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && $head[$columnKey]
599
                            && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
600
                        ) {
601
                            $grid .= $head[$columnKey];
602
                            $grid .= $this->dispatchSectionMarkupGeneratedEvent('before', $lP, $columnConfig);
603
                            $grid .= $content[$columnKey];
604
                        } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== ''
605
                            && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
606
                        ) {
607
                            if (!$hideRestrictedCols) {
608
                                $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('noAccess'));
609
                                $grid .= $this->dispatchSectionMarkupGeneratedEvent('before', $lP, $columnConfig);
610
                            }
611
                        } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== ''
612
                            && !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
613
                        ) {
614
                            if (!$hideRestrictedCols) {
615
                                $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) .
616
                                  ' (' . $this->getLanguageService()->getLL('noAccess') . ')');
617
                                $grid .= $this->dispatchSectionMarkupGeneratedEvent('before', $lP, $columnConfig);
618
                            }
619
                        } elseif (isset($columnConfig['name']) && $columnConfig['name'] !== '') {
620
                            $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name'])
621
                                . ' (' . $this->getLanguageService()->getLL('notAssigned') . ')');
622
                            $grid .= $this->dispatchSectionMarkupGeneratedEvent('before', $lP, $columnConfig);
623
                        } else {
624
                            $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('notAssigned'));
625
                            $grid .= $this->dispatchSectionMarkupGeneratedEvent('before', $lP, $columnConfig);
626
                        }
627
628
                        $grid .= $this->dispatchSectionMarkupGeneratedEvent('after', $lP, $columnConfig);
629
630
                        $grid .= '</td>';
631
                    }
632
                    $grid .= '</tr>';
633
                }
634
                if (!empty($content['unused'])) {
635
                    $grid .= '<tr>';
636
                    // Which tt_content colPos should be displayed inside this cell
637
                    $columnKey = 'unused';
638
                    // Render the grid cell
639
                    $colSpan = (int)$backendLayout['__config']['backend_layout.']['colCount'];
640
                    $grid .= '<td valign="top"' .
641
                        ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') .
642
                        ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') .
643
                        ' 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 .
644
                        ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . '">';
645
646
                    // Draw the pre-generated header with edit and new buttons if a colPos is assigned.
647
                    // If not, a new header without any buttons will be generated.
648
                    $grid .= $head[$columnKey] . $content[$columnKey];
649
                    $grid .= '</td></tr>';
650
                }
651
                $out .= $grid . '</table></div>';
652
            }
653
        }
654
        $elFromTable = $this->clipboard->elFromTable('tt_content');
655
        if (!empty($elFromTable) && $this->isContentEditable()) {
656
            $pasteItem = (int)substr((string)key($elFromTable), 11);
657
            $pasteRecord = BackendUtility::getRecord('tt_content', $pasteItem);
658
            $pasteTitle = $pasteRecord['header'] ?: (string)$pasteItem;
659
            $copyMode = $this->clipboard->clipData['normal']['mode'] ? '-' . $this->clipboard->clipData['normal']['mode'] : '';
660
            $inlineJavaScript = '
661
                     top.pasteIntoLinkTemplate = '
662
                . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-into', 'pasteIntoColumn')
663
                . ';
664
                    top.pasteAfterLinkTemplate = '
665
                . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-after', 'pasteAfterRecord')
666
                . ';';
667
        } else {
668
            $inlineJavaScript = '
669
                top.pasteIntoLinkTemplate = \'\';
670
                top.pasteAfterLinkTemplate = \'\';';
671
        }
672
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
673
        $pageRenderer->addJsInlineCode('pasteLinkTemplates', $inlineJavaScript);
674
        // If language mode, then make another presentation:
675
        // Notice that THIS presentation will override the value of $out!
676
        // But it needs the code above to execute since $languageColumn is filled with content we need!
677
        if ($this->tt_contentConfig['languageMode']) {
678
            return $this->generateLanguageView($languageIds, $defaultLanguageElementsByColumn, $languageColumn, $defLangBinding);
679
        }
680
        return $out;
681
    }
682
683
    /**
684
     * Shows the content elements of the selected languages in each column.
685
     * @param array $languageIds languages to render
686
     * @param array $defaultLanguageElementsByColumn
687
     * @param array $languageColumn
688
     * @param array $defLangBinding
689
     * @return string the compiled content
690
     */
691
    protected function generateLanguageView(
692
        array $languageIds,
693
        array $defaultLanguageElementsByColumn,
694
        array $languageColumn,
695
        array $defLangBinding
696
    ): string {
697
        // Get language selector:
698
        $languageSelector = $this->languageSelector($this->id);
699
        // Reset out - we will make new content here:
700
        $out = '';
701
        // Traverse languages found on the page and build up the table displaying them side by side:
702
        $cCont = [];
703
        $sCont = [];
704
        foreach ($languageIds as $languageId) {
705
            $languageMode = '';
706
            $labelClass = 'info';
707
            // Header:
708
            $languageId = (int)$languageId;
709
            // Determine language mode
710
            if ($languageId > 0 && isset($this->languageHasTranslationsCache[$languageId]['mode'])) {
711
                switch ($this->languageHasTranslationsCache[$languageId]['mode']) {
712
                    case 'mixed':
713
                        $languageMode = $this->getLanguageService()->getLL('languageModeMixed');
714
                        $labelClass = 'danger';
715
                        break;
716
                    case 'connected':
717
                        $languageMode = $this->getLanguageService()->getLL('languageModeConnected');
718
                        break;
719
                    case 'free':
720
                        $languageMode = $this->getLanguageService()->getLL('languageModeFree');
721
                        break;
722
                    default:
723
                        // we'll let opcode optimize this intentionally empty case
724
                }
725
            }
726
            $columnAttributes = [
727
                'valign' => 'top',
728
                'class' => 't3-page-column t3-page-column-lang-name',
729
                'data-language-uid' => (string)$languageId,
730
                'data-language-title' => $this->siteLanguages[$languageId]->getTitle(),
731
                'data-flag-identifier' => $this->siteLanguages[$languageId]->getFlagIdentifier()
732
            ];
733
734
            $cCont[$languageId] = '
735
					<td ' . GeneralUtility::implodeAttributes($columnAttributes, true) . '>
736
						<h2>' . htmlspecialchars($this->tt_contentConfig['languageCols'][$languageId]) . '</h2>
737
						' . ($languageMode !== '' ? '<span class="label label-' . $labelClass . '">' . $languageMode . '</span>' : '') . '
738
					</td>';
739
740
            $editLink = '';
741
            $recordIcon = '';
742
            $viewLink = '';
743
            // "View page" icon is added:
744
            if (!VersionState::cast($this->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
745
                $attributes = PreviewUriBuilder::create($this->id)
746
                    ->withRootLine(BackendUtility::BEgetRootLine($this->id))
747
                    ->withAdditionalQueryParameters('&L=' . $languageId)
748
                    ->serializeDispatcherAttributes();
749
                $viewLink = '<a href="#" class="btn btn-default btn-sm" ' . $attributes . ' 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>';
750
            }
751
            // Language overlay page header:
752
            if ($languageId) {
753
                $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, $languageId);
754
                if (is_array($pageLocalizationRecord)) {
755
                    $pageLocalizationRecord = reset($pageLocalizationRecord);
756
                }
757
                BackendUtility::workspaceOL('pages', $pageLocalizationRecord);
758
                $recordIcon = BackendUtility::wrapClickMenuOnIcon(
759
                    $this->iconFactory->getIconForRecord('pages', $pageLocalizationRecord, Icon::SIZE_SMALL)->render(),
760
                    'pages',
761
                    $pageLocalizationRecord['uid']
762
                );
763
                $urlParameters = [
764
                    'edit' => [
765
                        'pages' => [
766
                            $pageLocalizationRecord['uid'] => 'edit'
767
                        ]
768
                    ],
769
                    // Disallow manual adjustment of the language field for pages
770
                    'overrideVals' => [
771
                        'pages' => [
772
                            'sys_language_uid' => $languageId
773
                        ]
774
                    ],
775
                    'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
776
                ];
777
                $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
778
                if ($this->getBackendUser()->check('tables_modify', 'pages')) {
779
                    $editLink = '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
780
                        . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
781
                        . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
782
                }
783
784
                $defaultLanguageElements = [];
785
                array_walk($defaultLanguageElementsByColumn, function (array $columnContent) use (&$defaultLanguageElements) {
786
                    $defaultLanguageElements = array_merge($defaultLanguageElements, $columnContent);
787
                });
788
789
                $localizationButtons = [];
790
                $localizationButtons[] = $this->newLanguageButton(
791
                    $this->getNonTranslatedTTcontentUids($defaultLanguageElements, $this->id, $languageId),
792
                    $languageId
793
                );
794
795
                $languageLabel =
796
                    '<div class="btn-group">'
797
                    . $viewLink
798
                    . $editLink
799
                    . (!empty($localizationButtons) ? implode(LF, $localizationButtons) : '')
800
                    . '</div>'
801
                    . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20))
802
                ;
803
            } else {
804
                if ($this->getBackendUser()->checkLanguageAccess(0)) {
805
                    $recordIcon = BackendUtility::wrapClickMenuOnIcon(
806
                        $this->iconFactory->getIconForRecord('pages', $this->pageRecord, Icon::SIZE_SMALL)->render(),
807
                        'pages',
808
                        $this->id
809
                    );
810
                    $urlParameters = [
811
                        'edit' => [
812
                            'pages' => [
813
                                $this->id => 'edit'
814
                            ]
815
                        ],
816
                        'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
817
                    ];
818
                    $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
819
                    if ($this->getBackendUser()->check('tables_modify', 'pages')) {
820
                        $editLink = '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
821
                            . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
822
                            . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
823
                    }
824
                }
825
826
                $languageLabel =
827
                    '<div class="btn-group">'
828
                    . $viewLink
829
                    . $editLink
830
                    . '</div>'
831
                    . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($this->pageRecord['title'], 20));
832
            }
833
            $sCont[$languageId] = '
834
					<td class="t3-page-column t3-page-lang-label nowrap">' . $languageLabel . '</td>';
835
        }
836
        // Add headers:
837
        $out .= '<tr>' . implode('', $cCont) . '</tr>';
838
        $out .= '<tr>' . implode('', $sCont) . '</tr>';
839
        unset($cCont, $sCont);
840
841
        // Traverse previously built content for the columns:
842
        foreach ($languageColumn as $cKey => $cCont) {
843
            $out .= '<tr>';
844
            foreach ($cCont as $languageId => $columnContent) {
845
                $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>';
846
            }
847
            $out .= '</tr>';
848
            if ($this->defLangBinding && !empty($defLangBinding[$cKey])) {
849
                $maxItemsCount = max(array_map('count', $defLangBinding[$cKey]));
850
                for ($i = 0; $i < $maxItemsCount; $i++) {
851
                    $defUid = $defaultLanguageElementsByColumn[$cKey][$i] ?? 0;
852
                    $cCont = [];
853
                    foreach ($languageIds as $languageId) {
854
                        if ($languageId > 0
855
                            && is_array($defLangBinding[$cKey][$languageId])
856
                            && !$this->checkIfTranslationsExistInLanguage($defaultLanguageElementsByColumn[$cKey], $languageId)
857
                            && count($defLangBinding[$cKey][$languageId]) > $i
858
                        ) {
859
                            $slice = array_slice($defLangBinding[$cKey][$languageId], $i, 1);
860
                            $element = $slice[0] ?? '';
861
                        } else {
862
                            $element = $defLangBinding[$cKey][$languageId][$defUid] ?? '';
863
                        }
864
                        $cCont[] = $element;
865
                    }
866
                    $out .= '
867
                        <tr>
868
							<td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">' . implode('</td>
869
                            <td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">', $cCont) . '</td>
870
						</tr>';
871
                }
872
            }
873
        }
874
        // Finally, wrap it all in a table and add the language selector on top of it:
875
        return $languageSelector . '
876
                <div class="t3-grid-container">
877
                    <table cellpadding="0" cellspacing="0" class="t3-page-columns t3-grid-table t3js-page-columns">
878
						' . $out . '
879
                    </table>
880
				</div>';
881
    }
882
883
    /**
884
     * Gets content records per column.
885
     * This is required for correct workspace overlays.
886
     *
887
     * @param string $table Name of table storing content records, by default tt_content
888
     * @param int $id Page Id to be used (not used at all, but part of the API, see $this->pidSelect)
889
     * @param array $columns colPos values to be considered to be shown
890
     * @param string $additionalWhereClause Additional where clause for database select
891
     * @return array Associative array for each column (colPos)
892
     */
893
    protected function getContentRecordsPerColumn($table, $id, array $columns, $additionalWhereClause = '')
894
    {
895
        $contentRecordsPerColumn = array_fill_keys($columns, []);
896
        $columns = array_flip($columns);
897
        $queryBuilder = $this->getQueryBuilder(
898
            $table,
899
            $id,
900
            [
901
                $additionalWhereClause
902
            ]
903
        );
904
905
        // Traverse any selected elements and render their display code:
906
        /** @var Statement $result */
907
        $result = $queryBuilder->execute();
908
        $results = $this->getResult($result);
909
        $unused = [];
910
        $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] ?? [];
911
        foreach ($results as $record) {
912
            $used = isset($columns[$record['colPos']]);
913
            foreach ($hookArray as $_funcRef) {
914
                $_params = ['columns' => $columns, 'record' => $record, 'used' => $used];
915
                $used = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
916
            }
917
            if ($used) {
918
                $columnValue = (string)$record['colPos'];
919
                $contentRecordsPerColumn[$columnValue][] = $record;
920
            } else {
921
                $unused[] = $record;
922
            }
923
        }
924
        if (!empty($unused)) {
925
            $contentRecordsPerColumn['unused'] = $unused;
926
        }
927
        return $contentRecordsPerColumn;
928
    }
929
930
    /**
931
     * Draw header for a content element column:
932
     *
933
     * @param string $colName Column name
934
     * @param string $editParams Edit params (Syntax: &edit[...] for FormEngine)
935
     * @return string HTML table
936
     */
937
    public function tt_content_drawColHeader($colName, $editParams = '')
938
    {
939
        $icons = '';
940
        // Edit whole of column:
941
        if ($editParams && $this->hasContentModificationAndAccessPermissions() && $this->getBackendUser()->checkLanguageAccess(0)) {
942
            $link = $this->uriBuilder->buildUriFromRoute('record_edit') . $editParams . '&returnUrl=' . rawurlencode($GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri());
943
            $icons = '<a href="' . htmlspecialchars($link) . '"  title="'
944
                . htmlspecialchars($this->getLanguageService()->getLL('editColumn')) . '">'
945
                . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
946
            $icons = '<div class="t3-page-column-header-icons">' . $icons . '</div>';
947
        }
948
        return '<div class="t3-page-column-header">
949
					' . $icons . '
950
					<div class="t3-page-column-header-label">' . htmlspecialchars($colName) . '</div>
951
				</div>';
952
    }
953
954
    /**
955
     * Draw a paste icon either for pasting into a column or for pasting after a record
956
     *
957
     * @param int $pasteItem ID of the item in the clipboard
958
     * @param string $pasteTitle Title for the JS modal
959
     * @param string $copyMode copy or cut
960
     * @param string $cssClass CSS class to determine if pasting is done into column or after record
961
     * @param string $title title attribute of the generated link
962
     *
963
     * @return string Generated HTML code with link and icon
964
     */
965
    protected function tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, $cssClass, $title)
966
    {
967
        $pasteIcon = json_encode(
968
            ' <button type="button"'
969
            . ' data-bs-content="' . htmlspecialchars((string)$pasteItem) . '"'
970
            . ' data-title="' . htmlspecialchars($pasteTitle) . '"'
971
            . ' data-severity="warning"'
972
            . ' class="t3js-paste t3js-paste' . htmlspecialchars($copyMode) . ' ' . htmlspecialchars($cssClass) . ' btn btn-default btn-sm"'
973
            . ' title="' . htmlspecialchars($this->getLanguageService()->getLL($title)) . '">'
974
            . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
975
            . '</button>'
976
        );
977
        return $pasteIcon;
978
    }
979
980
    /**
981
     * Draw the footer for a single tt_content element
982
     *
983
     * @param array $row Record array
984
     * @return string HTML of the footer
985
     * @throws \UnexpectedValueException
986
     */
987
    protected function tt_content_drawFooter(array $row)
988
    {
989
        $content = '';
990
        // Get processed values:
991
        $info = [];
992
        $this->getProcessedValue('tt_content', 'starttime,endtime,fe_group,space_before_class,space_after_class', $row, $info);
993
994
        // Content element annotation
995
        if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) {
996
            $info[] = htmlspecialchars($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]);
997
        }
998
999
        // Call drawFooter hooks
1000
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) {
1001
            $hookObject = GeneralUtility::makeInstance($className);
1002
            if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) {
1003
                throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1404378171);
1004
            }
1005
            $hookObject->preProcess($this, $info, $row);
1006
        }
1007
1008
        // Display info from records fields:
1009
        if (!empty($info)) {
1010
            $content = '<div class="t3-page-ce-info">
1011
				' . implode('<br>', $info) . '
1012
				</div>';
1013
        }
1014
        if (!empty($content)) {
1015
            $content = '<div class="t3-page-ce-footer">' . $content . '</div>';
1016
        }
1017
        return $content;
1018
    }
1019
1020
    /**
1021
     * Draw the header for a single tt_content element
1022
     *
1023
     * @param array $row Record array
1024
     * @param int $space Amount of pixel space above the header. UNUSED
1025
     * @param bool $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown.
1026
     * @param bool $langMode If set, we are in language mode and flags will be shown for languages
1027
     * @param bool $dragDropEnabled If set the move button must be hidden
1028
     * @return string HTML table with the record header.
1029
     */
1030
    public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = false, $langMode = false, $dragDropEnabled = false)
1031
    {
1032
        $backendUser = $this->getBackendUser();
1033
        $out = '';
1034
        // Render control panel for the element
1035
        if ($backendUser->recordEditAccessInternals('tt_content', $row) && $this->isContentEditable($row['sys_language_uid'])) {
1036
            // Edit content element:
1037
            $urlParameters = [
1038
                'edit' => [
1039
                    'tt_content' => [
1040
                        $row['uid'] => 'edit'
1041
                    ]
1042
                ],
1043
                'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() . '#element-tt_content-' . $row['uid'],
1044
            ];
1045
            $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $row['uid'];
1046
1047
            $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url)
1048
                . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit'))
1049
                . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1050
            // Hide element:
1051
            $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled'];
1052
            if ($hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField]
1053
                && (!$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude']
1054
                    || $backendUser->check('non_exclude_fields', 'tt_content:' . $hiddenField))
1055
            ) {
1056
                if ($row[$hiddenField]) {
1057
                    $value = 0;
1058
                    $label = 'unHide';
1059
                } else {
1060
                    $value = 1;
1061
                    $label = 'hide';
1062
                }
1063
                $params = '&data[tt_content][' . ($row['_ORIG_uid'] ?: $row['uid'])
1064
                    . '][' . $hiddenField . ']=' . $value;
1065
                $out .= '<a class="btn btn-default" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
1066
                    . '#element-tt_content-' . $row['uid'] . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($label)) . '">'
1067
                    . $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . '</a>';
1068
            }
1069
            // Delete
1070
            $disableDelete = (bool)\trim(
1071
                $backendUser->getTSConfig()['options.']['disableDelete.']['tt_content']
1072
                ?? $backendUser->getTSConfig()['options.']['disableDelete']
1073
                ?? '0'
1074
            );
1075
            if (!$disableDelete) {
1076
                $params = '&cmd[tt_content][' . $row['uid'] . '][delete]=1';
1077
                $refCountMsg = BackendUtility::referenceCount(
1078
                    'tt_content',
1079
                    $row['uid'],
1080
                    ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
1081
                    (string)$this->getReferenceCount('tt_content', $row['uid'])
1082
                ) . BackendUtility::translationCount(
1083
                    'tt_content',
1084
                    $row['uid'],
1085
                    ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
1086
                );
1087
                $confirm = $this->getLanguageService()->getLL('deleteWarning')
1088
                    . $refCountMsg;
1089
                $out .= '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) . '"'
1090
                    . ' data-severity="warning"'
1091
                    . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')) . '"'
1092
                    . ' data-bs-content="' . htmlspecialchars($confirm) . '" '
1093
                    . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
1094
                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('deleteItem')) . '">'
1095
                    . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1096
                if ($out && $this->hasContentModificationAndAccessPermissions()) {
1097
                    $out = '<div class="btn-group btn-group-sm" role="group">' . $out . '</div>';
1098
                } else {
1099
                    $out = '';
1100
                }
1101
            }
1102
            if (!$disableMoveAndNewButtons) {
1103
                $moveButtonContent = '';
1104
                $displayMoveButtons = false;
1105
                // Move element up:
1106
                if ($this->tt_contentData['prev'][$row['uid']]) {
1107
                    $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']];
1108
                    $moveButtonContent .= '<a class="btn btn-default" href="'
1109
                        . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
1110
                        . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
1111
                        . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
1112
                    if (!$dragDropEnabled) {
1113
                        $displayMoveButtons = true;
1114
                    }
1115
                } else {
1116
                    $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
1117
                }
1118
                // Move element down:
1119
                if ($this->tt_contentData['next'][$row['uid']]) {
1120
                    $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']];
1121
                    $moveButtonContent .= '<a class="btn btn-default" href="'
1122
                        . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
1123
                        . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
1124
                        . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
1125
                    if (!$dragDropEnabled) {
1126
                        $displayMoveButtons = true;
1127
                    }
1128
                } else {
1129
                    $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
1130
                }
1131
                if ($displayMoveButtons) {
1132
                    $out .= '<div class="btn-group btn-group-sm" role="group">' . $moveButtonContent . '</div>';
1133
                }
1134
            }
1135
        }
1136
        $allowDragAndDrop = $this->isDragAndDropAllowed($row);
1137
        $additionalIcons = [];
1138
        $additionalIcons[] = $this->getIcon('tt_content', $row) . ' ';
1139
        if ($langMode && isset($this->siteLanguages[(int)$row['sys_language_uid']])) {
1140
            $additionalIcons[] = $this->renderLanguageFlag($this->siteLanguages[(int)$row['sys_language_uid']]);
1141
        }
1142
        // Get record locking status:
1143
        if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) {
1144
            $additionalIcons[] = '<a href="#" data-bs-toggle="tooltip" title="' . htmlspecialchars($lockInfo['msg']) . '">'
1145
                . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</a>';
1146
        }
1147
        // Call stats information hook
1148
        $_params = ['tt_content', $row['uid'], &$row];
1149
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) {
1150
            $additionalIcons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1151
        }
1152
1153
        // Wrap the whole header
1154
        // NOTE: end-tag for <div class="t3-page-ce-body"> is in getTable_tt_content()
1155
        return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
1156
					<div class="t3-page-ce-header-icons-left">' . implode('', $additionalIcons) . '</div>
1157
					<div class="t3-page-ce-header-icons-right">' . ($out ? '<div class="btn-toolbar">' . $out . '</div>' : '') . '</div>
1158
				</div>
1159
				<div class="t3-page-ce-body">';
1160
    }
1161
1162
    /**
1163
     * Gets the number of records referencing the record with the UID $uid in
1164
     * the table $tableName.
1165
     *
1166
     * @param string $tableName
1167
     * @param int $uid
1168
     * @return int The number of references to record $uid in table
1169
     */
1170
    protected function getReferenceCount(string $tableName, int $uid): int
1171
    {
1172
        if (!isset($this->referenceCount[$tableName][$uid])) {
1173
            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1174
            $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid);
1175
            $this->referenceCount[$tableName][$uid] = $numberOfReferences;
1176
        }
1177
        return $this->referenceCount[$tableName][$uid];
1178
    }
1179
1180
    /**
1181
     * Determine whether Drag & Drop should be allowed
1182
     *
1183
     * @param array $row
1184
     * @return bool
1185
     */
1186
    protected function isDragAndDropAllowed(array $row)
1187
    {
1188
        if ((int)$row['l18n_parent'] === 0 &&
1189
            (
1190
                $this->getBackendUser()->isAdmin()
1191
                || ((int)$row['editlock'] === 0 && (int)$this->pageinfo['editlock'] === 0)
1192
                && $this->hasContentModificationAndAccessPermissions()
1193
                && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $row['CType'], $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'])
1194
            )
1195
        ) {
1196
            return true;
1197
        }
1198
        return false;
1199
    }
1200
1201
    /**
1202
     * Draws the preview content for a content element
1203
     *
1204
     * @param array $row Content element
1205
     * @return string HTML
1206
     * @throws \UnexpectedValueException
1207
     */
1208
    public function tt_content_drawItem($row)
1209
    {
1210
        $out = '';
1211
        $outHeader = $this->renderContentElementHeader($row);
1212
        $drawItem = true;
1213
        // Hook: Render an own preview of a record
1214
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) {
1215
            $hookObject = GeneralUtility::makeInstance($className);
1216
            if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) {
1217
                throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1218547409);
1218
            }
1219
            $hookObject->preProcess($this, $drawItem, $outHeader, $out, $row);
1220
        }
1221
1222
        // If the previous hook did not render something,
1223
        // then check if a Fluid-based preview template was defined for this CType
1224
        // and render it via Fluid. Possible option:
1225
        // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html
1226
        if ($drawItem) {
1227
            $fluidPreview = $this->renderContentElementPreviewFromFluidTemplate($row);
1228
            if ($fluidPreview !== null) {
1229
                $out .= $fluidPreview;
1230
                $drawItem = false;
1231
            }
1232
        }
1233
1234
        // Draw preview of the item depending on its CType (if not disabled by previous hook)
1235
        if ($drawItem) {
1236
            $out .= $this->renderContentElementPreview($row);
1237
        }
1238
        $out = $outHeader . '<span class="exampleContent">' . $out . '</span>';
1239
        if ($this->isDisabled('tt_content', $row)) {
1240
            return '<span class="text-muted">' . $out . '</span>';
1241
        }
1242
        return $out;
1243
    }
1244
1245
    public function renderContentElementHeader(array $row): string
1246
    {
1247
        $outHeader = '';
1248
        // Make header:
1249
        if ($row['header']) {
1250
            $hiddenHeaderNote = '';
1251
            // If header layout is set to 'hidden', display an accordant note:
1252
            if ($row['header_layout'] == 100) {
1253
                $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>';
1254
            }
1255
            $outHeader = $row['date']
1256
                ? htmlspecialchars($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />'
1257
                : '';
1258
            $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row)
1259
                . $hiddenHeaderNote . '</strong><br />';
1260
        }
1261
        return $outHeader;
1262
    }
1263
1264
    public function renderContentElementPreviewFromFluidTemplate(array $row): ?string
1265
    {
1266
        $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
1267
        $fluidTemplateFile = '';
1268
1269
        if ($row['CType'] === 'list' && !empty($row['list_type'])
1270
            && !empty($tsConfig['list.'][$row['list_type']])
1271
        ) {
1272
            $fluidTemplateFile = $tsConfig['list.'][$row['list_type']];
1273
        } elseif (!empty($tsConfig[$row['CType']])) {
1274
            $fluidTemplateFile = $tsConfig[$row['CType']];
1275
        }
1276
1277
        if ($fluidTemplateFile) {
1278
            $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
1279
            if ($fluidTemplateFile) {
1280
                try {
1281
                    $view = GeneralUtility::makeInstance(StandaloneView::class);
1282
                    $view->setTemplatePathAndFilename($fluidTemplateFile);
1283
                    $view->assignMultiple($row);
1284
                    if (!empty($row['pi_flexform'])) {
1285
                        $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
1286
                        $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
1287
                    }
1288
                    return $view->render();
1289
                } catch (\Exception $e) {
1290
                    $this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [
0 ignored issues
show
Bug introduced by
The method warning() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1290
                    $this->logger->/** @scrutinizer ignore-call */ 
1291
                                   warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1291
                        'uid' => $row['uid'],
1292
                        'file' => $fluidTemplateFile,
1293
                        $e->getMessage(),
1294
                    ]);
1295
1296
                    if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
1297
                        $view = GeneralUtility::makeInstance(StandaloneView::class);
1298
                        $view->assign('error', [
1299
                            'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()),
1300
                            'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFile),
1301
                        ]);
1302
                        $view->setTemplateSource('<f:be.infobox title="{error.title}" state="2">{error.message}</f:be.infobox>');
1303
                        return $view->render();
1304
                    }
1305
                }
1306
            }
1307
        }
1308
        return null;
1309
    }
1310
1311
    /**
1312
     * Renders the preview part of a content element
1313
     * @param array $row given tt_content database record
1314
     * @return string
1315
     */
1316
    public function renderContentElementPreview(array $row): string
1317
    {
1318
        $previewHtml = '';
1319
        switch ($row['CType']) {
1320
            case 'header':
1321
                if ($row['subheader']) {
1322
                    $previewHtml = $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br />';
1323
                }
1324
                break;
1325
            case 'bullets':
1326
            case 'table':
1327
                if ($row['bodytext']) {
1328
                    $previewHtml = $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
1329
                }
1330
                break;
1331
            case 'uploads':
1332
                if ($row['media']) {
1333
                    $previewHtml = $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />';
1334
                }
1335
                break;
1336
            case 'shortcut':
1337
                if (!empty($row['records'])) {
1338
                    $shortcutContent = [];
1339
                    $recordList = explode(',', $row['records']);
1340
                    foreach ($recordList as $recordIdentifier) {
1341
                        $split = BackendUtility::splitTable_Uid($recordIdentifier);
1342
                        $tableName = empty($split[0]) ? 'tt_content' : $split[0];
1343
                        $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]);
1344
                        if (is_array($shortcutRecord)) {
1345
                            $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render();
1346
                            $icon = BackendUtility::wrapClickMenuOnIcon(
1347
                                $icon,
1348
                                $tableName,
1349
                                $shortcutRecord['uid']
1350
                            );
1351
                            $shortcutContent[] = $icon
1352
                                . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord));
1353
                        }
1354
                    }
1355
                    $previewHtml = implode('<br />', $shortcutContent) . '<br />';
1356
                }
1357
                break;
1358
            case 'list':
1359
                $hookOut = '';
1360
                $_params = ['pObj' => &$this, 'row' => $row];
1361
                foreach (
1362
                    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ??
1363
                    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ??
1364
                    [] as $_funcRef
1365
                ) {
1366
                    $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1367
                }
1368
                if ((string)$hookOut !== '') {
1369
                    $previewHtml = $hookOut;
1370
                } elseif (!empty($row['list_type'])) {
1371
                    $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']);
1372
                    if (!empty($label)) {
1373
                        $previewHtml = $this->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />';
0 ignored issues
show
Bug introduced by
The method getLanguageService() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1373
                        $previewHtml = $this->linkEditContent('<strong>' . htmlspecialchars($this->/** @scrutinizer ignore-call */ getLanguageService()->sL($label)) . '</strong>', $row) . '<br />';

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1374
                    } else {
1375
                        $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']);
1376
                        $previewHtml = '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
1377
                    }
1378
                } else {
1379
                    $previewHtml = '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>';
1380
                }
1381
                $previewHtml .= htmlspecialchars($this->getLanguageService()->sL(
1382
                    BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages'])
1383
                )) . '<br />';
1384
                break;
1385
            default:
1386
                $contentType = $this->CType_labels[$row['CType']];
1387
                if (!isset($contentType)) {
1388
                    $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']);
1389
                }
1390
1391
                if ($contentType) {
1392
                    $previewHtml = $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
1393
                    if ($row['bodytext']) {
1394
                        $previewHtml .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
1395
                    }
1396
                    if ($row['image']) {
1397
                        $previewHtml .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />';
1398
                    }
1399
                } else {
1400
                    $message = sprintf(
1401
                        $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
1402
                        $row['CType']
1403
                    );
1404
                    $previewHtml = '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
1405
                }
1406
        }
1407
        return $previewHtml;
1408
    }
1409
1410
    /**
1411
     * Filters out all tt_content uids which are already translated so only non-translated uids is left.
1412
     * Selects across columns, but within in the same PID. Columns are expect to be the same
1413
     * for translations and original but this may be a conceptual error (?)
1414
     *
1415
     * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
1416
     * @param int $id The page UID from which to fetch untranslated records (unused, remains in place for compatibility)
1417
     * @param int $lP Sys language UID
1418
     * @return array Modified $defLanguageCount
1419
     */
1420
    public function getNonTranslatedTTcontentUids($defaultLanguageUids, $id, $lP)
1421
    {
1422
        if ($lP && !empty($defaultLanguageUids)) {
1423
            // Select all translations here:
1424
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1425
                ->getQueryBuilderForTable('tt_content');
1426
            $queryBuilder->getRestrictions()
1427
                ->removeAll()
1428
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1429
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0));
1430
            $queryBuilder
1431
                ->select('*')
1432
                ->from('tt_content')
1433
                ->where(
1434
                    $queryBuilder->expr()->eq(
1435
                        'sys_language_uid',
1436
                        $queryBuilder->createNamedParameter($lP, \PDO::PARAM_INT)
1437
                    ),
1438
                    $queryBuilder->expr()->in(
1439
                        'l18n_parent',
1440
                        $queryBuilder->createNamedParameter($defaultLanguageUids, Connection::PARAM_INT_ARRAY)
1441
                    )
1442
                );
1443
1444
            $result = $queryBuilder->execute();
1445
1446
            // Flip uids:
1447
            $defaultLanguageUids = array_flip($defaultLanguageUids);
1448
            // Traverse any selected elements and unset original UID if any:
1449
            while ($row = $result->fetch()) {
1450
                BackendUtility::workspaceOL('tt_content', $row);
1451
                unset($defaultLanguageUids[$row['l18n_parent']]);
1452
            }
1453
            // Flip again:
1454
            $defaultLanguageUids = array_keys($defaultLanguageUids);
1455
        }
1456
        return $defaultLanguageUids;
1457
    }
1458
1459
    /**
1460
     * Creates button which is used to create copies of records..
1461
     *
1462
     * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
1463
     * @param int $lP Sys language UID
1464
     * @return string "Copy languages" button, if available.
1465
     */
1466
    public function newLanguageButton($defaultLanguageUids, $lP)
1467
    {
1468
        $lP = (int)$lP;
1469
        if (!$this->doEdit || !$lP || !$this->hasContentModificationAndAccessPermissions()) {
1470
            return '';
1471
        }
1472
        $theNewButton = '';
1473
1474
        $localizationTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['localization.'] ?? [];
1475
        $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true);
1476
        $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true);
1477
        if (!empty($this->languageHasTranslationsCache[$lP])) {
1478
            if (isset($this->languageHasTranslationsCache[$lP]['hasStandAloneContent'])) {
1479
                $allowTranslate = false;
1480
            }
1481
            if (isset($this->languageHasTranslationsCache[$lP]['hasTranslations'])) {
1482
                $allowCopy = $allowCopy && !$this->languageHasTranslationsCache[$lP]['hasTranslations'];
1483
            }
1484
        }
1485
1486
        if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) {
1487
            foreach ($this->contentElementCache[$lP] as $column => $records) {
1488
                foreach ($records as $record) {
1489
                    $key = array_search($record['l10n_source'], $defaultLanguageUids);
1490
                    if ($key !== false) {
1491
                        unset($defaultLanguageUids[$key]);
1492
                    }
1493
                }
1494
            }
1495
        }
1496
1497
        if (!empty($defaultLanguageUids)) {
1498
            $theNewButton =
1499
                '<a'
1500
                    . ' href="#"'
1501
                    . ' class="btn btn-default btn-sm t3js-localize disabled"'
1502
                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
1503
                    . ' data-page="' . htmlspecialchars($this->getLocalizedPageTitle()) . '"'
1504
                    . ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"'
1505
                    . ' data-allow-copy="' . (int)$allowCopy . '"'
1506
                    . ' data-allow-translate="' . (int)$allowTranslate . '"'
1507
                    . ' data-table="tt_content"'
1508
                    . ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"'
1509
                    . ' data-language-id="' . $lP . '"'
1510
                    . ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"'
1511
                . '>'
1512
                . $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render()
1513
                . ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate'))
1514
                . '</a>';
1515
        }
1516
1517
        return $theNewButton;
1518
    }
1519
1520
    /**
1521
     * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE.
1522
     * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button
1523
     *
1524
     * @param string $str String to link. Must be prepared for HTML output.
1525
     * @param array $row The row.
1526
     * @return string If the whole thing was editable ($this->doEdit) $str is return with link around. Otherwise just $str.
1527
     * @see getTable_tt_content()
1528
     */
1529
    public function linkEditContent($str, $row)
1530
    {
1531
        if ($this->doEdit
1532
            && $this->hasContentModificationAndAccessPermissions()
1533
            && $this->getBackendUser()->recordEditAccessInternals('tt_content', $row)
1534
        ) {
1535
            $urlParameters = [
1536
                'edit' => [
1537
                    'tt_content' => [
1538
                        $row['uid'] => 'edit'
1539
                    ]
1540
                ],
1541
                'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() . '#element-tt_content-' . $row['uid']
1542
            ];
1543
            $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1544
            return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>';
1545
        }
1546
        return $str;
1547
    }
1548
1549
    /**
1550
     * Make selector box for creating new translation in a language
1551
     * Displays only languages which are not yet present for the current page and
1552
     * that are not disabled with page TS.
1553
     *
1554
     * @param int $id Page id for which to create a new translation record of pages
1555
     * @return string HTML <select> element (if there were items for the box anyways...)
1556
     * @see getTable_tt_content()
1557
     */
1558
    public function languageSelector($id)
1559
    {
1560
        if (!$this->getBackendUser()->check('tables_modify', 'pages')) {
1561
            return '';
1562
        }
1563
        $id = (int)$id;
1564
1565
        // First, select all languages that are available for the current user
1566
        $availableTranslations = [];
1567
        foreach ($this->siteLanguages as $language) {
1568
            if ($language->getLanguageId() <= 0) {
1569
                continue;
1570
            }
1571
            $availableTranslations[$language->getLanguageId()] = $language->getTitle();
1572
        }
1573
1574
        // Then, subtract the languages which are already on the page:
1575
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1576
        $queryBuilder->getRestrictions()->removeAll()
1577
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1578
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
1579
        $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
1580
            ->from('pages')
1581
            ->where(
1582
                $queryBuilder->expr()->eq(
1583
                    $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
1584
                    $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
1585
                )
1586
            );
1587
        $statement = $queryBuilder->execute();
1588
        while ($row = $statement->fetch()) {
1589
            unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]);
1590
        }
1591
        // If any languages are left, make selector:
1592
        if (!empty($availableTranslations)) {
1593
            $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->getLL('new_language')) . '</option>';
1594
            foreach ($availableTranslations as $languageUid => $languageTitle) {
1595
                // Build localize command URL to DataHandler (tce_db)
1596
                // which redirects to FormEngine (record_edit)
1597
                // which, when finished editing should return back to the current page (returnUrl)
1598
                $parameters = [
1599
                    'justLocalized' => 'pages:' . $id . ':' . $languageUid,
1600
                    'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()
1601
                ];
1602
                $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters);
1603
                $targetUrl = BackendUtility::getLinkToDataHandlerAction(
1604
                    '&cmd[pages][' . $id . '][localize]=' . $languageUid,
1605
                    $redirectUrl
1606
                );
1607
1608
                $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
1609
            }
1610
1611
            return '<div class="row row-cols-auto align-items-end g-3 mb-3">'
1612
                . '<div class="col">'
1613
                . '<select class="form-select" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">'
1614
                . $output
1615
                . '</select></div></div>';
1616
        }
1617
        return '';
1618
    }
1619
1620
    /**
1621
     * Traverse the result pointer given, adding each record to array and setting some internal values at the same time.
1622
     *
1623
     * @param Statement|Result $result DBAL Statement or Result
1624
     * @return array The selected rows returned in this array.
1625
     */
1626
    public function getResult($result): array
1627
    {
1628
        $output = [];
1629
        // Traverse the result:
1630
        while ($row = $result->fetch()) {
1631
            BackendUtility::workspaceOL('tt_content', $row, -99, true);
1632
            if ($row) {
1633
                // Add the row to the array:
1634
                $output[] = $row;
1635
            }
1636
        }
1637
        $this->generateTtContentDataArray($output);
1638
        // Return selected records
1639
        return $output;
1640
    }
1641
1642
    /********************************
1643
     *
1644
     * Various helper functions
1645
     *
1646
     ********************************/
1647
1648
    /**
1649
     * Initializes the clipboard for generating paste links
1650
     *
1651
     *
1652
     * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction()
1653
     * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction()
1654
     */
1655
    protected function initializeClipboard()
1656
    {
1657
        // Start clipboard
1658
        $this->clipboard = GeneralUtility::makeInstance(Clipboard::class);
1659
1660
        // Initialize - reads the clipboard content from the user session
1661
        $this->clipboard->initializeClipboard();
1662
1663
        // This locks the clipboard to the Normal for this request.
1664
        $this->clipboard->lockToNormal();
1665
1666
        // Clean up pad
1667
        $this->clipboard->cleanCurrent();
1668
1669
        // Save the clipboard content
1670
        $this->clipboard->endClipboard();
1671
    }
1672
1673
    /**
1674
     * Generates the data for previous and next elements which is needed for movements.
1675
     *
1676
     * @param array $rowArray
1677
     */
1678
    protected function generateTtContentDataArray(array $rowArray)
1679
    {
1680
        if (empty($this->tt_contentData)) {
1681
            $this->tt_contentData = [
1682
                'next' => [],
1683
                'prev' => [],
1684
            ];
1685
        }
1686
        foreach ($rowArray as $key => $value) {
1687
            // Create information for next and previous content elements
1688
            if (isset($rowArray[$key - 1])) {
1689
                if (isset($rowArray[$key - 2])) {
1690
                    $this->tt_contentData['prev'][$value['uid']] = -$rowArray[$key - 2]['uid'];
1691
                } else {
1692
                    $this->tt_contentData['prev'][$value['uid']] = $value['pid'];
1693
                }
1694
                $this->tt_contentData['next'][$rowArray[$key - 1]['uid']] = -$value['uid'];
1695
            }
1696
        }
1697
    }
1698
1699
    /**
1700
     * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc.
1701
     *
1702
     * @param string $input Input string
1703
     * @return string Output string
1704
     */
1705
    public function renderText($input)
1706
    {
1707
        $input = strip_tags($input);
1708
        $input = GeneralUtility::fixed_lgd_cs($input, 1500);
1709
        return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false));
1710
    }
1711
1712
    /**
1713
     * Creates the icon image tag for record from table and wraps it in a link which will trigger the click menu.
1714
     *
1715
     * @param string $table Table name
1716
     * @param array $row Record array
1717
     * @return string HTML for the icon
1718
     */
1719
    public function getIcon($table, $row)
1720
    {
1721
        // Initialization
1722
        $toolTip = BackendUtility::getRecordToolTip($row, $table);
1723
        $icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
1724
        // The icon with link
1725
        if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) {
1726
            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
1727
        }
1728
        return $icon;
1729
    }
1730
1731
    /**
1732
     * Creates processed values for all field names in $fieldList based on values from $row array.
1733
     * The result is 'returned' through $info which is passed as a reference
1734
     *
1735
     * @param string $table Table name
1736
     * @param string $fieldList Comma separated list of fields.
1737
     * @param array $row Record from which to take values for processing.
1738
     * @param array $info Array to which the processed values are added.
1739
     */
1740
    public function getProcessedValue($table, $fieldList, array $row, array &$info)
1741
    {
1742
        // Splitting values from $fieldList
1743
        $fieldArr = explode(',', $fieldList);
1744
        // Traverse fields from $fieldList
1745
        foreach ($fieldArr as $field) {
1746
            if ($row[$field]) {
1747
                $info[] = '<strong>' . htmlspecialchars($this->itemLabels[$field]) . '</strong> '
1748
                    . htmlspecialchars(BackendUtility::getProcessedValue($table, $field, $row[$field]) ?? '');
1749
            }
1750
        }
1751
    }
1752
1753
    /**
1754
     * Returns TRUE, if the record given as parameters is NOT visible based on hidden/starttime/endtime (if available)
1755
     *
1756
     * @param string $table Tablename of table to test
1757
     * @param array $row Record row.
1758
     * @return bool Returns TRUE, if disabled.
1759
     */
1760
    public function isDisabled($table, $row)
1761
    {
1762
        $enableCols = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
1763
        return $enableCols['disabled'] && $row[$enableCols['disabled']]
1764
            || $enableCols['starttime'] && $row[$enableCols['starttime']] > $GLOBALS['EXEC_TIME']
1765
            || $enableCols['endtime'] && $row[$enableCols['endtime']] && $row[$enableCols['endtime']] < $GLOBALS['EXEC_TIME'];
1766
    }
1767
1768
    /*****************************************
1769
     *
1770
     * External renderings
1771
     *
1772
     *****************************************/
1773
1774
    /**
1775
     * Create thumbnail code for record/field but not linked
1776
     *
1777
     * @param mixed[] $row Record array
1778
     * @param string $table Table (record is from)
1779
     * @param string $field Field name for which thumbnail are to be rendered.
1780
     * @return string HTML for thumbnails, if any.
1781
     */
1782
    public function getThumbCodeUnlinked($row, $table, $field)
1783
    {
1784
        return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false);
1785
    }
1786
1787
    /**
1788
     * Checks whether translated Content Elements exist in the desired language
1789
     * If so, deny creating new ones via the UI
1790
     *
1791
     * @param array $contentElements
1792
     * @param int $language
1793
     * @return bool
1794
     */
1795
    protected function checkIfTranslationsExistInLanguage(array $contentElements, int $language)
1796
    {
1797
        // If in default language, you may always create new entries
1798
        // Also, you may override this strict behavior via user TS Config
1799
        // If you do so, you're on your own and cannot rely on any support by the TYPO3 core
1800
        // We jump out here since we don't need to do the expensive loop operations
1801
        $allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false);
1802
        if ($language === 0 || $allowInconsistentLanguageHandling) {
1803
            return false;
1804
        }
1805
        /**
1806
         * Build up caches
1807
         */
1808
        if (!isset($this->languageHasTranslationsCache[$language])) {
1809
            foreach ($contentElements as $columns) {
1810
                foreach ($columns as $contentElement) {
1811
                    if ((int)$contentElement['l18n_parent'] === 0) {
1812
                        $this->languageHasTranslationsCache[$language]['hasStandAloneContent'] = true;
1813
                        $this->languageHasTranslationsCache[$language]['mode'] = 'free';
1814
                    }
1815
                    if ((int)$contentElement['l18n_parent'] > 0) {
1816
                        $this->languageHasTranslationsCache[$language]['hasTranslations'] = true;
1817
                        $this->languageHasTranslationsCache[$language]['mode'] = 'connected';
1818
                    }
1819
                }
1820
            }
1821
            if (!isset($this->languageHasTranslationsCache[$language])) {
1822
                $this->languageHasTranslationsCache[$language]['hasTranslations'] = false;
1823
            }
1824
            // Check whether we have a mix of both
1825
            if (isset($this->languageHasTranslationsCache[$language]['hasStandAloneContent'])
1826
                && $this->languageHasTranslationsCache[$language]['hasTranslations']
1827
            ) {
1828
                $this->languageHasTranslationsCache[$language]['mode'] = 'mixed';
1829
                $siteLanguage = $this->siteLanguages[$language];
1830
                $message = GeneralUtility::makeInstance(
1831
                    FlashMessage::class,
1832
                    $this->getLanguageService()->getLL('staleTranslationWarning'),
1833
                    sprintf($this->getLanguageService()->getLL('staleTranslationWarningTitle'), $siteLanguage->getTitle()),
1834
                    FlashMessage::WARNING
1835
                );
1836
                $service = GeneralUtility::makeInstance(FlashMessageService::class);
1837
                $queue = $service->getMessageQueueByIdentifier();
1838
                $queue->addMessage($message);
1839
            }
1840
        }
1841
1842
        return $this->languageHasTranslationsCache[$language]['hasTranslations'];
1843
    }
1844
1845
    /**
1846
     * @return BackendLayoutView
1847
     */
1848
    protected function getBackendLayoutView()
1849
    {
1850
        return GeneralUtility::makeInstance(BackendLayoutView::class);
1851
    }
1852
1853
    /**
1854
     * @return BackendUserAuthentication
1855
     */
1856
    protected function getBackendUser()
1857
    {
1858
        return $GLOBALS['BE_USER'];
1859
    }
1860
1861
    /**
1862
     * Create thumbnail code for record/field
1863
     *
1864
     * @param mixed[] $row Record array
1865
     * @param string $table Table (record is from)
1866
     * @param string $field Field name for which thumbnail are to be rendered.
1867
     * @return string HTML for thumbnails, if any.
1868
     */
1869
    public function thumbCode($row, $table, $field)
1870
    {
1871
        return BackendUtility::thumbCode($row, $table, $field);
1872
    }
1873
1874
    /**
1875
     * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted.
1876
     *
1877
     * @param string $table Table name
1878
     * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions
1879
     * @param string[] $additionalConstraints Additional part for where clause
1880
     * @param string[] $fields Field list to select, * for all
1881
     * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder
1882
     */
1883
    public function getQueryBuilder(
1884
        string $table,
1885
        int $pageId,
1886
        array $additionalConstraints = [],
1887
        array $fields = ['*']
1888
    ): QueryBuilder {
1889
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1890
            ->getQueryBuilderForTable($table);
1891
        $queryBuilder->getRestrictions()
1892
            ->removeAll()
1893
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1894
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
1895
        $queryBuilder
1896
            ->select(...$fields)
1897
            ->from($table);
1898
1899
        if (!empty($additionalConstraints)) {
1900
            $queryBuilder->andWhere(...$additionalConstraints);
1901
        }
1902
1903
        $queryBuilder = $this->prepareQueryBuilder($table, $pageId, $fields, $additionalConstraints, $queryBuilder);
1904
1905
        return $queryBuilder;
1906
    }
1907
1908
    /**
1909
     * Return the modified QueryBuilder object ($queryBuilder) which will be
1910
     * used to select the records from a table $table with pid = $this->pidList
1911
     *
1912
     * @param string $table Table name
1913
     * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions
1914
     * @param string[] $fieldList List of fields to select from the table
1915
     * @param string[] $additionalConstraints Additional part for where clause
1916
     * @param QueryBuilder $queryBuilder
1917
     * @return QueryBuilder
1918
     */
1919
    protected function prepareQueryBuilder(
1920
        string $table,
1921
        int $pageId,
1922
        array $fieldList,
1923
        array $additionalConstraints,
1924
        QueryBuilder $queryBuilder
1925
    ): QueryBuilder {
1926
        $parameters = [
1927
            'table' => $table,
1928
            'fields' => $fieldList,
1929
            'groupBy' => null,
1930
            'orderBy' => null
1931
        ];
1932
1933
        $sortBy = (string)($GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby']);
1934
        foreach (QueryHelper::parseOrderBy($sortBy) as $orderBy) {
1935
            $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]);
1936
        }
1937
1938
        // Build the query constraints
1939
        $queryBuilder->andWhere(
1940
            $queryBuilder->expr()->eq(
1941
                $table . '.pid',
1942
                $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1943
            )
1944
        );
1945
1946
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] ?? [] as $className) {
1947
            $hookObject = GeneralUtility::makeInstance($className);
1948
            if (method_exists($hookObject, 'modifyQuery')) {
1949
                $hookObject->modifyQuery(
1950
                    $parameters,
1951
                    $table,
1952
                    $pageId,
1953
                    $additionalConstraints,
1954
                    $fieldList,
1955
                    $queryBuilder
1956
                );
1957
            }
1958
        }
1959
1960
        return $queryBuilder;
1961
    }
1962
1963
    /**
1964
     * Renders the language flag and language title, but only if an icon is given, otherwise just the language
1965
     *
1966
     * @param SiteLanguage $language
1967
     * @return string
1968
     */
1969
    protected function renderLanguageFlag(SiteLanguage $language)
1970
    {
1971
        $title = htmlspecialchars($language->getTitle());
1972
        if ($language->getFlagIdentifier()) {
1973
            $icon = $this->iconFactory->getIcon(
1974
                $language->getFlagIdentifier(),
1975
                Icon::SIZE_SMALL
1976
            )->render();
1977
            return '<span title="' . $title . '" class="t3js-flag">' . $icon . '</span>&nbsp;<span class="t3js-language-title">' . $title . '</span>';
1978
        }
1979
        return $title;
1980
    }
1981
1982
    /**
1983
     * Fetch the site language objects for the given $pageId and store it in $this->siteLanguages
1984
     *
1985
     * @param int $pageId
1986
     * @throws SiteNotFoundException
1987
     */
1988
    protected function resolveSiteLanguages(int $pageId)
1989
    {
1990
        try {
1991
            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
1992
        } catch (SiteNotFoundException $e) {
1993
            $site = new NullSite();
1994
        }
1995
        $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUser(), true, $pageId);
1996
    }
1997
1998
    /**
1999
     * @return string $title
2000
     */
2001
    protected function getLocalizedPageTitle(): string
2002
    {
2003
        if (($this->tt_contentConfig['sys_language_uid'] ?? 0) > 0) {
2004
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2005
                ->getQueryBuilderForTable('pages');
2006
            $queryBuilder->getRestrictions()
2007
                ->removeAll()
2008
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2009
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
2010
            $localizedPage = $queryBuilder
2011
                ->select('*')
2012
                ->from('pages')
2013
                ->where(
2014
                    $queryBuilder->expr()->eq(
2015
                        $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2016
                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
2017
                    ),
2018
                    $queryBuilder->expr()->eq(
2019
                        $GLOBALS['TCA']['pages']['ctrl']['languageField'],
2020
                        $queryBuilder->createNamedParameter($this->tt_contentConfig['sys_language_uid'], \PDO::PARAM_INT)
2021
                    )
2022
                )
2023
                ->setMaxResults(1)
2024
                ->execute()
2025
                ->fetch();
2026
            BackendUtility::workspaceOL('pages', $localizedPage);
2027
            return $localizedPage['title'];
2028
        }
2029
        return $this->pageinfo['title'];
2030
    }
2031
2032
    /**
2033
     * Check if page can be edited by current user
2034
     *
2035
     * @return bool
2036
     */
2037
    protected function isPageEditable()
2038
    {
2039
        if ($this->getBackendUser()->isAdmin()) {
2040
            return true;
2041
        }
2042
        return !$this->pageinfo['editlock'] && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::PAGE_EDIT);
2043
    }
2044
2045
    /**
2046
     * Check if content can be edited by current user
2047
     *
2048
     * @param int|null $languageId
2049
     * @return bool
2050
     */
2051
    protected function isContentEditable(?int $languageId = null)
2052
    {
2053
        if ($this->getBackendUser()->isAdmin()) {
2054
            return true;
2055
        }
2056
        return !$this->pageinfo['editlock']
2057
            && $this->hasContentModificationAndAccessPermissions()
2058
            && ($languageId === null || $this->getBackendUser()->checkLanguageAccess($languageId));
2059
    }
2060
2061
    /**
2062
     * Check if current user has modification and access permissions for content set
2063
     *
2064
     * @return bool
2065
     */
2066
    protected function hasContentModificationAndAccessPermissions(): bool
2067
    {
2068
        return $this->getBackendUser()->check('tables_modify', 'tt_content')
2069
            && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT);
2070
    }
2071
2072
    /**
2073
     * Returns the language service
2074
     * @return LanguageService
2075
     */
2076
    protected function getLanguageService()
2077
    {
2078
        return $GLOBALS['LANG'];
2079
    }
2080
2081
    /**
2082
     * @param string $position Which event should be triggered? Possible options: before or after
2083
     * @param int $lP The language id you want to show data for
2084
     * @param array $columnConfig Array with the configuration of the current column
2085
     * @return string
2086
     */
2087
    protected function dispatchSectionMarkupGeneratedEvent(string $position, int $lP, array $columnConfig): string
2088
    {
2089
        if ($position === 'before') {
2090
            $event = new BeforeSectionMarkupGeneratedEvent($this, $lP, $columnConfig);
2091
        } else {
2092
            $event = new AfterSectionMarkupGeneratedEvent($this, $lP, $columnConfig);
2093
        }
2094
2095
        $this->eventDispatcher->dispatch($event);
2096
        return $event->getContent();
2097
    }
2098
}
2099