[BUGFIX] Prevent double pointer parameter in record list
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / RecordList / DatabaseRecordList.php
1 <?php
2 namespace TYPO3\CMS\Recordlist\RecordList;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
18 use TYPO3\CMS\Backend\Module\BaseScriptClass;
19 use TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface;
20 use TYPO3\CMS\Backend\Routing\UriBuilder;
21 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
22 use TYPO3\CMS\Backend\Template\DocumentTemplate;
23 use TYPO3\CMS\Backend\Template\ModuleTemplate;
24 use TYPO3\CMS\Backend\Tree\View\PageTreeView;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
26 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27 use TYPO3\CMS\Core\Cache\CacheManager;
28 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
29 use TYPO3\CMS\Core\Database\Connection;
30 use TYPO3\CMS\Core\Database\ConnectionPool;
31 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32 use TYPO3\CMS\Core\Database\Query\QueryHelper;
33 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
34 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
35 use TYPO3\CMS\Core\Database\ReferenceIndex;
36 use TYPO3\CMS\Core\Imaging\Icon;
37 use TYPO3\CMS\Core\Imaging\IconFactory;
38 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
39 use TYPO3\CMS\Core\LinkHandling\LinkService;
40 use TYPO3\CMS\Core\Localization\LanguageService;
41 use TYPO3\CMS\Core\Log\LogManager;
42 use TYPO3\CMS\Core\Messaging\FlashMessage;
43 use TYPO3\CMS\Core\Messaging\FlashMessageService;
44 use TYPO3\CMS\Core\Service\DependencyOrderingService;
45 use TYPO3\CMS\Core\Type\Bitmask\Permission;
46 use TYPO3\CMS\Core\Utility\CsvUtility;
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\Frontend\Page\PageRepository;
52
53 /**
54 * Class for rendering of Web>List module
55 * @internal This class is a specific TYPO3 Backend implementation and is not part of the TYPO3's Core API.
56 */
57 class DatabaseRecordList
58 {
59 use PublicPropertyDeprecationTrait;
60
61 /**
62 * @var array
63 */
64 protected $deprecatedPublicProperties = [
65 'newWizards' => 'Using $newWizards of class DatabaseRecordList from outside is discouraged, property will be removed in TYPO3 v10.0.',
66 ];
67
68 // *********
69 // External:
70 // *********
71
72 /**
73 * Used to indicate which tables (values in the array) that can have a
74 * create-new-record link. If the array is empty, all tables are allowed.
75 *
76 * @var string[]
77 */
78 public $allowedNewTables = [];
79
80 /**
81 * Used to indicate which tables (values in the array) that cannot have a
82 * create-new-record link. If the array is empty, all tables are allowed.
83 *
84 * @var string[]
85 */
86 public $deniedNewTables = [];
87
88 /**
89 * If TRUE, the control panel will contain links to the create-new wizards for
90 * pages and tt_content elements (normally, the link goes to just creating a new
91 * element without the wizards!).
92 *
93 * @var bool
94 * @deprecated and unused since TYPO3 v9, will be removed in TYPO3 v10.0
95 */
96 public $newWizards = false;
97
98 /**
99 * If TRUE, will disable the rendering of clipboard + control panels.
100 *
101 * @var bool
102 */
103 public $dontShowClipControlPanels = false;
104
105 /**
106 * If TRUE, will show the clipboard in the field list.
107 *
108 * @var bool
109 */
110 public $showClipboard = false;
111
112 /**
113 * If TRUE, will DISABLE all control panels in lists. (Takes precedence)
114 *
115 * @var bool
116 */
117 public $noControlPanels = false;
118
119 /**
120 * If TRUE, clickmenus will be rendered
121 *
122 * @var bool
123 */
124 public $clickMenuEnabled = true;
125
126 /**
127 * Count of record rows in view
128 *
129 * @var int
130 */
131 public $totalRowCount;
132
133 /**
134 * Space icon used for alignment
135 *
136 * @var string
137 */
138 public $spaceIcon;
139
140 /**
141 * Disable single table view
142 *
143 * @var bool
144 */
145 public $disableSingleTableView = false;
146
147 // *********
148 // Internal:
149 // *********
150
151 /**
152 * Set to the page record (see writeTop())
153 *
154 * @var string[]
155 */
156 public $pageRow = [];
157
158 /**
159 * Shared module configuration, used by localization features
160 *
161 * @var array
162 */
163 public $modSharedTSconfig = [];
164
165 /**
166 * Contains page translation languages
167 *
168 * @var array
169 */
170 public $pageOverlays = [];
171
172 /**
173 * Contains sys language icons and titles
174 *
175 * @var array
176 */
177 public $languageIconTitles = [];
178
179 /**
180 * Tables which should not list their translations
181 *
182 * @var string
183 */
184 public $hideTranslations = '';
185
186 /**
187 * If set, the listing is returned as CSV instead.
188 *
189 * @var bool
190 */
191 public $csvOutput = false;
192
193 /**
194 * Cache for record path
195 *
196 * @var mixed[]
197 */
198 public $recPath_cache = [];
199
200 /**
201 * Field, to sort list by
202 *
203 * @var string
204 */
205 public $sortField;
206
207 /**
208 * default Max items shown per table in "multi-table mode", may be overridden by tables.php
209 *
210 * @var int
211 */
212 public $itemsLimitPerTable = 20;
213
214 /**
215 * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes;
216 *
217 * @var array
218 */
219 public $addElement_tdParams = [];
220
221 /**
222 * Page id
223 *
224 * @var int
225 */
226 public $id;
227
228 /**
229 * @var int
230 */
231 public $no_noWrap = 0;
232
233 /**
234 * Set to zero, if you don't want a left-margin with addElement function
235 *
236 * @var int
237 */
238 public $setLMargin = 1;
239
240 /**
241 * Used for tracking duplicate values of fields
242 *
243 * @var string[]
244 */
245 public $duplicateStack = [];
246
247 /**
248 * Current script name
249 *
250 * @var string
251 */
252 public $script = 'index.php';
253
254 /**
255 * If TRUE, records are listed only if a specific table is selected.
256 *
257 * @var bool
258 */
259 public $listOnlyInSingleTableMode = false;
260
261 /**
262 * Script URL
263 *
264 * @var string
265 */
266 public $thisScript = '';
267
268 /**
269 * JavaScript code accumulation
270 *
271 * @var string
272 */
273 public $JScode = '';
274
275 /**
276 * @var TranslationConfigurationProvider
277 */
278 public $translateTools;
279
280 /**
281 * default Max items shown per table in "single-table mode", may be overridden by tables.php
282 *
283 * @var int
284 */
285 public $itemsLimitSingleTable = 100;
286
287 /**
288 * Array of collapsed / uncollapsed tables in multi table view
289 *
290 * @var int[][]
291 */
292 public $tablesCollapsed = [];
293
294 /**
295 * @var array[] Module configuration
296 */
297 public $modTSconfig;
298
299 /**
300 * String with accumulated HTML content
301 *
302 * @var string
303 */
304 public $HTMLcode = '';
305
306 /**
307 * Keys are fieldnames and values are td-css-classes to add in addElement();
308 *
309 * @var array
310 */
311 public $addElement_tdCssClass = [];
312
313 /**
314 * Thumbnails on records containing files (pictures)
315 *
316 * @var bool
317 */
318 public $thumbs = 0;
319
320 /**
321 * Used for tracking next/prev uids
322 *
323 * @var int[][]
324 */
325 public $currentTable = [];
326
327 /**
328 * Indicates if all available fields for a user should be selected or not.
329 *
330 * @var int
331 */
332 public $allFields = 0;
333
334 /**
335 * Number of records to show
336 *
337 * @var int
338 */
339 public $showLimit = 0;
340
341 /**
342 * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column.
343 *
344 * @var array
345 */
346 public $fieldArray = [];
347
348 /**
349 * Tables which should not get listed
350 *
351 * @var string
352 */
353 public $hideTables = '';
354
355 /**
356 * Containing which fields to display in extended mode
357 *
358 * @var string[]
359 */
360 public $displayFields;
361
362 /**
363 * If set this is <td> CSS-classname for odd columns in addElement. Used with db_layout / pages section
364 *
365 * @var string
366 */
367 public $oddColumnsCssClass = '';
368
369 /**
370 * Not used in this class - but maybe extension classes...
371 * Max length of strings
372 *
373 * @var int
374 */
375 public $fixedL = 30;
376
377 /**
378 * Page select permissions
379 *
380 * @var string
381 */
382 public $perms_clause = '';
383
384 /**
385 * Return URL
386 *
387 * @var string
388 */
389 public $returnUrl = '';
390
391 /**
392 * Tablename if single-table mode
393 *
394 * @var string
395 */
396 public $table = '';
397
398 /**
399 * Some permissions...
400 *
401 * @var int
402 */
403 public $calcPerms = 0;
404
405 /**
406 * Mode for what happens when a user clicks the title of a record.
407 *
408 * @var string
409 */
410 public $clickTitleMode = '';
411
412 /**
413 * @var int
414 */
415 public $showIcon = 1;
416
417 /**
418 * Levels to search down.
419 *
420 * @var int
421 */
422 public $searchLevels = '';
423
424 /**
425 * "LIMIT " in SQL...
426 *
427 * @var int
428 */
429 public $iLimit = 0;
430
431 /**
432 * Set to the total number of items for a table when selecting.
433 *
434 * @var string
435 */
436 public $totalItems = '';
437
438 /**
439 * OBSOLETE - NOT USED ANYMORE. leftMargin
440 *
441 * @var int
442 */
443 public $leftMargin = 0;
444
445 /**
446 * TSconfig which overwrites TCA-Settings
447 *
448 * @var mixed[][]
449 */
450 public $tableTSconfigOverTCA = [];
451
452 /**
453 * Loaded with page record with version overlay if any.
454 *
455 * @var string[]
456 */
457 public $pageRecord = [];
458
459 /**
460 * Fields to display for the current table
461 *
462 * @var string[]
463 */
464 public $setFields = [];
465
466 /**
467 * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard
468 *
469 * @var int
470 */
471 public $counter = 0;
472
473 /**
474 * Pointer for browsing list
475 *
476 * @var int
477 */
478 public $firstElementNumber = 0;
479
480 /**
481 * Counting the elements no matter what...
482 *
483 * @var int
484 */
485 public $eCounter = 0;
486
487 /**
488 * Search string
489 *
490 * @var string
491 */
492 public $searchString = '';
493
494 /**
495 * Field, indicating to sort in reverse order.
496 *
497 * @var bool
498 */
499 public $sortRev;
500
501 /**
502 * String, can contain the field name from a table which must have duplicate values marked.
503 *
504 * @var string
505 */
506 public $duplicateField;
507
508 /**
509 * Specify a list of tables which are the only ones allowed to be displayed.
510 *
511 * @var string
512 */
513 public $tableList = '';
514
515 /**
516 * Used to accumulate CSV lines for CSV export.
517 *
518 * @var string[]
519 */
520 protected $csvLines = [];
521
522 /**
523 * Clipboard object
524 *
525 * @var \TYPO3\CMS\Backend\Clipboard\Clipboard
526 */
527 public $clipObj;
528
529 /**
530 * Tracking names of elements (for clipboard use)
531 *
532 * @var string[]
533 */
534 public $CBnames = [];
535
536 /**
537 * [$tablename][$uid] = number of references to this record
538 *
539 * @var int[][]
540 */
541 protected $referenceCount = [];
542
543 /**
544 * Translations of the current record
545 *
546 * @var string[]
547 */
548 public $translations;
549
550 /**
551 * select fields for the query which fetches the translations of the current
552 * record
553 *
554 * @var string
555 */
556 public $selFieldList;
557
558 /**
559 * @var mixed[]
560 */
561 public $pageinfo;
562
563 /**
564 * Injected by RecordList
565 *
566 * @var string[]
567 */
568 public $MOD_MENU;
569
570 /**
571 * If defined the records are editable
572 *
573 * @var bool
574 */
575 protected $editable = true;
576
577 /**
578 * @var IconFactory
579 */
580 protected $iconFactory;
581
582 /**
583 * Array with before/after setting for tables
584 * Structure:
585 * 'tableName' => [
586 * 'before' => ['A', ...]
587 * 'after' => []
588 * ]
589 *
590 * @var array[]
591 */
592 protected $tableDisplayOrder = [];
593
594 /**
595 * Override the page ids taken into account by getPageIdConstraint()
596 *
597 * @var array
598 */
599 protected $overridePageIdList = [];
600
601 /**
602 * Override/add urlparameters in listUrl() method
603 * @var mixed[]
604 */
605 protected $overrideUrlParameters = [];
606
607 /**
608 * Current link: array with table names and uid
609 *
610 * @var array
611 */
612 protected $currentLink = [];
613
614 /**
615 * Only used to render translated records, used in list module to show page translations
616 *
617 * @var bool
618 */
619 protected $showOnlyTranslatedRecords = false;
620
621 /**
622 * Constructor
623 */
624 public function __construct()
625 {
626 if (isset($GLOBALS['BE_USER']->uc['titleLen']) && $GLOBALS['BE_USER']->uc['titleLen'] > 0) {
627 $this->fixedL = $GLOBALS['BE_USER']->uc['titleLen'];
628 }
629 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
630 $this->getTranslateTools();
631 $this->determineScriptUrl();
632
633 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
634 }
635
636 /**
637 * Create the panel of buttons for submitting the form or otherwise perform
638 * operations.
639 *
640 * @return string[] All available buttons as an assoc. array
641 */
642 public function getButtons()
643 {
644 $module = $this->getModule();
645 $backendUser = $this->getBackendUserAuthentication();
646 $lang = $this->getLanguageService();
647 $buttons = [
648 'csh' => '',
649 'view' => '',
650 'edit' => '',
651 'hide_unhide' => '',
652 'move' => '',
653 'new_record' => '',
654 'paste' => '',
655 'level_up' => '',
656 'cache' => '',
657 'reload' => '',
658 'shortcut' => '',
659 'back' => '',
660 'csv' => '',
661 'export' => ''
662 ];
663 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
664 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
665 // Get users permissions for this page record:
666 $localCalcPerms = $backendUser->calcPerms($this->pageRow);
667 // CSH
668 if ((string)$this->id === '') {
669 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_noId');
670 } elseif (!$this->id) {
671 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_root');
672 } else {
673 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module');
674 }
675 if (isset($this->id)) {
676 // View Exclude doktypes 254,255 Configuration:
677 // mod.web_list.noViewWithDokTypes = 254,255
678 if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) {
679 $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true);
680 } else {
681 //default exclusion: doktype 254 (folder), 255 (recycler)
682 $noViewDokTypes = [
683 PageRepository::DOKTYPE_SYSFOLDER,
684 PageRepository::DOKTYPE_RECYCLER
685 ];
686 }
687 if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) {
688 $onClick = htmlspecialchars(BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id)));
689 $buttons['view'] = '<a href="#" onclick="' . $onClick . '" title="'
690 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">'
691 . $this->iconFactory->getIcon('actions-document-view', Icon::SIZE_SMALL)->render() . '</a>';
692 }
693 // New record on pages that are not locked by editlock
694 if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) {
695 $onClick = htmlspecialchars('return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');');
696 $buttons['new_record'] = '<a href="#" onclick="' . $onClick . '" title="'
697 . htmlspecialchars($lang->getLL('newRecordGeneral')) . '">'
698 . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</a>';
699 }
700 // If edit permissions are set, see
701 // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
702 if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions() && $this->getBackendUserAuthentication()->checkLanguageAccess(0)) {
703 // Edit
704 $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit';
705 $onClick = htmlspecialchars(BackendUtility::editOnClick($params, '', -1));
706 $buttons['edit'] = '<a href="#" onclick="' . $onClick . '" title="' . htmlspecialchars($lang->getLL('editPage')) . '">'
707 . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render()
708 . '</a>';
709 }
710 // Paste
711 if (($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) {
712 $elFromTable = $this->clipObj->elFromTable('');
713 if (!empty($elFromTable)) {
714 $confirmText = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
715 $buttons['paste'] = '<a'
716 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $this->id)) . '"'
717 . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
718 . ' class="t3js-modal-trigger"'
719 . ' data-severity="warning"'
720 . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
721 . ' data-content="' . htmlspecialchars($confirmText) . '"'
722 . '>'
723 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
724 . '</a>';
725 }
726 }
727 // Cache
728 $buttons['cache'] = '<a href="' . htmlspecialchars($this->listURL() . '&clear_cache=1') . '" title="'
729 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache')) . '">'
730 . $this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL)->render() . '</a>';
731 if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks'])
732 || (isset($module->modTSconfig['properties']['noExportRecordsLinks'])
733 && !$module->modTSconfig['properties']['noExportRecordsLinks']))
734 ) {
735 // CSV
736 $buttons['csv'] = '<a href="' . htmlspecialchars($this->listURL() . '&csv=1') . '" title="'
737 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv')) . '">'
738 . $this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL)->render() . '</a>';
739 // Export
740 if (ExtensionManagementUtility::isLoaded('impexp')) {
741 $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']);
742 $buttons['export'] = '<a href="' . htmlspecialchars($url . '&tx_impexp[list][]='
743 . rawurlencode($this->table . ':' . $this->id)) . '" title="'
744 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export')) . '">'
745 . $this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL)->render() . '</a>';
746 }
747 }
748 // Reload
749 $buttons['reload'] = '<a href="' . htmlspecialchars($this->listURL()) . '" title="'
750 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload')) . '">'
751 . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a>';
752 // Shortcut
753 if ($backendUser->mayMakeShortcut()) {
754 $buttons['shortcut'] = $this->getDocumentTemplate()->makeShortcutIcon(
755 'id, M, imagemode, pointer, table, search_field, search_levels, showLimit, sortField, sortRev',
756 implode(',', array_keys($this->MOD_MENU)),
757 'web_list'
758 );
759 }
760 // Back
761 if ($this->returnUrl) {
762 $href = htmlspecialchars(GeneralUtility::linkThisUrl($this->returnUrl, ['id' => $this->id]));
763 $buttons['back'] = '<a href="' . $href . '" class="typo3-goBack" title="'
764 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack')) . '">'
765 . $this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL)->render() . '</a>';
766 }
767 }
768 return $buttons;
769 }
770
771 /**
772 * Create the panel of buttons for submitting the form or otherwise perform
773 * operations.
774 *
775 * @param ModuleTemplate $moduleTemplate
776 */
777 public function getDocHeaderButtons(ModuleTemplate $moduleTemplate)
778 {
779 $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
780 $module = $this->getModule();
781 $backendUser = $this->getBackendUserAuthentication();
782 $lang = $this->getLanguageService();
783 // Get users permissions for this page record:
784 $localCalcPerms = $backendUser->calcPerms($this->pageRow);
785 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
786 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
787 // CSH
788 if ((string)$this->id === '') {
789 $fieldName = 'list_module_noId';
790 } elseif (!$this->id) {
791 $fieldName = 'list_module_root';
792 } else {
793 $fieldName = 'list_module';
794 }
795 $cshButton = $buttonBar->makeHelpButton()
796 ->setModuleName('xMOD_csh_corebe')
797 ->setFieldName($fieldName);
798 $buttonBar->addButton($cshButton);
799 if (isset($this->id)) {
800 // View Exclude doktypes 254,255 Configuration:
801 // mod.web_list.noViewWithDokTypes = 254,255
802 if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) {
803 $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true);
804 } else {
805 //default exclusion: doktype 254 (folder), 255 (recycler)
806 $noViewDokTypes = [
807 PageRepository::DOKTYPE_SYSFOLDER,
808 PageRepository::DOKTYPE_RECYCLER
809 ];
810 }
811 // New record on pages that are not locked by editlock
812 if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) {
813 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');';
814 $newRecordButton = $buttonBar->makeLinkButton()
815 ->setHref('#')
816 ->setOnClick($onClick)
817 ->setTitle($lang->getLL('newRecordGeneral'))
818 ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
819 $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
820 }
821 if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) {
822 $onClick = BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id));
823 $viewButton = $buttonBar->makeLinkButton()
824 ->setHref('#')
825 ->setOnClick($onClick)
826 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
827 ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL));
828 $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
829 }
830 // If edit permissions are set, see
831 // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
832 if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions()) {
833 // Edit
834 $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit';
835 $onClick = BackendUtility::editOnClick($params, '', -1);
836 $editButton = $buttonBar->makeLinkButton()
837 ->setHref('#')
838 ->setOnClick($onClick)
839 ->setTitle($lang->getLL('editPage'))
840 ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL));
841 $buttonBar->addButton($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
842 }
843 // Paste
844 if ($this->showClipboard && ($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) {
845 $elFromTable = $this->clipObj->elFromTable('');
846 if (!empty($elFromTable)) {
847 $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
848 $pasteButton = $buttonBar->makeLinkButton()
849 ->setHref($this->clipObj->pasteUrl('', $this->id))
850 ->setTitle($lang->getLL('clip_paste'))
851 ->setClasses('t3js-modal-trigger')
852 ->setDataAttributes([
853 'severity' => 'warning',
854 'content' => $confirmMessage,
855 'title' => $lang->getLL('clip_paste')
856 ])
857 ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL));
858 $buttonBar->addButton($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
859 }
860 }
861 // Cache
862 $clearCacheButton = $buttonBar->makeLinkButton()
863 ->setHref($this->listURL() . '&clear_cache=1')
864 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache'))
865 ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL));
866 $buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT);
867 if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks'])
868 || (isset($module->modTSconfig['properties']['noExportRecordsLinks'])
869 && !$module->modTSconfig['properties']['noExportRecordsLinks']))
870 ) {
871 // CSV
872 $csvButton = $buttonBar->makeLinkButton()
873 ->setHref($this->listURL() . '&csv=1')
874 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv'))
875 ->setIcon($this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL));
876 $buttonBar->addButton($csvButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
877 // Export
878 if (ExtensionManagementUtility::isLoaded('impexp')) {
879 $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']);
880 $exportButton = $buttonBar->makeLinkButton()
881 ->setHref($url . '&tx_impexp[list][]=' . rawurlencode($this->table . ':' . $this->id))
882 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export'))
883 ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL));
884 $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 40);
885 }
886 }
887 // Reload
888 $reloadButton = $buttonBar->makeLinkButton()
889 ->setHref($this->listURL())
890 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
891 ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
892 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
893 // Shortcut
894 if ($backendUser->mayMakeShortcut()) {
895 $shortCutButton = $buttonBar->makeShortcutButton()
896 ->setModuleName('web_list')
897 ->setGetVariables([
898 'id',
899 'route',
900 'imagemode',
901 'pointer',
902 'table',
903 'search_field',
904 'search_levels',
905 'showLimit',
906 'sortField',
907 'sortRev'
908 ])
909 ->setSetVariables(array_keys($this->MOD_MENU));
910 $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
911 }
912 // Back
913 if ($this->returnUrl) {
914 $backButton = $buttonBar->makeLinkButton()
915 ->setHref($this->returnUrl)
916 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
917 ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
918 $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT);
919 }
920 }
921 }
922
923 /**
924 * Creates the listing of records from a single table
925 *
926 * @param string $table Table name
927 * @param int $id Page id
928 * @param string $rowList List of fields to show in the listing. Pseudo fields will be added including the record header.
929 * @throws \UnexpectedValueException
930 * @return string HTML table with the listing for the record.
931 */
932 public function getTable($table, $id, $rowList = '')
933 {
934 $rowListArray = GeneralUtility::trimExplode(',', $rowList, true);
935 // if no columns have been specified, show description (if configured)
936 if (!empty($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']) && empty($rowListArray)) {
937 $rowListArray[] = $GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'];
938 }
939 $backendUser = $this->getBackendUserAuthentication();
940 $lang = $this->getLanguageService();
941 // Init
942 $addWhere = '';
943 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
944 $titleCol = $GLOBALS['TCA'][$table]['ctrl']['label'];
945 $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
946 $l10nEnabled = $GLOBALS['TCA'][$table]['ctrl']['languageField']
947 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
948 $tableCollapsed = (bool)$this->tablesCollapsed[$table];
949 // prepare space icon
950 $this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
951 // Cleaning rowlist for duplicates and place the $titleCol as the first column always!
952 $this->fieldArray = [];
953 // title Column
954 // Add title column
955 $this->fieldArray[] = $titleCol;
956 // Control-Panel
957 if (!GeneralUtility::inList($rowList, '_CONTROL_')) {
958 $this->fieldArray[] = '_CONTROL_';
959 }
960 // Clipboard
961 if ($this->showClipboard) {
962 $this->fieldArray[] = '_CLIPBOARD_';
963 }
964 // Ref
965 if (!$this->dontShowClipControlPanels) {
966 $this->fieldArray[] = '_REF_';
967 }
968 // Path
969 if ($this->searchLevels) {
970 $this->fieldArray[] = '_PATH_';
971 }
972 // Localization
973 if ($l10nEnabled) {
974 $this->fieldArray[] = '_LOCALIZATION_';
975 // Do not show the "Localize to:" field when only translated records should be shown
976 if (!$this->showOnlyTranslatedRecords) {
977 $this->fieldArray[] = '_LOCALIZATION_b';
978 }
979 // Only restrict to the default language if no search request is in place
980 // And if only translations should be shown
981 if ($this->searchString === '' && !$this->showOnlyTranslatedRecords) {
982 $addWhere = (string)$queryBuilder->expr()->orX(
983 $queryBuilder->expr()->lte($GLOBALS['TCA'][$table]['ctrl']['languageField'], 0),
984 $queryBuilder->expr()->eq($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 0)
985 );
986 }
987 }
988 // Cleaning up:
989 $this->fieldArray = array_unique(array_merge($this->fieldArray, $rowListArray));
990 if ($this->noControlPanels) {
991 $tempArray = array_flip($this->fieldArray);
992 unset($tempArray['_CONTROL_']);
993 unset($tempArray['_CLIPBOARD_']);
994 $this->fieldArray = array_keys($tempArray);
995 }
996 // Creating the list of fields to include in the SQL query:
997 $selectFields = $this->fieldArray;
998 $selectFields[] = 'uid';
999 $selectFields[] = 'pid';
1000 // adding column for thumbnails
1001 if ($thumbsCol) {
1002 $selectFields[] = $thumbsCol;
1003 }
1004 if ($table === 'pages') {
1005 $selectFields[] = 'module';
1006 $selectFields[] = 'extendToSubpages';
1007 $selectFields[] = 'nav_hide';
1008 $selectFields[] = 'doktype';
1009 $selectFields[] = 'shortcut';
1010 $selectFields[] = 'shortcut_mode';
1011 $selectFields[] = 'mount_pid';
1012 }
1013 if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
1014 $selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']);
1015 }
1016 foreach (['type', 'typeicon_column', 'editlock'] as $field) {
1017 if ($GLOBALS['TCA'][$table]['ctrl'][$field]) {
1018 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl'][$field];
1019 }
1020 }
1021 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1022 $selectFields[] = 't3ver_id';
1023 $selectFields[] = 't3ver_state';
1024 $selectFields[] = 't3ver_wsid';
1025 }
1026 if ($l10nEnabled) {
1027 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
1028 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1029 }
1030 if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1031 $selectFields = array_merge(
1032 $selectFields,
1033 GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true)
1034 );
1035 }
1036 // Unique list!
1037 $selectFields = array_unique($selectFields);
1038 $fieldListFields = $this->makeFieldList($table, 1);
1039 if (empty($fieldListFields) && $GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) {
1040 $message = sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessage'), $table, $table);
1041 $messageTitle = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessageTitle');
1042 /** @var FlashMessage $flashMessage */
1043 $flashMessage = GeneralUtility::makeInstance(
1044 FlashMessage::class,
1045 $message,
1046 $messageTitle,
1047 FlashMessage::WARNING,
1048 true
1049 );
1050 /** @var FlashMessageService $flashMessageService */
1051 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
1052 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
1053 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1054 $defaultFlashMessageQueue->enqueue($flashMessage);
1055 }
1056 // Making sure that the fields in the field-list ARE in the field-list from TCA!
1057 $selectFields = array_intersect($selectFields, $fieldListFields);
1058 // Implode it into a list of fields for the SQL-statement.
1059 $selFieldList = implode(',', $selectFields);
1060 $this->selFieldList = $selFieldList;
1061 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable'] ?? [] as $className) {
1062 $hookObject = GeneralUtility::makeInstance($className);
1063 if (!$hookObject instanceof RecordListGetTableHookInterface) {
1064 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListGetTableHookInterface::class, 1195114460);
1065 }
1066 $hookObject->getDBlistQuery($table, $id, $addWhere, $selFieldList, $this);
1067 }
1068 $additionalConstraints = empty($addWhere) ? [] : [QueryHelper::stripLogicalOperatorPrefix($addWhere)];
1069 $selFieldList = GeneralUtility::trimExplode(',', $selFieldList, true);
1070
1071 // Create the SQL query for selecting the elements in the listing:
1072 // do not do paging when outputting as CSV
1073 if ($this->csvOutput) {
1074 $this->iLimit = 0;
1075 }
1076 if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
1077 // Get the two previous rows for sorting if displaying page > 1
1078 $this->firstElementNumber -= 2;
1079 $this->iLimit += 2;
1080 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1081 $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints);
1082 $this->firstElementNumber += 2;
1083 $this->iLimit -= 2;
1084 } else {
1085 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1086 $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints);
1087 }
1088
1089 // Finding the total amount of records on the page
1090 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
1091 $this->setTotalItems($table, $id, $additionalConstraints);
1092
1093 // Init:
1094 $queryResult = $queryBuilder->execute();
1095 $dbCount = 0;
1096 $out = '';
1097 $tableHeader = '';
1098 $listOnlyInSingleTableMode = $this->listOnlyInSingleTableMode && !$this->table;
1099 // If the count query returned any number of records, we perform the real query,
1100 // selecting records.
1101 if ($this->totalItems) {
1102 // Fetch records only if not in single table mode
1103 if ($listOnlyInSingleTableMode) {
1104 $dbCount = $this->totalItems;
1105 } else {
1106 // Set the showLimit to the number of records when outputting as CSV
1107 if ($this->csvOutput) {
1108 $this->showLimit = $this->totalItems;
1109 $this->iLimit = $this->totalItems;
1110 $dbCount = $this->totalItems;
1111 } else {
1112 if ($this->firstElementNumber + $this->showLimit <= $this->totalItems) {
1113 $dbCount = $this->showLimit + 2;
1114 } else {
1115 $dbCount = $this->totalItems - $this->firstElementNumber + 2;
1116 }
1117 }
1118 }
1119 }
1120 // If any records was selected, render the list:
1121 if ($dbCount) {
1122 // Use a custom table title for translated pages
1123 if ($table == 'pages' && $this->showOnlyTranslatedRecords) {
1124 $tableTitle = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:pageTranslation'));
1125 } else {
1126 $tableTitle = htmlspecialchars($lang->sL($GLOBALS['TCA'][$table]['ctrl']['title']));
1127 if ($tableTitle === '') {
1128 $tableTitle = $table;
1129 }
1130 }
1131 // Header line is drawn
1132 $theData = [];
1133 if ($this->disableSingleTableView) {
1134 $theData[$titleCol] = '<span class="c-table">' . BackendUtility::wrapInHelp($table, '', $tableTitle)
1135 . '</span> (<span class="t3js-table-total-items">' . $this->totalItems . '</span>)';
1136 } else {
1137 $icon = $this->table
1138 ? '<span title="' . htmlspecialchars($lang->getLL('contractView')) . '">' . $this->iconFactory->getIcon('actions-view-table-collapse', Icon::SIZE_SMALL)->render() . '</span>'
1139 : '<span title="' . htmlspecialchars($lang->getLL('expandView')) . '">' . $this->iconFactory->getIcon('actions-view-table-expand', Icon::SIZE_SMALL)->render() . '</span>';
1140 $theData[$titleCol] = $this->linkWrapTable($table, $tableTitle . ' (<span class="t3js-table-total-items">' . $this->totalItems . '</span>) ' . $icon);
1141 }
1142 if ($listOnlyInSingleTableMode) {
1143 $tableHeader .= BackendUtility::wrapInHelp($table, '', $theData[$titleCol]);
1144 } else {
1145 // Render collapse button if in multi table mode
1146 $collapseIcon = '';
1147 if (!$this->table) {
1148 $href = htmlspecialchars($this->listURL() . '&collapse[' . $table . ']=' . ($tableCollapsed ? '0' : '1'));
1149 $title = $tableCollapsed
1150 ? htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.expandTable'))
1151 : htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.collapseTable'));
1152 $icon = '<span class="collapseIcon">' . $this->iconFactory->getIcon(($tableCollapsed ? 'actions-view-list-expand' : 'actions-view-list-collapse'), Icon::SIZE_SMALL)->render() . '</span>';
1153 $collapseIcon = '<a href="' . $href . '" title="' . $title . '" class="pull-right t3js-toggle-recordlist" data-table="' . htmlspecialchars($table) . '" data-toggle="collapse" data-target="#recordlist-' . htmlspecialchars($table) . '">' . $icon . '</a>';
1154 }
1155 $tableHeader .= $theData[$titleCol] . $collapseIcon;
1156 }
1157 // Render table rows only if in multi table view or if in single table view
1158 $rowOutput = '';
1159 if (!$listOnlyInSingleTableMode || $this->table) {
1160 // Fixing an order table for sortby tables
1161 $this->currentTable = [];
1162 $currentIdList = [];
1163 $doSort = $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField;
1164 $prevUid = 0;
1165 $prevPrevUid = 0;
1166 // Get first two rows and initialize prevPrevUid and prevUid if on page > 1
1167 if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
1168 $row = $queryResult->fetch();
1169 $prevPrevUid = -((int)$row['uid']);
1170 $row = $queryResult->fetch();
1171 $prevUid = $row['uid'];
1172 }
1173 $accRows = [];
1174 // Accumulate rows here
1175 while ($row = $queryResult->fetch()) {
1176 if (!$this->isRowListingConditionFulfilled($table, $row)) {
1177 continue;
1178 }
1179 // In offline workspace, look for alternative record:
1180 BackendUtility::workspaceOL($table, $row, $backendUser->workspace, true);
1181 if (is_array($row)) {
1182 $accRows[] = $row;
1183 $currentIdList[] = $row['uid'];
1184 if ($doSort) {
1185 if ($prevUid) {
1186 $this->currentTable['prev'][$row['uid']] = $prevPrevUid;
1187 $this->currentTable['next'][$prevUid] = '-' . $row['uid'];
1188 $this->currentTable['prevUid'][$row['uid']] = $prevUid;
1189 }
1190 $prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid'];
1191 $prevUid = $row['uid'];
1192 }
1193 }
1194 }
1195 $this->totalRowCount = count($accRows);
1196 // CSV initiated
1197 if ($this->csvOutput) {
1198 $this->initCSV();
1199 }
1200 // Render items:
1201 $this->CBnames = [];
1202 $this->duplicateStack = [];
1203 $this->eCounter = $this->firstElementNumber;
1204 $cc = 0;
1205 foreach ($accRows as $row) {
1206 // Render item row if counter < limit
1207 if ($cc < $this->iLimit) {
1208 $cc++;
1209 $this->translations = false;
1210 $rowOutput .= $this->renderListRow($table, $row, $cc, $titleCol, $thumbsCol);
1211 // If no search happened it means that the selected
1212 // records are either default or All language and here we will not select translations
1213 // which point to the main record:
1214 if ($l10nEnabled && $this->searchString === '') {
1215 // For each available translation, render the record:
1216 if (is_array($this->translations)) {
1217 foreach ($this->translations as $lRow) {
1218 // $lRow isn't always what we want - if record was moved we've to work with the
1219 // placeholder records otherwise the list is messed up a bit
1220 if ($row['_MOVE_PLH_uid'] && $row['_MOVE_PLH_pid']) {
1221 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1222 ->getQueryBuilderForTable($table);
1223 $queryBuilder->getRestrictions()
1224 ->removeAll()
1225 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1226 $predicates = [
1227 $queryBuilder->expr()->eq(
1228 't3ver_move_id',
1229 $queryBuilder->createNamedParameter((int)$lRow['uid'], \PDO::PARAM_INT)
1230 ),
1231 $queryBuilder->expr()->eq(
1232 'pid',
1233 $queryBuilder->createNamedParameter((int)$row['_MOVE_PLH_pid'], \PDO::PARAM_INT)
1234 ),
1235 $queryBuilder->expr()->eq(
1236 't3ver_wsid',
1237 $queryBuilder->createNamedParameter((int)$row['t3ver_wsid'], \PDO::PARAM_INT)
1238 ),
1239 ];
1240
1241 $tmpRow = $queryBuilder
1242 ->select(...$selFieldList)
1243 ->from($table)
1244 ->andWhere(...$predicates)
1245 ->execute()
1246 ->fetch();
1247
1248 $lRow = is_array($tmpRow) ? $tmpRow : $lRow;
1249 }
1250 // In offline workspace, look for alternative record:
1251 BackendUtility::workspaceOL($table, $lRow, $backendUser->workspace, true);
1252 if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
1253 $currentIdList[] = $lRow['uid'];
1254 $rowOutput .= $this->renderListRow($table, $lRow, $cc, $titleCol, $thumbsCol, 18);
1255 }
1256 }
1257 }
1258 }
1259 }
1260 // Counter of total rows incremented:
1261 $this->eCounter++;
1262 }
1263 // Record navigation is added to the beginning and end of the table if in single
1264 // table mode
1265 if ($this->table) {
1266 $rowOutput = $this->renderListNavigation('top') . $rowOutput . $this->renderListNavigation('bottom');
1267 } else {
1268 // Show that there are more records than shown
1269 if ($this->totalItems > $this->itemsLimitPerTable) {
1270 $countOnFirstPage = $this->totalItems > $this->itemsLimitSingleTable ? $this->itemsLimitSingleTable : $this->totalItems;
1271 $hasMore = $this->totalItems > $this->itemsLimitSingleTable;
1272 $colspan = $this->showIcon ? count($this->fieldArray) + 1 : count($this->fieldArray);
1273 $rowOutput .= '<tr><td colspan="' . $colspan . '">
1274 <a href="' . htmlspecialchars($this->listURL() . '&table=' . rawurlencode($table)) . '" class="btn btn-default">'
1275 . '<span class="t3-icon fa fa-chevron-down"></span> <i>[1 - ' . $countOnFirstPage . ($hasMore ? '+' : '') . ']</i></a>
1276 </td></tr>';
1277 }
1278 }
1279 // The header row for the table is now created:
1280 $out .= $this->renderListHeader($table, $currentIdList);
1281 }
1282
1283 $collapseClass = $tableCollapsed && !$this->table ? 'collapse' : 'collapse in';
1284 $dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded';
1285
1286 // The list of records is added after the header:
1287 $out .= $rowOutput;
1288 // ... and it is all wrapped in a table:
1289 $out = '
1290
1291
1292
1293 <!--
1294 DB listing of elements: "' . htmlspecialchars($table) . '"
1295 -->
1296 <div class="panel panel-space panel-default recordlist">
1297 <div class="panel-heading">
1298 ' . $tableHeader . '
1299 </div>
1300 <div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($table) . '">
1301 <div class="table-fit">
1302 <table data-table="' . htmlspecialchars($table) . '" class="table table-striped table-hover' . ($listOnlyInSingleTableMode ? ' typo3-dblist-overview' : '') . '">
1303 ' . $out . '
1304 </table>
1305 </div>
1306 </div>
1307 </div>
1308 ';
1309 // Output csv if...
1310 // This ends the page with exit.
1311 if ($this->csvOutput) {
1312 $this->outputCSV($table);
1313 }
1314 }
1315 // Return content:
1316 return $out;
1317 }
1318
1319 /**
1320 * Get viewOnClick link for pages or tt_content records
1321 *
1322 * @param string $table
1323 * @param array $row
1324 *
1325 * @return string
1326 */
1327 protected function getOnClickForRow(string $table, array $row): string
1328 {
1329 if ($table === 'tt_content') {
1330 // Link to a content element, possibly translated and with anchor
1331 $additionalParams = '';
1332 $language = (int)$row[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']];
1333 if ($language > 0) {
1334 $additionalParams = '&L=' . $language;
1335 }
1336 $onClick = BackendUtility::viewOnClick(
1337 $this->id,
1338 '',
1339 null,
1340 '#c' . $row['uid'],
1341 '',
1342 $additionalParams
1343 );
1344 } elseif ($table === 'pages' && $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] > 0) {
1345 // Link to a page translation needs uid of default language page as id
1346 $onClick = BackendUtility::viewOnClick(
1347 $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']],
1348 '',
1349 null,
1350 '',
1351 '',
1352 '&L=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]
1353 );
1354 } else {
1355 // Link to a page in the default language
1356 $onClick = BackendUtility::viewOnClick($row['uid']);
1357 }
1358 return $onClick;
1359 }
1360
1361 /**
1362 * Check if all row listing conditions are fulfilled.
1363 *
1364 * This function serves as a dummy method to be overridden in extending classes.
1365 *
1366 * @param string $table Table name
1367 * @param string[] $row Record
1368 * @return bool True, if all conditions are fulfilled.
1369 */
1370 protected function isRowListingConditionFulfilled($table, $row)
1371 {
1372 return true;
1373 }
1374
1375 /**
1376 * Rendering a single row for the list
1377 *
1378 * @param string $table Table name
1379 * @param mixed[] $row Current record
1380 * @param int $cc Counter, counting for each time an element is rendered (used for alternating colors)
1381 * @param string $titleCol Table field (column) where header value is found
1382 * @param string $thumbsCol Table field (column) where (possible) thumbnails can be found
1383 * @param int $indent Indent from left.
1384 * @return string Table row for the element
1385 * @internal
1386 * @see getTable()
1387 */
1388 public function renderListRow($table, $row, $cc, $titleCol, $thumbsCol, $indent = 0)
1389 {
1390 if (!is_array($row)) {
1391 return '';
1392 }
1393 $rowOutput = '';
1394 $id_orig = null;
1395 // If in search mode, make sure the preview will show the correct page
1396 if ((string)$this->searchString !== '') {
1397 $id_orig = $this->id;
1398 $this->id = $row['pid'];
1399 }
1400
1401 $tagAttributes = [
1402 'class' => ['t3js-entity'],
1403 'data-table' => $table,
1404 'title' => 'id=' . $row['uid'],
1405 ];
1406
1407 // Add active class to record of current link
1408 if (
1409 isset($this->currentLink['tableNames'])
1410 && (int)$this->currentLink['uid'] === (int)$row['uid']
1411 && GeneralUtility::inList($this->currentLink['tableNames'], $table)
1412 ) {
1413 $tagAttributes['class'][] = 'active';
1414 }
1415 // Add special classes for first and last row
1416 if ($cc == 1 && $indent == 0) {
1417 $tagAttributes['class'][] = 'firstcol';
1418 }
1419 if ($cc == $this->totalRowCount || $cc == $this->iLimit) {
1420 $tagAttributes['class'][] = 'lastcol';
1421 }
1422 // Overriding with versions background color if any:
1423 if (!empty($row['_CSSCLASS'])) {
1424 $tagAttributes['class'] = [$row['_CSSCLASS']];
1425 }
1426 // Incr. counter.
1427 $this->counter++;
1428 // The icon with link
1429 $toolTip = BackendUtility::getRecordToolTip($row, $table);
1430 $additionalStyle = $indent ? ' style="margin-left: ' . $indent . 'px;"' : '';
1431 $iconImg = '<span ' . $toolTip . ' ' . $additionalStyle . '>'
1432 . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render()
1433 . '</span>';
1434 $theIcon = $this->clickMenuEnabled ? BackendUtility::wrapClickMenuOnIcon($iconImg, $table, $row['uid']) : $iconImg;
1435 // Preparing and getting the data-array
1436 $theData = [];
1437 $localizationMarkerClass = '';
1438 foreach ($this->fieldArray as $fCol) {
1439 if ($fCol == $titleCol) {
1440 $recTitle = BackendUtility::getRecordTitle($table, $row, false, true);
1441 $warning = '';
1442 // If the record is edit-locked by another user, we will show a little warning sign:
1443 $lockInfo = BackendUtility::isRecordLocked($table, $row['uid']);
1444 if ($lockInfo) {
1445 $warning = '<span data-toggle="tooltip" data-placement="right" data-title="' . htmlspecialchars($lockInfo['msg']) . '">'
1446 . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>';
1447 }
1448 $theData[$fCol] = $theData['__label'] = $warning . $this->linkWrapItems($table, $row['uid'], $recTitle, $row);
1449 // Render thumbnails, if:
1450 // - a thumbnail column exists
1451 // - there is content in it
1452 // - the thumbnail column is visible for the current type
1453 $type = 0;
1454 if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) {
1455 $typeColumn = $GLOBALS['TCA'][$table]['ctrl']['type'];
1456 $type = $row[$typeColumn];
1457 }
1458 // If current type doesn't exist, set it to 0 (or to 1 for historical reasons,
1459 // if 0 doesn't exist)
1460 if (!isset($GLOBALS['TCA'][$table]['types'][$type])) {
1461 $type = isset($GLOBALS['TCA'][$table]['types'][0]) ? 0 : 1;
1462 }
1463
1464 $visibleColumns = $this->getVisibleColumns($GLOBALS['TCA'][$table], $type);
1465
1466 if ($this->thumbs &&
1467 trim($row[$thumbsCol]) &&
1468 preg_match('/(^|(.*(;|,)?))' . $thumbsCol . '(((;|,).*)|$)/', $visibleColumns) === 1
1469 ) {
1470 $thumbCode = '<br />' . $this->thumbCode($row, $table, $thumbsCol);
1471 $theData[$fCol] .= $thumbCode;
1472 $theData['__label'] .= $thumbCode;
1473 }
1474 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])
1475 && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] != 0
1476 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0
1477 ) {
1478 // It's a translated record with a language parent
1479 $localizationMarkerClass = ' localization';
1480 }
1481 } elseif ($fCol === 'pid') {
1482 $theData[$fCol] = $row[$fCol];
1483 } elseif ($fCol === '_PATH_') {
1484 $theData[$fCol] = $this->recPath($row['pid']);
1485 } elseif ($fCol === '_REF_') {
1486 $theData[$fCol] = $this->createReferenceHtml($table, $row['uid']);
1487 } elseif ($fCol === '_CONTROL_') {
1488 $theData[$fCol] = $this->makeControl($table, $row);
1489 } elseif ($fCol === '_CLIPBOARD_') {
1490 $theData[$fCol] = $this->makeClip($table, $row);
1491 } elseif ($fCol === '_LOCALIZATION_') {
1492 list($lC1, $lC2) = $this->makeLocalizationPanel($table, $row);
1493 $theData[$fCol] = $lC1;
1494 $theData[$fCol . 'b'] = '<div class="btn-group">' . $lC2 . '</div>';
1495 } elseif ($fCol === '_LOCALIZATION_b') {
1496 // deliberately empty
1497 } else {
1498 $pageId = $table === 'pages' ? $row['uid'] : $row['pid'];
1499 $tmpProc = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 100, $row['uid'], true, $pageId);
1500 $theData[$fCol] = $this->linkUrlMail(htmlspecialchars($tmpProc), $row[$fCol]);
1501 if ($this->csvOutput) {
1502 $row[$fCol] = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 0, $row['uid']);
1503 }
1504 }
1505 }
1506 // Reset the ID if it was overwritten
1507 if ((string)$this->searchString !== '') {
1508 $this->id = $id_orig;
1509 }
1510 // Add row to CSV list:
1511 if ($this->csvOutput) {
1512 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['customizeCsvRow'] ?? [];
1513 if (!empty($hooks)) {
1514 $hookParameters = [
1515 'databaseRow' => &$row,
1516 'tableName' => $table,
1517 'pageId' => $this->id
1518 ];
1519 foreach ($hooks as $hookFunction) {
1520 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
1521 }
1522 }
1523 $this->addToCSV($row);
1524 }
1525 // Add classes to table cells
1526 $this->addElement_tdCssClass[$titleCol] = 'col-title col-responsive' . $localizationMarkerClass;
1527 $this->addElement_tdCssClass['__label'] = $this->addElement_tdCssClass[$titleCol];
1528 $this->addElement_tdCssClass['_CONTROL_'] = 'col-control';
1529 if ($this->getModule()->MOD_SETTINGS['clipBoard']) {
1530 $this->addElement_tdCssClass['_CLIPBOARD_'] = 'col-clipboard';
1531 }
1532 $this->addElement_tdCssClass['_PATH_'] = 'col-path';
1533 $this->addElement_tdCssClass['_LOCALIZATION_'] = 'col-localizationa';
1534 $this->addElement_tdCssClass['_LOCALIZATION_b'] = 'col-localizationb';
1535 // Create element in table cells:
1536 $theData['uid'] = $row['uid'];
1537 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])
1538 && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])
1539 ) {
1540 $theData['_l10nparent_'] = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1541 }
1542
1543 $tagAttributes = array_map(
1544 function ($attributeValue) {
1545 if (is_array($attributeValue)) {
1546 return implode(' ', $attributeValue);
1547 }
1548 return $attributeValue;
1549 },
1550 $tagAttributes
1551 );
1552
1553 $rowOutput .= $this->addElement(1, $theIcon, $theData, GeneralUtility::implodeAttributes($tagAttributes, true));
1554 // Finally, return table row element:
1555 return $rowOutput;
1556 }
1557
1558 /**
1559 * Gets the number of records referencing the record with the UID $uid in
1560 * the table $tableName.
1561 *
1562 * @param string $tableName
1563 * @param int $uid
1564 * @return int The number of references to record $uid in table
1565 */
1566 protected function getReferenceCount($tableName, $uid)
1567 {
1568 if (!isset($this->referenceCount[$tableName][$uid])) {
1569 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1570 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid);
1571 $this->referenceCount[$tableName][$uid] = $numberOfReferences;
1572 }
1573 return $this->referenceCount[$tableName][$uid];
1574 }
1575
1576 /**
1577 * Rendering the header row for a table
1578 *
1579 * @param string $table Table name
1580 * @param int[] $currentIdList Array of the currently displayed uids of the table
1581 * @throws \UnexpectedValueException
1582 * @return string Header table row
1583 * @internal
1584 * @see getTable()
1585 */
1586 public function renderListHeader($table, $currentIdList)
1587 {
1588 $lang = $this->getLanguageService();
1589 // Init:
1590 $theData = [];
1591 $icon = '';
1592 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1593 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1594 // Traverse the fields:
1595 foreach ($this->fieldArray as $fCol) {
1596 // Calculate users permissions to edit records in the table:
1597 $permsEdit = $this->calcPerms & ($table === 'pages' ? 2 : 16) && $this->overlayEditLockPermissions($table);
1598 switch ((string)$fCol) {
1599 case '_PATH_':
1600 // Path
1601 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_')) . ']</i>';
1602 break;
1603 case '_REF_':
1604 // References
1605 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c__REF_')) . ']</i>';
1606 break;
1607 case '_LOCALIZATION_':
1608 // Path
1609 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._LOCALIZATION_')) . ']</i>';
1610 break;
1611 case '_LOCALIZATION_b':
1612 // Path
1613 $theData[$fCol] = htmlspecialchars($lang->getLL('Localize'));
1614 break;
1615 case '_CLIPBOARD_':
1616 if (!$this->getModule()->MOD_SETTINGS['clipBoard']) {
1617 break;
1618 }
1619 // Clipboard:
1620 $cells = [];
1621 // If there are elements on the clipboard for this table, and the parent page is not locked by editlock
1622 // then display the "paste into" icon:
1623 $elFromTable = $this->clipObj->elFromTable($table);
1624 if (!empty($elFromTable) && $this->overlayEditLockPermissions($table)) {
1625 $href = htmlspecialchars($this->clipObj->pasteUrl($table, $this->id));
1626 $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable);
1627 $cells['pasteAfter'] = '<a class="btn btn-default t3js-modal-trigger"'
1628 . ' href="' . $href . '"'
1629 . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
1630 . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"'
1631 . ' data-content="' . htmlspecialchars($confirmMessage) . '"'
1632 . ' data-severity="warning">'
1633 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render()
1634 . '</a>';
1635 }
1636 // If the numeric clipboard pads are enabled, display the control icons for that:
1637 if ($this->clipObj->current !== 'normal') {
1638 // The "select" link:
1639 $spriteIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render();
1640 $cells['copyMarked'] = $this->linkClipboardHeaderIcon($spriteIcon, $table, 'setCB', '', $lang->getLL('clip_selectMarked'));
1641 // The "edit marked" link:
1642 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1643 . '&edit[' . $table . '][{entityIdentifiers:editList}]=edit'
1644 . '&returnUrl={T3_THIS_LOCATION}';
1645 $cells['edit'] = '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1646 . ' data-uri="' . htmlspecialchars($editUri) . '"'
1647 . ' title="' . htmlspecialchars($lang->getLL('clip_editMarked')) . '">'
1648 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1649 // The "Delete marked" link:
1650 $cells['delete'] = $this->linkClipboardHeaderIcon(
1651 $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(),
1652 $table,
1653 'delete',
1654 sprintf($lang->getLL('clip_deleteMarkedWarning'), $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
1655 $lang->getLL('clip_deleteMarked')
1656 );
1657 // The "Select all" link:
1658 $onClick = htmlspecialchars('checkOffCB(' . GeneralUtility::quoteJSvalue(implode(',', $this->CBnames)) . ', this); return false;');
1659 $cells['markAll'] = '<a class="btn btn-default" rel="" href="#" onclick="' . $onClick . '" title="'
1660 . htmlspecialchars($lang->getLL('clip_markRecords')) . '">'
1661 . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
1662 } else {
1663 $cells['empty'] = '';
1664 }
1665 /*
1666 * hook: renderListHeaderActions: Allows to change the clipboard icons of the Web>List table headers
1667 * usage: Above each listed table in Web>List a header row is shown.
1668 * This hook allows to modify the icons responsible for the clipboard functions
1669 * (shown above the clipboard checkboxes when a clipboard other than "Normal" is selected),
1670 * or other "Action" functions which perform operations on the listed records.
1671 */
1672 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
1673 $hookObject = GeneralUtility::makeInstance($className);
1674 if (!$hookObject instanceof RecordListHookInterface) {
1675 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567850);
1676 }
1677 $cells = $hookObject->renderListHeaderActions($table, $currentIdList, $cells, $this);
1678 }
1679 $theData[$fCol] = '';
1680 if (isset($cells['edit']) && isset($cells['delete'])) {
1681 $theData[$fCol] .= '<div class="btn-group" role="group">' . $cells['edit'] . $cells['delete'] . '</div>';
1682 unset($cells['edit'], $cells['delete']);
1683 }
1684 $theData[$fCol] .= '<div class="btn-group" role="group">' . implode('', $cells) . '</div>';
1685 break;
1686 case '_CONTROL_':
1687 // Control panel:
1688 if ($this->isEditable($table)) {
1689 // If new records can be created on this page, add links:
1690 $permsAdditional = ($table === 'pages' ? 8 : 16);
1691 if ($this->calcPerms & $permsAdditional && $this->showNewRecLink($table)) {
1692 $spriteIcon = $table === 'pages'
1693 ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL)
1694 : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL);
1695 if ($table === 'tt_content') {
1696 // If mod.newContentElementWizard.override is set, use that extension's create new content wizard instead:
1697 $newContentElementWizard = BackendUtility::getPagesTSconfig($this->pageinfo['uid'])['mod.']['newContentElementWizard.']['override']
1698 ?? 'new_content_element_wizard';
1699 $url = (string)$uriBuilder->buildUriFromRoute(
1700 $newContentElementWizard,
1701 [
1702 'id' => $this->id,
1703 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
1704 ]
1705 );
1706 $icon = '<a href="#" data-url="' . htmlspecialchars($url) . '" '
1707 . 'data-title="' . htmlspecialchars($lang->getLL('new')) . '"'
1708 . 'class="btn btn-default t3js-toggle-new-content-element-wizard">'
1709 . $spriteIcon->render()
1710 . '</a>';
1711 } elseif ($table === 'pages') {
1712 $parameters = ['id' => $this->id, 'pagesOnly' => 1, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')];
1713 $href = (string)$uriBuilder->buildUriFromRoute('db_new', $parameters);
1714 $icon = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="' . htmlspecialchars($lang->getLL('new')) . '">'
1715 . $spriteIcon->render() . '</a>';
1716 } else {
1717 $params = '&edit[' . $table . '][' . $this->id . ']=new';
1718 if ($table === 'pages') {
1719 $params .= '&overrideVals[pages][doktype]=' . (int)$this->pageRow['doktype'];
1720 }
1721 $icon = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
1722 . '" title="' . htmlspecialchars($lang->getLL('new')) . '">' . $spriteIcon->render() . '</a>';
1723 }
1724 }
1725 // If the table can be edited, add link for editing ALL SHOWN fields for all listed records:
1726 if ($permsEdit && $this->table && is_array($currentIdList)) {
1727 $entityIdentifiers = 'entityIdentifiers';
1728 if ($this->clipNumPane()) {
1729 $entityIdentifiers .= ':editList';
1730 }
1731 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1732 . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit'
1733 . '&columnsOnly=' . implode(',', $this->fieldArray)
1734 . '&returnUrl={T3_THIS_LOCATION}';
1735 $icon .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1736 . ' data-uri="' . htmlspecialchars($editUri) . '"'
1737 . ' title="' . htmlspecialchars($lang->getLL('editShownColumns')) . '">'
1738 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1739 $icon = '<div class="btn-group" role="group">' . $icon . '</div>';
1740 }
1741 // Add an empty entry, so column count fits again after moving this into $icon
1742 $theData[$fCol] = '&nbsp;';
1743 }
1744 break;
1745 default:
1746 // Regular fields header:
1747 $theData[$fCol] = '';
1748
1749 // Check if $fCol is really a field and get the label and remove the colons
1750 // at the end
1751 $sortLabel = BackendUtility::getItemLabel($table, $fCol);
1752 if ($sortLabel !== null) {
1753 $sortLabel = htmlspecialchars($lang->sL($sortLabel));
1754 $sortLabel = rtrim(trim($sortLabel), ':');
1755 } else {
1756 // No TCA field, only output the $fCol variable with square brackets []
1757 $sortLabel = htmlspecialchars($fCol);
1758 $sortLabel = '<i>[' . rtrim(trim($sortLabel), ':') . ']</i>';
1759 }
1760
1761 if ($this->table && is_array($currentIdList)) {
1762 // If the numeric clipboard pads are selected, show duplicate sorting link:
1763 if ($this->clipNumPane()) {
1764 $theData[$fCol] .= '<a class="btn btn-default" href="' . htmlspecialchars($this->listURL('', '-1') . '&duplicateField=' . $fCol)
1765 . '" title="' . htmlspecialchars($lang->getLL('clip_duplicates')) . '">'
1766 . $this->iconFactory->getIcon('actions-document-duplicates-select', Icon::SIZE_SMALL)->render() . '</a>';
1767 }
1768 // If the table can be edited, add link for editing THIS field for all
1769 // listed records:
1770 if ($this->isEditable($table) && $permsEdit && $GLOBALS['TCA'][$table]['columns'][$fCol]) {
1771 $entityIdentifiers = 'entityIdentifiers';
1772 if ($this->clipNumPane()) {
1773 $entityIdentifiers .= ':editList';
1774 }
1775 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit')
1776 . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit'
1777 . '&columnsOnly=' . $fCol
1778 . '&returnUrl={T3_THIS_LOCATION}';
1779 $iTitle = sprintf($lang->getLL('editThisColumn'), $sortLabel);
1780 $theData[$fCol] .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"'
1781 . ' data-uri="' . htmlspecialchars($editUri) . '"'
1782 . ' title="' . htmlspecialchars($iTitle) . '">'
1783 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1784 }
1785 if (strlen($theData[$fCol]) > 0) {
1786 $theData[$fCol] = '<div class="btn-group" role="group">' . $theData[$fCol] . '</div> ';
1787 }
1788 }
1789 $theData[$fCol] .= $this->addSortLink($sortLabel, $fCol, $table);
1790 }
1791 }
1792 /*
1793 * hook: renderListHeader: Allows to change the contents of columns/cells of the Web>List table headers
1794 * usage: Above each listed table in Web>List a header row is shown.
1795 * Containing the labels of all shown fields and additional icons to create new records for this
1796 * table or perform special clipboard tasks like mark and copy all listed records to clipboard, etc.
1797 */
1798 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
1799 $hookObject = GeneralUtility::makeInstance($className);
1800 if (!$hookObject instanceof RecordListHookInterface) {
1801 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567855);
1802 }
1803 $theData = $hookObject->renderListHeader($table, $currentIdList, $theData, $this);
1804 }
1805
1806 // Create and return header table row:
1807 return '<thead>' . $this->addElement(1, $icon, $theData, '', '', '', 'th') . '</thead>';
1808 }
1809
1810 /**
1811 * Get pointer for first element on the page
1812 *
1813 * @param int $page Page number starting with 1
1814 * @return int Pointer to first element on the page (starting with 0)
1815 */
1816 protected function getPointerForPage($page)
1817 {
1818 return ($page - 1) * $this->iLimit;
1819 }
1820
1821 /**
1822 * Creates a page browser for tables with many records
1823 *
1824 * @param string $renderPart Distinguish between 'top' and 'bottom' part of the navigation (above or below the records)
1825 * @return string Navigation HTML
1826 */
1827 protected function renderListNavigation($renderPart = 'top')
1828 {
1829 $totalPages = ceil($this->totalItems / $this->iLimit);
1830 // Show page selector if not all records fit into one page
1831 if ($totalPages <= 1) {
1832 return '';
1833 }
1834 $content = '';
1835 $listURL = $this->listURL('', $this->table, 'firstElementNumber');
1836 // 1 = first page
1837 // 0 = first element
1838 $currentPage = floor($this->firstElementNumber / $this->iLimit) + 1;
1839 // Compile first, previous, next, last and refresh buttons
1840 if ($currentPage > 1) {
1841 $labelFirst = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:first'));
1842 $labelPrevious = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:previous'));
1843 $first = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage(1) . '" title="' . $labelFirst . '">'
1844 . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</a></li>';
1845 $previous = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage - 1) . '" title="' . $labelPrevious . '">'
1846 . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</a></li>';
1847 } else {
1848 $first = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</span></li>';
1849 $previous = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</span></li>';
1850 }
1851 if ($currentPage < $totalPages) {
1852 $labelNext = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:next'));
1853 $labelLast = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:last'));
1854 $next = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage + 1) . '" title="' . $labelNext . '">'
1855 . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</a></li>';
1856 $last = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($totalPages) . '" title="' . $labelLast . '">'
1857 . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</a></li>';
1858 } else {
1859 $next = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</span></li>';
1860 $last = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</span></li>';
1861 }
1862 $reload = '<li><a href="#" onclick="document.dblistForm.action=' . GeneralUtility::quoteJSvalue($listURL
1863 . '&pointer=') . '+calculatePointer(document.getElementById(' . GeneralUtility::quoteJSvalue('jumpPage-' . $renderPart)
1864 . ').value); document.dblistForm.submit(); return true;" title="'
1865 . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:reload')) . '">'
1866 . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a></li>';
1867 if ($renderPart === 'top') {
1868 // Add js to traverse a page select input to a pointer value
1869 $content = '
1870 <script type="text/javascript">
1871 /*<![CDATA[*/
1872 function calculatePointer(page) {
1873 if (page > ' . $totalPages . ') {
1874 page = ' . $totalPages . ';
1875 }
1876 if (page < 1) {
1877 page = 1;
1878 }
1879 return (page - 1) * ' . $this->iLimit . ';
1880 }
1881 /*]]>*/
1882 </script>
1883 ';
1884 }
1885 $pageNumberInput = '
1886 <input type="number" min="1" max="' . $totalPages . '" value="' . $currentPage . '" size="3" class="form-control input-sm paginator-input" id="jumpPage-' . $renderPart . '" name="jumpPage-'
1887 . $renderPart . '" onkeyup="if (event.keyCode == 13) { document.dblistForm.action=' . htmlspecialchars(GeneralUtility::quoteJSvalue($listURL . '&pointer='))
1888 . '+calculatePointer(this.value); document.dblistForm.submit(); } return true;" />
1889 ';
1890 $pageIndicatorText = sprintf(
1891 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:pageIndicator'),
1892 $pageNumberInput,
1893 $totalPages
1894 );
1895 $pageIndicator = '<li><span>' . $pageIndicatorText . '</span></li>';
1896 if ($this->totalItems > $this->firstElementNumber + $this->iLimit) {
1897 $lastElementNumber = $this->firstElementNumber + $this->iLimit;
1898 } else {
1899 $lastElementNumber = $this->totalItems;
1900 }
1901 $rangeIndicator = '<li><span>' . sprintf(
1902 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:rangeIndicator'),
1903 $this->firstElementNumber + 1,
1904 $lastElementNumber
1905 ) . '</span></li>';
1906
1907 $titleColumn = $this->fieldArray[0];
1908 $data = [
1909 $titleColumn => $content . '
1910 <nav class="pagination-wrap">
1911 <ul class="pagination pagination-block">
1912 ' . $first . '
1913 ' . $previous . '
1914 ' . $rangeIndicator . '
1915 ' . $pageIndicator . '
1916 ' . $next . '
1917 ' . $last . '
1918 ' . $reload . '
1919 </ul>
1920 </nav>
1921 '
1922 ];
1923 return $this->addElement(1, '', $data);
1924 }
1925
1926 /*********************************
1927 *
1928 * Rendering of various elements
1929 *
1930 *********************************/
1931
1932 /**
1933 * Creates the control panel for a single record in the listing.
1934 *
1935 * @param string $table The table
1936 * @param mixed[] $row The record for which to make the control panel.
1937 * @throws \UnexpectedValueException
1938 * @return string HTML table with the control panel (unless disabled)
1939 */
1940 public function makeControl($table, $row)
1941 {
1942 $backendUser = $this->getBackendUserAuthentication();
1943 $userTsConfig = $backendUser->getTSConfig();
1944 $module = $this->getModule();
1945 $rowUid = $row['uid'];
1946 if (ExtensionManagementUtility::isLoaded('workspaces') && isset($row['_ORIG_uid'])) {
1947 $rowUid = $row['_ORIG_uid'];
1948 }
1949 $cells = [
1950 'primary' => [],
1951 'secondary' => []
1952 ];
1953 // Enables to hide the move elements for localized records - doesn't make much sense to perform these options for them
1954 // For page translations these icons should never be shown
1955 $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0;
1956 // If the listed table is 'pages' we have to request the permission settings for each page:
1957 $localCalcPerms = 0;
1958 if ($table === 'pages') {
1959 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
1960 }
1961 $permsEdit = $table === 'pages'
1962 && $backendUser->checkLanguageAccess(0)
1963 && $localCalcPerms & Permission::PAGE_EDIT
1964 || $table !== 'pages'
1965 && $this->calcPerms & Permission::CONTENT_EDIT
1966 && $backendUser->recordEditAccessInternals($table, $row);
1967 $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit);
1968 // "Show" link (only pages and tt_content elements)
1969 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
1970 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
1971
1972 if ($table === 'pages' || $table === 'tt_content') {
1973 $onClick = $this->getOnClickForRow($table, $row);
1974 $viewAction = '<a class="btn btn-default" href="#" onclick="'
1975 . htmlspecialchars(
1976 $onClick
1977 ) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">';
1978 if ($table === 'pages') {
1979 $viewAction .= $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render();
1980 } else {
1981 $viewAction .= $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render();
1982 }
1983 $viewAction .= '</a>';
1984 $this->addActionToCellGroup($cells, $viewAction, 'view');
1985 }
1986 // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
1987 if ($permsEdit) {
1988 $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit';
1989 $iconIdentifier = 'actions-open';
1990 if ($table === 'pages') {
1991 // Disallow manual adjustment of the language field for pages
1992 $params .= '&overrideVals[pages][sys_language_uid]=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
1993 $iconIdentifier = 'actions-page-open';
1994 }
1995 $overlayIdentifier = !$this->isEditable($table) ? 'overlay-readonly' : null;
1996 $editAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
1997 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL, $overlayIdentifier)->render() . '</a>';
1998 } else {
1999 $editAction = $this->spaceIcon;
2000 }
2001 $this->addActionToCellGroup($cells, $editAction, 'edit');
2002 // "Info": (All records)
2003 $onClick = 'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;';
2004 $viewBigAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('showInfo')) . '">'
2005 . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>';
2006 $this->addActionToCellGroup($cells, $viewBigAction, 'viewBig');
2007 // "Move" wizard link for pages/tt_content elements:
2008 if ($permsEdit && ($table === 'tt_content' || $table === 'pages')) {
2009 if ($isL10nOverlay) {
2010 $moveAction = $this->spaceIcon;
2011 } else {
2012 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('move_element') . '&table=' . $table . '&uid=' . $row['uid']) . ');';
2013 $linkTitleLL = htmlspecialchars($this->getLanguageService()->getLL('move_' . ($table === 'tt_content' ? 'record' : 'page')));
2014 $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-move', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-document-move', Icon::SIZE_SMALL));
2015 $moveAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $linkTitleLL . '">' . $icon->render() . '</a>';
2016 }
2017 $this->addActionToCellGroup($cells, $moveAction, 'move');
2018 }
2019 // If the table is NOT a read-only table, then show these links:
2020 if ($this->isEditable($table)) {
2021 // "Revert" link (history/undo)
2022 if ((bool)\trim($userTsConfig['options.']['showHistory.'][$table] ?? $userTsConfig['options.']['showHistory'] ?? '1')) {
2023 $moduleUrl = (string)$uriBuilder->buildUriFromRoute('record_history', ['element' => $table . ':' . $row['uid']]);
2024 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue($moduleUrl) . ',\'#latest\');';
2025 $historyAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="'
2026 . htmlspecialchars($this->getLanguageService()->getLL('history')) . '">'
2027 . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render() . '</a>';
2028 $this->addActionToCellGroup($cells, $historyAction, 'history');
2029 }
2030 // "Edit Perms" link:
2031 if ($table === 'pages' && $backendUser->check('modules', 'system_BeuserTxPermission') && ExtensionManagementUtility::isLoaded('beuser')) {
2032 if ($isL10nOverlay) {
2033 $permsAction = $this->spaceIcon;
2034 } else {
2035 $href = (string)$uriBuilder->buildUriFromRoute('system_BeuserTxPermission') . '&id=' . $row['uid'] . '&tx_beuser_system_beusertxpermission[action]=edit' . $this->makeReturnUrl();
2036 $permsAction = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="'
2037 . htmlspecialchars($this->getLanguageService()->getLL('permissions')) . '">'
2038 . $this->iconFactory->getIcon('actions-lock', Icon::SIZE_SMALL)->render() . '</a>';
2039 }
2040 $this->addActionToCellGroup($cells, $permsAction, 'perms');
2041 }
2042 // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row
2043 // or if default values can depend on previous record):
2044 if (($GLOBALS['TCA'][$table]['ctrl']['sortby'] || $GLOBALS['TCA'][$table]['ctrl']['useColumnsForDefaultValues']) && $permsEdit) {
2045 if ($table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT || $table === 'pages' && $this->calcPerms & Permission::PAGE_NEW) {
2046 if ($table === 'pages' && $isL10nOverlay) {
2047 $this->addActionToCellGroup($cells, $this->spaceIcon, 'new');
2048 } elseif ($this->showNewRecLink($table)) {
2049 $params = '&edit[' . $table . '][' . -($row['_MOVE_PLH'] ? $row['_MOVE_PLH_uid'] : $row['uid']) . ']=new';
2050 $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL));
2051 $titleLabel = 'new';
2052 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
2053 $titleLabel .= ($table === 'pages' ? 'Page' : 'Record');
2054 }
2055 $newAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1))
2056 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($titleLabel)) . '">'
2057 . $icon->render() . '</a>';
2058 $this->addActionToCellGroup($cells, $newAction, 'new');
2059 }
2060 }
2061 }
2062 // "Up/Down" links
2063 if ($permsEdit && $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField && !$this->searchLevels) {
2064 if (!$isL10nOverlay && isset($this->currentTable['prev'][$row['uid']])) {
2065 // Up
2066 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prev'][$row['uid']];
2067 $moveUpAction = '<a class="btn btn-default" href="#" onclick="'
2068 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2069 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">'
2070 . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>';
2071 } else {
2072 $moveUpAction = $this->spaceIcon;
2073 }
2074 $this->addActionToCellGroup($cells, $moveUpAction, 'moveUp');
2075
2076 if (!$isL10nOverlay && $this->currentTable['next'][$row['uid']]) {
2077 // Down
2078 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['next'][$row['uid']];
2079 $moveDownAction = '<a class="btn btn-default" href="#" onclick="'
2080 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2081 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">'
2082 . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>';
2083 } else {
2084 $moveDownAction = $this->spaceIcon;
2085 }
2086 $this->addActionToCellGroup($cells, $moveDownAction, 'moveDown');
2087 }
2088 // "Hide/Unhide" links:
2089 $hiddenField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2090
2091 if (
2092 !empty($GLOBALS['TCA'][$table]['columns'][$hiddenField])
2093 && (empty($GLOBALS['TCA'][$table]['columns'][$hiddenField]['exclude'])
2094 || $backendUser->check('non_exclude_fields', $table . ':' . $hiddenField))
2095 ) {
2096 if (!$permsEdit || $this->isRecordCurrentBackendUser($table, $row)) {
2097 $hideAction = $this->spaceIcon;
2098 } else {
2099 $hideTitle = htmlspecialchars($this->getLanguageService()->getLL('hide' . ($table === 'pages' ? 'Page' : '')));
2100 $unhideTitle = htmlspecialchars($this->getLanguageService()->getLL('unHide' . ($table === 'pages' ? 'Page' : '')));
2101 if ($row[$hiddenField]) {
2102 $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=0';
2103 $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="hidden" href="#"'
2104 . ' data-params="' . htmlspecialchars($params) . '"'
2105 . ' title="' . $unhideTitle . '"'
2106 . ' data-toggle-title="' . $hideTitle . '">'
2107 . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '</a>';
2108 } else {
2109 $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=1';
2110 $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="visible" href="#"'
2111 . ' data-params="' . htmlspecialchars($params) . '"'
2112 . ' title="' . $hideTitle . '"'
2113 . ' data-toggle-title="' . $unhideTitle . '">'
2114 . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '</a>';
2115 }
2116 }
2117 $this->addActionToCellGroup($cells, $hideAction, 'hide');
2118 }
2119 // "Delete" link:
2120 $disableDelete = (bool)\trim($userTsConfig['options.']['disableDelete.'][$table] ?? $userTsConfig['options.']['disableDelete'] ?? '0');
2121 if ($permsEdit && !$disableDelete && ($table === 'pages' && $localCalcPerms & Permission::PAGE_DELETE || $table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT)) {
2122 // Check if the record version is in "deleted" state, because that will switch the action to "restore"
2123 if ($backendUser->workspace > 0 && isset($row['t3ver_state']) && (int)$row['t3ver_state'] === 2) {
2124 $actionName = 'restore';
2125 $refCountMsg = '';
2126 } else {
2127 $actionName = 'delete';
2128 $refCountMsg = BackendUtility::referenceCount(
2129 $table,
2130 $row['uid'],
2131 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
2132 $this->getReferenceCount($table, $row['uid'])
2133 ) . BackendUtility::translationCount(
2134 $table,
2135 $row['uid'],
2136 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
2137 );
2138 }
2139
2140 if ($this->isRecordCurrentBackendUser($table, $row)) {
2141 $deleteAction = $this->spaceIcon;
2142 } else {
2143 $title = BackendUtility::getRecordTitle($table, $row);
2144 $warningText = $this->getLanguageService()->getLL($actionName . 'Warning') . ' "' . $title . '" ' . '[' . $table . ':' . $row['uid'] . ']' . $refCountMsg;
2145
2146 $params = 'cmd[' . $table . '][' . $row['uid'] . '][delete]=1';
2147 $icon = $this->iconFactory->getIcon('actions-edit-' . $actionName, Icon::SIZE_SMALL)->render();
2148 $linkTitle = htmlspecialchars($this->getLanguageService()->getLL($actionName));
2149 $l10nParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '';
2150 $deleteAction = '<a class="btn btn-default t3js-record-delete" href="#" '
2151 . ' data-l10parent="' . ($l10nParentField ? htmlspecialchars($row[$l10nParentField]) : '') . '"'
2152 . ' data-params="' . htmlspecialchars($params) . '" data-title="' . htmlspecialchars($title) . '"'
2153 . ' data-message="' . htmlspecialchars($warningText) . '" title="' . $linkTitle . '"'
2154 . '>' . $icon . '</a>';
2155 }
2156 } else {
2157 $deleteAction = $this->spaceIcon;
2158 }
2159 $this->addActionToCellGroup($cells, $deleteAction, 'delete');
2160 // "Levels" links: Moving pages into new levels...
2161 if ($permsEdit && $table === 'pages' && !$this->searchLevels) {
2162 // Up (Paste as the page right after the current parent page)
2163 if ($this->calcPerms & Permission::PAGE_NEW) {
2164 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . -$this->id;
2165 $moveLeftAction = '<a class="btn btn-default" href="#" onclick="'
2166 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2167 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('prevLevel')) . '">'
2168 . $this->iconFactory->getIcon('actions-move-left', Icon::SIZE_SMALL)->render() . '</a>';
2169 $this->addActionToCellGroup($cells, $isL10nOverlay ? $this->spaceIcon : $moveLeftAction, 'moveLeft');
2170 }
2171 // Down (Paste as subpage to the page right above)
2172 if (!$isL10nOverlay && $this->currentTable['prevUid'][$row['uid']]) {
2173 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $this->currentTable['prevUid'][$row['uid']]));
2174 if ($localCalcPerms & Permission::PAGE_NEW) {
2175 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prevUid'][$row['uid']];
2176 $moveRightAction = '<a class="btn btn-default" href="#" onclick="'
2177 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');')
2178 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('nextLevel')) . '">'
2179 . $this->iconFactory->getIcon('actions-move-right', Icon::SIZE_SMALL)->render() . '</a>';
2180 } else {
2181 $moveRightAction = $this->spaceIcon;
2182 }
2183 } else {
2184 $moveRightAction = $this->spaceIcon;
2185 }
2186 $this->addActionToCellGroup($cells, $moveRightAction, 'moveRight');
2187 }
2188 }
2189 /*
2190 * hook: recStatInfoHooks: Allows to insert HTML before record icons on various places
2191 */
2192 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [];
2193 if (!empty($hooks)) {
2194 $stat = '';
2195 $_params = [$table, $row['uid']];
2196 foreach ($hooks as $_funcRef) {
2197 $stat .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2198 }
2199 $this->addActionToCellGroup($cells, $stat, 'stat');
2200 }
2201 /*
2202 * hook: makeControl: Allows to change control icons of records in list-module
2203 * usage: This hook method gets passed the current $cells array as third parameter.
2204 * This array contains values for the icons/actions generated for each record in Web>List.
2205 * Each array entry is accessible by an index-key.
2206 * The order of the icons is depending on the order of those array entries.
2207 */
2208 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? false)) {
2209 // for compatibility reason, we move all icons to the rootlevel
2210 // before calling the hooks
2211 foreach ($cells as $section => $actions) {
2212 foreach ($actions as $actionKey => $action) {
2213 $cells[$actionKey] = $action;
2214 }
2215 }
2216 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] as $className) {
2217 $hookObject = GeneralUtility::makeInstance($className);
2218 if (!$hookObject instanceof RecordListHookInterface) {
2219 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567840);
2220 }
2221 $cells = $hookObject->makeControl($table, $row, $cells, $this);
2222 }
2223 // now sort icons again into primary and secondary sections
2224 // after all hooks are processed
2225 $hookCells = $cells;
2226 foreach ($hookCells as $key => $value) {
2227 if ($key === 'primary' || $key === 'secondary') {
2228 continue;
2229 }
2230 $this->addActionToCellGroup($cells, $value, $key);
2231 }
2232 }
2233 $output = '<!-- CONTROL PANEL: ' . $table . ':' . $row['uid'] . ' -->';
2234 foreach ($cells as $classification => $actions) {
2235 $visibilityClass = ($classification !== 'primary' && !$module->MOD_SETTINGS['bigControlPanel'] ? 'collapsed' : 'expanded');
2236 if ($visibilityClass === 'collapsed') {
2237 $cellOutput = '';
2238 foreach ($actions as $action) {
2239 $cellOutput .= $action;
2240 }
2241 $output .= ' <div class="btn-group">' .
2242 '<span id="actions_' . $table . '_' . $row['uid'] . '" class="btn-group collapse collapse-horizontal width">' . $cellOutput . '</span>' .
2243 '<a href="#actions_' . $table . '_' . $row['uid'] . '" class="btn btn-default collapsed" data-toggle="collapse" aria-expanded="false"><span class="t3-icon fa fa-ellipsis-h"></span></a>' .
2244 '</div>';
2245 } else {
2246 $output .= ' <div class="btn-group" role="group">' . implode('', $actions) . '</div>';
2247 }
2248 }
2249 return $output;
2250 }
2251
2252 /**
2253 * Creates the clipboard panel for a single record in the listing.
2254 *
2255 * @param string $table The table
2256 * @param mixed[] $row The record for which to make the clipboard panel.
2257 * @throws \UnexpectedValueException
2258 * @return string HTML table with the clipboard panel (unless disabled)
2259 */
2260 public function makeClip($table, $row)
2261 {
2262 // Return blank, if disabled:
2263 if (!$this->getModule()->MOD_SETTINGS['clipBoard']) {
2264 return '';
2265 }
2266 $cells = [];
2267 $cells['pasteAfter'] = ($cells['pasteInto'] = $this->spaceIcon);
2268 // Enables to hide the copy, cut and paste icons for localized records - doesn't make much sense to perform these options for them
2269 // For page translations these icons should never be shown
2270 $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0;
2271 // Return blank, if disabled:
2272 // Whether a numeric clipboard pad is active or the normal pad we will see different content of the panel:
2273 // For the "Normal" pad:
2274 if ($this->clipObj->current === 'normal') {
2275 // Show copy/cut icons:
2276 $isSel = (string)$this->clipObj->isSelected($table, $row['uid']);
2277 if ($isL10nOverlay || !$this->overlayEditLockPermissions($table, $row)) {
2278 $cells['copy'] = $this->spaceIcon;
2279 $cells['cut'] = $this->spaceIcon;
2280 } else {
2281 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL);
2282 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut', Icon::SIZE_SMALL);
2283
2284 if ($isSel === 'copy') {
2285 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy-release', Icon::SIZE_SMALL);
2286 } elseif ($isSel === 'cut') {
2287 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut-release', Icon::SIZE_SMALL);
2288 }
2289
2290 $cells['copy'] = '<a class="btn btn-default" href="#" onclick="'
2291 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2292 $table,
2293 $row['uid'],
2294 1,
2295 $isSel === 'copy',
2296 ['returnUrl' => '']
2297 )) . ');')
2298 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy')) . '">'
2299 . $copyIcon->render() . '</a>';
2300
2301 // Check permission to cut page or content
2302 if ($table === 'pages') {
2303 $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
2304 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
2305 } else {
2306 $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT;
2307 }
2308 $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit);
2309
2310 // If the listed table is 'pages' we have to request the permission settings for each page:
2311 if ($table === 'pages') {
2312 if ($permsEdit) {
2313 $cells['cut'] = '<a class="btn btn-default" href="#" onclick="'
2314 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2315 $table,
2316 $row['uid'],
2317 0,
2318 $isSel === 'cut',
2319 ['returnUrl' => '']
2320 )) . ');')
2321 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">'
2322 . $cutIcon->render() . '</a>';
2323 } else {
2324 $cells['cut'] = $this->spaceIcon;
2325 }
2326 } else {
2327 if ($this->calcPerms & Permission::CONTENT_EDIT) {
2328 $cells['cut'] = '<a class="btn btn-default" href="#" onclick="'
2329 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB(
2330 $table,
2331 $row['uid'],
2332 0,
2333 $isSel === 'cut',
2334 ['returnUrl' => '']
2335 )) . ');')
2336 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">'
2337 . $cutIcon->render() . '</a>';
2338 } else {
2339 $cells['cut'] = $this->spaceIcon;
2340 }
2341 }
2342 }
2343 } else {
2344 // For the numeric clipboard pads (showing checkboxes where one can select elements on/off)
2345 // Setting name of the element in ->CBnames array:
2346 $n = $table . '|' . $row['uid'];
2347 $this->CBnames[] = $n;
2348 // Check if the current element is selected and if so, prepare to set the checkbox as selected:
2349 $checked = $this->clipObj->isSelected($table, $row['uid']) ? 'checked="checked" ' : '';
2350 // If the "duplicateField" value is set then select all elements which are duplicates...
2351 if ($this->duplicateField && isset($row[$this->duplicateField])) {
2352 $checked = '';
2353 if (in_array($row[$this->duplicateField], $this->duplicateStack)) {
2354 $checked = 'checked="checked" ';
2355 }
2356 $this->duplicateStack[] = $row[$this->duplicateField];
2357 }
2358 // Adding the checkbox to the panel:
2359 $cells['select'] = $isL10nOverlay
2360 ? $this->spaceIcon
2361 : '<input type="hidden" name="CBH[' . $n . ']" value="0" /><label class="btn btn-default btn-checkbox"><input type="checkbox"'
2362 . ' name="CBC[' . $n . ']" value="1" ' . $checked . '/><span class="t3-icon fa"></span></label>';
2363 }
2364 // Now, looking for selected elements from the current table:
2365 $elFromTable = $this->clipObj->elFromTable($table);
2366 if (!empty($elFromTable) && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
2367 // IF elements are found, they can be individually ordered and are not locked by editlock, then add a "paste after" icon:
2368 $cells['pasteAfter'] = $isL10nOverlay || !$this->overlayEditLockPermissions($table, $row)
2369 ? $this->spaceIcon
2370 : '<a class="btn btn-default t3js-modal-trigger"'
2371 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl($table, -$row['uid'])) . '"'
2372 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"'
2373 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"'
2374 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'after', $elFromTable)) . '"'
2375 . ' data-severity="warning">'
2376 . $this->iconFactory->getIcon('actions-document-paste-after', Icon::SIZE_SMALL)->render() . '</a>';
2377 }
2378 // Now, looking for elements in general:
2379 $elFromTable = $this->clipObj->elFromTable('');
2380 if ($table === 'pages' && !$isL10nOverlay && !empty($elFromTable)) {
2381 $cells['pasteInto'] = '<a class="btn btn-default t3js-modal-trigger"'
2382 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $row['uid'])) . '"'
2383 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
2384 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"'
2385 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'into', $elFromTable)) . '"'
2386 . ' data-severity="warning">'
2387 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '</a>';
2388 }
2389 /*
2390 * hook: makeClip: Allows to change clip-icons of records in list-module
2391 * usage: This hook method gets passed the current $cells array as third parameter.
2392 * This array contains values for the clipboard icons generated for each record in Web>List.
2393 * Each array entry is accessible by an index-key.
2394 * The order of the icons is depending on the order of those array entries.
2395 */
2396 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) {
2397 $hookObject = GeneralUtility::makeInstance($className);
2398 if (!$hookObject instanceof RecordListHookInterface) {
2399 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567845);
2400 }
2401 $cells = $hookObject->makeClip($table, $row, $cells, $this);
2402 }
2403 return '<div class="btn-group" role="group">' . implode('', $cells) . '</div>';
2404 }
2405
2406 /**
2407 * Creates the HTML for a reference count for the record with the UID $uid
2408 * in the table $tableName.
2409 *
2410 * @param string $tableName
2411 * @param int $uid
2412 * @return string HTML of reference a link, will be empty if there are no
2413 */
2414 protected function createReferenceHtml($tableName, $uid)
2415 {
2416 $referenceCount = GeneralUtility::makeInstance(ConnectionPool::class)
2417 ->getConnectionForTable('sys_refindex')
2418 ->count(
2419 '*',
2420 'sys_refindex',
2421 [
2422 'ref_table' => $tableName,
2423 'ref_uid' => (int)$uid,
2424 'deleted' => 0,
2425 ]
2426 );
2427
2428 return $this->generateReferenceToolTip(
2429 $referenceCount,
2430 GeneralUtility::quoteJSvalue($tableName) . ', ' . GeneralUtility::quoteJSvalue($uid)
2431 );
2432 }
2433
2434 /**
2435 * Creates the localization panel
2436 *
2437 * @param string $table The table
2438 * @param mixed[] $row The record for which to make the localization panel.
2439 * @return string[] Array with key 0/1 with content for column 1 and 2
2440 */
2441 public function makeLocalizationPanel($table, $row)
2442 {
2443 $out = [
2444 0 => '',
2445 1 => ''
2446 ];
2447 // Reset translations
2448 $this->translations = [];
2449
2450 // Language title and icon:
2451 $out[0] = $this->languageFlag($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]);
2452 // Guard clause so we can quickly return if a record is localized to "all languages"
2453 // It should only be possible to localize a record off default (uid 0)
2454 // Reasoning: The Parent is for ALL languages... why overlay with a localization?
2455 if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === -1) {
2456 return $out;
2457 }
2458
2459 $translations = $this->translateTools->translationInfo($table, $row['uid'], 0, $row, $this->selFieldList);
2460 if (is_array($translations)) {
2461 $this->translations = $translations['translations'];
2462 // Traverse page translations and add icon for each language that does NOT yet exist:
2463 $lNew = '';
2464 foreach ($this->pageOverlays as $lUid_OnPage => $lsysRec) {
2465 if ($this->isEditable($table) && !isset($translations['translations'][$lUid_OnPage]) && $this->getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)) {
2466 $url = $this->listURL();
2467 $href = BackendUtility::getLinkToDataHandlerAction(
2468 '&cmd[' . $table . '][' . $row['uid'] . '][localize]=' . $lUid_OnPage,
2469 $url . '&justLocalized=' . rawurlencode($table . ':' . $row['uid'] . ':' . $lUid_OnPage)
2470 );
2471 $language = BackendUtility::getRecord('sys_language', $lUid_OnPage, 'title');
2472 if ($this->languageIconTitles[$lUid_OnPage]['flagIcon']) {
2473 $lC = $this->iconFactory->getIcon($this->languageIconTitles[$lUid_OnPage]['flagIcon'], Icon::SIZE_SMALL)->render();
2474 } else {
2475 $lC = $this->languageIconTitles[$lUid_OnPage]['title'];
2476 }
2477 $lC = '<a href="' . htmlspecialchars($href) . '" title="'
2478 . htmlspecialchars($language['title']) . '" class="btn btn-default">' . $lC . '</a> ';
2479 $lNew .= $lC;
2480 }
2481 }
2482 if ($lNew) {
2483 $out[1] .= $lNew;
2484 }
2485 } elseif ($row['l18n_parent']) {
2486 $out[0] = '&nbsp;&nbsp;&nbsp;&nbsp;' . $out[0];
2487 }
2488 return $out;
2489 }
2490
2491 /**
2492 * Creates a checkbox list for selecting fields to display from a table:
2493 *
2494 * @param string $table Table name
2495 * @param bool $formFields If TRUE, form-fields will be wrapped around the table.
2496 * @return string HTML table with the selector check box (name: displayFields['.$table.'][])
2497 */
2498 public function fieldSelectBox($table, $formFields = true)
2499 {
2500 $lang = $this->getLanguageService();
2501 // Init:
2502 $formElements = ['', ''];
2503 if ($formFields) {
2504 $formElements = ['<form action="' . htmlspecialchars($this->listURL()) . '" method="post" name="fieldSelectBox">', '</form>'];
2505 }
2506 // Load already selected fields, if any:
2507 $setFields = is_array($this->setFields[$table]) ? $this->setFields[$table] : [];
2508 // Request fields from table:
2509 $fields = $this->makeFieldList($table, false, true);
2510 // Add pseudo "control" fields
2511 $fields[] = '_PATH_';
2512 $fields[] = '_REF_';
2513 $fields[] = '_LOCALIZATION_';
2514 $fields[] = '_CONTROL_';
2515 $fields[] = '_CLIPBOARD_';
2516 // Create a checkbox for each field:
2517 $checkboxes = [];
2518 $checkAllChecked = true;
2519 $tsConfig = BackendUtility::getPagesTSconfig($this->id);
2520 $tsConfigOfTable = is_array($tsConfig['TCEFORM.'][$table . '.']) ? $tsConfig['TCEFORM.'][$table . '.'] : null;
2521 foreach ($fields as $fieldName) {
2522 // Hide field if hidden
2523 if ($tsConfigOfTable && is_array($tsConfigOfTable[$fieldName . '.']) && isset($tsConfigOfTable[$fieldName . '.']['disabled']) && (int)$tsConfigOfTable[$fieldName . '.']['disabled'] === 1) {
2524 continue;
2525 }
2526 // Determine, if checkbox should be checked
2527 if (in_array($fieldName, $setFields, true) || $fieldName === $this->fieldArray[0]) {
2528 $checked = ' checked="checked"';
2529 } else {
2530 $checkAllChecked = false;
2531 $checked = '';
2532 }
2533 // Field label
2534 $fieldLabel = is_array($GLOBALS['TCA'][$table]['columns'][$fieldName])
2535 ? rtrim($lang->sL($GLOBALS['TCA'][$table]['columns'][$fieldName]['label']), ':')
2536 : '';
2537 $checkboxes[] = '<tr><td class="col-checkbox"><input type="checkbox" id="check-' . $fieldName . '" name="displayFields['
2538 . $table . '][]" value="' . $fieldName . '" ' . $checked
2539 . ($fieldName === $this->fieldArray[0] ? ' disabled="disabled"' : '') . '></td><td class="col-title">'
2540 . '<label class="label-block" for="check-' . $fieldName . '">' . htmlspecialchars($fieldLabel) . ' <span class="text-muted text-monospace">[' . htmlspecialchars($fieldName) . ']</span></label></td></tr>';
2541 }
2542 // Table with the field selector::
2543 $content = $formElements[0] . '
2544 <input type="hidden" name="displayFields[' . $table . '][]" value="">
2545 <div class="table-fit table-scrollable">
2546 <table border="0" cellpadding="0" cellspacing="0" class="table table-transparent table-hover">
2547 <thead>
2548 <tr>
2549 <th class="col-checkbox checkbox" colspan="2">
2550 <label><input type="checkbox" class="checkbox checkAll" ' . ($checkAllChecked ? ' checked="checked"' : '') . '>
2551 ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall')) . '</label>
2552 </th>
2553 </tr>
2554 </thead>
2555 <tbody>
2556 ' . implode('', $checkboxes) . '
2557 </tbody>
2558 </table>
2559 </div>
2560 <input type="submit" name="search" class="btn btn-default" value="'
2561 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.setFields')) . '"/>
2562 ' . $formElements[1];
2563 return '<div class="fieldSelectBox">' . $content . '</div>';
2564 }
2565
2566 /*********************************
2567 *
2568 * Helper functions
2569 *
2570 *********************************/
2571 /**
2572 * Creates a link around $string. The link contains an onclick action
2573 * which submits the script with some clipboard action.
2574 * Currently, this is used for setting elements / delete elements.
2575 *
2576 * @param string $string The HTML content to link (image/text)
2577 * @param string $table Table name
2578 * @param string $cmd Clipboard command (eg. "setCB" or "delete")
2579 * @param string $warning Warning text, if any ("delete" uses this for confirmation
2580 * @param string $title title attribute for the anchor
2581 * @return string <a> tag wrapped link.
2582 */
2583 public function linkClipboardHeaderIcon($string, $table, $cmd, $warning = '', $title = '')
2584 {
2585 $jsCode = 'document.dblistForm.cmd.value=' . GeneralUtility::quoteJSvalue($cmd)
2586 . ';document.dblistForm.cmd_table.value='
2587 . GeneralUtility::quoteJSvalue($table)
2588 . ';document.dblistForm.submit();';
2589
2590 $attributes = [];
2591 if ($title !== '') {
2592 $attributes['title'] = $title;
2593 }
2594 if ($warning) {
2595 $attributes['class'] = 'btn btn-default t3js-modal-trigger';
2596 $attributes['data-href'] = 'javascript:' . $jsCode;
2597 $attributes['data-severity'] = 'warning';
2598 $attributes['data-title'] = $title;
2599 $attributes['data-content'] = $warning;
2600 } else {
2601 $attributes['class'] = 'btn btn-default';
2602 $attributes['onclick'] = $jsCode . 'return false;';
2603 }
2604
2605 $attributesString = '';
2606 foreach ($attributes as $key => $value) {
2607 $attributesString .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
2608 }
2609 return '<a href="#" ' . $attributesString . '>' . $string . '</a>';
2610 }
2611
2612 /**
2613 * Returns TRUE if a numeric clipboard pad is selected/active
2614 *
2615 * @return bool
2616 */
2617 public function clipNumPane()
2618 {
2619 return in_array('_CLIPBOARD_', $this->fieldArray) && $this->clipObj->current !== 'normal';
2620 }
2621
2622 /**
2623 * Creates a sort-by link on the input string ($code).
2624 * It will automatically detect if sorting should be ascending or descending depending on $this->sortRev.
2625 * Also some fields will not be possible to sort (including if single-table-view is disabled).
2626 *
2627 * @param string $code The string to link (text)
2628 * @param string $field The fieldname represented by the title ($code)
2629 * @param string $table Table name
2630 * @return string Linked $code variable
2631 */
2632 public function addSortLink($code, $field, $table)
2633 {
2634 // Certain circumstances just return string right away (no links):
2635 if ($field === '_CONTROL_' || $field === '_LOCALIZATION_' || $field === '_CLIPBOARD_' || $field === '_REF_' || $this->disableSingleTableView) {
2636 return $code;
2637 }
2638 // If "_PATH_" (showing record path) is selected, force sorting by pid field (will at least group the records!)
2639 if ($field === '_PATH_') {
2640 $field = 'pid';
2641 }
2642 // Create the sort link:
2643 $sortUrl = $this->listURL('', '-1', 'sortField,sortRev,table,firstElementNumber') . '&table=' . $table
2644 . '&sortField=' . $field . '&sortRev=' . ($this->sortRev || $this->sortField != $field ? 0 : 1);
2645 $sortArrow = $this->sortField === $field
2646 ? $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render()
2647 : '';
2648 // Return linked field:
2649 return '<a href="' . htmlspecialchars($sortUrl) . '">' . $code . $sortArrow . '</a>';
2650 }
2651
2652 /**
2653 * Returns the path for a certain pid
2654 * The result is cached internally for the session, thus you can call
2655 * this function as much as you like without performance problems.
2656 *
2657 * @param int $pid The page id for which to get the path
2658 * @return mixed[] The path.
2659 */
2660 public function recPath($pid)
2661 {
2662 if (!isset($this->recPath_cache[$pid])) {
2663 $this->recPath_cache[$pid] = BackendUtility::getRecordPath($pid, $this->perms_clause, 20);
2664 }
2665 return $this->recPath_cache[$pid];
2666 }
2667
2668 /**
2669 * Returns TRUE if a link for creating new records should be displayed for $table