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