[FEATURE] Show only sys_languages in Backend which are available
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / View / PageLayoutView.php
1 <?php
2
3 namespace TYPO3\CMS\Backend\View;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\Driver\Statement;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
21 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
22 use TYPO3\CMS\Backend\Controller\Page\LocalizationController;
23 use TYPO3\CMS\Backend\Controller\PageLayoutController;
24 use TYPO3\CMS\Backend\Routing\UriBuilder;
25 use TYPO3\CMS\Backend\Tree\View\PageTreeView;
26 use TYPO3\CMS\Backend\Utility\BackendUtility;
27 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
28 use TYPO3\CMS\Core\Database\Connection;
29 use TYPO3\CMS\Core\Database\ConnectionPool;
30 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
31 use TYPO3\CMS\Core\Database\Query\QueryHelper;
32 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
33 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
34 use TYPO3\CMS\Core\Database\ReferenceIndex;
35 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
36 use TYPO3\CMS\Core\Imaging\Icon;
37 use TYPO3\CMS\Core\Imaging\IconFactory;
38 use TYPO3\CMS\Core\Localization\LanguageService;
39 use TYPO3\CMS\Core\Messaging\FlashMessage;
40 use TYPO3\CMS\Core\Messaging\FlashMessageService;
41 use TYPO3\CMS\Core\Page\PageRenderer;
42 use TYPO3\CMS\Core\Service\DependencyOrderingService;
43 use TYPO3\CMS\Core\Service\FlexFormService;
44 use TYPO3\CMS\Core\Site\Entity\Site;
45 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
46 use TYPO3\CMS\Core\Site\PseudoSiteFinder;
47 use TYPO3\CMS\Core\Site\SiteFinder;
48 use TYPO3\CMS\Core\Type\Bitmask\Permission;
49 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
50 use TYPO3\CMS\Core\Utility\GeneralUtility;
51 use TYPO3\CMS\Core\Utility\HttpUtility;
52 use TYPO3\CMS\Core\Utility\MathUtility;
53 use TYPO3\CMS\Core\Utility\StringUtility;
54 use TYPO3\CMS\Core\Versioning\VersionState;
55 use TYPO3\CMS\Fluid\View\StandaloneView;
56 use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
57
58 /**
59 * Child class for the Web > Page module
60 */
61 class PageLayoutView implements LoggerAwareInterface
62 {
63 use LoggerAwareTrait;
64
65 /**
66 * If TRUE, users/groups are shown in the page info box.
67 *
68 * @var bool
69 */
70 public $pI_showUser = false;
71
72 /**
73 * The number of successive records to edit when showing content elements.
74 *
75 * @var int
76 */
77 public $nextThree = 3;
78
79 /**
80 * If TRUE, disables the edit-column icon for tt_content elements
81 *
82 * @var bool
83 */
84 public $pages_noEditColumns = false;
85
86 /**
87 * If TRUE, new-wizards are linked to rather than the regular new-element list.
88 *
89 * @var bool
90 */
91 public $option_newWizard = true;
92
93 /**
94 * If set to "1", will link a big button to content element wizard.
95 *
96 * @var int
97 */
98 public $ext_function = 0;
99
100 /**
101 * If TRUE, elements will have edit icons (probably this is whether the user has permission to edit the page content). Set externally.
102 *
103 * @var bool
104 */
105 public $doEdit = true;
106
107 /**
108 * Age prefixes for displaying times. May be set externally to localized values.
109 *
110 * @var string
111 */
112 public $agePrefixes = ' min| hrs| days| yrs| min| hour| day| year';
113
114 /**
115 * Array of tables to be listed by the Web > Page module in addition to the default tables.
116 *
117 * @var array
118 */
119 public $externalTables = [];
120
121 /**
122 * "Pseudo" Description -table name
123 *
124 * @var string
125 */
126 public $descrTable;
127
128 /**
129 * If set TRUE, the language mode of tt_content elements will be rendered with hard binding between
130 * default language content elements and their translations!
131 *
132 * @var bool
133 */
134 public $defLangBinding = false;
135
136 /**
137 * External, static: Configuration of tt_content element display:
138 *
139 * @var array
140 */
141 public $tt_contentConfig = [
142 // Boolean: Display info-marks or not
143 'showInfo' => 1,
144 // Boolean: Display up/down arrows and edit icons for tt_content records
145 'showCommands' => 1,
146 'languageCols' => 0,
147 'languageMode' => 0,
148 'languageColsPointer' => 0,
149 'showHidden' => 1,
150 // Displays hidden records as well
151 'sys_language_uid' => 0,
152 // Which language
153 'cols' => '1,0,2,3',
154 'activeCols' => '1,0,2,3'
155 // Which columns can be accessed by current BE user
156 ];
157
158 /**
159 * Contains icon/title of pages which are listed in the tables menu (see getTableMenu() function )
160 *
161 * @var array
162 */
163 public $activeTables = [];
164
165 /**
166 * @var array
167 */
168 public $tt_contentData = [
169 'nextThree' => [],
170 'prev' => [],
171 'next' => []
172 ];
173
174 /**
175 * Used to store labels for CTypes for tt_content elements
176 *
177 * @var array
178 */
179 public $CType_labels = [];
180
181 /**
182 * Used to store labels for the various fields in tt_content elements
183 *
184 * @var array
185 */
186 public $itemLabels = [];
187
188 /**
189 * Indicates if all available fields for a user should be selected or not.
190 *
191 * @var int
192 */
193 public $allFields = 0;
194
195 /**
196 * Number of records to show
197 *
198 * @var int
199 */
200 public $showLimit = 0;
201
202 /**
203 * Shared module configuration, used by localization features
204 *
205 * @var array
206 */
207 public $modSharedTSconfig = [];
208
209 /**
210 * Tables which should not get listed
211 *
212 * @var string
213 */
214 public $hideTables = '';
215
216 /**
217 * Containing which fields to display in extended mode
218 *
219 * @var string[]
220 */
221 public $displayFields;
222
223 /**
224 * Tables which should not list their translations
225 *
226 * @var string
227 */
228 public $hideTranslations = '';
229
230 /**
231 * If set, csvList is outputted.
232 *
233 * @var bool
234 */
235 public $csvOutput = false;
236
237 /**
238 * Cache for record path
239 *
240 * @var mixed[]
241 */
242 public $recPath_cache = [];
243
244 /**
245 * Field, to sort list by
246 *
247 * @var string
248 */
249 public $sortField;
250
251 /**
252 * default Max items shown per table in "multi-table mode", may be overridden by tables.php
253 *
254 * @var int
255 */
256 public $itemsLimitPerTable = 20;
257
258 /**
259 * Page select permissions
260 *
261 * @var string
262 */
263 public $perms_clause = '';
264
265 /**
266 * Page id
267 *
268 * @var int
269 */
270 public $id;
271
272 /**
273 * Return URL
274 *
275 * @var string
276 */
277 public $returnUrl = '';
278
279 /**
280 * Tablename if single-table mode
281 *
282 * @var string
283 */
284 public $table = '';
285
286 /**
287 * Some permissions...
288 *
289 * @var int
290 */
291 public $calcPerms = 0;
292
293 /**
294 * Mode for what happens when a user clicks the title of a record.
295 *
296 * @var string
297 */
298 public $clickTitleMode = '';
299
300 /**
301 * Levels to search down.
302 *
303 * @var int
304 */
305 public $searchLevels = '';
306
307 /**
308 * "LIMIT " in SQL...
309 *
310 * @var int
311 */
312 public $iLimit = 0;
313
314 /**
315 * Set to the total number of items for a table when selecting.
316 *
317 * @var string
318 */
319 public $totalItems = '';
320
321 /**
322 * TSconfig which overwrites TCA-Settings
323 *
324 * @var mixed[][]
325 */
326 public $tableTSconfigOverTCA = [];
327
328 /**
329 * Loaded with page record with version overlay if any.
330 *
331 * @var string[]
332 */
333 public $pageRecord = [];
334
335 /**
336 * Used for tracking duplicate values of fields
337 *
338 * @var string[]
339 */
340 public $duplicateStack = [];
341
342 /**
343 * Fields to display for the current table
344 *
345 * @var string[]
346 */
347 public $setFields = [];
348
349 /**
350 * Current script name
351 *
352 * @var string
353 */
354 public $script = 'index.php';
355
356 /**
357 * If TRUE, records are listed only if a specific table is selected.
358 *
359 * @var bool
360 */
361 public $listOnlyInSingleTableMode = false;
362
363 /**
364 * JavaScript code accumulation
365 *
366 * @var string
367 */
368 public $JScode = '';
369
370 /**
371 * Pointer for browsing list
372 *
373 * @var int
374 */
375 public $firstElementNumber = 0;
376
377 /**
378 * Counting the elements no matter what...
379 *
380 * @var int
381 */
382 public $eCounter = 0;
383
384 /**
385 * Search string
386 *
387 * @var string
388 */
389 public $searchString = '';
390
391 /**
392 * default Max items shown per table in "single-table mode", may be overridden by tables.php
393 *
394 * @var int
395 */
396 public $itemsLimitSingleTable = 100;
397
398 /**
399 * Field, indicating to sort in reverse order.
400 *
401 * @var bool
402 */
403 public $sortRev;
404
405 /**
406 * String, can contain the field name from a table which must have duplicate values marked.
407 *
408 * @var string
409 */
410 public $duplicateField;
411
412 /**
413 * Specify a list of tables which are the only ones allowed to be displayed.
414 *
415 * @var string
416 */
417 public $tableList = '';
418
419 /**
420 * Array of collapsed / uncollapsed tables in multi table view
421 *
422 * @var int[][]
423 */
424 public $tablesCollapsed = [];
425
426 /**
427 * @var array[] Module configuration
428 */
429 public $modTSconfig;
430
431 /**
432 * HTML output
433 *
434 * @var string
435 */
436 public $HTMLcode = '';
437
438 /**
439 * Thumbnails on records containing files (pictures)
440 *
441 * @var bool
442 */
443 public $thumbs = 0;
444
445 /**
446 * Used for tracking next/prev uids
447 *
448 * @var int[][]
449 */
450 public $currentTable = [];
451
452 /**
453 * OBSOLETE - NOT USED ANYMORE. leftMargin
454 *
455 * @var int
456 */
457 public $leftMargin = 0;
458
459 /**
460 * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
461 *
462 * @var array
463 */
464 public $fieldArray = [];
465
466 /**
467 * Set to zero, if you don't want a left-margin with addElement function
468 *
469 * @var int
470 */
471 public $setLMargin = 1;
472
473 /**
474 * Contains page translation languages
475 *
476 * @var array
477 */
478 public $pageOverlays = [];
479
480 /**
481 * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard
482 *
483 * @var int
484 */
485 public $counter = 0;
486
487 /**
488 * Contains sys language icons and titles
489 *
490 * @var array
491 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Use site languages instead.
492 */
493 public $languageIconTitles = [];
494
495 /**
496 * Contains site languages for this page ID
497 *
498 * @var SiteLanguage[]
499 */
500 protected $siteLanguages = [];
501
502 /**
503 * Script URL
504 *
505 * @var string
506 */
507 public $thisScript = '';
508
509 /**
510 * If set this is <td> CSS-classname for odd columns in addElement. Used with db_layout / pages section
511 *
512 * @var string
513 */
514 public $oddColumnsCssClass = '';
515
516 /**
517 * Not used in this class - but maybe extension classes...
518 * Max length of strings
519 *
520 * @var int
521 */
522 public $fixedL = 30;
523
524 /**
525 * @var TranslationConfigurationProvider
526 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
527 */
528 public $translateTools;
529
530 /**
531 * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes;
532 *
533 * @var array
534 */
535 public $addElement_tdParams = [];
536
537 /**
538 * @var int
539 */
540 public $no_noWrap = 0;
541
542 /**
543 * @var int
544 */
545 public $showIcon = 1;
546
547 /**
548 * Keys are fieldnames and values are td-css-classes to add in addElement();
549 *
550 * @var array
551 */
552 public $addElement_tdCssClass = [];
553
554 /**
555 * @var \TYPO3\CMS\Backend\Clipboard\Clipboard
556 */
557 protected $clipboard;
558
559 /**
560 * User permissions
561 *
562 * @var int
563 */
564 public $ext_CALC_PERMS;
565
566 /**
567 * Current ids page record
568 *
569 * @var array
570 */
571 protected $pageinfo;
572
573 /**
574 * Caches the available languages in a colPos
575 *
576 * @var array
577 */
578 protected $languagesInColumnCache = [];
579
580 /**
581 * Caches the amount of content elements as a matrix
582 *
583 * @var array
584 * @internal
585 */
586 protected $contentElementCache = [];
587
588 /**
589 * @var IconFactory
590 */
591 protected $iconFactory;
592
593 /**
594 * Stores whether a certain language has translations in it
595 *
596 * @var array
597 */
598 protected $languageHasTranslationsCache = [];
599
600 /**
601 * @var LocalizationController
602 */
603 protected $localizationController;
604
605 /**
606 * Override the page ids taken into account by getPageIdConstraint()
607 *
608 * @var array
609 */
610 protected $overridePageIdList = [];
611
612 /**
613 * Override/add urlparameters in listUrl() method
614 *
615 * @var string[]
616 */
617 protected $overrideUrlParameters = [];
618
619 /**
620 * Array with before/after setting for tables
621 * Structure:
622 * 'tableName' => [
623 * 'before' => ['A', ...]
624 * 'after' => []
625 * ]
626 * @var array[]
627 */
628 protected $tableDisplayOrder = [];
629
630 /**
631 * Cache the number of references to a record
632 *
633 * @var array
634 */
635 protected $referenceCount = [];
636
637 /**
638 * Construct to initialize class variables.
639 */
640 public function __construct()
641 {
642 if (isset($GLOBALS['BE_USER']->uc['titleLen']) && $GLOBALS['BE_USER']->uc['titleLen'] > 0) {
643 $this->fixedL = $GLOBALS['BE_USER']->uc['titleLen'];
644 }
645 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
646 // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Remove this instance along with the property.
647 $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
648 $this->determineScriptUrl();
649 $this->localizationController = GeneralUtility::makeInstance(LocalizationController::class);
650 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
651 $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
652 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
653 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization');
654 }
655
656 /*****************************************
657 *
658 * Renderings
659 *
660 *****************************************/
661 /**
662 * Adds the code of a single table
663 *
664 * @param string $table Table name
665 * @param int $id Current page id
666 * @param string $fields
667 * @return string HTML for listing.
668 */
669 public function getTable($table, $id, $fields = '')
670 {
671 if (isset($this->externalTables[$table])) {
672 return $this->getExternalTables($id, $table);
673 }
674 // Branch out based on table name:
675 switch ($table) {
676 case 'pages':
677 return $this->getTable_pages($id);
678 break;
679 case 'tt_content':
680 return $this->getTable_tt_content($id);
681 break;
682 default:
683 return '';
684 }
685 }
686
687 /**
688 * Renders an external table from page id
689 *
690 * @param int $id Page id
691 * @param string $table Name of the table
692 * @return string HTML for the listing
693 */
694 public function getExternalTables($id, $table)
695 {
696 $this->pageinfo = BackendUtility::readPageAccess($id, '');
697 $type = $this->getPageLayoutController()->MOD_SETTINGS[$table];
698 if (!isset($type)) {
699 $type = 0;
700 }
701 // eg. "name;title;email;company,image"
702 $fList = $this->externalTables[$table][$type]['fList'];
703 // The columns are separeted by comma ','.
704 // Values separated by semicolon ';' are shown in the same column.
705 $icon = $this->externalTables[$table][$type]['icon'];
706 $addWhere = $this->externalTables[$table][$type]['addWhere'];
707 // Create listing
708 $out = $this->makeOrdinaryList($table, $id, $fList, $icon, $addWhere);
709 return $out;
710 }
711
712 /**
713 * Renders records from the pages table from page id
714 * (Used to get information about the page tree content by "Web>Info"!)
715 *
716 * @param int $id Page id
717 * @return string HTML for the listing
718 */
719 public function getTable_pages($id)
720 {
721 // Initializing:
722 $out = '';
723 $lang = $this->getLanguageService();
724 // Select current page:
725 if (!$id) {
726 // The root has a pseudo record in pageinfo...
727 $row = $this->getPageLayoutController()->pageinfo;
728 } else {
729 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
730 ->getQueryBuilderForTable('pages');
731 $queryBuilder->getRestrictions()
732 ->removeAll()
733 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
734 $row = $queryBuilder
735 ->select('*')
736 ->from('pages')
737 ->where(
738 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)),
739 $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
740 )
741 ->execute()
742 ->fetch();
743 BackendUtility::workspaceOL('pages', $row);
744 }
745 // If there was found a page:
746 if (is_array($row)) {
747 // Getting select-depth:
748 $depth = (int)$this->getPageLayoutController()->MOD_SETTINGS['pages_levels'];
749 // Overriding a few things:
750 $this->no_noWrap = 0;
751 // Items
752 $this->eCounter = $this->firstElementNumber;
753 // Creating elements:
754 list($flag, $code) = $this->fwd_rwd_nav();
755 $out .= $code;
756 $editUids = [];
757 if ($flag) {
758 // Getting children:
759 $theRows = $this->getPageRecordsRecursive($row['uid'], $depth);
760 if ($this->getBackendUser()->doesUserHaveAccess($row, 2) && $row['uid'] > 0) {
761 $editUids[] = $row['uid'];
762 }
763 $out .= $this->pages_drawItem($row, $this->fieldArray);
764 // Traverse all pages selected:
765 foreach ($theRows as $sRow) {
766 if ($this->getBackendUser()->doesUserHaveAccess($sRow, 2)) {
767 $editUids[] = $sRow['uid'];
768 }
769 $out .= $this->pages_drawItem($sRow, $this->fieldArray);
770 }
771 $this->eCounter++;
772 }
773 // Header line is drawn
774 $theData = [];
775 $editIdList = implode(',', $editUids);
776 // Traverse fields (as set above) in order to create header values:
777 foreach ($this->fieldArray as $field) {
778 if ($editIdList
779 && isset($GLOBALS['TCA']['pages']['columns'][$field])
780 && $field !== 'uid'
781 && !$this->pages_noEditColumns
782 ) {
783 $iTitle = sprintf(
784 $lang->getLL('editThisColumn'),
785 rtrim(trim($lang->sL(BackendUtility::getItemLabel('pages', $field))), ':')
786 );
787 $urlParameters = [
788 'edit' => [
789 'pages' => [
790 $editIdList => 'edit'
791 ]
792 ],
793 'columnsOnly' => $field,
794 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
795 ];
796 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
797 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
798 $eI = '<a class="btn btn-default" href="' . htmlspecialchars($url)
799 . '" title="' . htmlspecialchars($iTitle) . '">'
800 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
801 } else {
802 $eI = '';
803 }
804 switch ($field) {
805 case 'title':
806 $theData[$field] = '&nbsp;' . $eI . '<strong>'
807 . $lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label'])
808 . '</strong>';
809 break;
810 case 'uid':
811 $theData[$field] = '&nbsp;<strong>ID</strong>';
812 break;
813 default:
814 if (substr($field, 0, 6) === 'table_') {
815 $f2 = substr($field, 6);
816 if ($GLOBALS['TCA'][$f2]) {
817 $theData[$field] = '&nbsp;' .
818 '<span title="' .
819 htmlspecialchars($lang->sL($GLOBALS['TCA'][$f2]['ctrl']['title'])) .
820 '">' .
821 $this->iconFactory->getIconForRecord($f2, [], Icon::SIZE_SMALL)->render() .
822 '</span>';
823 }
824 } else {
825 $theData[$field] = '&nbsp;&nbsp;' . $eI . '<strong>'
826 . htmlspecialchars($lang->sL($GLOBALS['TCA']['pages']['columns'][$field]['label']))
827 . '</strong>';
828 }
829 }
830 }
831 $out = '<div class="table-fit">'
832 . '<table class="table table-striped table-hover typo3-page-pages">'
833 . '<thead>'
834 . $this->addElement(1, '', $theData)
835 . '</thead>'
836 . '<tbody>'
837 . $out
838 . '</tbody>'
839 . '</table>'
840 . '</div>';
841 }
842 return $out;
843 }
844
845 /**
846 * Renders Content Elements from the tt_content table from page id
847 *
848 * @param int $id Page id
849 * @return string HTML for the listing
850 */
851 public function getTable_tt_content($id)
852 {
853 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
854 ->getConnectionForTable('tt_content')
855 ->getExpressionBuilder();
856 $this->pageinfo = BackendUtility::readPageAccess($this->id, '');
857 $this->initializeLanguages();
858 $this->initializeClipboard();
859 $pageTitleParamForAltDoc = '&recTitle=' . rawurlencode(BackendUtility::getRecordTitle('pages', BackendUtility::getRecordWSOL('pages', $id), true));
860 /** @var $pageRenderer PageRenderer */
861 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
862 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/DragDrop');
863 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
864 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste');
865 if ($this->isPageEditable()) {
866 $languageOverlayId = 0;
867 $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->tt_contentConfig['sys_language_uid']);
868 if (is_array($pageLocalizationRecord)) {
869 $pageLocalizationRecord = reset($pageLocalizationRecord);
870 }
871 if (!empty($pageLocalizationRecord['uid'])) {
872 $languageOverlayId = $pageLocalizationRecord['uid'];
873 }
874 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', 'function(PageActions) {
875 PageActions.setPageId(' . (int)$this->id . ');
876 PageActions.setLanguageOverlayId(' . $languageOverlayId . ');
877 }');
878 }
879 // Get labels for CTypes and tt_content element fields in general:
880 $this->CType_labels = [];
881 foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $val) {
882 $this->CType_labels[$val[1]] = $this->getLanguageService()->sL($val[0]);
883 }
884 $this->itemLabels = [];
885 foreach ($GLOBALS['TCA']['tt_content']['columns'] as $name => $val) {
886 $this->itemLabels[$name] = $this->getLanguageService()->sL($val['label']);
887 }
888 $languageColumn = [];
889 $out = '';
890
891 // Setting language list:
892 $langList = $this->tt_contentConfig['sys_language_uid'];
893 if ($this->tt_contentConfig['languageMode']) {
894 if ($this->tt_contentConfig['languageColsPointer']) {
895 $langList = '0,' . $this->tt_contentConfig['languageColsPointer'];
896 } else {
897 $langList = implode(',', array_keys($this->tt_contentConfig['languageCols']));
898 }
899 $languageColumn = [];
900 }
901 $langListArr = GeneralUtility::intExplode(',', $langList);
902 $defaultLanguageElementsByColumn = [];
903 $defLangBinding = [];
904 // For each languages... :
905 // If not languageMode, then we'll only be through this once.
906 foreach ($langListArr as $lP) {
907 $lP = (int)$lP;
908
909 if (!isset($this->contentElementCache[$lP])) {
910 $this->contentElementCache[$lP] = [];
911 }
912
913 if (count($langListArr) === 1 || $lP === 0) {
914 $showLanguage = $expressionBuilder->in('sys_language_uid', [$lP, -1]);
915 } else {
916 $showLanguage = $expressionBuilder->eq('sys_language_uid', $lP);
917 }
918 $content = [];
919 $head = [];
920
921 $backendLayout = $this->getBackendLayoutView()->getSelectedBackendLayout($this->id);
922 $columns = $backendLayout['__colPosList'];
923 // Select content records per column
924 $contentRecordsPerColumn = $this->getContentRecordsPerColumn('table', $id, $columns, $showLanguage);
925 $cList = array_keys($contentRecordsPerColumn);
926 // For each column, render the content into a variable:
927 foreach ($cList as $columnId) {
928 if (!isset($this->contentElementCache[$lP])) {
929 $this->contentElementCache[$lP] = [];
930 }
931
932 if (!$lP) {
933 $defaultLanguageElementsByColumn[$columnId] = [];
934 }
935
936 // Start wrapping div
937 $content[$columnId] .= '<div data-colpos="' . $columnId . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper';
938 if (empty($contentRecordsPerColumn[$columnId])) {
939 $content[$columnId] .= ' t3-page-ce-empty';
940 }
941 $content[$columnId] .= '">';
942 // Add new content at the top most position
943 $link = '';
944 if ($this->isContentEditable()
945 && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
946 ) {
947 if ($this->option_newWizard) {
948 $urlParameters = [
949 'id' => $id,
950 'sys_language_uid' => $lP,
951 'colPos' => $columnId,
952 'uid_pid' => $id,
953 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
954 ];
955 $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override']
956 ?? 'new_content_element_wizard';
957 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
958 $url = (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters);
959 } else {
960 $urlParameters = [
961 'edit' => [
962 'tt_content' => [
963 $id => 'new'
964 ]
965 ],
966 'defVals' => [
967 'tt_content' => [
968 'colPos' => $columnId,
969 'sys_language_uid' => $lP
970 ]
971 ],
972 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
973 ];
974 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
975 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
976 }
977 $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement'));
978 $link = '<a href="#" data-url="' . htmlspecialchars($url) . '" '
979 . 'title="' . $title . '"'
980 . 'data-title="' . $title . '"'
981 . 'class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard">'
982 . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
983 . ' '
984 . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
985 }
986 if ($this->getBackendUser()->checkLanguageAccess($lP) && $columnId !== 'unused') {
987 $content[$columnId] .= '
988 <div class="t3-page-ce t3js-page-ce" data-page="' . (int)$id . '" id="' . StringUtility::getUniqueId() . '">
989 <div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-' . 'page-' . $id . '-' . StringUtility::getUniqueId() . '">'
990 . $link
991 . '</div>
992 <div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div>
993 </div>
994 ';
995 }
996 $editUidList = '';
997 if (!isset($contentRecordsPerColumn[$columnId]) || !is_array($contentRecordsPerColumn[$columnId])) {
998 $message = GeneralUtility::makeInstance(
999 FlashMessage::class,
1000 $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:error.invalidBackendLayout'),
1001 '',
1002 FlashMessage::WARNING
1003 );
1004 $service = GeneralUtility::makeInstance(FlashMessageService::class);
1005 $queue = $service->getMessageQueueByIdentifier();
1006 $queue->addMessage($message);
1007 } else {
1008 $rowArr = $contentRecordsPerColumn[$columnId];
1009 $this->generateTtContentDataArray($rowArr);
1010
1011 foreach ((array)$rowArr as $rKey => $row) {
1012 $this->contentElementCache[$lP][$columnId][$row['uid']] = $row;
1013 if ($this->tt_contentConfig['languageMode']) {
1014 $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
1015 }
1016 if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
1017 $singleElementHTML = '';
1018 if (!$lP && ($this->defLangBinding || $row['sys_language_uid'] != -1)) {
1019 $defaultLanguageElementsByColumn[$columnId][] = ($row['_ORIG_uid'] ?? $row['uid']);
1020 }
1021 $editUidList .= $row['uid'] . ',';
1022 $disableMoveAndNewButtons = $this->defLangBinding && $lP > 0 && $this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP);
1023 if (!$this->tt_contentConfig['languageMode']) {
1024 $singleElementHTML .= '<div class="t3-page-ce-dragitem" id="' . StringUtility::getUniqueId() . '">';
1025 }
1026 $singleElementHTML .= $this->tt_content_drawHeader(
1027 $row,
1028 $this->tt_contentConfig['showInfo'] ? 15 : 5,
1029 $disableMoveAndNewButtons,
1030 true,
1031 $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)
1032 );
1033 $innerContent = '<div ' . ($row['_ORIG_uid'] ? ' class="ver-element"' : '') . '>'
1034 . $this->tt_content_drawItem($row) . '</div>';
1035 $singleElementHTML .= '<div class="t3-page-ce-body-inner">' . $innerContent . '</div>'
1036 . $this->tt_content_drawFooter($row);
1037 $isDisabled = $this->isDisabled('tt_content', $row);
1038 $statusHidden = $isDisabled ? ' t3-page-ce-hidden t3js-hidden-record' : '';
1039 $displayNone = !$this->tt_contentConfig['showHidden'] && $isDisabled ? ' style="display: none;"' : '';
1040 $highlightHeader = '';
1041 if ($this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid']) && (int)$row['l18n_parent'] === 0) {
1042 $highlightHeader = ' t3-page-ce-danger';
1043 } elseif ($columnId === 'unused') {
1044 $highlightHeader = ' t3-page-ce-warning';
1045 }
1046 $singleElementHTML = '<div class="t3-page-ce' . $highlightHeader . ' t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
1047 . $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '"' . $displayNone . '>' . $singleElementHTML . '</div>';
1048
1049 if ($this->tt_contentConfig['languageMode']) {
1050 $singleElementHTML .= '<div class="t3-page-ce t3js-page-ce">';
1051 }
1052 $singleElementHTML .= '<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $columnId . '-' . 'page-' . $id .
1053 '-' . StringUtility::getUniqueId() . '">';
1054 // Add icon "new content element below"
1055 if (!$disableMoveAndNewButtons
1056 && $this->isContentEditable()
1057 && $this->getBackendUser()->checkLanguageAccess($lP)
1058 && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
1059 && $columnId !== 'unused'
1060 ) {
1061 // New content element:
1062 if ($this->option_newWizard) {
1063 $urlParameters = [
1064 'id' => $row['pid'],
1065 'sys_language_uid' => $row['sys_language_uid'],
1066 'colPos' => $row['colPos'],
1067 'uid_pid' => -$row['uid'],
1068 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1069 ];
1070 $routeName = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['newContentElementWizard.']['override']
1071 ?? 'new_content_element_wizard';
1072 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1073 $url = (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters);
1074 } else {
1075 $urlParameters = [
1076 'edit' => [
1077 'tt_content' => [
1078 -$row['uid'] => 'new'
1079 ]
1080 ],
1081 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1082 ];
1083 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1084 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1085 }
1086 $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement'));
1087 $singleElementHTML .= '<a href="#" data-url="' . htmlspecialchars($url) . '" '
1088 . 'title="' . $title . '"'
1089 . 'data-title="' . $title . '"'
1090 . 'class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard">'
1091 . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render()
1092 . ' '
1093 . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>';
1094 }
1095 $singleElementHTML .= '</div></div><div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div></div>';
1096 if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) {
1097 $defLangBinding[$columnId][$lP][$row[$lP ? 'l18n_parent' : 'uid'] ?: $row['uid']] = $singleElementHTML;
1098 } else {
1099 $content[$columnId] .= $singleElementHTML;
1100 }
1101 } else {
1102 unset($rowArr[$rKey]);
1103 }
1104 }
1105 $content[$columnId] .= '</div>';
1106 $colTitle = BackendUtility::getProcessedValue('tt_content', 'colPos', $columnId);
1107 $tcaItems = GeneralUtility::callUserFunction(\TYPO3\CMS\Backend\View\BackendLayoutView::class . '->getColPosListItemsParsed', $id, $this);
1108 foreach ($tcaItems as $item) {
1109 if ($item[1] == $columnId) {
1110 $colTitle = $this->getLanguageService()->sL($item[0]);
1111 }
1112 }
1113 if ($columnId === 'unused') {
1114 if (empty($unusedElementsMessage)) {
1115 $unusedElementsMessage = GeneralUtility::makeInstance(
1116 FlashMessage::class,
1117 $this->getLanguageService()->getLL('staleUnusedElementsWarning'),
1118 $this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'),
1119 FlashMessage::WARNING
1120 );
1121 $service = GeneralUtility::makeInstance(FlashMessageService::class);
1122 $queue = $service->getMessageQueueByIdentifier();
1123 $queue->addMessage($unusedElementsMessage);
1124 }
1125 $colTitle = $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.unused');
1126 $editParam = '';
1127 } else {
1128 $editParam = $this->doEdit && !empty($rowArr)
1129 ? '&edit[tt_content][' . $editUidList . ']=edit' . $pageTitleParamForAltDoc
1130 : '';
1131 }
1132 $head[$columnId] .= $this->tt_content_drawColHeader($colTitle, $editParam);
1133 }
1134 }
1135 // For each column, fit the rendered content into a table cell:
1136 $out = '';
1137 if ($this->tt_contentConfig['languageMode']) {
1138 // in language mode process the content elements, but only fill $languageColumn. output will be generated later
1139 $sortedLanguageColumn = [];
1140 foreach ($cList as $columnId) {
1141 if (GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnId) || $columnId === 'unused') {
1142 $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId];
1143
1144 // We sort $languageColumn again according to $cList as it may contain data already from above.
1145 $sortedLanguageColumn[$columnId] = $languageColumn[$columnId];
1146 }
1147 }
1148 if (!empty($languageColumn['unused'])) {
1149 $sortedLanguageColumn['unused'] = $languageColumn['unused'];
1150 }
1151 $languageColumn = $sortedLanguageColumn;
1152 } else {
1153 // GRID VIEW:
1154 $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">';
1155 // Add colgroups
1156 $colCount = (int)$backendLayout['__config']['backend_layout.']['colCount'];
1157 $rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount'];
1158 $grid .= '<colgroup>';
1159 for ($i = 0; $i < $colCount; $i++) {
1160 $grid .= '<col />';
1161 }
1162 $grid .= '</colgroup>';
1163
1164 // Check how to handle restricted columns
1165 $hideRestrictedCols = (bool)(BackendUtility::getPagesTSconfig($id)['mod.']['web_layout.']['hideRestrictedCols'] ?? false);
1166
1167 // Cycle through rows
1168 for ($row = 1; $row <= $rowCount; $row++) {
1169 $rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.'];
1170 if (!isset($rowConfig)) {
1171 continue;
1172 }
1173 $grid .= '<tr>';
1174 for ($col = 1; $col <= $colCount; $col++) {
1175 $columnConfig = $rowConfig['columns.'][$col . '.'];
1176 if (!isset($columnConfig)) {
1177 continue;
1178 }
1179 // Which tt_content colPos should be displayed inside this cell
1180 $columnKey = (int)$columnConfig['colPos'];
1181 // Render the grid cell
1182 $colSpan = (int)$columnConfig['colspan'];
1183 $rowSpan = (int)$columnConfig['rowspan'];
1184 $grid .= '<td valign="top"' .
1185 ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') .
1186 ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') .
1187 ' 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 .
1188 ((!isset($columnConfig['colPos']) || $columnConfig['colPos'] === '') ? ' t3-grid-cell-unassigned' : '') .
1189 ((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') : '') .
1190 ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') .
1191 ($rowSpan > 0 ? ' t3-gridCell-height' . $rowSpan : '') . '">';
1192
1193 // Draw the pre-generated header with edit and new buttons if a colPos is assigned.
1194 // If not, a new header without any buttons will be generated.
1195 if (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && $head[$columnKey]
1196 && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
1197 ) {
1198 $grid .= $head[$columnKey] . $content[$columnKey];
1199 } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== ''
1200 && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
1201 ) {
1202 if (!$hideRestrictedCols) {
1203 $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('noAccess'));
1204 }
1205 } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== ''
1206 && !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos'])
1207 ) {
1208 if (!$hideRestrictedCols) {
1209 $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) .
1210 ' (' . $this->getLanguageService()->getLL('noAccess') . ')');
1211 }
1212 } elseif (isset($columnConfig['name']) && $columnConfig['name'] !== '') {
1213 $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name'])
1214 . ' (' . $this->getLanguageService()->getLL('notAssigned') . ')');
1215 } else {
1216 $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('notAssigned'));
1217 }
1218
1219 $grid .= '</td>';
1220 }
1221 $grid .= '</tr>';
1222 }
1223 if (!empty($content['unused'])) {
1224 $grid .= '<tr>';
1225 // Which tt_content colPos should be displayed inside this cell
1226 $columnKey = 'unused';
1227 // Render the grid cell
1228 $colSpan = (int)$backendLayout['__config']['backend_layout.']['colCount'];
1229 $grid .= '<td valign="top"' .
1230 ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') .
1231 ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') .
1232 ' 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 .
1233 ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . '">';
1234
1235 // Draw the pre-generated header with edit and new buttons if a colPos is assigned.
1236 // If not, a new header without any buttons will be generated.
1237 $grid .= $head[$columnKey] . $content[$columnKey];
1238 $grid .= '</td></tr>';
1239 }
1240 $out .= $grid . '</table></div>';
1241 }
1242 }
1243 $elFromTable = $this->clipboard->elFromTable('tt_content');
1244 if (!empty($elFromTable) && $this->isPageEditable()) {
1245 $pasteItem = substr(key($elFromTable), 11);
1246 $pasteRecord = BackendUtility::getRecord('tt_content', (int)$pasteItem);
1247 $pasteTitle = $pasteRecord['header'] ? $pasteRecord['header'] : $pasteItem;
1248 $copyMode = $this->clipboard->clipData['normal']['mode'] ? '-' . $this->clipboard->clipData['normal']['mode'] : '';
1249 $addExtOnReadyCode = '
1250 top.pasteIntoLinkTemplate = '
1251 . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-into', 'pasteIntoColumn')
1252 . ';
1253 top.pasteAfterLinkTemplate = '
1254 . $this->tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-after', 'pasteAfterRecord')
1255 . ';';
1256 } else {
1257 $addExtOnReadyCode = '
1258 top.pasteIntoLinkTemplate = \'\';
1259 top.pasteAfterLinkTemplate = \'\';';
1260 }
1261 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
1262 $pageRenderer->addJsInlineCode('pasteLinkTemplates', $addExtOnReadyCode);
1263 // If language mode, then make another presentation:
1264 // Notice that THIS presentation will override the value of $out!
1265 // But it needs the code above to execute since $languageColumn is filled with content we need!
1266 if ($this->tt_contentConfig['languageMode']) {
1267 // Get language selector:
1268 $languageSelector = $this->languageSelector($id);
1269 // Reset out - we will make new content here:
1270 $out = '';
1271 // Traverse languages found on the page and build up the table displaying them side by side:
1272 $cCont = [];
1273 $sCont = [];
1274 foreach ($langListArr as $lP) {
1275 $languageMode = '';
1276 $labelClass = 'info';
1277 // Header:
1278 $lP = (int)$lP;
1279 // Determine language mode
1280 if ($lP > 0 && isset($this->languageHasTranslationsCache[$lP]['mode'])) {
1281 switch ($this->languageHasTranslationsCache[$lP]['mode']) {
1282 case 'mixed':
1283 $languageMode = $this->getLanguageService()->getLL('languageModeMixed');
1284 $labelClass = 'danger';
1285 break;
1286 case 'connected':
1287 $languageMode = $this->getLanguageService()->getLL('languageModeConnected');
1288 break;
1289 case 'free':
1290 $languageMode = $this->getLanguageService()->getLL('languageModeFree');
1291 break;
1292 default:
1293 // we'll let opcode optimize this intentionally empty case
1294 }
1295 }
1296 $cCont[$lP] = '
1297 <td valign="top" class="t3-page-column t3-page-column-lang-name" data-language-uid="' . $lP . '">
1298 <h2>' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '</h2>
1299 ' . ($languageMode !== '' ? '<span class="label label-' . $labelClass . '">' . $languageMode . '</span>' : '') . '
1300 </td>';
1301
1302 // "View page" icon is added:
1303 $viewLink = '';
1304 if (!VersionState::cast($this->getPageLayoutController()->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
1305 $onClick = BackendUtility::viewOnClick(
1306 $this->id,
1307 '',
1308 BackendUtility::BEgetRootLine($this->id),
1309 '',
1310 '',
1311 '&L=' . $lP
1312 );
1313 $viewLink = '<a href="#" class="btn btn-default btn-sm" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">' . $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render() . '</a>';
1314 }
1315 // Language overlay page header:
1316 if ($lP) {
1317 $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $id, $lP);
1318 if (is_array($pageLocalizationRecord)) {
1319 $pageLocalizationRecord = reset($pageLocalizationRecord);
1320 }
1321 BackendUtility::workspaceOL('pages', $pageLocalizationRecord);
1322 $recordIcon = BackendUtility::wrapClickMenuOnIcon(
1323 $this->iconFactory->getIconForRecord('pages', $pageLocalizationRecord, Icon::SIZE_SMALL)->render(),
1324 'pages',
1325 $pageLocalizationRecord['uid']
1326 );
1327 $urlParameters = [
1328 'edit' => [
1329 'pages' => [
1330 $pageLocalizationRecord['uid'] => 'edit'
1331 ]
1332 ],
1333 'overrideVals' => [
1334 'pages' => [
1335 'sys_language_uid' => $lP
1336 ]
1337 ],
1338 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1339 ];
1340 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1341 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1342 $editLink = (
1343 $this->getBackendUser()->check('tables_modify', 'pages')
1344 ? '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
1345 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
1346 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'
1347 : ''
1348 );
1349
1350 $defaultLanguageElements = [];
1351 array_walk($defaultLanguageElementsByColumn, function (array $columnContent) use (&$defaultLanguageElements) {
1352 $defaultLanguageElements = array_merge($defaultLanguageElements, $columnContent);
1353 });
1354
1355 $localizationButtons = [];
1356 $localizationButtons[] = $this->newLanguageButton(
1357 $this->getNonTranslatedTTcontentUids($defaultLanguageElements, $id, $lP),
1358 $lP
1359 );
1360
1361 $lPLabel =
1362 '<div class="btn-group">'
1363 . $viewLink
1364 . $editLink
1365 . (!empty($localizationButtons) ? implode(LF, $localizationButtons) : '')
1366 . '</div>'
1367 . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20))
1368 ;
1369 } else {
1370 $editLink = '';
1371 $recordIcon = '';
1372 if ($this->getBackendUser()->checkLanguageAccess(0)) {
1373 $recordIcon = BackendUtility::wrapClickMenuOnIcon(
1374 $this->iconFactory->getIconForRecord('pages', $this->pageRecord, Icon::SIZE_SMALL)->render(),
1375 'pages',
1376 $this->id
1377 );
1378 $urlParameters = [
1379 'edit' => [
1380 'pages' => [
1381 $this->id => 'edit'
1382 ]
1383 ],
1384 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1385 ];
1386 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1387 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1388 $editLink = (
1389 $this->getBackendUser()->check('tables_modify', 'pages')
1390 ? '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"'
1391 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
1392 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'
1393 : ''
1394 );
1395 }
1396
1397 $lPLabel =
1398 '<div class="btn-group">'
1399 . $viewLink
1400 . $editLink
1401 . '</div>'
1402 . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($this->pageRecord['title'], 20));
1403 }
1404 $sCont[$lP] = '
1405 <td class="t3-page-column t3-page-lang-label nowrap">' . $lPLabel . '</td>';
1406 }
1407 // Add headers:
1408 $out .= '<tr>' . implode($cCont) . '</tr>';
1409 $out .= '<tr>' . implode($sCont) . '</tr>';
1410 unset($cCont, $sCont);
1411
1412 // Traverse previously built content for the columns:
1413 foreach ($languageColumn as $cKey => $cCont) {
1414 $out .= '<tr>';
1415 foreach ($cCont as $languageId => $columnContent) {
1416 $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>';
1417 }
1418 $out .= '</tr>';
1419 if ($this->defLangBinding && !empty($defLangBinding[$cKey])) {
1420 $maxItemsCount = max(array_map('count', $defLangBinding[$cKey]));
1421 for ($i = 0; $i < $maxItemsCount; $i++) {
1422 $defUid = $defaultLanguageElementsByColumn[$cKey][$i] ?? 0;
1423 $cCont = [];
1424 foreach ($langListArr as $lP) {
1425 if ($lP > 0
1426 && is_array($defLangBinding[$cKey][$lP])
1427 && !$this->checkIfTranslationsExistInLanguage($defaultLanguageElementsByColumn[$cKey], $lP)
1428 && count($defLangBinding[$cKey][$lP]) > $i
1429 ) {
1430 $slice = array_slice($defLangBinding[$cKey][$lP], $i, 1);
1431 $element = $slice[0] ?? '';
1432 } else {
1433 $element = $defLangBinding[$cKey][$lP][$defUid] ?? '';
1434 }
1435 $cCont[] = $element;
1436 }
1437 $out .= '
1438 <tr>
1439 <td valign="top" class="t3-grid-cell">' . implode('</td>' . '
1440 <td valign="top" class="t3-grid-cell">', $cCont) . '</td>
1441 </tr>';
1442 }
1443 }
1444 }
1445 // Finally, wrap it all in a table and add the language selector on top of it:
1446 $out = $languageSelector . '
1447 <div class="t3-grid-container">
1448 <table cellpadding="0" cellspacing="0" class="t3-page-columns t3-grid-table t3js-page-columns">
1449 ' . $out . '
1450 </table>
1451 </div>';
1452 }
1453
1454 return $out;
1455 }
1456
1457 /**********************************
1458 *
1459 * Generic listing of items
1460 *
1461 **********************************/
1462 /**
1463 * Creates a standard list of elements from a table.
1464 *
1465 * @param string $table Table name
1466 * @param int $id Page id.
1467 * @param string $fList Comma list of fields to display
1468 * @param bool $icon If TRUE, icon is shown
1469 * @param string $addWhere Additional WHERE-clauses.
1470 * @return string HTML table
1471 */
1472 public function makeOrdinaryList($table, $id, $fList, $icon = false, $addWhere = '')
1473 {
1474 // Initialize
1475 $addWhere = empty($addWhere) ? [] : [QueryHelper::stripLogicalOperatorPrefix($addWhere)];
1476 $queryBuilder = $this->getQueryBuilder($table, $id, $addWhere);
1477 $this->setTotalItems($table, $id, $addWhere);
1478 $dbCount = 0;
1479 $result = false;
1480 // Make query for records if there were any records found in the count operation
1481 if ($this->totalItems) {
1482 $result = $queryBuilder->execute();
1483 // Will return FALSE, if $result is invalid
1484 $dbCount = $queryBuilder->count('uid')->execute()->fetchColumn(0);
1485 }
1486 // If records were found, render the list
1487 if (!$dbCount) {
1488 return '';
1489 }
1490 // Set fields
1491 $out = '';
1492 $this->fieldArray = GeneralUtility::trimExplode(',', '__cmds__,' . $fList . ',__editIconLink__', true);
1493 $theData = [];
1494 $theData = $this->headerFields($this->fieldArray, $table, $theData);
1495 // Title row
1496 $localizedTableTitle = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']));
1497 $out .= '<tr><th class="col-icon"></th>'
1498 . '<th colspan="' . (count($theData) - 2) . '"><span class="c-table">'
1499 . $localizedTableTitle . '</span> (' . $dbCount . ')</td>' . '<td class="col-icon"></td>'
1500 . '</tr>';
1501 // Column's titles
1502 if ($this->doEdit) {
1503 $urlParameters = [
1504 'edit' => [
1505 $table => [
1506 $this->id => 'new'
1507 ]
1508 ],
1509 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1510 ];
1511 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1512 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1513 $title = htmlspecialchars($this->getLanguageService()->getLL('new'));
1514 $theData['__cmds__'] = '<a href="#" data-url="' . htmlspecialchars($url) . '" class="t3js-toggle-new-content-element-wizard" '
1515 . 'title="' . $title . '"'
1516 . 'data-title="' . $title . '">'
1517 . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</a>';
1518 }
1519 $out .= $this->addElement(1, '', $theData, ' class="c-headLine"', 15, '', 'th');
1520 // Render Items
1521 $this->eCounter = $this->firstElementNumber;
1522 while ($row = $result->fetch()) {
1523 BackendUtility::workspaceOL($table, $row);
1524 if (is_array($row)) {
1525 list($flag, $code) = $this->fwd_rwd_nav();
1526 $out .= $code;
1527 if ($flag) {
1528 $Nrow = [];
1529 // Setting icons links
1530 if ($icon) {
1531 $Nrow['__cmds__'] = $this->getIcon($table, $row);
1532 }
1533 // Get values:
1534 $Nrow = $this->dataFields($this->fieldArray, $table, $row, $Nrow);
1535 // Attach edit icon
1536 if ($this->doEdit && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) {
1537 $urlParameters = [
1538 'edit' => [
1539 $table => [
1540 $row['uid'] => 'edit'
1541 ]
1542 ],
1543 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1544 ];
1545 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1546 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1547 $Nrow['__editIconLink__'] = '<a class="btn btn-default" href="' . htmlspecialchars($url)
1548 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">'
1549 . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1550 } else {
1551 $Nrow['__editIconLink__'] = $this->noEditIcon();
1552 }
1553 $out .= $this->addElement(1, '', $Nrow);
1554 }
1555 $this->eCounter++;
1556 }
1557 }
1558 // Wrap it all in a table:
1559 $out = '
1560 <!--
1561 Standard list of table "' . $table . '"
1562 -->
1563 <div class="table-fit"><table class="table table-hover table-striped">
1564 ' . $out . '
1565 </table></div>';
1566 return $out;
1567 }
1568
1569 /**
1570 * Adds content to all data fields in $out array
1571 *
1572 * Each field name in $fieldArr has a special feature which is that the field name can be specified as more field names.
1573 * Eg. "field1,field2;field3".
1574 * Field 2 and 3 will be shown in the same cell of the table separated by <br /> while field1 will have its own cell.
1575 *
1576 * @param array $fieldArr Array of fields to display
1577 * @param string $table Table name
1578 * @param array $row Record array
1579 * @param array $out Array to which the data is added
1580 * @return array $out array returned after processing.
1581 * @see makeOrdinaryList()
1582 */
1583 public function dataFields($fieldArr, $table, $row, $out = [])
1584 {
1585 // Check table validity
1586 if (!isset($GLOBALS['TCA'][$table])) {
1587 return $out;
1588 }
1589
1590 $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
1591 // Traverse fields
1592 foreach ($fieldArr as $fieldName) {
1593 if ($GLOBALS['TCA'][$table]['columns'][$fieldName]) {
1594 // Each field has its own cell (if configured in TCA)
1595 // If the column is a thumbnail column:
1596 if ($fieldName == $thumbsCol) {
1597 $out[$fieldName] = $this->thumbCode($row, $table, $fieldName);
1598 } else {
1599 // ... otherwise just render the output:
1600 $out[$fieldName] = nl2br(htmlspecialchars(trim(GeneralUtility::fixed_lgd_cs(
1601 BackendUtility::getProcessedValue($table, $fieldName, $row[$fieldName], 0, 0, 0, $row['uid']),
1602 250
1603 ))));
1604 }
1605 } else {
1606 // Each field is separated by <br /> and shown in the same cell (If not a TCA field, then explode
1607 // the field name with ";" and check each value there as a TCA configured field)
1608 $theFields = explode(';', $fieldName);
1609 // Traverse fields, separated by ";" (displayed in a single cell).
1610 foreach ($theFields as $fName2) {
1611 if ($GLOBALS['TCA'][$table]['columns'][$fName2]) {
1612 $out[$fieldName] .= '<strong>' . htmlspecialchars($this->getLanguageService()->sL(
1613 $GLOBALS['TCA'][$table]['columns'][$fName2]['label']
1614 )) . '</strong>' . '&nbsp;&nbsp;' . htmlspecialchars(GeneralUtility::fixed_lgd_cs(
1615 BackendUtility::getProcessedValue($table, $fName2, $row[$fName2], 0, 0, 0, $row['uid']),
1616 25
1617 )) . '<br />';
1618 }
1619 }
1620 }
1621 // If no value, add a nbsp.
1622 if (!$out[$fieldName]) {
1623 $out[$fieldName] = '&nbsp;';
1624 }
1625 // Wrap in dimmed-span tags if record is "disabled"
1626 if ($this->isDisabled($table, $row)) {
1627 $out[$fieldName] = '<span class="text-muted">' . $out[$fieldName] . '</span>';
1628 }
1629 }
1630 return $out;
1631 }
1632
1633 /**
1634 * Header fields made for the listing of records
1635 *
1636 * @param array $fieldArr Field names
1637 * @param string $table The table name
1638 * @param array $out Array to which the headers are added.
1639 * @return array $out returned after addition of the header fields.
1640 * @see makeOrdinaryList()
1641 */
1642 public function headerFields($fieldArr, $table, $out = [])
1643 {
1644 foreach ($fieldArr as $fieldName) {
1645 $ll = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label']));
1646 $out[$fieldName] = $ll ? $ll : '&nbsp;';
1647 }
1648 return $out;
1649 }
1650
1651 /**
1652 * Gets content records per column.
1653 * This is required for correct workspace overlays.
1654 *
1655 * @param string $table UNUSED (will always be queried from tt_content)
1656 * @param int $id Page Id to be used (not used at all, but part of the API, see $this->pidSelect)
1657 * @param array $columns colPos values to be considered to be shown
1658 * @param string $additionalWhereClause Additional where clause for database select
1659 * @return array Associative array for each column (colPos)
1660 */
1661 protected function getContentRecordsPerColumn($table, $id, array $columns, $additionalWhereClause = '')
1662 {
1663 $contentRecordsPerColumn = array_fill_keys($columns, []);
1664 $columns = array_flip($columns);
1665 $queryBuilder = $this->getQueryBuilder(
1666 'tt_content',
1667 $id,
1668 [
1669 $additionalWhereClause
1670 ]
1671 );
1672
1673 // Traverse any selected elements and render their display code:
1674 $results = $this->getResult($queryBuilder->execute());
1675 $unused = [];
1676 $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] ?? [];
1677 foreach ($results as $record) {
1678 $used = isset($columns[$record['colPos']]);
1679 foreach ($hookArray as $_funcRef) {
1680 $_params = ['columns' => $columns, 'record' => $record, 'used' => $used];
1681 $used = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1682 }
1683 if ($used) {
1684 $columnValue = (string)$record['colPos'];
1685 $contentRecordsPerColumn[$columnValue][] = $record;
1686 } else {
1687 $unused[] = $record;
1688 }
1689 }
1690 if (!empty($unused)) {
1691 $contentRecordsPerColumn['unused'] = $unused;
1692 }
1693 return $contentRecordsPerColumn;
1694 }
1695
1696 /**********************************
1697 *
1698 * Additional functions; Pages
1699 *
1700 **********************************/
1701
1702 /**
1703 * Adds pages-rows to an array, selecting recursively in the page tree.
1704 *
1705 * @param int $pid Starting page id to select from
1706 * @param string $iconPrefix Prefix for icon code.
1707 * @param int $depth Depth (decreasing)
1708 * @param array $rows Array which will accumulate page rows
1709 * @return array $rows with added rows.
1710 */
1711 protected function getPageRecordsRecursive(int $pid, int $depth, string $iconPrefix = '', array $rows = []): array
1712 {
1713 $depth--;
1714 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1715 $queryBuilder->getRestrictions()
1716 ->removeAll()
1717 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1718 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
1719
1720 $queryBuilder
1721 ->select('*')
1722 ->from('pages')
1723 ->where(
1724 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)),
1725 $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
1726 $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
1727 );
1728
1729 if (!empty($GLOBALS['TCA']['pages']['ctrl']['sortby'])) {
1730 $queryBuilder->orderBy($GLOBALS['TCA']['pages']['ctrl']['sortby']);
1731 }
1732
1733 if ($depth >= 0) {
1734 $result = $queryBuilder->execute();
1735 $rowCount = $queryBuilder->count('uid')->execute()->fetchColumn(0);
1736 $count = 0;
1737 while ($row = $result->fetch()) {
1738 BackendUtility::workspaceOL('pages', $row);
1739 if (is_array($row)) {
1740 $count++;
1741 $row['treeIcons'] = $iconPrefix
1742 . '<span class="treeline-icon treeline-icon-join'
1743 . ($rowCount === $count ? 'bottom' : '')
1744 . '"></span>';
1745 $rows[] = $row;
1746 // Get the branch
1747 $spaceOutIcons = '<span class="treeline-icon treeline-icon-'
1748 . ($rowCount === $count ? 'clear' : 'line')
1749 . '"></span>';
1750 $rows = $this->getPageRecordsRecursive(
1751 $row['uid'],
1752 $row['php_tree_stop'] ? 0 : $depth,
1753 $iconPrefix . $spaceOutIcons,
1754 $rows
1755 );
1756 }
1757 }
1758 }
1759
1760 return $rows;
1761 }
1762
1763 /**
1764 * Adds a list item for the pages-rendering
1765 *
1766 * @param array $row Record array
1767 * @param array $fieldArr Field list
1768 * @return string HTML for the item
1769 */
1770 public function pages_drawItem($row, $fieldArr)
1771 {
1772 // Initialization
1773 $theIcon = $this->getIcon('pages', $row);
1774 // Preparing and getting the data-array
1775 $theData = [];
1776 foreach ($fieldArr as $field) {
1777 switch ($field) {
1778 case 'title':
1779 $pTitle = htmlspecialchars(BackendUtility::getProcessedValue('pages', $field, $row[$field], 20));
1780 $theData[$field] = $row['treeIcons'] . $theIcon . $pTitle;
1781 break;
1782 case 'php_tree_stop':
1783 // Intended fall through
1784 case 'TSconfig':
1785 $theData[$field] = $row[$field] ? '<strong>x</strong>' : '&nbsp;';
1786 break;
1787 case 'uid':
1788 if ($this->getBackendUser()->doesUserHaveAccess($row, 2) && $row['uid'] > 0) {
1789 $urlParameters = [
1790 'edit' => [
1791 'pages' => [
1792 $row['uid'] => 'edit'
1793 ]
1794 ],
1795 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
1796 ];
1797 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1798 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
1799 $onClick = BackendUtility::viewOnClick($row['uid'], '', BackendUtility::BEgetRootLine($row['uid']));
1800
1801 $eI =
1802 '<a href="#" onclick="' . htmlspecialchars($onClick) . '" class="btn btn-default" title="' .
1803 $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">' .
1804 $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render() .
1805 '</a>';
1806 $eI .=
1807 '<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' .
1808 htmlspecialchars($this->getLanguageService()->getLL('editThisPage')) . '">' .
1809 $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render() .
1810 '</a>';
1811 } else {
1812 $eI = '';
1813 }
1814 $theData[$field] = '<div class="btn-group" role="group">' . $eI . '</div>';
1815 break;
1816 case 'shortcut':
1817 case 'shortcut_mode':
1818 if ((int)$row['doktype'] === \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_SHORTCUT) {
1819 $theData[$field] = $this->getPagesTableFieldValue($field, $row);
1820 }
1821 break;
1822 default:
1823 if (substr($field, 0, 6) === 'table_') {
1824 $f2 = substr($field, 6);
1825 if ($GLOBALS['TCA'][$f2]) {
1826 $c = $this->numberOfRecords($f2, $row['uid']);
1827 $theData[$field] = ($c ? $c : '');
1828 }
1829 } else {
1830 $theData[$field] = $this->getPagesTableFieldValue($field, $row);
1831 }
1832 }
1833 }
1834 $this->addElement_tdParams['title'] = $row['_CSSCLASS'] ? ' class="' . $row['_CSSCLASS'] . '"' : '';
1835 return $this->addElement(1, '', $theData);
1836 }
1837
1838 /**
1839 * Returns the HTML code for rendering a field in the pages table.
1840 * The row value is processed to a human readable form and the result is parsed through htmlspecialchars().
1841 *
1842 * @param string $field The name of the field of which the value should be rendered.
1843 * @param array $row The pages table row as an associative array.
1844 * @return string The rendered table field value.
1845 */
1846 protected function getPagesTableFieldValue($field, array $row)
1847 {
1848 return htmlspecialchars(BackendUtility::getProcessedValue('pages', $field, $row[$field]));
1849 }
1850
1851 /**********************************
1852 *
1853 * Additional functions; Content Elements
1854 *
1855 **********************************/
1856 /**
1857 * Draw header for a content element column:
1858 *
1859 * @param string $colName Column name
1860 * @param string $editParams Edit params (Syntax: &edit[...] for FormEngine)
1861 * @return string HTML table
1862 */
1863 public function tt_content_drawColHeader($colName, $editParams = '')
1864 {
1865 $iconsArr = [];
1866 // Create command links:
1867 if ($this->tt_contentConfig['showCommands']) {
1868 // Edit whole of column:
1869 if ($editParams && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT) && $this->getBackendUser()->checkLanguageAccess(0)) {
1870 $iconsArr['edit'] = '<a href="#" onclick="'
1871 . htmlspecialchars(BackendUtility::editOnClick($editParams)) . '" title="'
1872 . htmlspecialchars($this->getLanguageService()->getLL('editColumn')) . '">'
1873 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1874 }
1875 }
1876 $icons = '';
1877 if (!empty($iconsArr)) {
1878 $icons = '<div class="t3-page-column-header-icons">' . implode('', $iconsArr) . '</div>';
1879 }
1880 // Create header row:
1881 $out = '<div class="t3-page-column-header">
1882 ' . $icons . '
1883 <div class="t3-page-column-header-label">' . htmlspecialchars($colName) . '</div>
1884 </div>';
1885 return $out;
1886 }
1887
1888 /**
1889 * Draw a paste icon either for pasting into a column or for pasting after a record
1890 *
1891 * @param int $pasteItem ID of the item in the clipboard
1892 * @param string $pasteTitle Title for the JS modal
1893 * @param string $copyMode copy or cut
1894 * @param string $cssClass CSS class to determine if pasting is done into column or after record
1895 * @param string $title title attribute of the generated link
1896 *
1897 * @return string Generated HTML code with link and icon
1898 */
1899 protected function tt_content_drawPasteIcon($pasteItem, $pasteTitle, $copyMode, $cssClass, $title)
1900 {
1901 $pasteIcon = json_encode(
1902 ' <a data-content="' . htmlspecialchars($pasteItem) . '"'
1903 . ' data-title="' . htmlspecialchars($pasteTitle) . '"'
1904 . ' class="t3js-paste t3js-paste' . htmlspecialchars($copyMode) . ' ' . htmlspecialchars($cssClass) . ' btn btn-default btn-sm"'
1905 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL($title)) . '">'
1906 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
1907 . '</a>'
1908 );
1909 return $pasteIcon;
1910 }
1911
1912 /**
1913 * Draw the footer for a single tt_content element
1914 *
1915 * @param array $row Record array
1916 * @return string HTML of the footer
1917 * @throws \UnexpectedValueException
1918 */
1919 protected function tt_content_drawFooter(array $row)
1920 {
1921 $content = '';
1922 // Get processed values:
1923 $info = [];
1924 $this->getProcessedValue('tt_content', 'starttime,endtime,fe_group,space_before_class,space_after_class', $row, $info);
1925
1926 // Content element annotation
1927 if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) {
1928 $info[] = htmlspecialchars($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]);
1929 }
1930
1931 // Call drawFooter hooks
1932 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) {
1933 $hookObject = GeneralUtility::makeInstance($className);
1934 if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) {
1935 throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1404378171);
1936 }
1937 $hookObject->preProcess($this, $info, $row);
1938 }
1939
1940 // Display info from records fields:
1941 if (!empty($info)) {
1942 $content = '<div class="t3-page-ce-info">
1943 ' . implode('<br>', $info) . '
1944 </div>';
1945 }
1946 // Wrap it
1947 if (!empty($content)) {
1948 $content = '<div class="t3-page-ce-footer">' . $content . '</div>';
1949 }
1950 return $content;
1951 }
1952
1953 /**
1954 * Draw the header for a single tt_content element
1955 *
1956 * @param array $row Record array
1957 * @param int $space Amount of pixel space above the header. UNUSED
1958 * @param bool $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown.
1959 * @param bool $langMode If set, we are in language mode and flags will be shown for languages
1960 * @param bool $dragDropEnabled If set the move button must be hidden
1961 * @return string HTML table with the record header.
1962 */
1963 public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = false, $langMode = false, $dragDropEnabled = false)
1964 {
1965 $backendUser = $this->getBackendUser();
1966 $out = '';
1967 // If show info is set...;
1968 if ($this->tt_contentConfig['showInfo'] && $backendUser->recordEditAccessInternals('tt_content', $row)) {
1969 // Render control panel for the element:
1970 if ($this->tt_contentConfig['showCommands'] && $this->doEdit) {
1971 // Edit content element:
1972 $urlParameters = [
1973 'edit' => [
1974 'tt_content' => [
1975 $this->tt_contentData['nextThree'][$row['uid']] => 'edit'
1976 ]
1977 ],
1978 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'],
1979 ];
1980 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1981 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $row['uid'];
1982
1983 $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url)
1984 . '" title="' . htmlspecialchars($this->nextThree > 1
1985 ? sprintf($this->getLanguageService()->getLL('nextThree'), $this->nextThree)
1986 : $this->getLanguageService()->getLL('edit'))
1987 . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1988 // Hide element:
1989 $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled'];
1990 if ($hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField]
1991 && (!$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude']
1992 || $backendUser->check('non_exclude_fields', 'tt_content:' . $hiddenField))
1993 ) {
1994 if ($row[$hiddenField]) {
1995 $value = 0;
1996 $label = 'unHide';
1997 } else {
1998 $value = 1;
1999 $label = 'hide';
2000 }
2001 $params = '&data[tt_content][' . ($row['_ORIG_uid'] ? $row['_ORIG_uid'] : $row['uid'])
2002 . '][' . $hiddenField . ']=' . $value;
2003 $out .= '<a class="btn btn-default" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2004 . '#element-tt_content-' . $row['uid'] . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($label)) . '">'
2005 . $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . '</a>';
2006 }
2007 // Delete
2008 $disableDelete = (bool)\trim(
2009 $backendUser->getTSConfig()['options.']['disableDelete.']['tt_content']
2010 ?? $backendUser->getTSConfig()['options.']['disableDelete']
2011 ?? '0'
2012 );
2013 if (!$disableDelete) {
2014 $params = '&cmd[tt_content][' . $row['uid'] . '][delete]=1';
2015 $refCountMsg = BackendUtility::referenceCount(
2016 'tt_content',
2017 $row['uid'],
2018 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
2019 $this->getReferenceCount('tt_content', $row['uid'])
2020 ) . BackendUtility::translationCount(
2021 'tt_content',
2022 $row['uid'],
2023 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
2024 );
2025 $confirm = $this->getLanguageService()->getLL('deleteWarning')
2026 . $refCountMsg;
2027 $out .= '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) . '"'
2028 . ' data-severity="warning"'
2029 . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')) . '"'
2030 . ' data-content="' . htmlspecialchars($confirm) . '" '
2031 . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
2032 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('deleteItem')) . '">'
2033 . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
2034 if ($out && $backendUser->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) {
2035 $out = '<div class="btn-group btn-group-sm" role="group">' . $out . '</div>';
2036 } else {
2037 $out = '';
2038 }
2039 }
2040 if (!$disableMoveAndNewButtons) {
2041 $moveButtonContent = '';
2042 $displayMoveButtons = false;
2043 // Move element up:
2044 if ($this->tt_contentData['prev'][$row['uid']]) {
2045 $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']];
2046 $moveButtonContent .= '<a class="btn btn-default" href="'
2047 . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2048 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
2049 . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
2050 if (!$dragDropEnabled) {
2051 $displayMoveButtons = true;
2052 }
2053 } else {
2054 $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
2055 }
2056 // Move element down:
2057 if ($this->tt_contentData['next'][$row['uid']]) {
2058 $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']];
2059 $moveButtonContent .= '<a class="btn btn-default" href="'
2060 . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2061 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
2062 . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
2063 if (!$dragDropEnabled) {
2064 $displayMoveButtons = true;
2065 }
2066 } else {
2067 $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
2068 }
2069 if ($displayMoveButtons) {
2070 $out .= '<div class="btn-group btn-group-sm" role="group">' . $moveButtonContent . '</div>';
2071 }
2072 }
2073 }
2074 }
2075 $allowDragAndDrop = $this->isDragAndDropAllowed($row);
2076 $additionalIcons = [];
2077 $additionalIcons[] = $this->getIcon('tt_content', $row) . ' ';
2078 if ($langMode && isset($this->siteLanguages[(int)$row['sys_language_uid']])) {
2079 $additionalIcons[] = $this->renderLanguageFlag($this->siteLanguages[(int)$row['sys_language_uid']]);
2080 }
2081 // Get record locking status:
2082 if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) {
2083 $additionalIcons[] = '<a href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">'
2084 . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</a>';
2085 }
2086 // Call stats information hook
2087 $_params = ['tt_content', $row['uid'], &$row];
2088 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) {
2089 $additionalIcons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2090 }
2091
2092 // Wrap the whole header
2093 // NOTE: end-tag for <div class="t3-page-ce-body"> is in getTable_tt_content()
2094 return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
2095 <div class="t3-page-ce-header-icons-left">' . implode('', $additionalIcons) . '</div>
2096 <div class="t3-page-ce-header-icons-right">' . ($out ? '<div class="btn-toolbar">' . $out . '</div>' : '') . '</div>
2097 </div>
2098 <div class="t3-page-ce-body">';
2099 }
2100
2101 /**
2102 * Gets the number of records referencing the record with the UID $uid in
2103 * the table $tableName.
2104 *
2105 * @param string $tableName
2106 * @param int $uid
2107 * @return int The number of references to record $uid in table
2108 */
2109 protected function getReferenceCount(string $tableName, int $uid): int
2110 {
2111 if (!isset($this->referenceCount[$tableName][$uid])) {
2112 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
2113 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid);
2114 $this->referenceCount[$tableName][$uid] = $numberOfReferences;
2115 }
2116 return $this->referenceCount[$tableName][$uid];
2117 }
2118
2119 /**
2120 * Determine whether Drag & Drop should be allowed
2121 *
2122 * @param array $row
2123 * @return bool
2124 */
2125 protected function isDragAndDropAllowed(array $row)
2126 {
2127 if ((int)$row['l18n_parent'] === 0 &&
2128 (
2129 $this->getBackendUser()->isAdmin()
2130 || ((int)$row['editlock'] === 0 && (int)$this->pageinfo['editlock'] === 0)
2131 && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)
2132 && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $row['CType'], $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'])
2133 )
2134 ) {
2135 return true;
2136 }
2137 return false;
2138 }
2139
2140 /**
2141 * Draws the preview content for a content element
2142 *
2143 * @param array $row Content element
2144 * @return string HTML
2145 * @throws \UnexpectedValueException
2146 */
2147 public function tt_content_drawItem($row)
2148 {
2149 $out = '';
2150 $outHeader = '';
2151 // Make header:
2152
2153 if ($row['header']) {
2154 $infoArr = [];
2155 $this->getProcessedValue('tt_content', 'header_position,header_layout,header_link', $row, $infoArr);
2156 $hiddenHeaderNote = '';
2157 // If header layout is set to 'hidden', display an accordant note:
2158 if ($row['header_layout'] == 100) {
2159 $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>';
2160 }
2161 $outHeader = $row['date']
2162 ? htmlspecialchars($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />'
2163 : '';
2164 $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row)
2165 . $hiddenHeaderNote . '</strong><br />';
2166 }
2167 // Make content:
2168 $infoArr = [];
2169 $drawItem = true;
2170 // Hook: Render an own preview of a record
2171 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) {
2172 $hookObject = GeneralUtility::makeInstance($className);
2173 if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) {
2174 throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1218547409);
2175 }
2176 $hookObject->preProcess($this, $drawItem, $outHeader, $out, $row);
2177 }
2178
2179 // If the previous hook did not render something,
2180 // then check if a Fluid-based preview template was defined for this CType
2181 // and render it via Fluid. Possible option:
2182 // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html
2183 if ($drawItem) {
2184 $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
2185 $fluidTemplateFile = '';
2186
2187 if ($row['CType'] === 'list' && !empty($row['list_type'])
2188 && !empty($tsConfig['list.'][$row['list_type']])
2189 ) {
2190 $fluidTemplateFile = $tsConfig['list.'][$row['list_type']];
2191 } elseif (!empty($tsConfig[$row['CType']])) {
2192 $fluidTemplateFile = $tsConfig[$row['CType']];
2193 }
2194
2195 if ($fluidTemplateFile) {
2196 $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
2197 if ($fluidTemplateFile) {
2198 try {
2199 $view = GeneralUtility::makeInstance(StandaloneView::class);
2200 $view->setTemplatePathAndFilename($fluidTemplateFile);
2201 $view->assignMultiple($row);
2202 if (!empty($row['pi_flexform'])) {
2203 $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
2204 $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
2205 }
2206 $out = $view->render();
2207 $drawItem = false;
2208 } catch (\Exception $e) {
2209 // Catch any exception to avoid breaking the view
2210 }
2211 }
2212 }
2213 }
2214
2215 // Draw preview of the item depending on its CType (if not disabled by previous hook):
2216 if ($drawItem) {
2217 switch ($row['CType']) {
2218 case 'header':
2219 if ($row['subheader']) {
2220 $out .= $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br />';
2221 }
2222 break;
2223 case 'bullets':
2224 case 'table':
2225 if ($row['bodytext']) {
2226 $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
2227 }
2228 break;
2229 case 'uploads':
2230 if ($row['media']) {
2231 $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />';
2232 }
2233 break;
2234 case 'menu':
2235 $contentType = $this->CType_labels[$row['CType']];
2236 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
2237 // Add Menu Type
2238 $menuTypeLabel = $this->getLanguageService()->sL(
2239 BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'menu_type', $row['menu_type'])
2240 );
2241 $menuTypeLabel = $menuTypeLabel ?: 'invalid menu type';
2242 $out .= $this->linkEditContent($menuTypeLabel, $row);
2243 if ($row['menu_type'] !== '2' && ($row['pages'] || $row['selected_categories'])) {
2244 // Show pages if menu type is not "Sitemap"
2245 $out .= ':' . $this->linkEditContent($this->generateListForCTypeMenu($row), $row) . '<br />';
2246 }
2247 break;
2248 case 'shortcut':
2249 if (!empty($row['records'])) {
2250 $shortcutContent = [];
2251 $recordList = explode(',', $row['records']);
2252 foreach ($recordList as $recordIdentifier) {
2253 $split = BackendUtility::splitTable_Uid($recordIdentifier);
2254 $tableName = empty($split[0]) ? 'tt_content' : $split[0];
2255 $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]);
2256 if (is_array($shortcutRecord)) {
2257 $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render();
2258 $icon = BackendUtility::wrapClickMenuOnIcon(
2259 $icon,
2260 $tableName,
2261 $shortcutRecord['uid']
2262 );
2263 $shortcutContent[] = $icon
2264 . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord));
2265 }
2266 }
2267 $out .= implode('<br />', $shortcutContent) . '<br />';
2268 }
2269 break;
2270 case 'list':
2271 $hookOut = '';
2272 $_params = ['pObj' => &$this, 'row' => $row, 'infoArr' => $infoArr];
2273 foreach (
2274 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ??
2275 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ??
2276 [] as $_funcRef
2277 ) {
2278 $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2279 }
2280 if ((string)$hookOut !== '') {
2281 $out .= $hookOut;
2282 } elseif (!empty($row['list_type'])) {
2283 $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']);
2284 if (!empty($label)) {
2285 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />';
2286 } else {
2287 $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']);
2288 $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
2289 }
2290 } else {
2291 $out .= '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>';
2292 }
2293 $out .= htmlspecialchars($this->getLanguageService()->sL(
2294 BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages'])
2295 )) . '<br />';
2296 break;
2297 default:
2298 $contentType = $this->CType_labels[$row['CType']];
2299
2300 if (isset($contentType)) {
2301 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
2302 if ($row['bodytext']) {
2303 $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
2304 }
2305 if ($row['image']) {
2306 $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />';
2307 }
2308 } else {
2309 $message = sprintf(
2310 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
2311 $row['CType']
2312 );
2313 $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
2314 }
2315 }
2316 }
2317 // Wrap span-tags:
2318 $out = '
2319 <span class="exampleContent">' . $out . '</span>';
2320 // Add header:
2321 $out = $outHeader . $out;
2322 // Return values:
2323 if ($this->isDisabled('tt_content', $row)) {
2324 return '<span class="text-muted">' . $out . '</span>';
2325 }
2326 return $out;
2327 }
2328
2329 /**
2330 * Generates a list of selected pages or categories for the CType menu
2331 *
2332 * @param array $row row from pages
2333 * @return string
2334 */
2335 protected function generateListForCTypeMenu(array $row)
2336 {
2337 $table = 'pages';
2338 $field = 'pages';
2339 // get categories instead of pages
2340 if (strpos($row['menu_type'], 'categorized_') !== false) {
2341 $table = 'sys_category';
2342 $field = 'selected_categories';
2343 }
2344 if (trim($row[$field]) === '') {
2345 return '';
2346 }
2347 $content = '';
2348 $uidList = explode(',', $row[$field]);
2349 foreach ($uidList as $uid) {
2350 $uid = (int)$uid;
2351 $record = BackendUtility::getRecord($table, $uid, 'title');
2352 $content .= '<br>' . $record['title'] . ' (' . $uid . ')';
2353 }
2354 return $content;
2355 }
2356
2357 /**
2358 * Filters out all tt_content uids which are already translated so only non-translated uids is left.
2359 * Selects across columns, but within in the same PID. Columns are expect to be the same
2360 * for translations and original but this may be a conceptual error (?)
2361 *
2362 * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
2363 * @param int $id Page pid
2364 * @param int $lP Sys language UID
2365 * @return array Modified $defLanguageCount
2366 */
2367 public function getNonTranslatedTTcontentUids($defaultLanguageUids, $id, $lP)
2368 {
2369 if ($lP && !empty($defaultLanguageUids)) {
2370 // Select all translations here:
2371 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2372 ->getQueryBuilderForTable('tt_content');
2373 $queryBuilder->getRestrictions()
2374 ->removeAll()
2375 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2376 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, null, false));
2377 $queryBuilder
2378 ->select('*')
2379 ->from('tt_content')
2380 ->where(
2381 $queryBuilder->expr()->eq(
2382 'sys_language_uid',
2383 $queryBuilder->createNamedParameter($lP, \PDO::PARAM_INT)
2384 ),
2385 $queryBuilder->expr()->in(
2386 'l18n_parent',
2387 $queryBuilder->createNamedParameter($defaultLanguageUids, Connection::PARAM_INT_ARRAY)
2388 )
2389 );
2390
2391 $result = $queryBuilder->execute();
2392
2393 // Flip uids:
2394 $defaultLanguageUids = array_flip($defaultLanguageUids);
2395 // Traverse any selected elements and unset original UID if any:
2396 while ($row = $result->fetch()) {
2397 BackendUtility::workspaceOL('tt_content', $row);
2398 unset($defaultLanguageUids[$row['l18n_parent']]);
2399 }
2400 // Flip again:
2401 $defaultLanguageUids = array_keys($defaultLanguageUids);
2402 }
2403 return $defaultLanguageUids;
2404 }
2405
2406 /**
2407 * Creates button which is used to create copies of records..
2408 *
2409 * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
2410 * @param int $lP Sys language UID
2411 * @return string "Copy languages" button, if available.
2412 */
2413 public function newLanguageButton($defaultLanguageUids, $lP)
2414 {
2415 $lP = (int)$lP;
2416 if (!$this->doEdit || !$lP) {
2417 return '';
2418 }
2419 $theNewButton = '';
2420
2421 $localizationTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['localization.'] ?? [];
2422 $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true);
2423 $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true);
2424 if (!empty($this->languageHasTranslationsCache[$lP])) {
2425 if (isset($this->languageHasTranslationsCache[$lP]['hasStandAloneContent'])) {
2426 $allowTranslate = false;
2427 }
2428 if (isset($this->languageHasTranslationsCache[$lP]['hasTranslations'])) {
2429 $allowCopy = !$this->languageHasTranslationsCache[$lP]['hasTranslations'];
2430 }
2431 }
2432
2433 if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) {
2434 foreach ($this->contentElementCache[$lP] as $column => $records) {
2435 foreach ($records as $record) {
2436 $key = array_search($record['l10n_source'], $defaultLanguageUids);
2437 if ($key !== false) {
2438 unset($defaultLanguageUids[$key]);
2439 }
2440 }
2441 }
2442 }
2443
2444 if (!empty($defaultLanguageUids)) {
2445 $theNewButton =
2446 '<a'
2447 . ' href="#"'
2448 . ' class="btn btn-default btn-sm t3js-localize disabled"'
2449 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
2450 . ' data-page="' . htmlspecialchars($this->getLocalizedPageTitle()) . '"'
2451 . ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"'
2452 . ' data-allow-copy="' . (int)$allowCopy . '"'
2453 . ' data-allow-translate="' . (int)$allowTranslate . '"'
2454 . ' data-table="tt_content"'
2455 . ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"'
2456 . ' data-language-id="' . $lP . '"'
2457 . ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"'
2458 . '>'
2459 . $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render()
2460 . ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate'))
2461 . '</a>';
2462 }
2463
2464 return $theNewButton;
2465 }
2466
2467 /**
2468 * Creates onclick-attribute content for a new content element
2469 *
2470 * @param int $id Page id where to create the element.
2471 * @param int $colPos Preset: Column position value
2472 * @param int $sys_language Preset: Sys language value
2473 * @return string String for onclick attribute.
2474 * @see getTable_tt_content()
2475 */
2476 public function newContentElementOnClick($id, $colPos, $sys_language)
2477 {
2478 if ($this->option_newWizard) {
2479 $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override']
2480 ?? 'new_content_element_wizard';
2481 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2482 $url = $uriBuilder->buildUriFromRoute($routeName, [
2483 'id' => $id,
2484 'colPos' => $colPos,
2485 'sys_language_uid' => $sys_language,
2486 'uid_pid' => $id,
2487 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
2488 ]);
2489 $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$url) . ';';
2490 } else {
2491 $onClick = BackendUtility::editOnClick('&edit[tt_content][' . $id . ']=new&defVals[tt_content][colPos]='
2492 . $colPos . '&defVals[tt_content][sys_language_uid]=' . $sys_language);
2493 }
2494 return $onClick;
2495 }
2496
2497 /**
2498 * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE.
2499 * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button
2500 *
2501 * @param string $str String to link. Must be prepared for HTML output.
2502 * @param array $row The row.
2503 * @return string If the whole thing was editable ($this->doEdit) $str is return with link around. Otherwise just $str.
2504 * @see getTable_tt_content()
2505 */
2506 public function linkEditContent($str, $row)
2507 {
2508 if ($this->doEdit && $this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) {
2509 $urlParameters = [
2510 'edit' => [
2511 'tt_content' => [
2512 $row['uid'] => 'edit'
2513 ]
2514 ],
2515 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid']
2516 ];
2517 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2518 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
2519 // Return link
2520 return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>';
2521 }
2522 return $str;
2523 }
2524
2525 /**
2526 * Make selector box for creating new translation in a language
2527 * Displays only languages which are not yet present for the current page and
2528 * that are not disabled with page TS.
2529 *
2530 * @param int $id Page id for which to create a new translation record of pages
2531 * @return string <select> HTML element (if there were items for the box anyways...)
2532 * @see getTable_tt_content()
2533 */
2534 public function languageSelector($id)
2535 {
2536 if (!$this->getBackendUser()->check('tables_modify', 'pages')) {
2537 return '';
2538 }
2539 $id = (int)$id;
2540
2541 // First, select all languages that are available for the current user
2542 $availableTranslations = [];
2543 foreach ($this->siteLanguages as $language) {
2544 if ($language->getLanguageId() === 0) {
2545 continue;
2546 }
2547 $availableTranslations[$language->getLanguageId()] = $language->getTitle();
2548 }
2549
2550 // Then, subtract the languages which are already on the page:
2551 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
2552 $queryBuilder->getRestrictions()->removeAll()
2553 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2554 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2555 $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2556 ->from('pages')
2557 ->where(
2558 $queryBuilder->expr()->eq(
2559 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2560 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
2561 )
2562 );
2563 $statement = $queryBuilder->execute();
2564 while ($row = $statement->fetch()) {
2565 unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]);
2566 }
2567 // If any languages are left, make selector:
2568 if (!empty($availableTranslations)) {
2569 $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->getLL('new_language')) . '</option>';
2570 foreach ($availableTranslations as $languageUid => $languageTitle) {
2571 // Build localize command URL to DataHandler (tce_db)
2572 // which redirects to FormEngine (record_edit)
2573 // which, when finished editing should return back to the current page (returnUrl)
2574 $parameters = [
2575 'justLocalized' => 'pages:' . $id . ':' . $languageUid,
2576 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
2577 ];
2578 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2579 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters);
2580 $targetUrl = BackendUtility::getLinkToDataHandlerAction(
2581 '&cmd[pages][' . $id . '][localize]=' . $languageUid,
2582 $redirectUrl
2583 );
2584
2585 $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
2586 }
2587
2588 return '<div class="form-inline form-inline-spaced">'
2589 . '<div class="form-group">'
2590 . '<select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">'
2591 . $output
2592 . '</select></div></div>';
2593 }
2594 return '';
2595 }
2596
2597 /**
2598 * Traverse the result pointer given, adding each record to array and setting some internal values at the same time.
2599 *
2600 * @param Statement $result DBAL Statement
2601 * @param string $table Table name defaulting to tt_content
2602 * @return array The selected rows returned in this array.
2603 */
2604 public function getResult(Statement $result, string $table = 'tt_content'): array
2605 {
2606 $output = [];
2607 // Traverse the result:
2608 while ($row = $result->fetch()) {
2609 BackendUtility::workspaceOL($table, $row, -99, true);
2610 if ($row) {
2611 // Add the row to the array:
2612 $output[] = $row;
2613 }
2614 }
2615 $this->generateTtContentDataArray($output);
2616 // Return selected records
2617 return $output;
2618 }
2619
2620 /********************************
2621 *
2622 * Various helper functions
2623 *
2624 ********************************/
2625
2626 /**
2627 * Initializes the clipboard for generating paste links
2628 *
2629 *
2630 * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction()
2631 * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction()
2632 */
2633 protected function initializeClipboard()
2634 {
2635 // Start clipboard
2636 $this->clipboard = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Clipboard\Clipboard::class);
2637
2638 // Initialize - reads the clipboard content from the user session
2639 $this->clipboard->initializeClipboard();
2640
2641 // This locks the clipboard to the Normal for this request.
2642 $this->clipboard->lockToNormal();
2643
2644 // Clean up pad
2645 $this->clipboard->cleanCurrent();
2646
2647 // Save the clipboard content
2648 $this->clipboard->endClipboard();
2649 }
2650
2651 /**
2652 * Generates the data for previous and next elements which is needed for movements.
2653 *
2654 * @param array $rowArray
2655 */
2656 protected function generateTtContentDataArray(array $rowArray)
2657 {
2658 if (empty($this->tt_contentData)) {
2659 $this->tt_contentData = [
2660 'nextThree' => [],
2661 'next' => [],
2662 'prev' => [],
2663 ];
2664 }
2665 foreach ($rowArray as $key => $value) {
2666 // Create the list of the next three ids (for editing links...)
2667 for ($i = 0; $i < $this->nextThree; $i++) {
2668 if (isset($rowArray[$key - $i])
2669 && !GeneralUtility::inList($this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']], $value['uid'])
2670 ) {
2671 $this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']] .= $value['uid'] . ',';
2672 }
2673 }
2674
2675 // Create information for next and previous content elements
2676 if (isset($rowArray[$key - 1])) {
2677 if (isset($rowArray[$key - 2])) {
2678 $this->tt_contentData['prev'][$value['uid']] = -$rowArray[$key - 2]['uid'];
2679 } else {
2680 $this->tt_contentData['prev'][$value['uid']] = $value['pid'];
2681 }
2682 $this->tt_contentData['next'][$rowArray[$key - 1]['uid']] = -$value['uid'];
2683 }
2684 }
2685 }
2686
2687 /**
2688 * Counts and returns the number of records on the page with $pid
2689 *
2690 * @param string $table Table name
2691 * @param int $pid Page id
2692 * @return int Number of records.
2693 */
2694 public function numberOfRecords($table, $pid)
2695 {
2696 $count = 0;
2697 if ($GLOBALS['TCA'][$table]) {
2698 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2699 ->getQueryBuilderForTable($table);
2700 $queryBuilder->getRestrictions()
2701 ->removeAll()
2702 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2703 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2704 $count = (int)$queryBuilder->count('uid')
2705 ->from($table)
2706 ->where(
2707 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT))
2708 )
2709 ->execute()
2710 ->fetchColumn();
2711 }
2712
2713 return $count;
2714 }
2715
2716 /**
2717 * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc.
2718 *
2719 * @param string $input Input string
2720 * @return string Output string
2721 */
2722 public function renderText($input)
2723 {
2724 $input = strip_tags($input);
2725 $input = GeneralUtility::fixed_lgd_cs($input, 1500);
2726 return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false));
2727 }
2728
2729 /**
2730 * Creates the icon image tag for record from table and wraps it in a link which will trigger the click menu.
2731 *
2732 * @param string $table Table name
2733 * @param array $row Record array
2734 * @param string $enabledClickMenuItems Passthrough to wrapClickMenuOnIcon
2735 * @return string HTML for the icon
2736 */
2737 public function getIcon($table, $row, $enabledClickMenuItems = '')
2738 {
2739 // Initialization
2740 $toolTip = BackendUtility::getRecordToolTip($row, 'tt_content');
2741 $icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
2742 $this->counter++;
2743 // The icon with link
2744 if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) {
2745 $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
2746 }
2747 return $icon;
2748 }
2749
2750 /**
2751 * Creates processed values for all field names in $fieldList based on values from $row array.
2752 * The result is 'returned' through $info which is passed as a reference
2753 *
2754 * @param string $table Table name
2755 * @param string $fieldList Comma separated list of fields.
2756 * @param array $row Record from which to take values for processing.
2757 * @param array $info Array to which the processed values are added.
2758 */
2759 public function getProcessedValue($table, $fieldList, array $row, array &$info)
2760 {
2761 // Splitting values from $fieldList
2762 $fieldArr = explode(',', $fieldList);
2763 // Traverse fields from $fieldList
2764 foreach ($fieldArr as $field) {
2765 if ($row[$field]) {
2766 $info[] = '<strong>' . htmlspecialchars($this->itemLabels[$field]) . '</strong> '
2767 . htmlspecialchars(BackendUtility::getProcessedValue($table, $field, $row[$field]));
2768 }
2769 }
2770 }
2771
2772 /**
2773 * Returns TRUE, if the record given as parameters is NOT visible based on hidden/starttime/endtime (if available)
2774 *
2775 * @param string $table Tablename of table to test
2776 * @param array $row Record row.
2777 * @return bool Returns TRUE, if disabled.
2778 */
2779 public function isDisabled($table, $row)
2780 {
2781 $enableCols = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
2782 return $enableCols['disabled'] && $row[$enableCols['disabled']]
2783 || $enableCols['starttime'] && $row[$enableCols['starttime']] > $GLOBALS['EXEC_TIME']
2784 || $enableCols['endtime'] && $row[$enableCols['endtime']] && $row[$enableCols['endtime']] < $GLOBALS['EXEC_TIME'];
2785 }
2786
2787 /**
2788 * Returns icon for "no-edit" of a record.
2789 * Basically, the point is to signal that this record could have had an edit link if
2790 * the circumstances were right. A placeholder for the regular edit icon...
2791 *
2792 * @param string $label Label key from LOCAL_LANG
2793 * @return string IMG tag for icon.
2794 */
2795 public function noEditIcon($label = 'noEditItems')
2796 {
2797 $title = htmlspecialchars($this->getLanguageService()->getLL($label));
2798 return '<span title="' . $title . '">' . $this->iconFactory->getIcon('status-edit-read-only', Icon::SIZE_SMALL)->render() . '</span>';
2799 }
2800
2801 /*****************************************
2802 *
2803 * External renderings
2804 *
2805 *****************************************/
2806
2807 /**
2808 * Creates a menu of the tables that can be listed by this function
2809 * Only tables which has records on the page will be included.
2810 * Notice: The function also fills in the internal variable $this->activeTables with icon/titles.
2811 *
2812 * @param int $id Page id from which we are listing records (the function will look up if there are records on the page)
2813 * @return string HTML output.
2814 */
2815 public function getTableMenu($id)
2816 {
2817 // Initialize:
2818 $this->activeTables = [];
2819 $theTables = ['tt_content'];
2820 // External tables:
2821 if (is_array($this->externalTables)) {
2822 $theTables = array_unique(array_merge($theTables, array_keys($this->externalTables)));
2823 }
2824 $out = '';
2825 // Traverse tables to check:
2826 foreach ($theTables as $tName) {
2827 // Check access and whether the proper extensions are loaded:
2828 if ($this->getBackendUser()->check('tables_select', $tName)
2829 && (
2830 isset($this->externalTables[$tName])
2831 || $tName === 'fe_users' || $tName === 'tt_content'
2832 || \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($tName)
2833 )
2834 ) {
2835 // Make query to count records from page:
2836 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2837 ->getQueryBuilderForTable($tName);
2838 $queryBuilder->getRestrictions()
2839 ->removeAll()
2840 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2841 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2842 $count = $queryBuilder->count('uid')
2843 ->from($tName)
2844 ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
2845 ->execute()
2846 ->fetchColumn();
2847 // If records were found (or if "tt_content" is the table...):
2848 if ($count || $tName === 'tt_content') {
2849 // Add row to menu:
2850 $out .= '
2851 <td><a href="#' . $tName . '" title="' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title'])) . '"></a>'
2852 . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
2853 . '</td>';
2854 // ... and to the internal array, activeTables we also add table icon and title (for use elsewhere)
2855 $title = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title']))
2856 . ': ' . $count . ' ' . htmlspecialchars($this->getLanguageService()->getLL('records'));
2857 $this->activeTables[$tName] = '<span title="' . $title . '">'
2858 . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
2859 . '</span>'
2860 . '&nbsp;' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title']));
2861 }
2862 }
2863 }
2864 // Wrap cells in table tags:
2865 $out = '
2866 <!--
2867 Menu of tables on the page (table menu)
2868 -->
2869 <table border="0" cellpadding="0" cellspacing="0" id="typo3-page-tblMenu">
2870 <tr>' . $out . '
2871 </tr>
2872 </table>';
2873 // Return the content:
2874 return $out;
2875 }
2876
2877 /**
2878 * Create thumbnail code for record/field but not linked
2879 *
2880 * @param mixed[] $row Record array
2881 * @param string $table Table (record is from)
2882 * @param string $field Field name for which thumbnail are to be rendered.
2883 * @return string HTML for thumbnails, if any.
2884 */
2885 public function getThumbCodeUnlinked($row, $table, $field)
2886 {
2887 return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false);
2888 }
2889
2890 /**
2891 * Checks whether translated Content Elements exist in the desired language
2892 * If so, deny creating new ones via the UI
2893 *
2894 * @param array $contentElements
2895 * @param int $language
2896 * @return bool
2897 */
2898 protected function checkIfTranslationsExistInLanguage(array $contentElements, int $language)
2899 {
2900 // If in default language, you may always create new entries
2901 // Also, you may override this strict behavior via user TS Config
2902 // If you do so, you're on your own and cannot rely on any support by the TYPO3 core
2903 // We jump out here since we don't need to do the expensive loop operations
2904 $allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false);
2905 if ($language === 0 || $allowInconsistentLanguageHandling) {
2906 return false;
2907 }
2908 /**
2909 * Build up caches
2910 */
2911 if (!isset($this->languageHasTranslationsCache[$language])) {
2912 foreach ($contentElements as $columns) {
2913 foreach ($columns as $contentElement) {
2914 if ((int)$contentElement['l18n_parent'] === 0) {
2915 $this->languageHasTranslationsCache[$language]['hasStandAloneContent'] = true;
2916 $this->languageHasTranslationsCache[$language]['mode'] = 'free';
2917 }
2918 if ((int)$contentElement['l18n_parent'] > 0) {