[BUGFIX] Streamline paste modal color schema
[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 (strpos($field, 'table_') === 0) {
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 (strpos($field, 'table_') === 0) {
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 . ' data-severity="warning"'
1901 . ' class="t3js-paste t3js-paste' . htmlspecialchars($copyMode) . ' ' . htmlspecialchars($cssClass) . ' btn btn-default btn-sm"'
1902 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL($title)) . '">'
1903 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
1904 . '</a>'
1905 );
1906 return $pasteIcon;
1907 }
1908
1909 /**
1910 * Draw the footer for a single tt_content element
1911 *
1912 * @param array $row Record array
1913 * @return string HTML of the footer
1914 * @throws \UnexpectedValueException
1915 */
1916 protected function tt_content_drawFooter(array $row)
1917 {
1918 $content = '';
1919 // Get processed values:
1920 $info = [];
1921 $this->getProcessedValue('tt_content', 'starttime,endtime,fe_group,space_before_class,space_after_class', $row, $info);
1922
1923 // Content element annotation
1924 if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) {
1925 $info[] = htmlspecialchars($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]);
1926 }
1927
1928 // Call drawFooter hooks
1929 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) {
1930 $hookObject = GeneralUtility::makeInstance($className);
1931 if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) {
1932 throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1404378171);
1933 }
1934 $hookObject->preProcess($this, $info, $row);
1935 }
1936
1937 // Display info from records fields:
1938 if (!empty($info)) {
1939 $content = '<div class="t3-page-ce-info">
1940 ' . implode('<br>', $info) . '
1941 </div>';
1942 }
1943 // Wrap it
1944 if (!empty($content)) {
1945 $content = '<div class="t3-page-ce-footer">' . $content . '</div>';
1946 }
1947 return $content;
1948 }
1949
1950 /**
1951 * Draw the header for a single tt_content element
1952 *
1953 * @param array $row Record array
1954 * @param int $space Amount of pixel space above the header. UNUSED
1955 * @param bool $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown.
1956 * @param bool $langMode If set, we are in language mode and flags will be shown for languages
1957 * @param bool $dragDropEnabled If set the move button must be hidden
1958 * @return string HTML table with the record header.
1959 */
1960 public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = false, $langMode = false, $dragDropEnabled = false)
1961 {
1962 $backendUser = $this->getBackendUser();
1963 $out = '';
1964 // If show info is set...;
1965 if ($this->tt_contentConfig['showInfo'] && $backendUser->recordEditAccessInternals('tt_content', $row)) {
1966 // Render control panel for the element:
1967 if ($this->tt_contentConfig['showCommands'] && $this->doEdit) {
1968 // Edit content element:
1969 $urlParameters = [
1970 'edit' => [
1971 'tt_content' => [
1972 $this->tt_contentData['nextThree'][$row['uid']] => 'edit'
1973 ]
1974 ],
1975 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'],
1976 ];
1977 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1978 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $row['uid'];
1979
1980 $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url)
1981 . '" title="' . htmlspecialchars($this->nextThree > 1
1982 ? sprintf($this->getLanguageService()->getLL('nextThree'), $this->nextThree)
1983 : $this->getLanguageService()->getLL('edit'))
1984 . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1985 // Hide element:
1986 $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled'];
1987 if ($hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField]
1988 && (!$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude']
1989 || $backendUser->check('non_exclude_fields', 'tt_content:' . $hiddenField))
1990 ) {
1991 if ($row[$hiddenField]) {
1992 $value = 0;
1993 $label = 'unHide';
1994 } else {
1995 $value = 1;
1996 $label = 'hide';
1997 }
1998 $params = '&data[tt_content][' . ($row['_ORIG_uid'] ? $row['_ORIG_uid'] : $row['uid'])
1999 . '][' . $hiddenField . ']=' . $value;
2000 $out .= '<a class="btn btn-default" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2001 . '#element-tt_content-' . $row['uid'] . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($label)) . '">'
2002 . $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . '</a>';
2003 }
2004 // Delete
2005 $disableDelete = (bool)\trim(
2006 $backendUser->getTSConfig()['options.']['disableDelete.']['tt_content']
2007 ?? $backendUser->getTSConfig()['options.']['disableDelete']
2008 ?? '0'
2009 );
2010 if (!$disableDelete) {
2011 $params = '&cmd[tt_content][' . $row['uid'] . '][delete]=1';
2012 $refCountMsg = BackendUtility::referenceCount(
2013 'tt_content',
2014 $row['uid'],
2015 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
2016 $this->getReferenceCount('tt_content', $row['uid'])
2017 ) . BackendUtility::translationCount(
2018 'tt_content',
2019 $row['uid'],
2020 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
2021 );
2022 $confirm = $this->getLanguageService()->getLL('deleteWarning')
2023 . $refCountMsg;
2024 $out .= '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) . '"'
2025 . ' data-severity="warning"'
2026 . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')) . '"'
2027 . ' data-content="' . htmlspecialchars($confirm) . '" '
2028 . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
2029 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('deleteItem')) . '">'
2030 . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
2031 if ($out && $backendUser->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) {
2032 $out = '<div class="btn-group btn-group-sm" role="group">' . $out . '</div>';
2033 } else {
2034 $out = '';
2035 }
2036 }
2037 if (!$disableMoveAndNewButtons) {
2038 $moveButtonContent = '';
2039 $displayMoveButtons = false;
2040 // Move element up:
2041 if ($this->tt_contentData['prev'][$row['uid']]) {
2042 $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']];
2043 $moveButtonContent .= '<a class="btn btn-default" href="'
2044 . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2045 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
2046 . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
2047 if (!$dragDropEnabled) {
2048 $displayMoveButtons = true;
2049 }
2050 } else {
2051 $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
2052 }
2053 // Move element down:
2054 if ($this->tt_contentData['next'][$row['uid']]) {
2055 $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']];
2056 $moveButtonContent .= '<a class="btn btn-default" href="'
2057 . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params))
2058 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
2059 . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
2060 if (!$dragDropEnabled) {
2061 $displayMoveButtons = true;
2062 }
2063 } else {
2064 $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
2065 }
2066 if ($displayMoveButtons) {
2067 $out .= '<div class="btn-group btn-group-sm" role="group">' . $moveButtonContent . '</div>';
2068 }
2069 }
2070 }
2071 }
2072 $allowDragAndDrop = $this->isDragAndDropAllowed($row);
2073 $additionalIcons = [];
2074 $additionalIcons[] = $this->getIcon('tt_content', $row) . ' ';
2075 if ($langMode && isset($this->siteLanguages[(int)$row['sys_language_uid']])) {
2076 $additionalIcons[] = $this->renderLanguageFlag($this->siteLanguages[(int)$row['sys_language_uid']]);
2077 }
2078 // Get record locking status:
2079 if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) {
2080 $additionalIcons[] = '<a href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">'
2081 . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</a>';
2082 }
2083 // Call stats information hook
2084 $_params = ['tt_content', $row['uid'], &$row];
2085 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) {
2086 $additionalIcons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2087 }
2088
2089 // Wrap the whole header
2090 // NOTE: end-tag for <div class="t3-page-ce-body"> is in getTable_tt_content()
2091 return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
2092 <div class="t3-page-ce-header-icons-left">' . implode('', $additionalIcons) . '</div>
2093 <div class="t3-page-ce-header-icons-right">' . ($out ? '<div class="btn-toolbar">' . $out . '</div>' : '') . '</div>
2094 </div>
2095 <div class="t3-page-ce-body">';
2096 }
2097
2098 /**
2099 * Gets the number of records referencing the record with the UID $uid in
2100 * the table $tableName.
2101 *
2102 * @param string $tableName
2103 * @param int $uid
2104 * @return int The number of references to record $uid in table
2105 */
2106 protected function getReferenceCount(string $tableName, int $uid): int
2107 {
2108 if (!isset($this->referenceCount[$tableName][$uid])) {
2109 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
2110 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid);
2111 $this->referenceCount[$tableName][$uid] = $numberOfReferences;
2112 }
2113 return $this->referenceCount[$tableName][$uid];
2114 }
2115
2116 /**
2117 * Determine whether Drag & Drop should be allowed
2118 *
2119 * @param array $row
2120 * @return bool
2121 */
2122 protected function isDragAndDropAllowed(array $row)
2123 {
2124 if ((int)$row['l18n_parent'] === 0 &&
2125 (
2126 $this->getBackendUser()->isAdmin()
2127 || ((int)$row['editlock'] === 0 && (int)$this->pageinfo['editlock'] === 0)
2128 && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)
2129 && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $row['CType'], $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode'])
2130 )
2131 ) {
2132 return true;
2133 }
2134 return false;
2135 }
2136
2137 /**
2138 * Draws the preview content for a content element
2139 *
2140 * @param array $row Content element
2141 * @return string HTML
2142 * @throws \UnexpectedValueException
2143 */
2144 public function tt_content_drawItem($row)
2145 {
2146 $out = '';
2147 $outHeader = '';
2148 // Make header:
2149
2150 if ($row['header']) {
2151 $infoArr = [];
2152 $this->getProcessedValue('tt_content', 'header_position,header_layout,header_link', $row, $infoArr);
2153 $hiddenHeaderNote = '';
2154 // If header layout is set to 'hidden', display an accordant note:
2155 if ($row['header_layout'] == 100) {
2156 $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>';
2157 }
2158 $outHeader = $row['date']
2159 ? htmlspecialchars($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />'
2160 : '';
2161 $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row)
2162 . $hiddenHeaderNote . '</strong><br />';
2163 }
2164 // Make content:
2165 $infoArr = [];
2166 $drawItem = true;
2167 // Hook: Render an own preview of a record
2168 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) {
2169 $hookObject = GeneralUtility::makeInstance($className);
2170 if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) {
2171 throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1218547409);
2172 }
2173 $hookObject->preProcess($this, $drawItem, $outHeader, $out, $row);
2174 }
2175
2176 // If the previous hook did not render something,
2177 // then check if a Fluid-based preview template was defined for this CType
2178 // and render it via Fluid. Possible option:
2179 // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html
2180 if ($drawItem) {
2181 $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? [];
2182 $fluidTemplateFile = '';
2183
2184 if ($row['CType'] === 'list' && !empty($row['list_type'])
2185 && !empty($tsConfig['list.'][$row['list_type']])
2186 ) {
2187 $fluidTemplateFile = $tsConfig['list.'][$row['list_type']];
2188 } elseif (!empty($tsConfig[$row['CType']])) {
2189 $fluidTemplateFile = $tsConfig[$row['CType']];
2190 }
2191
2192 if ($fluidTemplateFile) {
2193 $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
2194 if ($fluidTemplateFile) {
2195 try {
2196 $view = GeneralUtility::makeInstance(StandaloneView::class);
2197 $view->setTemplatePathAndFilename($fluidTemplateFile);
2198 $view->assignMultiple($row);
2199 if (!empty($row['pi_flexform'])) {
2200 $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
2201 $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
2202 }
2203 $out = $view->render();
2204 $drawItem = false;
2205 } catch (\Exception $e) {
2206 // Catch any exception to avoid breaking the view
2207 }
2208 }
2209 }
2210 }
2211
2212 // Draw preview of the item depending on its CType (if not disabled by previous hook):
2213 if ($drawItem) {
2214 switch ($row['CType']) {
2215 case 'header':
2216 if ($row['subheader']) {
2217 $out .= $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br />';
2218 }
2219 break;
2220 case 'bullets':
2221 case 'table':
2222 if ($row['bodytext']) {
2223 $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
2224 }
2225 break;
2226 case 'uploads':
2227 if ($row['media']) {
2228 $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />';
2229 }
2230 break;
2231 case 'menu':
2232 $contentType = $this->CType_labels[$row['CType']];
2233 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
2234 // Add Menu Type
2235 $menuTypeLabel = $this->getLanguageService()->sL(
2236 BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'menu_type', $row['menu_type'])
2237 );
2238 $menuTypeLabel = $menuTypeLabel ?: 'invalid menu type';
2239 $out .= $this->linkEditContent($menuTypeLabel, $row);
2240 if ($row['menu_type'] !== '2' && ($row['pages'] || $row['selected_categories'])) {
2241 // Show pages if menu type is not "Sitemap"
2242 $out .= ':' . $this->linkEditContent($this->generateListForCTypeMenu($row), $row) . '<br />';
2243 }
2244 break;
2245 case 'shortcut':
2246 if (!empty($row['records'])) {
2247 $shortcutContent = [];
2248 $recordList = explode(',', $row['records']);
2249 foreach ($recordList as $recordIdentifier) {
2250 $split = BackendUtility::splitTable_Uid($recordIdentifier);
2251 $tableName = empty($split[0]) ? 'tt_content' : $split[0];
2252 $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]);
2253 if (is_array($shortcutRecord)) {
2254 $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render();
2255 $icon = BackendUtility::wrapClickMenuOnIcon(
2256 $icon,
2257 $tableName,
2258 $shortcutRecord['uid']
2259 );
2260 $shortcutContent[] = $icon
2261 . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord));
2262 }
2263 }
2264 $out .= implode('<br />', $shortcutContent) . '<br />';
2265 }
2266 break;
2267 case 'list':
2268 $hookOut = '';
2269 $_params = ['pObj' => &$this, 'row' => $row, 'infoArr' => $infoArr];
2270 foreach (
2271 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ??
2272 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ??
2273 [] as $_funcRef
2274 ) {
2275 $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2276 }
2277 if ((string)$hookOut !== '') {
2278 $out .= $hookOut;
2279 } elseif (!empty($row['list_type'])) {
2280 $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']);
2281 if (!empty($label)) {
2282 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />';
2283 } else {
2284 $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']);
2285 $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
2286 }
2287 } else {
2288 $out .= '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>';
2289 }
2290 $out .= htmlspecialchars($this->getLanguageService()->sL(
2291 BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages'])
2292 )) . '<br />';
2293 break;
2294 default:
2295 $contentType = $this->CType_labels[$row['CType']];
2296
2297 if (isset($contentType)) {
2298 $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />';
2299 if ($row['bodytext']) {
2300 $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />';
2301 }
2302 if ($row['image']) {
2303 $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />';
2304 }
2305 } else {
2306 $message = sprintf(
2307 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
2308 $row['CType']
2309 );
2310 $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>';
2311 }
2312 }
2313 }
2314 // Wrap span-tags:
2315 $out = '
2316 <span class="exampleContent">' . $out . '</span>';
2317 // Add header:
2318 $out = $outHeader . $out;
2319 // Return values:
2320 if ($this->isDisabled('tt_content', $row)) {
2321 return '<span class="text-muted">' . $out . '</span>';
2322 }
2323 return $out;
2324 }
2325
2326 /**
2327 * Generates a list of selected pages or categories for the CType menu
2328 *
2329 * @param array $row row from pages
2330 * @return string
2331 */
2332 protected function generateListForCTypeMenu(array $row)
2333 {
2334 $table = 'pages';
2335 $field = 'pages';
2336 // get categories instead of pages
2337 if (strpos($row['menu_type'], 'categorized_') !== false) {
2338 $table = 'sys_category';
2339 $field = 'selected_categories';
2340 }
2341 if (trim($row[$field]) === '') {
2342 return '';
2343 }
2344 $content = '';
2345 $uidList = explode(',', $row[$field]);
2346 foreach ($uidList as $uid) {
2347 $uid = (int)$uid;
2348 $record = BackendUtility::getRecord($table, $uid, 'title');
2349 $content .= '<br>' . $record['title'] . ' (' . $uid . ')';
2350 }
2351 return $content;
2352 }
2353
2354 /**
2355 * Filters out all tt_content uids which are already translated so only non-translated uids is left.
2356 * Selects across columns, but within in the same PID. Columns are expect to be the same
2357 * for translations and original but this may be a conceptual error (?)
2358 *
2359 * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
2360 * @param int $id Page pid
2361 * @param int $lP Sys language UID
2362 * @return array Modified $defLanguageCount
2363 */
2364 public function getNonTranslatedTTcontentUids($defaultLanguageUids, $id, $lP)
2365 {
2366 if ($lP && !empty($defaultLanguageUids)) {
2367 // Select all translations here:
2368 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2369 ->getQueryBuilderForTable('tt_content');
2370 $queryBuilder->getRestrictions()
2371 ->removeAll()
2372 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2373 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, null, false));
2374 $queryBuilder
2375 ->select('*')
2376 ->from('tt_content')
2377 ->where(
2378 $queryBuilder->expr()->eq(
2379 'sys_language_uid',
2380 $queryBuilder->createNamedParameter($lP, \PDO::PARAM_INT)
2381 ),
2382 $queryBuilder->expr()->in(
2383 'l18n_parent',
2384 $queryBuilder->createNamedParameter($defaultLanguageUids, Connection::PARAM_INT_ARRAY)
2385 )
2386 );
2387
2388 $result = $queryBuilder->execute();
2389
2390 // Flip uids:
2391 $defaultLanguageUids = array_flip($defaultLanguageUids);
2392 // Traverse any selected elements and unset original UID if any:
2393 while ($row = $result->fetch()) {
2394 BackendUtility::workspaceOL('tt_content', $row);
2395 unset($defaultLanguageUids[$row['l18n_parent']]);
2396 }
2397 // Flip again:
2398 $defaultLanguageUids = array_keys($defaultLanguageUids);
2399 }
2400 return $defaultLanguageUids;
2401 }
2402
2403 /**
2404 * Creates button which is used to create copies of records..
2405 *
2406 * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language
2407 * @param int $lP Sys language UID
2408 * @return string "Copy languages" button, if available.
2409 */
2410 public function newLanguageButton($defaultLanguageUids, $lP)
2411 {
2412 $lP = (int)$lP;
2413 if (!$this->doEdit || !$lP) {
2414 return '';
2415 }
2416 $theNewButton = '';
2417
2418 $localizationTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['localization.'] ?? [];
2419 $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true);
2420 $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true);
2421 if (!empty($this->languageHasTranslationsCache[$lP])) {
2422 if (isset($this->languageHasTranslationsCache[$lP]['hasStandAloneContent'])) {
2423 $allowTranslate = false;
2424 }
2425 if (isset($this->languageHasTranslationsCache[$lP]['hasTranslations'])) {
2426 $allowCopy = !$this->languageHasTranslationsCache[$lP]['hasTranslations'];
2427 }
2428 }
2429
2430 if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) {
2431 foreach ($this->contentElementCache[$lP] as $column => $records) {
2432 foreach ($records as $record) {
2433 $key = array_search($record['l10n_source'], $defaultLanguageUids);
2434 if ($key !== false) {
2435 unset($defaultLanguageUids[$key]);
2436 }
2437 }
2438 }
2439 }
2440
2441 if (!empty($defaultLanguageUids)) {
2442 $theNewButton =
2443 '<a'
2444 . ' href="#"'
2445 . ' class="btn btn-default btn-sm t3js-localize disabled"'
2446 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"'
2447 . ' data-page="' . htmlspecialchars($this->getLocalizedPageTitle()) . '"'
2448 . ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"'
2449 . ' data-allow-copy="' . (int)$allowCopy . '"'
2450 . ' data-allow-translate="' . (int)$allowTranslate . '"'
2451 . ' data-table="tt_content"'
2452 . ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"'
2453 . ' data-language-id="' . $lP . '"'
2454 . ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"'
2455 . '>'
2456 . $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render()
2457 . ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate'))
2458 . '</a>';
2459 }
2460
2461 return $theNewButton;
2462 }
2463
2464 /**
2465 * Creates onclick-attribute content for a new content element
2466 *
2467 * @param int $id Page id where to create the element.
2468 * @param int $colPos Preset: Column position value
2469 * @param int $sys_language Preset: Sys language value
2470 * @return string String for onclick attribute.
2471 * @see getTable_tt_content()
2472 */
2473 public function newContentElementOnClick($id, $colPos, $sys_language)
2474 {
2475 if ($this->option_newWizard) {
2476 $routeName = BackendUtility::getPagesTSconfig($id)['mod.']['newContentElementWizard.']['override']
2477 ?? 'new_content_element_wizard';
2478 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2479 $url = $uriBuilder->buildUriFromRoute($routeName, [
2480 'id' => $id,
2481 'colPos' => $colPos,
2482 'sys_language_uid' => $sys_language,
2483 'uid_pid' => $id,
2484 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
2485 ]);
2486 $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$url) . ';';
2487 } else {
2488 $onClick = BackendUtility::editOnClick('&edit[tt_content][' . $id . ']=new&defVals[tt_content][colPos]='
2489 . $colPos . '&defVals[tt_content][sys_language_uid]=' . $sys_language);
2490 }
2491 return $onClick;
2492 }
2493
2494 /**
2495 * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE.
2496 * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button
2497 *
2498 * @param string $str String to link. Must be prepared for HTML output.
2499 * @param array $row The row.
2500 * @return string If the whole thing was editable ($this->doEdit) $str is return with link around. Otherwise just $str.
2501 * @see getTable_tt_content()
2502 */
2503 public function linkEditContent($str, $row)
2504 {
2505 if ($this->doEdit && $this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) {
2506 $urlParameters = [
2507 'edit' => [
2508 'tt_content' => [
2509 $row['uid'] => 'edit'
2510 ]
2511 ],
2512 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid']
2513 ];
2514 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2515 $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
2516 // Return link
2517 return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>';
2518 }
2519 return $str;
2520 }
2521
2522 /**
2523 * Make selector box for creating new translation in a language
2524 * Displays only languages which are not yet present for the current page and
2525 * that are not disabled with page TS.
2526 *
2527 * @param int $id Page id for which to create a new translation record of pages
2528 * @return string <select> HTML element (if there were items for the box anyways...)
2529 * @see getTable_tt_content()
2530 */
2531 public function languageSelector($id)
2532 {
2533 if (!$this->getBackendUser()->check('tables_modify', 'pages')) {
2534 return '';
2535 }
2536 $id = (int)$id;
2537
2538 // First, select all languages that are available for the current user
2539 $availableTranslations = [];
2540 foreach ($this->siteLanguages as $language) {
2541 if ($language->getLanguageId() === 0) {
2542 continue;
2543 }
2544 $availableTranslations[$language->getLanguageId()] = $language->getTitle();
2545 }
2546
2547 // Then, subtract the languages which are already on the page:
2548 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
2549 $queryBuilder->getRestrictions()->removeAll()
2550 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2551 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2552 $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField'])
2553 ->from('pages')
2554 ->where(
2555 $queryBuilder->expr()->eq(
2556 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
2557 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
2558 )
2559 );
2560 $statement = $queryBuilder->execute();
2561 while ($row = $statement->fetch()) {
2562 unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]);
2563 }
2564 // If any languages are left, make selector:
2565 if (!empty($availableTranslations)) {
2566 $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->getLL('new_language')) . '</option>';
2567 foreach ($availableTranslations as $languageUid => $languageTitle) {
2568 // Build localize command URL to DataHandler (tce_db)
2569 // which redirects to FormEngine (record_edit)
2570 // which, when finished editing should return back to the current page (returnUrl)
2571 $parameters = [
2572 'justLocalized' => 'pages:' . $id . ':' . $languageUid,
2573 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
2574 ];
2575 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2576 $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters);
2577 $targetUrl = BackendUtility::getLinkToDataHandlerAction(
2578 '&cmd[pages][' . $id . '][localize]=' . $languageUid,
2579 $redirectUrl
2580 );
2581
2582 $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>';
2583 }
2584
2585 return '<div class="form-inline form-inline-spaced">'
2586 . '<div class="form-group">'
2587 . '<select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">'
2588 . $output
2589 . '</select></div></div>';
2590 }
2591 return '';
2592 }
2593
2594 /**
2595 * Traverse the result pointer given, adding each record to array and setting some internal values at the same time.
2596 *
2597 * @param Statement $result DBAL Statement
2598 * @param string $table Table name defaulting to tt_content
2599 * @return array The selected rows returned in this array.
2600 */
2601 public function getResult(Statement $result, string $table = 'tt_content'): array
2602 {
2603 $output = [];
2604 // Traverse the result:
2605 while ($row = $result->fetch()) {
2606 BackendUtility::workspaceOL($table, $row, -99, true);
2607 if ($row) {
2608 // Add the row to the array:
2609 $output[] = $row;
2610 }
2611 }
2612 $this->generateTtContentDataArray($output);
2613 // Return selected records
2614 return $output;
2615 }
2616
2617 /********************************
2618 *
2619 * Various helper functions
2620 *
2621 ********************************/
2622
2623 /**
2624 * Initializes the clipboard for generating paste links
2625 *
2626 *
2627 * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction()
2628 * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction()
2629 */
2630 protected function initializeClipboard()
2631 {
2632 // Start clipboard
2633 $this->clipboard = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Clipboard\Clipboard::class);
2634
2635 // Initialize - reads the clipboard content from the user session
2636 $this->clipboard->initializeClipboard();
2637
2638 // This locks the clipboard to the Normal for this request.
2639 $this->clipboard->lockToNormal();
2640
2641 // Clean up pad
2642 $this->clipboard->cleanCurrent();
2643
2644 // Save the clipboard content
2645 $this->clipboard->endClipboard();
2646 }
2647
2648 /**
2649 * Generates the data for previous and next elements which is needed for movements.
2650 *
2651 * @param array $rowArray
2652 */
2653 protected function generateTtContentDataArray(array $rowArray)
2654 {
2655 if (empty($this->tt_contentData)) {
2656 $this->tt_contentData = [
2657 'nextThree' => [],
2658 'next' => [],
2659 'prev' => [],
2660 ];
2661 }
2662 foreach ($rowArray as $key => $value) {
2663 // Create the list of the next three ids (for editing links...)
2664 for ($i = 0; $i < $this->nextThree; $i++) {
2665 if (isset($rowArray[$key - $i])
2666 && !GeneralUtility::inList($this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']], $value['uid'])
2667 ) {
2668 $this->tt_contentData['nextThree'][$rowArray[$key - $i]['uid']] .= $value['uid'] . ',';
2669 }
2670 }
2671
2672 // Create information for next and previous content elements
2673 if (isset($rowArray[$key - 1])) {
2674 if (isset($rowArray[$key - 2])) {
2675 $this->tt_contentData['prev'][$value['uid']] = -$rowArray[$key - 2]['uid'];
2676 } else {
2677 $this->tt_contentData['prev'][$value['uid']] = $value['pid'];
2678 }
2679 $this->tt_contentData['next'][$rowArray[$key - 1]['uid']] = -$value['uid'];
2680 }
2681 }
2682 }
2683
2684 /**
2685 * Counts and returns the number of records on the page with $pid
2686 *
2687 * @param string $table Table name
2688 * @param int $pid Page id
2689 * @return int Number of records.
2690 */
2691 public function numberOfRecords($table, $pid)
2692 {
2693 $count = 0;
2694 if ($GLOBALS['TCA'][$table]) {
2695 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2696 ->getQueryBuilderForTable($table);
2697 $queryBuilder->getRestrictions()
2698 ->removeAll()
2699 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2700 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2701 $count = (int)$queryBuilder->count('uid')
2702 ->from($table)
2703 ->where(
2704 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT))
2705 )
2706 ->execute()
2707 ->fetchColumn();
2708 }
2709
2710 return $count;
2711 }
2712
2713 /**
2714 * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc.
2715 *
2716 * @param string $input Input string
2717 * @return string Output string
2718 */
2719 public function renderText($input)
2720 {
2721 $input = strip_tags($input);
2722 $input = GeneralUtility::fixed_lgd_cs($input, 1500);
2723 return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false));
2724 }
2725
2726 /**
2727 * Creates the icon image tag for record from table and wraps it in a link which will trigger the click menu.
2728 *
2729 * @param string $table Table name
2730 * @param array $row Record array
2731 * @param string $enabledClickMenuItems Passthrough to wrapClickMenuOnIcon
2732 * @return string HTML for the icon
2733 */
2734 public function getIcon($table, $row, $enabledClickMenuItems = '')
2735 {
2736 // Initialization
2737 $toolTip = BackendUtility::getRecordToolTip($row, 'tt_content');
2738 $icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
2739 $this->counter++;
2740 // The icon with link
2741 if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) {
2742 $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
2743 }
2744 return $icon;
2745 }
2746
2747 /**
2748 * Creates processed values for all field names in $fieldList based on values from $row array.
2749 * The result is 'returned' through $info which is passed as a reference
2750 *
2751 * @param string $table Table name
2752 * @param string $fieldList Comma separated list of fields.
2753 * @param array $row Record from which to take values for processing.
2754 * @param array $info Array to which the processed values are added.
2755 */
2756 public function getProcessedValue($table, $fieldList, array $row, array &$info)
2757 {
2758 // Splitting values from $fieldList
2759 $fieldArr = explode(',', $fieldList);
2760 // Traverse fields from $fieldList
2761 foreach ($fieldArr as $field) {
2762 if ($row[$field]) {
2763 $info[] = '<strong>' . htmlspecialchars($this->itemLabels[$field]) . '</strong> '
2764 . htmlspecialchars(BackendUtility::getProcessedValue($table, $field, $row[$field]));
2765 }
2766 }
2767 }
2768
2769 /**
2770 * Returns TRUE, if the record given as parameters is NOT visible based on hidden/starttime/endtime (if available)
2771 *
2772 * @param string $table Tablename of table to test
2773 * @param array $row Record row.
2774 * @return bool Returns TRUE, if disabled.
2775 */
2776 public function isDisabled($table, $row)
2777 {
2778 $enableCols = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
2779 return $enableCols['disabled'] && $row[$enableCols['disabled']]
2780 || $enableCols['starttime'] && $row[$enableCols['starttime']] > $GLOBALS['EXEC_TIME']
2781 || $enableCols['endtime'] && $row[$enableCols['endtime']] && $row[$enableCols['endtime']] < $GLOBALS['EXEC_TIME'];
2782 }
2783
2784 /**
2785 * Returns icon for "no-edit" of a record.
2786 * Basically, the point is to signal that this record could have had an edit link if
2787 * the circumstances were right. A placeholder for the regular edit icon...
2788 *
2789 * @param string $label Label key from LOCAL_LANG
2790 * @return string IMG tag for icon.
2791 */
2792 public function noEditIcon($label = 'noEditItems')
2793 {
2794 $title = htmlspecialchars($this->getLanguageService()->getLL($label));
2795 return '<span title="' . $title . '">' . $this->iconFactory->getIcon('status-edit-read-only', Icon::SIZE_SMALL)->render() . '</span>';
2796 }
2797
2798 /*****************************************
2799 *
2800 * External renderings
2801 *
2802 *****************************************/
2803
2804 /**
2805 * Creates a menu of the tables that can be listed by this function
2806 * Only tables which has records on the page will be included.
2807 * Notice: The function also fills in the internal variable $this->activeTables with icon/titles.
2808 *
2809 * @param int $id Page id from which we are listing records (the function will look up if there are records on the page)
2810 * @return string HTML output.
2811 */
2812 public function getTableMenu($id)
2813 {
2814 // Initialize:
2815 $this->activeTables = [];
2816 $theTables = ['tt_content'];
2817 // External tables:
2818 if (is_array($this->externalTables)) {
2819 $theTables = array_unique(array_merge($theTables, array_keys($this->externalTables)));
2820 }
2821 $out = '';
2822 // Traverse tables to check:
2823 foreach ($theTables as $tName) {
2824 // Check access and whether the proper extensions are loaded:
2825 if ($this->getBackendUser()->check('tables_select', $tName)
2826 && (
2827 isset($this->externalTables[$tName])
2828 || $tName === 'fe_users' || $tName === 'tt_content'
2829 || \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($tName)
2830 )
2831 ) {
2832 // Make query to count records from page:
2833 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2834 ->getQueryBuilderForTable($tName);
2835 $queryBuilder->getRestrictions()
2836 ->removeAll()
2837 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2838 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2839 $count = $queryBuilder->count('uid')
2840 ->from($tName)
2841 ->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)))
2842 ->execute()
2843 ->fetchColumn();
2844 // If records were found (or if "tt_content" is the table...):
2845 if ($count || $tName === 'tt_content') {
2846 // Add row to menu:
2847 $out .= '
2848 <td><a href="#' . $tName . '" title="' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title'])) . '"></a>'
2849 . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
2850 . '</td>';
2851 // ... and to the internal array, activeTables we also add table icon and title (for use elsewhere)
2852 $title = htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title']))
2853 . ': ' . $count . ' ' . htmlspecialchars($this->getLanguageService()->getLL('records'));
2854 $this->activeTables[$tName] = '<span title="' . $title . '">'
2855 . $this->iconFactory->getIconForRecord($tName, [], Icon::SIZE_SMALL)->render()
2856 . '</span>'
2857 . '&nbsp;' . htmlspecialchars($this->getLanguageService()->sL($GLOBALS['TCA'][$tName]['ctrl']['title']));
2858 }
2859 }
2860 }
2861 // Wrap cells in table tags:
2862 $out = '
2863 <!--
2864 Menu of tables on the page (table menu)
2865 -->
2866 <table border="0" cellpadding="0" cellspacing="0" id="typo3-page-tblMenu">
2867 <tr>' . $out . '
2868 </tr>
2869 </table>';
2870 // Return the content:
2871 return $out;
2872 }
2873
2874 /**
2875 * Create thumbnail code for record/field but not linked
2876 *
2877 * @param mixed[] $row Record array
2878 * @param string $table Table (record is from)
2879 * @param string $field Field name for which thumbnail are to be rendered.
2880 * @return string HTML for