‪TYPO3CMS  ‪main
DatabaseRecordList.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\ServerRequestInterface;
34 use TYPO3\CMS\Backend\Utility\BackendUtility;
41 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
48 use TYPO3\CMS\Core\Imaging\IconSize;
62 
68 {
69  // *********
70  // External:
71  // *********
72 
79  public ‪$allowedNewTables = [];
80 
87  public ‪$deniedNewTables = [];
88 
94  public ‪$showClipboardActions = false;
95 
101  public ‪$noControlPanels = false;
102 
108  public ‪$clickMenuEnabled = true;
109 
113  protected string ‪$spaceIcon;
114 
120  public ‪$disableSingleTableView = false;
121 
122  // *********
123  // Internal:
124  // *********
125 
131  public ‪$pageRow = [];
132 
138  public string ‪$hideTranslations = '';
139 
145  protected array ‪$recPath_cache = [];
146 
150  public string ‪$sortField = '';
151 
155  protected ?‪ModuleData ‪$moduleData = null;
156 
162  public ‪$id;
163 
169  protected array ‪$duplicateStack = [];
170 
177 
181  public ‪$modTSconfig;
182 
186  protected array ‪$addElement_tdCssClass = [];
187 
193  public ‪$currentTable = [];
194 
198  public int ‪$showLimit = 0;
199 
205  public ‪$fieldArray = [];
206 
212  public ‪$hideTables = '';
213 
219  public ‪$perms_clause = '';
220 
226  public ‪$returnUrl = '';
227 
233  public ‪$table = '';
234 
240  public ‪$calcPerms;
241 
247  public ‪$clickTitleMode = '';
248 
252  protected int ‪$searchLevels = 0;
253 
259  public array ‪$tableTSconfigOverTCA = [];
260 
266  public array ‪$setFields = [];
267 
273  protected ‪$page = 0;
274 
280  public ‪$searchString = '';
281 
287  public bool ‪$sortRev = false;
288 
292  protected string ‪$duplicateField = '';
293 
299  public ‪$tableList = '';
300 
306  public ‪$clipObj;
307 
313  public ‪$CBnames = [];
314 
320  public bool ‪$displayColumnSelector = true;
321 
327  public bool ‪$displayRecordDownload = true;
328 
334  protected array ‪$referenceCount = [];
335 
339  protected bool ‪$editable = true;
340 
351  protected array ‪$tableDisplayOrder = [];
352 
356  protected array ‪$overridePageIdList = [];
357 
362  protected array ‪$overrideUrlParameters = [];
363 
367  protected array ‪$currentLink = [];
368 
372  protected bool ‪$showOnlyTranslatedRecords = false;
373 
384  protected array ‪$possibleTranslations = [];
385 
391  protected array ‪$languagesAllowedForUser = [];
392 
396  protected array ‪$pagePermsCache = [];
397  protected array ‪$showLocalizeColumn = [];
398 
399  protected ServerRequestInterface ‪$request;
400 
401  public function ‪__construct(
402  protected readonly ‪IconFactory $iconFactory,
403  protected readonly ‪UriBuilder $uriBuilder,
404  protected readonly ‪TranslationConfigurationProvider $translateTools,
405  protected readonly EventDispatcherInterface $eventDispatcher,
406  protected readonly ‪BackendViewFactory $backendViewFactory,
407  protected readonly ‪ModuleProvider $moduleProvider,
408  ) {
409  $this->calcPerms = new ‪Permission();
410  $this->spaceIcon = '<span class="btn btn-default disabled" aria-hidden="true">' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '</span>';
411  }
412 
413  public function ‪setRequest(ServerRequestInterface ‪$request)
414  {
415  $this->request = ‪$request;
416  }
417 
422  public function ‪getColumnsToRender(string ‪$table, bool $includeMetaColumns): array
423  {
424  $titleCol = ‪$GLOBALS['TCA'][‪$table]['ctrl']['label'] ?? '';
425 
426  // Setting fields selected in columnSelectorBox (saved in uc)
427  $rowListArray = [];
428  if (is_array($this->setFields[‪$table] ?? null)) {
429  $rowListArray = BackendUtility::getAllowedFieldsForTable(‪$table);
430  if ($includeMetaColumns) {
431  $rowListArray[] = '_PATH_';
432  $rowListArray[] = '_REF_';
433  }
434  $rowListArray = array_intersect($rowListArray, $this->setFields[‪$table]);
435  }
436  // if no columns have been specified, show description (if configured)
437  if (!empty(‪$GLOBALS['TCA'][‪$table]['ctrl']['descriptionColumn']) && empty($rowListArray)) {
438  $rowListArray[] = ‪$GLOBALS['TCA'][‪$table]['ctrl']['descriptionColumn'];
439  }
440 
441  // Initialize columns to select
442  $columnsToSelect = [$titleCol];
443  if ($includeMetaColumns) {
444  // If meta columns are enabled, add the record icon
445  array_unshift($columnsToSelect, 'icon');
446 
447  if ($this->noControlPanels === false) {
448  // Add _SELECTOR_ as first item in case control panels are not disabled
449  array_unshift($columnsToSelect, '_SELECTOR_');
450 
451  // Control-Panel
452  $columnsToSelect[] = '_CONTROL_';
453  }
454  // Path
455  if (!in_array('_PATH_', $rowListArray, true) && $this->searchLevels) {
456  $columnsToSelect[] = '_PATH_';
457  }
458  // Localization
459  if (BackendUtility::isTableLocalizable(‪$table)) {
460  $columnsToSelect[] = '_LOCALIZATION_';
461  // Do not show the "Localize to:" field when only translated records should be shown
462  if (!$this->‪showOnlyTranslatedRecords) {
463  $columnsToSelect[] = '_LOCALIZATION_b';
464  }
465  }
466  }
467  return array_unique(array_merge($columnsToSelect, $rowListArray));
468  }
469 
476  public function ‪getFieldsToSelect(string ‪$table, array $columnsToRender): array
477  {
478  $selectFields = $columnsToRender;
479  $selectFields[] = 'uid';
480  $selectFields[] = 'pid';
481  if (‪$table === 'pages') {
482  $selectFields[] = 'module';
483  $selectFields[] = 'extendToSubpages';
484  $selectFields[] = 'nav_hide';
485  $selectFields[] = 'doktype';
486  $selectFields[] = 'shortcut';
487  $selectFields[] = 'shortcut_mode';
488  $selectFields[] = 'mount_pid';
489  }
490  if (is_array(‪$GLOBALS['TCA'][‪$table]['ctrl']['enablecolumns'] ?? null)) {
491  $selectFields = array_merge($selectFields, array_values(‪$GLOBALS['TCA'][‪$table]['ctrl']['enablecolumns']));
492  }
493  foreach (['type', 'typeicon_column', 'editlock'] as $field) {
494  if (‪$GLOBALS['TCA'][‪$table]['ctrl'][$field] ?? false) {
495  $selectFields[] = ‪$GLOBALS['TCA'][‪$table]['ctrl'][$field];
496  }
497  }
498  if (BackendUtility::isTableWorkspaceEnabled(‪$table)) {
499  $selectFields[] = 't3ver_state';
500  $selectFields[] = 't3ver_wsid';
501  $selectFields[] = 't3ver_oid';
502  }
503  if (BackendUtility::isTableLocalizable(‪$table)) {
504  $selectFields[] = ‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'];
505  $selectFields[] = ‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'];
506  }
507  if (‪$GLOBALS['TCA'][‪$table]['ctrl']['label_alt'] ?? false) {
508  $selectFields = array_merge(
509  $selectFields,
510  ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TCA'][‪$table]['ctrl']['label_alt'], true)
511  );
512  }
513  // Unique list!
514  $selectFields = array_unique($selectFields);
515  $fieldListFields = BackendUtility::getAllowedFieldsForTable(‪$table, false);
516  // Making sure that the fields in the field-list ARE in the field-list from TCA!
517  return array_intersect($selectFields, $fieldListFields);
518  }
519 
527  public function ‪getTable(‪$table)
528  {
529  // Finding the total amount of records on the page
530  $queryBuilderTotalItems = $this->‪getQueryBuilder($table, ['*'], false, 0, 1);
531  $totalItems = (int)$queryBuilderTotalItems
532  ->count('*')
533  ->resetOrderBy()
534  ->executeQuery()
535  ->fetchOne();
536  if ($totalItems === 0) {
537  return '';
538  }
539  // Setting the limits for the amount of records to be displayed in the list and single table view.
540  // Using the default value and overwriting with page TSconfig and TCA config. The limit is forced
541  // to be in the range of 0 - 10000.
542 
543  // default 100 for single table view
544  $itemsLimitSingleTable = ‪MathUtility::forceIntegerInRange((int)(
545  ‪$GLOBALS['TCA'][‪$table]['interface']['maxSingleDBListItems'] ??
546  $this->modTSconfig['itemsLimitSingleTable'] ??
547  100
548  ), 0, 10000);
549 
550  // default 20 for list view
551  $itemsLimitPerTable = ‪MathUtility::forceIntegerInRange((int)(
552  ‪$GLOBALS['TCA'][‪$table]['interface']['maxDBListItems'] ??
553  $this->modTSconfig['itemsLimitPerTable'] ??
554  20
555  ), 0, 10000);
556 
557  // Set limit depending on the view (single table vs. default)
558  $itemsPerPage = $this->table ? $itemsLimitSingleTable : $itemsLimitPerTable;
559 
560  // Set limit defined by calling code
561  if ($this->showLimit) {
562  $itemsPerPage = ‪$this->showLimit;
563  }
564 
565  // Init
566  $titleCol = ‪$GLOBALS['TCA'][‪$table]['ctrl']['label'];
567  $l10nEnabled = BackendUtility::isTableLocalizable(‪$table);
568 
569  $this->‪fieldArray = $this->‪getColumnsToRender($table, true);
570  // Creating the list of fields to include in the SQL query
571  $selectFields = $this->‪getFieldsToSelect($table, $this->‪fieldArray);
572 
573  $firstElement = ($this->page - 1) * $itemsPerPage;
574  if ($firstElement > 2 && $itemsPerPage > 0) {
575  // Get the two previous rows for sorting if displaying page > 1
576  $firstElement -= 2;
577  $itemsPerPage += 2;
578  $queryBuilder = $this->‪getQueryBuilder($table, $selectFields, true, $firstElement, $itemsPerPage);
579  $firstElement += 2;
580  $itemsPerPage -= 2;
581  } else {
582  $queryBuilder = $this->‪getQueryBuilder($table, $selectFields, true, $firstElement, $itemsPerPage);
583  }
584 
585  $queryResult = $queryBuilder->executeQuery();
586  $columnsOutput = '';
587  $onlyShowRecordsInSingleTableMode = $this->listOnlyInSingleTableMode && !‪$this->table;
588  // Fetch records only if not in single table mode
589  if ($onlyShowRecordsInSingleTableMode) {
590  $dbCount = $totalItems;
591  } elseif ($firstElement + $itemsPerPage <= $totalItems) {
592  $dbCount = $itemsPerPage + 2;
593  } else {
594  $dbCount = $totalItems - $firstElement + 2;
595  }
596  // If any records was selected, render the list:
597  if ($dbCount === 0) {
598  return '';
599  }
600 
601  // Get configuration of collapsed tables from user uc
602  $lang = $this->‪getLanguageService();
603 
604  $tableIdentifier = ‪$table;
605  // Use a custom table title for translated pages
606  if (‪$table === 'pages' && $this->‪showOnlyTranslatedRecords) {
607  // pages records in list module are split into two own sections, one for pages with
608  // sys_language_uid = 0 "Page" and an own section for sys_language_uid > 0 "Page Translation".
609  // This if sets the different title for the page translation case and a unique table identifier
610  // which is used in DOM as id.
611  $tableTitle = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:pageTranslation'));
612  $tableIdentifier = 'pages_translated';
613  } else {
614  $tableTitle = htmlspecialchars($lang->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title']));
615  if ($tableTitle === '') {
616  $tableTitle = ‪$table;
617  }
618  }
619 
620  $backendUser = $this->‪getBackendUserAuthentication();
621  $tableCollapsed = (bool)($this->moduleData?->get('collapsedTables')[$tableIdentifier] ?? false);
622 
623  // Header line is drawn
624  $theData = [];
625  if ($this->disableSingleTableView) {
626  $theData[$titleCol] = $tableTitle . ' (<span class="t3js-table-total-items">' . $totalItems . '</span>)';
627  } else {
628  $icon = $this->table // @todo separate table header from contract/expand link
629  ? $this->iconFactory
630  ->getIcon('actions-view-table-collapse', IconSize::SMALL)
631  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:contractView'))
632  ->render()
633  : $this->iconFactory
634  ->getIcon('actions-view-table-expand', IconSize::SMALL)
635  ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:expandView'))
636  ->render();
637  $theData[$titleCol] = $this->‪linkWrapTable($table, $tableTitle . ' (<span class="t3js-table-total-items">' . $totalItems . '</span>) ' . $icon);
638  }
639  $tableActions = '';
640  $tableHeader = $theData[$titleCol];
641  if (!$onlyShowRecordsInSingleTableMode) {
642  // Add the "new record" button
643  $tableActions .= $this->‪createActionButtonNewRecord($table) ?? '';
644  // Show the select box
645  $tableActions .= $this->‪createActionButtonColumnSelector($table) ?? '';
646  // Create the Download button
647  $tableActions .= $this->‪createActionButtonDownload($table, $totalItems) ?? '';
648  // Render collapse button if in multi table mode
649  $tableActions .= $this->‪createActionButtonCollapse($table) ?? '';
650  }
651  $currentIdList = [];
652  // Render table rows only if in multi table view or if in single table view
653  $rowOutput = '';
654  if (!$onlyShowRecordsInSingleTableMode || $this->table) {
655  // Fixing an order table for sortby tables
656  $this->currentTable = [];
657  $allowManualSorting = (‪$GLOBALS['TCA'][‪$table]['ctrl']['sortby'] ?? false) && !$this->sortField;
658  $prevUid = 0;
659  $prevPrevUid = 0;
660  // Get first two rows and initialize prevPrevUid and prevUid if on page > 1
661  if ($firstElement > 2 && $itemsPerPage > 0) {
662  $row = $queryResult->fetchAssociative();
663  $prevPrevUid = -((int)$row['uid']);
664  $row = $queryResult->fetchAssociative();
665  $prevUid = $row['uid'];
666  }
667  $accRows = [];
668  // Accumulate rows here
669  while ($row = $queryResult->fetchAssociative()) {
670  if (!$this->‪isRowListingConditionFulfilled($table, $row)) {
671  continue;
672  }
673  // In offline workspace, look for alternative record
674  BackendUtility::workspaceOL(‪$table, $row, $backendUser->workspace, true);
675  if (is_array($row)) {
676  $accRows[] = $row;
677  $currentIdList[] = $row['uid'];
678  if ($allowManualSorting) {
679  if ($prevUid) {
680  $this->currentTable['prev'][$row['uid']] = $prevPrevUid;
681  $this->currentTable['next'][$prevUid] = '-' . $row['uid'];
682  $this->currentTable['prevUid'][$row['uid']] = $prevUid;
683  }
684  $prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid'];
685  $prevUid = $row['uid'];
686  }
687  }
688  }
689  // Render items:
690  $this->CBnames = [];
691  $this->duplicateStack = [];
692  $cc = 0;
693 
694  // If no search happened it means that the selected
695  // records are either default or All language and here we will not select translations
696  // which point to the main record:
697  $listTranslatedRecords = $l10nEnabled && $this->searchString === '' && !($this->hideTranslations === '*' || ‪GeneralUtility::inList($this->hideTranslations, ‪$table));
698  foreach ($accRows as $row) {
699  // Render item row if counter < limit
700  if ($cc < $itemsPerPage) {
701  $cc++;
702  // Reset translations
703  $translations = [];
704  // Initialize with FALSE which causes the localization panel to not be displayed as
705  // the record is already localized, in free mode or has sys_language_uid -1 set.
706  // Only set to TRUE if TranslationConfigurationProvider::translationInfo() returns
707  // an array indicating the record can be translated.
708  $translationEnabled = false;
709  // Guard clause so we can quickly return if a record is localized to "all languages"
710  // It should only be possible to localize a record off default (uid 0)
711  if ($l10nEnabled && ($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'] ?? null] ?? false) !== -1) {
712  $translationsRaw = $this->translateTools->translationInfo(‪$table, $row['uid'], 0, $row, $selectFields);
713  if (is_array($translationsRaw)) {
714  $translationEnabled = true;
715  $translations = $translationsRaw['translations'] ?? [];
716  }
717  }
718  $rowOutput .= $this->‪renderListRow($table, $row, 0, $translations, $translationEnabled);
719  if ($listTranslatedRecords) {
720  foreach ($translations ?? [] as $lRow) {
721  if (!$this->‪isRowListingConditionFulfilled($table, $lRow)) {
722  continue;
723  }
724  // In offline workspace, look for alternative record:
725  BackendUtility::workspaceOL(‪$table, $lRow, $backendUser->workspace, true);
726  if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField']])) {
727  $currentIdList[] = $lRow['uid'];
728  $rowOutput .= $this->‪renderListRow($table, $lRow, 1, [], false);
729  }
730  }
731  }
732  }
733  }
734  // Record navigation is added to the beginning and end of the table if in single table mode
735  if ($this->table) {
736  $pagination = $this->‪renderListNavigation($this->table, $totalItems, $itemsPerPage);
737  $rowOutput = $pagination . $rowOutput . $pagination;
738  } elseif ($totalItems > $itemsLimitPerTable) {
739  // Show that there are more records than shown
740  $rowOutput .= '
741  <tr data-multi-record-selection-element="true">
742  <td colspan="' . (count($this->‪fieldArray)) . '">
743  <a href="' . htmlspecialchars($this->‪listURL() . '&table=' . rawurlencode($tableIdentifier)) . '" class="btn btn-sm btn-default">
744  ' . $this->iconFactory->getIcon('actions-caret-down', IconSize::SMALL)->render() . '
745  ' . $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.expandTable') . '
746  </a>
747  </td>
748  </tr>';
749  }
750  // The header row for the table is now created
751  $columnsOutput = $this->‪renderListHeader($table, $currentIdList);
752  }
753 
754  // Initialize multi record selection actions
755  $multiRecordSelectionActions = '';
756  if ($this->noControlPanels === false) {
757  $multiRecordSelectionActions = '
758  <div class="recordlist-heading-row t3js-multi-record-selection-actions hidden">
759  <div class="recordlist-heading-title">
760  <strong>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selection')) . '</strong>
761  </div>
762  <div class="recordlist-heading-actions">
763  ' . $this->‪renderMultiRecordSelectionActions($table, $currentIdList) . '
764  </div>
765  </div>
766  ';
767  }
768 
769  $recordListMessages = '';
770  $recordlistMessageEntries = [];
771  if ($backendUser->workspace > 0 && ‪ExtensionManagementUtility::isLoaded('workspaces') && !BackendUtility::isTableWorkspaceEnabled(‪$table)) {
772  // In case the table is not editable in workspace inform the user about the missing actions
773  if ($backendUser->workspaceAllowsLiveEditingInTable(‪$table)) {
774  $recordlistMessageEntries[] = [
775  'message' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editingLiveRecordsWarning'),
776  'severity' => ContextualFeedbackSeverity::WARNING,
777  ];
778  } else {
779  $recordlistMessageEntries[] = [
780  'message' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.notEditableInWorkspace'),
781  'severity' => ContextualFeedbackSeverity::INFO,
782  ];
783  }
784  }
785 
786  foreach ($recordlistMessageEntries as $messageEntry) {
787  $recordListMessages .= '<div class="alert alert-' . $messageEntry['severity']->getCssClass() . '">';
788  $recordListMessages .= $this->iconFactory->getIcon($messageEntry['severity']->getIconIdentifier(), IconSize::SMALL)->render();
789  $recordListMessages .= ' ';
790  $recordListMessages .= htmlspecialchars($messageEntry['message'], ENT_QUOTES | ENT_HTML5);
791  $recordListMessages .= '</div>';
792  }
793 
794  $collapseClass = $tableCollapsed && !$this->table ? 'collapse' : 'collapse show';
795  $dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded';
796  return '
797  <div class="recordlist" id="t3-table-' . htmlspecialchars($tableIdentifier) . '" data-multi-record-selection-identifier="t3-table-' . htmlspecialchars($tableIdentifier) . '">
798  <form action="' . htmlspecialchars($this->‪listURL()) . '#t3-table-' . htmlspecialchars($tableIdentifier) . '" method="post" name="list-table-form-' . htmlspecialchars($tableIdentifier) . '">
799  <input type="hidden" name="cmd_table" value="' . htmlspecialchars($tableIdentifier) . '" />
800  <input type="hidden" name="cmd" />
801  <div class="recordlist-heading ' . ($multiRecordSelectionActions !== '' ? 'multi-record-selection-panel' : '') . '">
802  <div class="recordlist-heading-row">
803  <div class="recordlist-heading-title">' . $tableHeader . '</div>
804  <div class="recordlist-heading-actions">' . $tableActions . '</div>
805  </div>
806  ' . $multiRecordSelectionActions . '
807  </div>
808  ' . $recordListMessages . '
809  <div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($tableIdentifier) . '">
810  <div class="table-fit">
811  <table data-table="' . htmlspecialchars($tableIdentifier) . '" class="table table-striped table-hover">
812  <thead>
813  ' . $columnsOutput . '
814  </thead>
815  <tbody data-multi-record-selection-row-selection="true">
816  ' . $rowOutput . '
817  </tbody>
818  </table>
819  </div>
820  </div>
821  </form>
822  </div>
823  ';
824  }
825 
829  protected function ‪createActionButtonNewRecord(string ‪$table): ?‪ButtonInterface
830  {
831  if (!$this->‪isEditable($table)) {
832  return null;
833  }
834  if (!$this->‪showNewRecLink($table)) {
835  return null;
836  }
837  $permsAdditional = (‪$table === 'pages' ? ‪Permission::PAGE_NEW : ‪Permission::CONTENT_EDIT);
838  if (!$this->calcPerms->isGranted($permsAdditional)) {
839  return null;
840  }
841 
842  $tag = 'a';
843  $iconIdentifier = 'actions-plus';
844  $label = sprintf(
845  $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:newRecordOfType'),
846  $this->‪getLanguageService()->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title'])
847  );
848  $attributes = [
849  'data-recordlist-action' => 'new',
850  ];
851 
852  if (‪$table === 'tt_content') {
853  $tag = 'typo3-backend-new-content-element-wizard-button';
854  $attributes['url'] = (string)$this->uriBuilder->buildUriFromRoute(
855  'new_content_element_wizard',
856  [
857  'id' => $this->id,
858  'returnUrl' => $this->listURL(),
859  ]
860  );
861  } elseif (‪$table === 'pages') {
862  $iconIdentifier = 'actions-page-new';
863  $attributes['data-new'] = 'page';
864  $attributes['href'] = (string)$this->uriBuilder->buildUriFromRoute(
865  'db_new_pages',
866  ['id' => $this->id, 'returnUrl' => $this->listURL()]
867  );
868  } else {
869  $attributes['href'] = $this->uriBuilder->buildUriFromRoute(
870  'record_edit',
871  [
872  'edit' => [
873  ‪$table => [
874  $this->id => 'new',
875  ],
876  ],
877  'returnUrl' => $this->‪listURL(),
878  ]
879  );
880  }
881 
882  $button = GeneralUtility::makeInstance(GenericButton::class);
883  $button->setTag($tag);
884  $button->setLabel($label);
885  $button->setShowLabelText(true);
886  $button->setIcon($this->iconFactory->getIcon($iconIdentifier, IconSize::SMALL));
887  $button->setAttributes($attributes);
888 
889  return $button;
890  }
891 
892  protected function ‪createActionButtonDownload(string ‪$table, int $totalItems): ?ButtonInterface
893  {
894  // Do not render the download button for page translations or in case it is generally disabled
895  if (!$this->displayRecordDownload || $this->‪showOnlyTranslatedRecords) {
896  return null;
897  }
898 
899  $shouldRenderDownloadButton = true;
900  // See if it is disabled in general
901  if (isset($this->modTSconfig['displayRecordDownload'])) {
902  $shouldRenderDownloadButton = (bool)$this->modTSconfig['displayRecordDownload'];
903  }
904  // Table override was explicitly set
905  if (isset($this->tableTSconfigOverTCA[‪$table . '.']['displayRecordDownload'])) {
906  $shouldRenderDownloadButton = (bool)$this->tableTSconfigOverTCA[‪$table . '.']['displayRecordDownload'];
907  }
908  // Do not render button if disabled
909  if ($shouldRenderDownloadButton === false) {
910  return null;
911  }
912 
913  $downloadButtonLabel = $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_download.xlf:download');
914  $downloadButtonTitle = sprintf($this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_download.xlf:' . ($totalItems === 1 ? 'downloadRecord' : 'downloadRecords')), $totalItems);
915  $downloadCancelTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel');
916  $downloadSettingsUrl = (string)$this->uriBuilder->buildUriFromRoute(
917  'ajax_record_download_settings',
918  ['id' => $this->id, 'table' => ‪$table, 'searchString' => $this->searchString, 'searchLevels' => $this->searchLevels]
919  );
920  $downloadSettingsTitle = sprintf(
921  $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_download.xlf:' . ($totalItems === 1 ? 'downloadRecordSettings' : 'downloadRecordsSettings')),
922  $this->‪getLanguageService()->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title'] ?? '') ?: ‪$table,
923  $totalItems
924  );
925 
926  $button = GeneralUtility::makeInstance(GenericButton::class);
927  $button->setTag('typo3-recordlist-record-download-button');
928  $button->setLabel($downloadButtonLabel);
929  $button->setShowLabelText(true);
930  $button->setIcon($this->iconFactory->getIcon('actions-download', IconSize::SMALL));
931  $button->setAttributes([
932  'url' => $downloadSettingsUrl,
933  'subject' => $downloadSettingsTitle,
934  'ok' => $downloadButtonTitle,
935  'close' => $downloadCancelTitle,
936  'data-recordlist-action' => 'download',
937  ]);
938 
939  return $button;
940  }
941 
946  {
947  if ($this->displayColumnSelector === false) {
948  // Early return in case column selector is disabled
949  return null;
950  }
951 
952  $shouldRenderSelector = true;
953  // See if it is disabled in general
954  if (isset($this->modTSconfig['displayColumnSelector'])) {
955  $shouldRenderSelector = (bool)$this->modTSconfig['displayColumnSelector'];
956  }
957  // Table override was explicitly set to false
958  if (isset($this->modTSconfig['table.'][‪$table . '.']['displayColumnSelector'])) {
959  $shouldRenderSelector = (bool)$this->modTSconfig['table.'][‪$table . '.']['displayColumnSelector'];
960  }
961  // Do not render button if column selector is disabled
962  if ($shouldRenderSelector === false) {
963  return null;
964  }
965 
966  $lang = $this->‪getLanguageService();
967  $tableIdentifier = ‪$table . ((‪$table === 'pages' && ‪$this->showOnlyTranslatedRecords) ? '_translated' : '');
968  $columnSelectorUrl = (string)$this->uriBuilder->buildUriFromRoute(
969  'ajax_show_columns_selector',
970  ['id' => $this->id, 'table' => ‪$table]
971  );
972  $columnSelectorTitle = sprintf(
973  $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumnsSelection'),
974  $lang->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title'] ?? '') ?: ‪$table,
975  );
976 
977  $button = GeneralUtility::makeInstance(GenericButton::class);
978  $button->setTag('typo3-backend-column-selector-button');
979  $button->setLabel($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:showColumns'));
980  $button->setShowLabelText(true);
981  $button->setIcon($this->iconFactory->getIcon('actions-options', IconSize::SMALL));
982  $button->setAttributes([
983  'data-url' => $columnSelectorUrl,
984  'data-target' => $this->‪listURL() . '#t3-table-' . $tableIdentifier,
985  'data-title' => $columnSelectorTitle,
986  'data-button-ok' => $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView'),
987  'data-button-close' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'),
988  'data-error-message' => $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.error'),
989  'data-recordlist-action' => 'columns',
990  ]);
991 
992  return $button;
993  }
994 
995  protected function ‪createActionButtonCollapse(string ‪$table): ?‪ButtonInterface
996  {
997  if ($this->table !== '') {
998  return null;
999  }
1000 
1001  $tableIdentifier = ‪$table . ((‪$table === 'pages' && ‪$this->showOnlyTranslatedRecords) ? '_translated' : '');
1002  $tableCollapsed = (bool)($this->moduleData?->get('collapsedTables')[$tableIdentifier] ?? false);
1003 
1004  $button = GeneralUtility::makeInstance(GenericButton::class);
1005  $button->setLabel(sprintf(
1006  $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:collapseExpandTable'),
1007  $this->‪getLanguageService()->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title'])
1008  ));
1009  $button->setClasses('t3js-toggle-recordlist');
1010  $button->setIcon($this->iconFactory->getIcon(($tableCollapsed ? 'actions-view-list-expand' : 'actions-view-list-collapse'), IconSize::SMALL));
1011  $button->setAttributes([
1012  'aria-expanded' => ($tableCollapsed ? 'false' : 'true'),
1013  'data-table' => $tableIdentifier,
1014  'data-recordlist-action' => 'toggle',
1015  'data-bs-toggle' => 'collapse',
1016  'data-bs-target' => '#recordlist-' . $tableIdentifier,
1017  ]);
1018 
1019  return $button;
1020  }
1021 
1025  protected function ‪getPreviewUriBuilder(string ‪$table, array $row): ‪PreviewUriBuilder
1026  {
1027  if (‪$table === 'tt_content') {
1028  // Link to a content element, possibly translated and with anchor
1029  $previewUriBuilder = ‪PreviewUriBuilder::create($this->id)
1030  ->withSection('#c' . $row['uid'])
1031  ->withLanguage((int)($row[‪$GLOBALS['TCA']['tt_content']['ctrl']['languageField'] ?? null] ?? 0));
1032  } elseif (‪$table === 'pages' && ($row[‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null] ?? 0) > 0) {
1033  // Link to a page translation needs uid of default language page as id
1034  $previewUriBuilder = ‪PreviewUriBuilder::create((int)$row[‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']])
1035  ->withSection('#c' . $row['uid'])
1036  ->withLanguage((int)($row[‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null] ?? 0));
1037  } else {
1038  // Link to a page in the default language
1039  $previewUriBuilder = ‪PreviewUriBuilder::create((int)($row['uid'] ?? 0));
1040  }
1041  return $previewUriBuilder;
1042  }
1043 
1053  protected function ‪isRowListingConditionFulfilled(‪$table, $row)
1054  {
1055  return true;
1056  }
1057 
1070  public function ‪renderListRow(‪$table, array $row, int $indent, array $translations, bool $translationEnabled)
1071  {
1072  $titleCol = ‪$GLOBALS['TCA'][‪$table]['ctrl']['label'] ?? '';
1073  $languageService = $this->‪getLanguageService();
1074  $rowOutput = '';
1075  $id_orig = ‪$this->id;
1076  // If in search mode, make sure the preview will show the correct page
1077  if ((string)$this->searchString !== '') {
1078  $this->id = $row['pid'];
1079  }
1080 
1081  $tagAttributes = [
1082  'class' => [],
1083  'data-table' => ‪$table,
1084  'title' => 'id=' . $row['uid'],
1085  ];
1086 
1087  // Add active class to record of current link
1088  if (
1089  isset($this->currentLink['tableNames'])
1090  && (int)$this->currentLink['uid'] === (int)$row['uid']
1091  && ‪GeneralUtility::inList($this->currentLink['tableNames'], ‪$table)
1092  ) {
1093  $tagAttributes['class'][] = 'active';
1094  }
1095  // Overriding with versions background color if any:
1096  if (!empty($row['_CSSCLASS'])) {
1097  $tagAttributes['class'] = [$row['_CSSCLASS']];
1098  }
1099 
1100  $tagAttributes['class'][] = 't3js-entity';
1101 
1102  // Preparing and getting the data-array
1103  $theData = [];
1104  $deletePlaceholderClass = '';
1105  foreach ($this->‪fieldArray as $fCol) {
1106  if ($fCol === $titleCol) {
1107  $recTitle = BackendUtility::getRecordTitle(‪$table, $row, false, true);
1108  $warning = '';
1109  // If the record is edit-locked by another user, we will show a little warning sign:
1110  $lockInfo = BackendUtility::isRecordLocked(‪$table, $row['uid']);
1111  if ($lockInfo) {
1112  $warning = '<span tabindex="0"'
1113  . ' title="' . htmlspecialchars($lockInfo['msg']) . '"'
1114  . ' aria-label="' . htmlspecialchars($lockInfo['msg']) . '">'
1115  . $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render()
1116  . '</span>';
1117  }
1118  if ($this->‪isRecordDeletePlaceholder($row)) {
1119  // Delete placeholder records do not link to formEngine edit and are rendered strike-through
1120  $deletePlaceholderClass = ' deletePlaceholder';
1121  $theData[$fCol] = $theData['__label'] =
1122  $warning
1123  . '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:row.deletePlaceholder.title')) . '">'
1124  . htmlspecialchars($recTitle)
1125  . '</span>';
1126  } else {
1127  $theData[$fCol] = $theData['__label'] = $warning . $this->‪linkWrapItems($table, $row['uid'], $recTitle, $row);
1128  }
1129  } elseif ($fCol === 'pid') {
1130  $theData[$fCol] = $row[$fCol];
1131  } elseif ($fCol === '_SELECTOR_') {
1132  if (‪$table !== 'pages' || !$this->‪showOnlyTranslatedRecords) {
1133  // Add checkbox for all tables except the special page translations table
1134  $theData[$fCol] = $this->‪makeCheckbox($table, $row);
1135  } else {
1136  // Remove "_SELECTOR_", which is always the first item, from the field list
1137  array_splice($this->‪fieldArray, 0, 1);
1138  }
1139  } elseif ($fCol === 'icon') {
1140  $icon = $this->iconFactory
1141  ->getIconForRecord(‪$table, $row, IconSize::SMALL)
1142  ->setTitle(BackendUtility::getRecordIconAltText($row, ‪$table, false))
1143  ->render();
1144  $theData[$fCol] = ''
1145  . ($indent ? '<span class="indent indent-inline-block" style="--indent-level: ' . $indent . '"></span> ' : '')
1146  . (($this->clickMenuEnabled && !$this->‪isRecordDeletePlaceholder($row)) ? BackendUtility::wrapClickMenuOnIcon($icon, ‪$table, $row['uid']) : $icon);
1147  } elseif ($fCol === '_PATH_') {
1148  $theData[$fCol] = $this->‪recPath($row['pid']);
1149  } elseif ($fCol === '_REF_') {
1150  $theData[$fCol] = $this->‪generateReferenceToolTip($table, $row['uid']);
1151  } elseif ($fCol === '_CONTROL_') {
1152  $theData[$fCol] = $this->‪makeControl($table, $row);
1153  } elseif ($fCol === '_LOCALIZATION_') {
1154  // Language flag an title
1155  $theData[$fCol] = $this->‪languageFlag($table, $row);
1156  // Localize record
1157  $localizationPanel = $translationEnabled ? $this->‪makeLocalizationPanel($table, $row, $translations) : '';
1158  if ($localizationPanel !== '') {
1159  $theData['_LOCALIZATION_b'] = '<div class="btn-group">' . $localizationPanel . '</div>';
1160  $this->showLocalizeColumn[‪$table] = true;
1161  }
1162  } elseif ($fCol !== '_LOCALIZATION_b') {
1163  // default for all other columns, except "_LOCALIZATION_b"
1164  $pageId = ‪$table === 'pages' ? $row['uid'] : $row['pid'];
1165  $tmpProc = BackendUtility::getProcessedValueExtra(‪$table, $fCol, $row[$fCol], 100, $row['uid'], true, $pageId);
1166  $theData[$fCol] = $this->‪linkUrlMail(htmlspecialchars((string)$tmpProc), (string)($row[$fCol] ?? ''));
1167  }
1168  }
1169  // Reset the ID if it was overwritten
1170  if ((string)$this->searchString !== '') {
1171  $this->id = $id_orig;
1172  }
1173  // Add classes to table cells
1174  $this->addElement_tdCssClass['_SELECTOR_'] = 'col-checkbox';
1175  $this->addElement_tdCssClass[$titleCol] = 'col-title col-responsive' . $deletePlaceholderClass;
1176  $this->addElement_tdCssClass['__label'] = $this->addElement_tdCssClass[$titleCol];
1177  $this->addElement_tdCssClass['icon'] = 'col-icon';
1178  $this->addElement_tdCssClass['_CONTROL_'] = 'col-control';
1179  $this->addElement_tdCssClass['_PATH_'] = 'col-path';
1180  $this->addElement_tdCssClass['_LOCALIZATION_'] = 'col-localizationa';
1181  $this->addElement_tdCssClass['_LOCALIZATION_b'] = 'col-localizationb';
1182  // Create element in table cells:
1183  $theData['uid'] = $row['uid'];
1184  if (isset(‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'])
1185  && isset(‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'])
1186  ) {
1187  $theData['_l10nparent_'] = $row[‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField']];
1188  }
1189 
1190  $tagAttributes = array_map(
1191  static function (array|string $attributeValue): string {
1192  if (is_array($attributeValue)) {
1193  return implode(' ', $attributeValue);
1194  }
1195  return $attributeValue;
1196  },
1197  $tagAttributes
1198  );
1199 
1200  $rowOutput .= $this->‪addElement($theData, GeneralUtility::implodeAttributes($tagAttributes, true));
1201  // Finally, return table row element:
1202  return $rowOutput;
1203  }
1204 
1213  protected function ‪getReferenceCount($tableName, ‪$uid)
1214  {
1215  if (!isset($this->referenceCount[$tableName][‪$uid])) {
1216  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1217  $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, ‪$uid);
1218  $this->referenceCount[$tableName][‪$uid] = $numberOfReferences;
1219  }
1220  return $this->referenceCount[$tableName][‪$uid];
1221  }
1222 
1233  public function ‪renderListHeader(‪$table, $currentIdList)
1234  {
1235  $tsConfig = BackendUtility::getPagesTSconfig($this->id)['TCEFORM.'][$table . '.'] ?? null;
1236  $tsConfigOfTable = is_array($tsConfig) ? $tsConfig : null;
1237 
1238  $lang = $this->‪getLanguageService();
1239  // Init:
1240  $theData = [];
1241  // Traverse the fields:
1242  foreach ($this->‪fieldArray as $fCol) {
1243  // Calculate users permissions to edit records in the table:
1244  if (‪$table === 'pages') {
1245  $permsEdit = $this->calcPerms->editPagePermissionIsGranted();
1246  } else {
1247  $permsEdit = $this->calcPerms->editContentPermissionIsGranted();
1248  }
1249 
1250  $permsEdit = $permsEdit && $this->‪overlayEditLockPermissions($table);
1251  switch ((string)$fCol) {
1252  case '_SELECTOR_':
1253  if (‪$table !== 'pages' || !$this->‪showOnlyTranslatedRecords) {
1254  // Add checkbox actions for all tables except the special page translations table
1255  $theData[$fCol] = $this->‪renderCheckboxActions();
1256  } else {
1257  // Remove "_SELECTOR_", which is always the first item, from the field list
1258  array_splice($this->‪fieldArray, 0, 1);
1259  }
1260  break;
1261  case 'icon':
1262  // In case no checkboxes are rendered (page translations or disabled) add the icon
1263  // column, otherwise the selector column is using "colspan=2"
1264  if (!in_array('_SELECTOR_', $this->‪fieldArray, true)
1265  || ($table === 'pages' && $this->‪showOnlyTranslatedRecords)
1266  ) {
1267  $theData[$fCol] = '';
1268  }
1269  break;
1270  case '_CONTROL_':
1271  $theData[$fCol] = '<i class="hidden">' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._CONTROL_')) . '</i>';
1272  // In single table view, add button to edit displayed fields of marked / listed records
1273  if ($this->table && $permsEdit && is_array($currentIdList) && $this->‪isEditable($table)) {
1274  $label = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:editShownColumns'));
1275  $theData[$fCol] = '<button type="button"'
1276  . ' class="btn btn-default t3js-record-edit-multiple"'
1277  . ' title="' . $label . '"'
1278  . ' aria-label="' . $label . '"'
1279  . ' data-return-url="' . htmlspecialchars($this->‪listURL()) . '"'
1280  . ' data-columns-only="' . htmlspecialchars(implode(',', $this->‪fieldArray)) . '">'
1281  . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render()
1282  . '</button>';
1283  }
1284  break;
1285  case '_PATH_':
1286  // Path
1287  $theData[$fCol] = '<i>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_')) . '</i>';
1288  break;
1289  case '_REF_':
1290  // References
1291  $theData[$fCol] = '<i>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._REF_')) . '</i>';
1292  break;
1293  case '_LOCALIZATION_':
1294  // Show language of record
1295  $theData[$fCol] = '<i>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._LOCALIZATION_')) . '</i>';
1296  break;
1297  case '_LOCALIZATION_b':
1298  // Show translation options
1299  if ($this->showLocalizeColumn[‪$table] ?? false) {
1300  $theData[$fCol] = '<i>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:Localize')) . '</i>';
1301  }
1302  break;
1303  default:
1304  // Regular fields header
1305  $theData[$fCol] = '';
1306 
1307  // Check if $fCol is really a field and get the label and remove the colons at the end
1308  $sortLabel = BackendUtility::getItemLabel(‪$table, $fCol);
1309  if ($sortLabel !== null) {
1310  // Field label
1311  $fieldTSConfig = [];
1312  if (isset($tsConfigOfTable[$fCol . '.'])
1313  && is_array($tsConfigOfTable[$fCol . '.'])
1314  ) {
1315  $fieldTSConfig = $tsConfigOfTable[$fCol . '.'];
1316  }
1317  $sortLabel = $lang->translateLabel(
1318  $fieldTSConfig['label.'] ?? [],
1319  $fieldTSConfig['label'] ?? $sortLabel
1320  );
1321  $sortLabel = htmlspecialchars(rtrim(trim($sortLabel), ':'));
1322  } elseif ($specialLabel = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $fCol)) {
1323  // Special label exists for this field (Probably a management field, e.g. sorting)
1324  $sortLabel = htmlspecialchars($specialLabel);
1325  } else {
1326  // No TCA field, only output the $fCol variable with square brackets []
1327  $sortLabel = htmlspecialchars($fCol);
1328  $sortLabel = '<i>[' . rtrim(trim($sortLabel), ':') . ']</i>';
1329  }
1330 
1331  if ($this->table && is_array($currentIdList)) {
1332  // If the numeric clipboard pads are selected, show duplicate sorting link:
1333  if ($this->noControlPanels === false
1334  && $this->‪isClipboardFunctionalityEnabled($table)
1335  && $this->clipObj->current !== 'normal'
1336  ) {
1337  $theData[$fCol] .= '<a class="btn btn-default" href="' . htmlspecialchars($this->‪listURL() . '&duplicateField=' . $fCol)
1338  . '" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_duplicates')) . '">'
1339  . $this->iconFactory->getIcon('actions-document-duplicates-select', IconSize::SMALL)->render() . '</a>';
1340  }
1341  // If the table can be edited, add link for editing THIS field for all
1342  // listed records:
1343  if ($this->‪isEditable($table) && $permsEdit && (‪$GLOBALS['TCA'][‪$table]['columns'][$fCol] ?? false)) {
1344  $iTitle = sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:editThisColumn'), $sortLabel);
1345  $theData[$fCol] .= '<button type="button"'
1346  . ' class="btn btn-default t3js-record-edit-multiple"'
1347  . ' title="' . htmlspecialchars($iTitle) . '"'
1348  . ' aria-label="' . htmlspecialchars($iTitle) . '"'
1349  . ' data-return-url="' . htmlspecialchars($this->‪listURL()) . '"'
1350  . ' data-columns-only="' . htmlspecialchars($fCol) . '">'
1351  . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render()
1352  . '</button>';
1353  }
1354  if (strlen($theData[$fCol]) > 0) {
1355  $theData[$fCol] = '<div class="btn-group">' . $theData[$fCol] . '</div> ';
1356  }
1357  }
1358  $theData[$fCol] .= $this->‪addSortLink($sortLabel, $fCol, ‪$table);
1359  }
1360  }
1361 
1362  $event = $this->eventDispatcher->dispatch(
1363  new ModifyRecordListHeaderColumnsEvent($theData, ‪$table, $currentIdList, $this)
1364  );
1365 
1366  // Create and return header table row:
1367  return $this->‪addElement($event->getColumns(), GeneralUtility::implodeAttributes($event->getHeaderAttributes(), true), 'th');
1368  }
1369 
1375  protected function ‪renderListNavigation(string ‪$table, int $totalItems, int $itemsPerPage): string
1376  {
1377  $currentPage = ‪$this->page;
1378  $paginationColumns = count($this->‪fieldArray);
1379  $totalPages = (int)ceil($totalItems / $itemsPerPage);
1380  // Show page selector if not all records fit into one page
1381  if ($totalPages <= 1) {
1382  return '';
1383  }
1384  if ($totalItems > $currentPage * $itemsPerPage) {
1385  $lastElementNumber = $currentPage * $itemsPerPage;
1386  } else {
1387  $lastElementNumber = $totalItems;
1388  }
1389  $view = $this->backendViewFactory->create($this->request);
1390  return $view->assignMultiple([
1391  'currentUrl' => $this->‪listURL('', $table, 'pointer'),
1392  'currentPage' => $currentPage,
1393  'totalPages' => $totalPages,
1394  'firstElement' => ((($currentPage - 1) * $itemsPerPage) + 1),
1395  'lastElement' => $lastElementNumber,
1396  'colspan' => $paginationColumns,
1397  ])
1398  ->render('ListNavigation');
1399  }
1400 
1401  /*********************************
1402  *
1403  * Rendering of various elements
1404  *
1405  *********************************/
1406 
1415  public function ‪makeControl(‪$table, $row)
1416  {
1417  $backendUser = $this->‪getBackendUserAuthentication();
1418  $userTsConfig = $backendUser->getTSConfig();
1419  $rowUid = $row['uid'];
1420  if (isset($row['_ORIG_uid'])) {
1421  $rowUid = $row['_ORIG_uid'];
1422  }
1423  $isDeletePlaceHolder = $this->‪isRecordDeletePlaceholder($row);
1424  $cells = [
1425  'primary' => [],
1426  'secondary' => [],
1427  ];
1428 
1429  // Hide the move elements for localized records - doesn't make much sense to perform these options for them
1430  $isL10nOverlay = (int)($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? null] ?? 0) !== 0;
1431  $localCalcPerms = $this->‪getPagePermissionsForRecord($table, $row);
1432  if (‪$table === 'pages') {
1433  $permsEdit = ($backendUser->checkLanguageAccess($row[‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null] ?? 0))
1434  && $localCalcPerms->editPagePermissionIsGranted();
1435  } else {
1436  $permsEdit = $localCalcPerms->editContentPermissionIsGranted() && $backendUser->recordEditAccessInternals(‪$table, $row);
1437  }
1438  $permsEdit = $this->‪overlayEditLockPermissions($table, $row, $permsEdit);
1439 
1440  // "Show" link (only pages and tt_content elements)
1441  $tsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_list.'] ?? [];
1442  if ((
1443  $table === 'pages'
1444  && isset($row['doktype'])
1445  && !in_array((int)$row['doktype'], $this->‪getNoViewWithDokTypes($tsConfig), true)
1446  )
1447  || (
1448  ‪$table === 'tt_content'
1449  && isset($this->pageRow['doktype'])
1450  && !in_array((int)$this->pageRow['doktype'], $this->‪getNoViewWithDokTypes($tsConfig), true)
1451  )
1452  ) {
1453  if (!$isDeletePlaceHolder
1454  && ($attributes = $this->‪getPreviewUriBuilder($table, $row)->serializeDispatcherAttributes()) !== null
1455  ) {
1456  $viewAction = '<button'
1457  . ' type="button"'
1458  . ' class="btn btn-default" ' . $attributes
1459  . ' title="' . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">';
1460  if (‪$table === 'pages') {
1461  $viewAction .= $this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)->render();
1462  } else {
1463  $viewAction .= $this->iconFactory->getIcon('actions-view', IconSize::SMALL)->render();
1464  }
1465  $viewAction .= '</button>';
1466  $this->‪addActionToCellGroup($cells, $viewAction, 'view');
1467  } else {
1468  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'view');
1469  }
1470  } else {
1471  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'view');
1472  }
1473 
1474  // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
1475  if ($permsEdit && !$isDeletePlaceHolder && $this->‪isEditable($table)) {
1476  $params = [
1477  'edit' => [
1478  ‪$table => [
1479  $row['uid'] => 'edit',
1480  ],
1481  ],
1482  ];
1483  $iconIdentifier = 'actions-open';
1484  if (‪$table === 'pages') {
1485  // Disallow manual adjustment of the language field for pages
1486  $params['overrideVals']['pages']['sys_language_uid'] = $row[‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null] ?? 0;
1487  $iconIdentifier = 'actions-page-open';
1488  }
1489  $params['returnUrl'] = $this->‪listURL();
1490  $editLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
1491  $editAction = '<a class="btn btn-default" href="' . htmlspecialchars($editLink) . '"'
1492  . ' title="' . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')) . '">' . $this->iconFactory->getIcon($iconIdentifier, IconSize::SMALL)->render() . '</a>';
1493  } else {
1494  $editAction = ‪$this->spaceIcon;
1495  }
1496  $this->‪addActionToCellGroup($cells, $editAction, 'edit');
1497 
1498  // "Info"
1499  if (!$isDeletePlaceHolder) {
1500  $label = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo'));
1501  $viewBigAction = '<button type="button" aria-haspopup="dialog"'
1502  . ' class="btn btn-default" '
1503  . $this->‪createShowItemTagAttributes($table . ',' . ($row['uid'] ?? 0))
1504  . ' title="' . $label . '"'
1505  . ' aria-label="' . $label . '">'
1506  . $this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)->render()
1507  . '</button>';
1508  $this->‪addActionToCellGroup($cells, $viewBigAction, 'viewBig');
1509  } else {
1510  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'viewBig');
1511  }
1512 
1513  // "Move" wizard link for pages/tt_content elements:
1514  if ($permsEdit && (‪$table === 'tt_content' || ‪$table === 'pages') && $this->‪isEditable($table)) {
1515  if ($isL10nOverlay || $isDeletePlaceHolder) {
1516  $moveAction = ‪$this->spaceIcon;
1517  } elseif (‪$table === 'pages') {
1518  $linkTitleLL = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:move_page'));
1519  $icon = $this->iconFactory->getIcon('actions-page-move', IconSize::SMALL);
1520  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('move_page', [
1521  'uid' => $row['uid'],
1522  'table' => ‪$table,
1523  'expandPage' => $row['pid'] ?? 0,
1524  ]);
1525  $moveAction = '<typo3-move-record-wizard-button class="btn btn-default" subject="' . $linkTitleLL . '" url="' . htmlspecialchars(‪$url) . '" aria-label="' . $linkTitleLL . '" table="' . (string)‪$table . '">' . $icon->render() . '</a>';
1526  } else {
1527  $linkTitleLL = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:move_record'));
1528  $icon = $this->iconFactory->getIcon('actions-document-move', IconSize::SMALL);
1529  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('move_element', [
1530  'uid' => $row['uid'],
1531  'returnUrl' => $this->listURL(),
1532  ]);
1533  $moveAction = '<a class="btn btn-default" href="' . htmlspecialchars(‪$url) . '" aria-label="' . $linkTitleLL . '">' . $icon->render() . '</a>';
1534  }
1535  $this->‪addActionToCellGroup($cells, $moveAction, 'move');
1536  }
1537 
1538  // If the table is NOT a read-only table, then show these links:
1539  if ($this->‪isEditable($table)) {
1540  // "Revert" link (history/undo)
1541  if (\trim($userTsConfig['options.']['showHistory.'][‪$table] ?? $userTsConfig['options.']['showHistory'] ?? '1')) {
1542  if (!$isDeletePlaceHolder) {
1543  $moduleUrl = $this->uriBuilder->buildUriFromRoute('record_history', [
1544  'element' => ‪$table . ':' . $row['uid'],
1545  'returnUrl' => $this->‪listURL(),
1546  ]) . '#latest';
1547  $historyAction = '<a class="btn btn-default" href="' . htmlspecialchars($moduleUrl) . '" title="'
1548  . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:history')) . '">'
1549  . $this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL)->render() . '</a>';
1550  $this->‪addActionToCellGroup($cells, $historyAction, 'history');
1551  } else {
1552  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'history');
1553  }
1554  }
1555 
1556  // "Edit Perms" link:
1557  if (‪$table === 'pages' && $this->moduleProvider->accessGranted('permissions_pages', $backendUser)) {
1558  if ($isL10nOverlay || $isDeletePlaceHolder) {
1559  $permsAction = ‪$this->spaceIcon;
1560  } else {
1561  $params = [
1562  'id' => $row['uid'],
1563  'action' => 'edit',
1564  'returnUrl' => $this->‪listURL(),
1565  ];
1566  $href = (string)$this->uriBuilder->buildUriFromRoute('permissions_pages', $params);
1567  $permsAction = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="'
1568  . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:permissions')) . '">'
1569  . $this->iconFactory->getIcon('actions-lock', IconSize::SMALL)->render() . '</a>';
1570  }
1571  $this->‪addActionToCellGroup($cells, $permsAction, 'perms');
1572  }
1573 
1574  // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row
1575  // or if default values can depend on previous record):
1576  if (((‪$GLOBALS['TCA'][‪$table]['ctrl']['sortby'] ?? false) || (‪$GLOBALS['TCA'][‪$table]['ctrl']['useColumnsForDefaultValues'] ?? false)) && $permsEdit) {
1577  $neededPermission = ‪$table === 'pages' ? ‪Permission::PAGE_NEW : ‪Permission::CONTENT_EDIT;
1578  if ($this->calcPerms->isGranted($neededPermission)) {
1579  if ($isL10nOverlay || $isDeletePlaceHolder) {
1580  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'new');
1581  } elseif ($this->‪showNewRecLink($table)) {
1582  $params = [
1583  'edit' => [
1584  ‪$table => [
1585  (0 - (($row['_MOVE_PLH'] ?? 0) ? $row['_MOVE_PLH_uid'] : $row['uid'])) => 'new',
1586  ],
1587  ],
1588  'returnUrl' => $this->‪listURL(),
1589  ];
1590  $icon = (‪$table === 'pages' ? $this->iconFactory->getIcon('actions-page-new', IconSize::SMALL) : $this->iconFactory->getIcon('actions-plus', IconSize::SMALL));
1591  $titleLabel = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:new');
1592  if (‪$GLOBALS['TCA'][$table]['ctrl']['sortby'] ?? false) {
1593  $titleLabel = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:newRecord');
1594  if ($table === 'pages') {
1595  $titleLabel = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:newPage');
1596  }
1597  }
1598  $newLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
1599  $newAction = '<a class="btn btn-default" href="' . htmlspecialchars($newLink) . '" title="' . htmlspecialchars($titleLabel) . '">'
1600  . $icon->render() . '</a>';
1601  $this->‪addActionToCellGroup($cells, $newAction, 'new');
1602  }
1603  }
1604  }
1605 
1606  // "Hide/Unhide" links:
1607  $hiddenField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['enablecolumns']['disabled'] ?? null;
1608  if ($hiddenField !== null
1609  && !empty(‪$GLOBALS['TCA'][‪$table]['columns'][$hiddenField])
1610  && (empty(‪$GLOBALS['TCA'][‪$table]['columns'][$hiddenField]['exclude']) || $backendUser->check('non_exclude_fields', ‪$table . ':' . $hiddenField))
1611  ) {
1612  if (!$permsEdit || $isDeletePlaceHolder || $this->‪isRecordCurrentBackendUser($table, $row)) {
1613  $hideAction = ‪$this->spaceIcon;
1614  } else {
1615  $visibleTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($table === 'pages' ? 'Page' : ''));
1616  $visibleIcon = 'actions-edit-hide';
1617  $visibleValue = '0';
1618  $hiddenTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($table === 'pages' ? 'Page' : ''));
1619  $hiddenIcon = 'actions-edit-unhide';
1620  $hiddenValue = '1';
1621  if ($row[$hiddenField] ?? false) {
1622  $titleLabel = $hiddenTitle;
1623  $iconIdentifier = $hiddenIcon;
1624  $status = 'hidden';
1625  } else {
1626  $titleLabel = $visibleTitle;
1627  $iconIdentifier = $visibleIcon;
1628  $status = 'visible';
1629  }
1630  $attributesString = GeneralUtility::implodeAttributes(
1631  [
1632  'class' => 'btn btn-default',
1633  'type' => 'button',
1634  'title' => $titleLabel,
1635  'data-datahandler-action' => 'visibility',
1636  'data-datahandler-table' => ‪$table,
1637  'data-datahandler-uid' => $rowUid,
1638  'data-datahandler-field' => $hiddenField,
1639  'data-datahandler-status' => $status,
1640  'data-datahandler-visible-label' => $visibleTitle,
1641  'data-datahandler-visible-value' => $visibleValue,
1642  'data-datahandler-visible-icon' => $visibleIcon,
1643  'data-datahandler-hidden-label' => $hiddenTitle,
1644  'data-datahandler-hidden-value' => $hiddenValue,
1645  'data-datahandler-hidden-icon' => $hiddenIcon,
1646  ],
1647  true
1648  );
1649  $hideAction = '<button ' . $attributesString . '>'
1650  . $this->iconFactory->getIcon($iconIdentifier, IconSize::SMALL)
1651  . '</button>';
1652  }
1653  $this->‪addActionToCellGroup($cells, $hideAction, 'hide');
1654  }
1655 
1656  // "Up/Down" links
1657  if ($permsEdit && (‪$GLOBALS['TCA'][‪$table]['ctrl']['sortby'] ?? false) && !$this->sortField && !$this->searchLevels) {
1658  if (!$isL10nOverlay && !$isDeletePlaceHolder && isset($this->currentTable['prev'][$row['uid']])) {
1659  // Up
1660  $params = [];
1661  $params['redirect'] = $this->‪listURL();
1662  $params['cmd'][‪$table][$row['uid']]['move'] = $this->currentTable['prev'][$row['uid']];
1663  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
1664  $moveUpAction = '<a class="btn btn-default" href="' . htmlspecialchars(‪$url) . '" title="' . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">'
1665  . $this->iconFactory->getIcon('actions-move-up', IconSize::SMALL)->render() . '</a>';
1666  } else {
1667  $moveUpAction = ‪$this->spaceIcon;
1668  }
1669  $this->‪addActionToCellGroup($cells, $moveUpAction, 'moveUp');
1670 
1671  if (!$isL10nOverlay && !$isDeletePlaceHolder && !empty($this->currentTable['next'][$row['uid']])) {
1672  // Down
1673  $params = [];
1674  $params['redirect'] = $this->‪listURL();
1675  $params['cmd'][‪$table][$row['uid']]['move'] = $this->currentTable['next'][$row['uid']];
1676  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
1677  $moveDownAction = '<a class="btn btn-default" href="' . htmlspecialchars(‪$url) . '" title="' . htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">'
1678  . $this->iconFactory->getIcon('actions-move-down', IconSize::SMALL)->render() . '</a>';
1679  } else {
1680  $moveDownAction = ‪$this->spaceIcon;
1681  }
1682  $this->‪addActionToCellGroup($cells, $moveDownAction, 'moveDown');
1683  }
1684 
1685  // "Delete" link:
1686  $disableDelete = (bool)\trim((string)($userTsConfig['options.']['disableDelete.'][‪$table] ?? $userTsConfig['options.']['disableDelete'] ?? ''));
1687  if ($permsEdit
1688  && !$disableDelete
1689  && ((‪$table === 'pages' && $localCalcPerms->deletePagePermissionIsGranted()) || (‪$table !== 'pages' && $this->calcPerms->editContentPermissionIsGranted()))
1690  && !$this->‪isRecordCurrentBackendUser($table, $row)
1691  && !$isDeletePlaceHolder
1692  ) {
1693  $actionName = 'delete';
1694  $recordInfo = BackendUtility::getRecordTitle(‪$table, $row);
1695  if ($this->‪getBackendUserAuthentication()->shallDisplayDebugInformation()) {
1696  $recordInfo .= ' [' . ‪$table . ':' . $row['uid'] . ']';
1697  }
1698  $refCountMsg = BackendUtility::referenceCount(
1699  ‪$table,
1700  $row['uid'],
1701  LF . $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'),
1702  (string)$this->‪getReferenceCount($table, $row['uid'])
1703  ) . BackendUtility::translationCount(
1704  ‪$table,
1705  $row['uid'],
1706  LF . $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
1707  );
1708  $warningText = sprintf($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:' . $actionName . 'Warning'), trim($recordInfo)) . $refCountMsg;
1709  $params = 'cmd[' . ‪$table . '][' . $row['uid'] . '][delete]=1';
1710  $icon = $this->iconFactory->getIcon('actions-edit-' . $actionName, IconSize::SMALL)->render();
1711  $linkTitle = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:' . $actionName));
1712  $titleText = $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title');
1713  $l10nParentField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? '';
1714  $deleteAction = '<button type="button" class="btn btn-default t3js-record-delete"'
1715  . ' title="' . $linkTitle . '"'
1716  . ' aria-label="' . $linkTitle . '"'
1717  . ' aria-haspopup="dialog"'
1718  . ' data-button-ok-text="' . htmlspecialchars($linkTitle) . '"'
1719  . ' data-l10parent="' . ($l10nParentField ? htmlspecialchars((string)$row[$l10nParentField]) : '') . '"'
1720  . ' data-params="' . htmlspecialchars($params) . '"'
1721  . ' data-message="' . htmlspecialchars($warningText) . '"'
1722  . ' data-title="' . htmlspecialchars($titleText) . '">'
1723  . $icon
1724  . '</button>';
1725  } else {
1726  $deleteAction = ‪$this->spaceIcon;
1727  }
1728  $this->‪addActionToCellGroup($cells, $deleteAction, 'delete');
1729 
1730  // "Levels" links: Moving pages into new levels...
1731  if ($permsEdit && ‪$table === 'pages' && !$this->searchLevels) {
1732  // Up (Paste as the page right after the current parent page)
1733  if ($this->calcPerms->createPagePermissionIsGranted()) {
1734  if (!$isDeletePlaceHolder && !$isL10nOverlay) {
1735  $params = [];
1736  $params['redirect'] = $this->‪listURL();
1737  $params['cmd'][‪$table][$row['uid']]['move'] = -‪$this->id;
1738  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
1739  $label = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:prevLevel'));
1740  $moveLeftAction = '<a class="btn btn-default"'
1741  . ' href="' . htmlspecialchars(‪$url) . '"'
1742  . ' title="' . $label . '"'
1743  . ' aria-label="' . $label . '">'
1744  . $this->iconFactory->getIcon('actions-move-left', IconSize::SMALL)->render()
1745  . '</a>';
1746  $this->‪addActionToCellGroup($cells, $moveLeftAction, 'moveLeft');
1747  } else {
1748  $this->‪addActionToCellGroup($cells, $this->spaceIcon, 'moveLeft');
1749  }
1750  }
1751  // Down (Paste as subpage to the page right above)
1752  if (!$isL10nOverlay && !$isDeletePlaceHolder && !empty($this->currentTable['prevUid'][$row['uid']])) {
1753  $localCalcPerms = $this->‪getPagePermissionsForRecord(
1754  'pages',
1755  BackendUtility::getRecord('pages', $this->currentTable['prevUid'][$row['uid']]) ?? []
1756  );
1757  if ($localCalcPerms->createPagePermissionIsGranted()) {
1758  $params = [];
1759  $params['redirect'] = $this->‪listURL();
1760  $params['cmd'][‪$table][$row['uid']]['move'] = $this->currentTable['prevUid'][$row['uid']];
1761  ‪$url = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
1762  $label = htmlspecialchars($this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:nextLevel'));
1763  $moveRightAction = '<a class="btn btn-default"'
1764  . ' href="' . htmlspecialchars(‪$url) . '"'
1765  . ' title="' . $label . '"'
1766  . ' aria-label="' . $label . '">'
1767  . $this->iconFactory->getIcon('actions-move-right', IconSize::SMALL)->render() . '</a>';
1768  } else {
1769  $moveRightAction = ‪$this->spaceIcon;
1770  }
1771  } else {
1772  $moveRightAction = ‪$this->spaceIcon;
1773  }
1774  $this->‪addActionToCellGroup($cells, $moveRightAction, 'moveRight');
1775  }
1776  }
1777 
1778  // Add clipboard related actions
1779  $this->‪makeClip($table, $row, $cells);
1780 
1781  $event = $this->eventDispatcher->dispatch(
1782  new ModifyRecordListRecordActionsEvent($cells, ‪$table, $row, $this)
1783  );
1784 
1785  ‪$output = '';
1786  foreach ($event->getActions() as $classification => $actions) {
1787  if ($classification !== 'primary') {
1788  $cellOutput = '';
1789  foreach ($actions as $action) {
1790  if ($action === $this->spaceIcon) {
1791  continue;
1792  }
1793  // This is a backwards-compat layer for the existing hook items, which will be removed in TYPO3 v12.
1794  $action = str_replace('btn btn-default', 'dropdown-item dropdown-item-spaced', $action);
1795  $title = [];
1796  preg_match('/title="([^"]*)"/', $action, $title);
1797  if (empty($title)) {
1798  preg_match('/aria-label="([^"]*)"/', $action, $title);
1799  }
1800  if (!empty($title[1] ?? '')) {
1801  $action = str_replace(
1802  [
1803  '</a>',
1804  '</button>',
1805  ],
1806  [
1807  ' ' . $title[1] . '</a>',
1808  ' ' . $title[1] . '</button>',
1809  ],
1810  $action
1811  );
1812  // In case we added the title as tag content, we can remove the attribute,
1813  // since this is duplicated and would trigger a tooltip with the same content.
1814  if (!empty($title[0] ?? '')) {
1815  $action = str_replace($title[0], '', $action);
1816  }
1817  }
1818  $cellOutput .= '<li>' . $action . '</li>';
1819  }
1820 
1821  if ($cellOutput !== '') {
1822  $icon = $this->iconFactory->getIcon('actions-menu-alternative', IconSize::SMALL);
1823  $title = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more');
1824  ‪$output .= ' <div class="btn-group dropdown" title="' . htmlspecialchars($title) . '">' .
1825  '<a href="#actions_' . ‪$table . '_' . $row['uid'] . '" class="btn btn-default dropdown-toggle dropdown-toggle-no-chevron" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">' . $icon->render() . '</a>' .
1826  '<ul id="actions_' . ‪$table . '_' . $row['uid'] . '" class="dropdown-menu">' . $cellOutput . '</ul>' .
1827  '</div>';
1828  } else {
1829  ‪$output .= ' <div class="btn-group">' . $this->spaceIcon . '</div>';
1830  }
1831  } else {
1832  ‪$output .= ' <div class="btn-group">' . implode('', $actions) . '</div>';
1833  }
1834  }
1835 
1836  return ‪$output;
1837  }
1838 
1846  public function ‪makeClip(string ‪$table, array $row, array &$cells): void
1847  {
1848  // Return, if disabled:
1849  if (!$this->‪isClipboardFunctionalityEnabled($table, $row)) {
1850  return;
1851  }
1852  $clipboardCells = [];
1853  $isEditable = $this->‪isEditable($table);
1854 
1855  if ($this->clipObj->current !== 'normal') {
1856  $clipboardCells['copy'] = $clipboardCells['cut'] = ‪$this->spaceIcon;
1857  } else {
1858  $this->‪addDividerToCellGroup($cells);
1859  $isSel = $this->clipObj->isSelected(‪$table, $row['uid']);
1860 
1861  $copyTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSel === 'copy' ? 'copyrelease' : 'copy'));
1862  $copyUrl = $this->clipObj->selUrlDB(‪$table, (int)$row['uid'], true, $isSel === 'copy');
1863  $clipboardCells['copy'] = '
1864  <a class="btn btn-default" href="' . htmlspecialchars($copyUrl) . '" title="' . htmlspecialchars($copyTitle) . '" aria-label="' . htmlspecialchars($copyTitle) . '">
1865  ' . $this->iconFactory->getIcon($isSel === 'copy' ? 'actions-edit-copy-release' : 'actions-edit-copy', IconSize::SMALL)->render() . '
1866  </a>';
1867 
1868  // Calculate permission to cut page or content
1869  if (‪$table === 'pages') {
1870  $localCalcPerms = $this->‪getPagePermissionsForRecord('pages', $row);
1871  $permsEdit = $localCalcPerms->editPagePermissionIsGranted();
1872  } else {
1873  $permsEdit = $this->calcPerms->editContentPermissionIsGranted() && $this->‪getBackendUserAuthentication()->recordEditAccessInternals($table, $row);
1874  }
1875  if (!$isEditable || !$this->‪overlayEditLockPermissions($table, $row, $permsEdit)) {
1876  $clipboardCells['cut'] = ‪$this->spaceIcon;
1877  } else {
1878  $cutTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($isSel === 'cut' ? 'cutrelease' : 'cut'));
1879  $cutUrl = $this->clipObj->selUrlDB(‪$table, (int)$row['uid'], false, $isSel === 'cut');
1880  $clipboardCells['cut'] = '
1881  <a class="btn btn-default" href="' . htmlspecialchars($cutUrl) . '" title="' . htmlspecialchars($cutTitle) . '" aria-label="' . htmlspecialchars($cutTitle) . '">
1882  ' . $this->iconFactory->getIcon($isSel === 'cut' ? 'actions-edit-cut-release' : 'actions-edit-cut', IconSize::SMALL)->render() . '
1883  </a>';
1884  }
1885  }
1886 
1887  // Now, looking for selected elements from the current table:
1888  $elFromTable = $this->clipObj->elFromTable(‪$table);
1889  if (!$isEditable
1890  || empty(‪$GLOBALS['TCA'][‪$table]['ctrl']['sortby'])
1891  || $this->clipObj->elFromTable(‪$table) === []
1892  || !$this->overlayEditLockPermissions(‪$table, $row)
1893  ) {
1894  $clipboardCells['pasteAfter'] = ‪$this->spaceIcon;
1895  } else {
1896  $this->‪addDividerToCellGroup($cells);
1897  $pasteAfterUrl = $this->clipObj->pasteUrl(‪$table, -$row['uid']);
1898  $pasteAfterTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_pasteAfter');
1899  $pasteAfterContent = $this->clipObj->confirmMsgText(‪$table, $row, 'after', $elFromTable);
1900  $clipboardCells['pasteAfter'] = '
1901  <button type="button" class="btn btn-default t3js-modal-trigger" data-severity="warning" aria-haspopup="dialog" title="' . htmlspecialchars($pasteAfterTitle) . '" aria-label="' . htmlspecialchars($pasteAfterTitle) . '" data-uri="' . htmlspecialchars($pasteAfterUrl) . '" data-bs-content="' . htmlspecialchars($pasteAfterContent) . '">
1902  ' . $this->iconFactory->getIcon('actions-document-paste-after', IconSize::SMALL)->render() . '
1903  </button>';
1904  }
1905 
1906  // Now, looking for elements in general:
1907  if (‪$table !== 'pages' || !$isEditable || $this->clipObj->elFromTable() === []) {
1908  $clipboardCells['pasteInto'] = ‪$this->spaceIcon;
1909  } else {
1911  $pasteIntoUrl = $this->clipObj->pasteUrl('', $row['uid']);
1912  $pasteIntoTitle = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_pasteInto');
1913  $pasteIntoContent = $this->clipObj->confirmMsgText(‪$table, $row, 'into', $elFromTable);
1914  $clipboardCells['pasteInto'] = '
1915  <button type="button" class="btn btn-default t3js-modal-trigger" aria-haspopup="dialog" data-severity="warning" title="' . htmlspecialchars($pasteIntoTitle) . '" aria-label="' . htmlspecialchars($pasteIntoTitle) . '" data-uri="' . htmlspecialchars($pasteIntoUrl) . '" data-bs-content="' . htmlspecialchars($pasteIntoContent) . '">
1916  ' . $this->iconFactory->getIcon('actions-document-paste-into', IconSize::SMALL)->render() . '
1917  </button>';
1918  }
1919 
1920  // Add the clipboard actions to the cell group
1921  foreach ($clipboardCells as $key => $value) {
1922  $this->‪addActionToCellGroup($cells, $value, $key);
1923  }
1924  }
1925 
1933  public function ‪makeCheckbox(string ‪$table, array $row): string
1934  {
1935  // Early return if current record is a "delete placeholder" or a translation
1936  if ($this->‪isRecordDeletePlaceholder($row)
1937  || (int)($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? null] ?? 0) !== 0
1938  ) {
1939  return '';
1940  }
1941 
1942  // In case clipObj is not set, just add a checkbox without any clipboard functionality
1943  if ($this->clipObj === null) {
1944  return '
1945  <span class="form-check form-check-type-toggle">
1946  <input class="form-check-input t3js-multi-record-selection-check" type="checkbox" />
1947  </span>';
1948  }
1949 
1950  // For the numeric clipboard pads (showing checkboxes where one can select elements on/off)
1951  // Setting name of the element in ->CBnames array:
1952  ‪$identifier = ‪$table . '|' . $row['uid'];
1953  $this->CBnames[] = ‪$identifier;
1954  $isSelected = false;
1955  // If the "duplicateField" value is set then select all elements which are duplicates...
1956  if ($this->duplicateField && isset($row[$this->duplicateField])) {
1957  $isSelected = in_array((string)$row[$this->duplicateField], $this->duplicateStack, true);
1958  $this->duplicateStack[] = (string)$row[$this->duplicateField];
1959  }
1960  // Adding the checkbox to the panel:
1961  return '
1962  <span class="form-check form-check-type-toggle">
1963  <input class="form-check-input t3js-multi-record-selection-check" type="checkbox" name="CBC[' . ‪$identifier . ']" value="1" ' . ($isSelected ? 'checked="checked" ' : '') . '/>
1964  </span>';
1965  }
1966 
1973  public function ‪makeLocalizationPanel(‪$table, $row, array $translations): string
1974  {
1975  $out = '';
1976  // All records excluding pages
1978  if (‪$table === 'pages') {
1979  // Calculate possible translations for pages
1980  ‪$possibleTranslations = array_map(static fn(SiteLanguage $siteLanguage): int => $siteLanguage->getLanguageId(), $this->languagesAllowedForUser);
1981  ‪$possibleTranslations = array_filter(‪$possibleTranslations, static fn(int $languageUid): bool => $languageUid > 0);
1982  }
1983 
1984  // Traverse page translations and add icon for each language that does NOT yet exist and is included in site configuration:
1985  $pageId = (int)(‪$table === 'pages' ? $row['uid'] : $row['pid']);
1986  $languageInformation = $this->translateTools->getSystemLanguages($pageId);
1987 
1988  foreach (‪$possibleTranslations as $lUid_OnPage) {
1989  if ($this->‪isEditable($table)
1990  && !$this->‪isRecordDeletePlaceholder($row)
1991  && !isset($translations[$lUid_OnPage])
1992  && $this->‪getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)
1993  ) {
1994  $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute(
1995  'record_edit',
1996  [
1997  'justLocalized' => ‪$table . ':' . $row['uid'] . ':' . $lUid_OnPage,
1998  'returnUrl' => $this->listURL(),
1999  ]
2000  );
2001  $params = [];
2002  $params['redirect'] = $redirectUrl;
2003  $params['cmd'][‪$table][$row['uid']]['localize'] = $lUid_OnPage;
2004  $href = (string)$this->uriBuilder->buildUriFromRoute('tce_db', $params);
2005  $title = htmlspecialchars($languageInformation[$lUid_OnPage]['title'] ?? '');
2006 
2007  $lC = ($languageInformation[$lUid_OnPage]['flagIcon'] ?? false)
2008  ? $this->iconFactory->getIcon($languageInformation[$lUid_OnPage]['flagIcon'], IconSize::SMALL)->render()
2009  : $title;
2010 
2011  $out .= '<a href="' . htmlspecialchars($href) . '"'
2012  . ' class="btn btn-default t3js-action-localize"'
2013  . ' title="' . $title . '">'
2014  . $lC . '</a> ';
2015  }
2016  }
2017  return $out;
2018  }
2019 
2020  /*********************************
2021  *
2022  * Helper functions
2023  *
2024  *********************************/
2025 
2036  public function ‪addSortLink($label, $field, ‪$table): string
2037  {
2038  // Certain circumstances just return string right away (no links):
2039  if ($this->disableSingleTableView
2040  || in_array($field, ['_SELECTOR', '_CONTROL_', '_LOCALIZATION_', '_REF_'], true)
2041  ) {
2042  return $label;
2043  }
2044 
2045  // If "_PATH_" (showing record path) is selected, force sorting by pid field (will at least group the records!)
2046  if ($field === '_PATH_') {
2047  $field = 'pid';
2048  }
2049 
2050  // Create the sort link:
2051  ‪$url = $this->‪listURL('', $table, 'sortField,sortRev,table,pointer')
2052  . '&sortField=' . $field . '&sortRev=' . ($this->sortRev || $this->sortField != $field ? 0 : 1);
2053  $icon = $this->sortField === $field
2054  ? $this->iconFactory->getIcon('actions-sort-amount-' . ($this->sortRev ? 'down' : 'up'), IconSize::SMALL)->render()
2055  : $this->iconFactory->getIcon('actions-sort-amount', IconSize::SMALL)->render();
2056 
2057  // Return linked field:
2058  $attributes = [
2059  'class' => 'table-sorting-button ' . ($this->sortField === $field ? 'table-sorting-button-active' : ''),
2060  'href' => ‪$url,
2061  ];
2062 
2063  return '<a ' . GeneralUtility::implodeAttributes($attributes, true) . '>
2064  <span class="table-sorting-label">' . $label . '</span>
2065  <span class="table-sorting-icon">' . $icon . '</span>
2066  </a>';
2067  }
2068 
2077  public function ‪recPath($pid)
2078  {
2079  if (!isset($this->recPath_cache[$pid])) {
2080  $this->recPath_cache[$pid] = BackendUtility::getRecordPath($pid, $this->perms_clause, 20);
2081  }
2082  return $this->recPath_cache[$pid];
2083  }
2084 
2089  protected function ‪getPagePermissionsForRecord(string ‪$table, array $row): ‪Permission
2090  {
2091  // If the listed table is 'pages' we have to request the permission settings for each page.
2092  // If the listed table is not 'pages' we have to request the permission settings from the parent page
2093  $pageId = (int)(‪$table === 'pages' ? ($row['l10n_parent'] ?: $row['uid']) : $row['pid']);
2094  if (!isset($this->pagePermsCache[$pageId])) {
2095  $this->pagePermsCache[$pageId] = new ‪Permission($this->‪getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $pageId)));
2096  }
2097  return $this->pagePermsCache[$pageId];
2098  }
2099 
2106  public function ‪showNewRecLink(‪$table)
2107  {
2108  // No deny/allow tables are set:
2109  if (empty($this->allowedNewTables) && empty($this->deniedNewTables)) {
2110  return true;
2111  }
2112  return !in_array(‪$table, $this->deniedNewTables)
2113  && (empty($this->allowedNewTables) || in_array(‪$table, $this->allowedNewTables));
2114  }
2115 
2123  public function ‪addActionToCellGroup(&$cells, $action, $actionKey)
2124  {
2125  $cellsMap = [
2126  'primary' => [
2127  'edit', 'hide', 'delete', 'moveUp', 'moveDown',
2128  ],
2129  'secondary' => [
2130  'view', 'viewBig', 'history', 'stat', 'perms', 'new', 'move', 'moveLeft', 'moveRight', 'version', 'divider', 'copy', 'cut', 'pasteAfter', 'pasteInto',
2131  ],
2132  ];
2133  $classification = in_array($actionKey, $cellsMap['primary']) ? 'primary' : 'secondary';
2134  $cells[$classification][$actionKey] = $action;
2135  unset($cells[$actionKey]);
2136  }
2137 
2145  protected function ‪isRecordCurrentBackendUser(‪$table, $row)
2146  {
2147  return ‪$table === 'be_users' && (int)($row['uid'] ?? 0) === (int)$this->‪getBackendUserAuthentication()->user['uid'];
2148  }
2149 
2153  protected function ‪isRecordDeletePlaceholder(array $row): bool
2154  {
2155  return $this->‪getBackendUserAuthentication()->workspace > 0
2156  && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER;
2157  }
2158 
2159  public function ‪setIsEditable(bool $isEditable): void
2160  {
2161  $this->editable = $isEditable;
2162  }
2167  public function ‪isEditable(string ‪$table): bool
2168  {
2169  $backendUser = $this->‪getBackendUserAuthentication();
2170  return !(‪$GLOBALS['TCA'][‪$table]['ctrl']['readOnly'] ?? false)
2171  && $this->editable
2172  && ($backendUser->isAdmin() || $backendUser->check('tables_modify', ‪$table))
2173  && (BackendUtility::isTableWorkspaceEnabled(‪$table) || $backendUser->workspaceAllowsLiveEditingInTable(‪$table));
2174  }
2175 
2186  protected function ‪overlayEditLockPermissions(‪$table, $row = [], $editPermission = true)
2187  {
2188  if ($editPermission && !$this->‪getBackendUserAuthentication()->isAdmin()) {
2189  // If no $row is submitted we only check for general edit lock of current page (except for table "pages")
2190  $pageHasEditLock = !empty($this->pageRow['editlock']);
2191  if (empty($row)) {
2192  return (‪$table === 'pages') || !$pageHasEditLock;
2193  }
2194  if ((‪$table === 'pages' && ($row['editlock'] ?? false)) || (‪$table !== 'pages' && $pageHasEditLock)) {
2195  $editPermission = false;
2196  } elseif (isset(‪$GLOBALS['TCA'][‪$table]['ctrl']['editlock']) && ($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['editlock']] ?? false)) {
2197  $editPermission = false;
2198  }
2199  }
2200  return $editPermission;
2201  }
2202 
2203  public function ‪setModuleData(ModuleData ‪$moduleData): void
2204  {
2205  $this->moduleData = ‪$moduleData;
2206  }
2207 
2218  public function ‪start(‪$id, ‪$table, $pointer, $search = '', $levels = 0, ‪$showLimit = 0)
2219  {
2220  $backendUser = $this->‪getBackendUserAuthentication();
2221  // Setting internal variables:
2222  // sets the parent id
2223  $this->id = (int)‪$id;
2224  if (‪$GLOBALS['TCA'][‪$table] ?? false) {
2225  // Setting single table mode, if table exists:
2226  $this->table = ‪$table;
2227  }
2228  $this->page = ‪MathUtility::forceIntegerInRange((int)$pointer, 1, 1000);
2229  $this->showLimit = ‪MathUtility::forceIntegerInRange((int)‪$showLimit, 0, 10000);
2230  $this->searchString = trim($search);
2231  $this->searchLevels = (int)$levels;
2232  $this->sortField = (string)($this->request->getParsedBody()['sortField'] ?? $this->request->getQueryParams()['sortField'] ?? '');
2233  $this->sortRev = (bool)($this->request->getParsedBody()['sortRev'] ?? $this->request->getQueryParams()['sortRev'] ?? false);
2234  $this->duplicateField = (string)($this->request->getParsedBody()['duplicateField'] ?? $this->request->getQueryParams()['duplicateField'] ?? '');
2235 
2236  // If there is a current link to a record, set the current link uid and get the table name from the link handler configuration
2237  $currentLinkValue = trim($this->overrideUrlParameters['P']['currentValue'] ?? '');
2238  if ($currentLinkValue) {
2239  $linkService = GeneralUtility::makeInstance(LinkService::class);
2240  try {
2241  $currentLinkParts = $linkService->resolve($currentLinkValue);
2242  if ($currentLinkParts['type'] === 'record' && isset($currentLinkParts['identifier'])) {
2243  $this->currentLink['tableNames'] = ‪$this->tableList;
2244  $this->currentLink['uid'] = (int)$currentLinkParts['uid'];
2245  }
2246  } catch (UnknownLinkHandlerException $e) {
2247  }
2248  }
2249 
2250  // $table might be NULL at this point in the code. As the expressionBuilder
2251  // is used to limit returned records based on the page permissions and the
2252  // uid field of the pages it can hardcoded to work on the pages table.
2253  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2254  ->getQueryBuilderForTable('pages')
2255  ->expr();
2256  $permsClause = $expressionBuilder->and($backendUser->getPagePermsClause(‪Permission::PAGE_SHOW));
2257  // This will hide records from display - it has nothing to do with user rights!!
2258  $pidList = ‪GeneralUtility::intExplode(',', (string)($backendUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? ''), true);
2259  if (!empty($pidList)) {
2260  $permsClause = $permsClause->with($expressionBuilder->notIn('pages.uid', $pidList));
2261  }
2262  $this->perms_clause = (string)$permsClause;
2263 
2264  $this->possibleTranslations = $this->‪getPossibleTranslations($this->id);
2265  $this->setFields = $this->‪getBackendUserAuthentication()->getModuleData('list/displayFields') ?? [];
2266  }
2267 
2273  public function ‪generateList(): string
2274  {
2275  $tableNames = $this->‪getTablesToRender();
2276  ‪$output = '';
2277  foreach ($tableNames as $tableName) {
2278  ‪$output .= $this->‪getTable($tableName);
2279  }
2280  return ‪$output;
2281  }
2282 
2289  protected function ‪getTablesToRender(): array
2290  {
2291  $hideTablesArray = ‪GeneralUtility::trimExplode(',', $this->hideTables);
2292  $backendUser = $this->‪getBackendUserAuthentication();
2293 
2294  // pre-process tables and add sorting instructions
2295  $tableNames = array_flip(array_keys(‪$GLOBALS['TCA']));
2296  foreach ($tableNames as $tableName => $_) {
2297  $hideTable = false;
2298 
2299  // Checking if the table should be rendered:
2300  // Checks that we see only permitted/requested tables:
2301  if (($this->table && $tableName !== $this->table)
2302  || ($this->tableList && !‪GeneralUtility::inList($this->tableList, (string)$tableName))
2303  || !$backendUser->check('tables_select', $tableName)
2304  ) {
2305  $hideTable = true;
2306  }
2307 
2308  if (!$hideTable) {
2309  // Don't show table if hidden by TCA ctrl section
2310  // Don't show table if hidden by page TSconfig mod.web_list.hideTables
2311  $hideTable = !empty(‪$GLOBALS['TCA'][$tableName]['ctrl']['hideTable'])
2312  || in_array($tableName, $hideTablesArray, true)
2313  || in_array('*', $hideTablesArray, true);
2314  // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table
2315  $hideTable = (bool)($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'] ?? $hideTable);
2316  }
2317  if ($hideTable) {
2318  unset($tableNames[$tableName]);
2319  } else {
2320  if (isset($this->tableDisplayOrder[$tableName])) {
2321  // Copy display order information
2322  $tableNames[$tableName] = $this->tableDisplayOrder[$tableName];
2323  } else {
2324  $tableNames[$tableName] = [];
2325  }
2326  }
2327  }
2328  try {
2329  $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class)
2330  ->orderByDependencies($tableNames);
2331  } catch (\UnexpectedValueException $e) {
2332  // If you have circular dependencies we just keep the original order and give a notice
2333  // Example mod.web_list.tableDisplayOrder.pages.after = tt_content
2334  $lang = $this->‪getLanguageService();
2335  $header = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.tableDisplayOrder.title');
2336  $msg = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.tableDisplayOrder.message');
2337  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $header, ContextualFeedbackSeverity::WARNING, true);
2338  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
2339  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
2340  $defaultFlashMessageQueue->enqueue($flashMessage);
2341  $orderedTableNames = $tableNames;
2342  }
2343  return array_keys($orderedTableNames);
2344  }
2345 
2353  public function ‪getQueryBuilder(
2354  string ‪$table,
2355  array ‪$fields = ['*'],
2356  bool $addSorting = true,
2357  int $firstResult = 0,
2358  int $maxResult = 0
2359  ): QueryBuilder {
2360  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2361  ->getQueryBuilderForTable(‪$table);
2362  $queryBuilder->getRestrictions()
2363  ->removeAll()
2364  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2365  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUserAuthentication()->workspace));
2366  $queryBuilder
2367  ->select(...‪$fields)
2368  ->from(‪$table);
2369 
2370  // Additional constraints
2371  if ((‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'] ?? false)
2372  && (‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? false)) {
2373  // Only restrict to the default language if no search request is in place
2374  // And if only translations should be shown
2375  if ($this->searchString === '' && !$this->‪showOnlyTranslatedRecords) {
2376  $queryBuilder->andWhere(
2377  $queryBuilder->expr()->or(
2378  $queryBuilder->expr()->lte(‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'], 0),
2379  $queryBuilder->expr()->eq(‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'], 0)
2380  )
2381  );
2382  }
2383  }
2384  if (‪$table === 'pages' && $this->‪showOnlyTranslatedRecords) {
2385  $queryBuilder->andWhere($queryBuilder->expr()->in(
2386  ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'],
2387  $queryBuilder->quoteArrayBasedValueListToIntegerList(
2388  array_keys($this->languagesAllowedForUser)
2389  )
2390  ));
2391  }
2392  // Former prepareQueryBuilder
2393  if ($maxResult > 0) {
2394  $queryBuilder->setMaxResults($maxResult);
2395  }
2396  if ($firstResult > 0) {
2397  $queryBuilder->setFirstResult($firstResult);
2398  }
2399  if ($addSorting) {
2400  if ($this->sortField && in_array($this->sortField, BackendUtility::getAllowedFieldsForTable(‪$table, false))) {
2401  $queryBuilder->orderBy($this->sortField, $this->sortRev ? 'DESC' : 'ASC');
2402  } else {
2403  $orderBy = (‪$GLOBALS['TCA'][‪$table]['ctrl']['sortby'] ?? '') ?: ‪$GLOBALS['TCA'][‪$table]['ctrl']['default_sortby'] ?? '';
2404  $orderBys = ‪QueryHelper::parseOrderBy($orderBy);
2405  foreach ($orderBys as $orderBy) {
2406  $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]);
2407  }
2408  }
2409  }
2410 
2411  // Build the query constraints
2412  $queryBuilder = $this->‪addPageIdConstraint($table, $queryBuilder, $this->searchLevels);
2413  $searchWhere = $this->‪makeSearchString($table, $this->id, $queryBuilder);
2414  if (!empty($searchWhere)) {
2415  $queryBuilder->andWhere($searchWhere);
2416  }
2417 
2418  // Filtering on displayable pages (permissions):
2419  if (‪$table === 'pages' && $this->perms_clause) {
2420  $queryBuilder->andWhere($this->perms_clause);
2421  }
2422 
2423  // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set
2424  if (!empty(‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'])
2425  && (‪GeneralUtility::inList($this->hideTranslations, ‪$table) || $this->hideTranslations === '*')
2426  ) {
2427  $queryBuilder->andWhere(
2428  $queryBuilder->expr()->eq(
2429  ‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'],
2430  0
2431  )
2432  );
2433  } elseif (!empty(‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField']) && $this->‪showOnlyTranslatedRecords) {
2434  // When only translated records should be shown, it is necessary to use l10n_parent=pageId, instead of
2435  // a check to the PID
2436  $queryBuilder->andWhere(
2437  $queryBuilder->expr()->eq(
2438  ‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'],
2439  $queryBuilder->createNamedParameter(
2440  $this->id,
2442  )
2443  )
2444  );
2445  }
2446 
2447  // @todo This event should contain the $addSorting value, so listener knows when to add ORDER-BY stuff.
2448  // Additionally, having QueryBuilder order-by with `addSorting: false` should be deprecated along
2449  // with the additional event flag.
2451  $queryBuilder,
2452  ‪$table,
2453  $this->id,
2454  ‪$fields,
2455  $firstResult,
2456  $maxResult,
2457  $this
2458  );
2459  $this->eventDispatcher->dispatch($event);
2460  return $event->getQueryBuilder();
2461  }
2462 
2471  protected function ‪makeSearchString(string ‪$table, int $currentPid, QueryBuilder $queryBuilder)
2472  {
2473  $expressionBuilder = $queryBuilder->expr();
2474  $constraints = [];
2475  $tablePidField = ‪$table === 'pages' ? 'uid' : 'pid';
2476  // Make query only if table is valid and a search string is actually defined
2477  if (empty($this->searchString)) {
2478  return '';
2479  }
2480 
2481  $searchableFields = [];
2482  // Get fields from ctrl section of TCA first
2483  if (isset(‪$GLOBALS['TCA'][‪$table]['ctrl']['searchFields'])) {
2484  $searchableFields = ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TCA'][‪$table]['ctrl']['searchFields'], true);
2485  }
2486 
2487  if (‪MathUtility::canBeInterpretedAsInteger($this->searchString)) {
2488  $constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString);
2489  foreach ($searchableFields as $fieldName) {
2490  if (!isset(‪$GLOBALS['TCA'][‪$table]['columns'][$fieldName])) {
2491  continue;
2492  }
2493  $fieldConfig = ‪$GLOBALS['TCA'][‪$table]['columns'][$fieldName]['config'];
2494  $fieldType = $fieldConfig['type'];
2495  if (($fieldType === 'number' && ($fieldConfig['format'] ?? 'integer') === 'integer')
2496  || ($fieldType === 'datetime' && !in_array($fieldConfig['dbType'] ?? '', ‪QueryHelper::getDateTimeTypes(), true))
2497  ) {
2498  if (!isset($fieldConfig['search']['pidonly'])
2499  || ($fieldConfig['search']['pidonly'] && $currentPid > 0)
2500  ) {
2501  $constraints[] = $expressionBuilder->and(
2502  $expressionBuilder->eq($fieldName, (int)$this->searchString),
2503  $expressionBuilder->eq($tablePidField, (int)$currentPid)
2504  );
2505  }
2506  } elseif ($this->‪isTextFieldType($fieldType)) {
2507  $constraints[] = $expressionBuilder->like(
2508  $fieldName,
2509  $queryBuilder->quote('%' . $this->searchString . '%')
2510  );
2511  }
2512  }
2513  } elseif (!empty($searchableFields)) {
2514  $like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%');
2515  foreach ($searchableFields as $fieldName) {
2516  if (!isset(‪$GLOBALS['TCA'][‪$table]['columns'][$fieldName])) {
2517  continue;
2518  }
2519  $fieldConfig = ‪$GLOBALS['TCA'][‪$table]['columns'][$fieldName]['config'];
2520  $fieldType = $fieldConfig['type'];
2521  $searchConstraint = $expressionBuilder->and(
2522  $expressionBuilder->comparison(
2523  'LOWER(' . $queryBuilder->castFieldToTextType($fieldName) . ')',
2524  'LIKE',
2525  'LOWER(' . $like . ')'
2526  )
2527  );
2528  if (is_array($fieldConfig['search'] ?? null)) {
2529  $searchConfig = $fieldConfig['search'];
2530  if ($searchConfig['case'] ?? false) {
2531  // Replace case insensitive default constraint
2532  $searchConstraint = $expressionBuilder->and($expressionBuilder->like($fieldName, $like));
2533  }
2534  if (($searchConfig['pidonly'] ?? false) && $currentPid > 0) {
2535  $searchConstraint = $searchConstraint->with($expressionBuilder->eq($tablePidField, (int)$currentPid));
2536  }
2537  if ($searchConfig['andWhere'] ?? false) {
2538  $searchConstraint = $searchConstraint->with(
2539  ‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), ‪QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere']))
2540  );
2541  }
2542  }
2543  if ($this->‪isTextFieldType($fieldType) && $searchConstraint->count() !== 0) {
2544  $constraints[] = $searchConstraint;
2545  }
2546  }
2547  }
2548  // If no search field conditions have been built ensure no results are returned
2549  if (empty($constraints)) {
2550  return '0=1';
2551  }
2552 
2553  return (string)$expressionBuilder->or(...$constraints);
2554  }
2555 
2564  public function ‪linkWrapTable(string ‪$table, string $label): string
2565  {
2566  if ($this->table !== ‪$table) {
2567  ‪$url = $this->‪listURL('', $table, 'pointer');
2568  } else {
2569  ‪$url = $this->‪listURL('', '', 'sortField,sortRev,table,pointer');
2570  }
2571  return '<a href="' . htmlspecialchars(‪$url) . '">' . $label . '</a>';
2572  }
2573 
2583  public function ‪linkWrapItems(‪$table, ‪$uid, $code, $row)
2584  {
2585  $lang = $this->‪getLanguageService();
2586  $origCode = $code;
2587  // If the title is blank, make a "no title" label:
2588  if ((string)$code === '') {
2589  $code = '<i>[' . htmlspecialchars(
2590  $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')
2591  ) . ']</i> - '
2592  . htmlspecialchars(BackendUtility::getRecordTitle(‪$table, $row));
2593  } else {
2594  $code = htmlspecialchars($code);
2595  }
2596  switch ((string)$this->clickTitleMode) {
2597  case 'edit':
2598  // If the listed table is 'pages' we have to request the permission settings for each page:
2599  if (‪$table === 'pages') {
2600  $localCalcPerms = $this->‪getPagePermissionsForRecord('pages', $row);
2601  $permsEdit = $localCalcPerms->editPagePermissionIsGranted();
2602  } else {
2603  $backendUser = $this->‪getBackendUserAuthentication();
2604  $permsEdit = $this->calcPerms->editContentPermissionIsGranted() && $backendUser->recordEditAccessInternals(‪$table, $row);
2605  }
2606  // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id)
2607  if ($permsEdit && $this->‪isEditable($table)) {
2608  $params = [
2609  'edit' => [
2610  ‪$table => [
2611  $row['uid'] => 'edit',
2612  ],
2613  ],
2614  'returnUrl' => $this->‪listURL(),
2615  ];
2616  $editLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
2617  $label = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit'));
2618  $code = '<a href="' . htmlspecialchars($editLink) . '"'
2619  . ' title="' . $label . '"'
2620  . ' aria-label="' . $label . '">'
2621  . $code . '</a>';
2622  }
2623  break;
2624  case 'show':
2625  // "Show" link (only pages and tt_content elements)
2626  if ((‪$table === 'pages' || ‪$table === 'tt_content')
2627  && ($attributes = $this->‪getPreviewUriBuilder($table, $row)->serializeDispatcherAttributes()) !== null
2628  ) {
2629  $title = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'));
2630  $code = '<button ' . $attributes
2631  . ' title="' . $title . '"'
2632  . ' aria-label="' . $title . '">'
2633  . $code . '</button>';
2634  }
2635  break;
2636  case 'info':
2637  // "Info": (All records)
2638  $label = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo'));
2639  $code = '<a href="#" role="button"' // @todo add handler that triggers click on space key
2640  . $this->‪createShowItemTagAttributes($table . ',' . (int)$row['uid'])
2641  . ' title="' . $label . '"'
2642  . ' aria-label="' . $label . '"'
2643  . ' aria-haspopup="dialog">'
2644  . $code
2645  . '</a>';
2646  break;
2647  default:
2648  // Output the label now:
2649  if (‪$table === 'pages') {
2650  $code = '<a href="' . htmlspecialchars(
2651  $this->‪listURL((string)$uid, '', 'pointer')
2652  ) . '">' . $code . '</a>';
2653  } else {
2654  $code = $this->‪linkUrlMail($code, $origCode);
2655  }
2656  }
2657  return $code;
2658  }
2659 
2667  protected function ‪linkUrlMail(string $code, string $testString): string
2668  {
2669  // Check for URL:
2670  $scheme = parse_url($testString, PHP_URL_SCHEME);
2671  if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') {
2672  return '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
2673  }
2674  // Check for email:
2675  if (GeneralUtility::validEmail($testString)) {
2676  return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>';
2677  }
2678  // Return if nothing else...
2679  return $code;
2680  }
2681 
2692  public function ‪listURL($altId = '', ‪$table = '-1', $exclList = '')
2693  {
2694  $urlParameters = [];
2695  if ((string)$altId !== '') {
2696  $urlParameters['id'] = $altId;
2697  } else {
2698  $urlParameters['id'] = ‪$this->id;
2699  }
2700  if (‪$table === '-1') {
2701  $urlParameters['table'] = ‪$this->table;
2702  } else {
2703  $urlParameters['table'] = ‪$table;
2704  }
2705  if ($this->returnUrl) {
2706  $urlParameters['returnUrl'] = ‪$this->returnUrl;
2707  }
2708  if ((!$exclList || !‪GeneralUtility::inList($exclList, 'searchTerm')) && $this->searchString) {
2709  $urlParameters['searchTerm'] = ‪$this->searchString;
2710  }
2711  if ($this->searchLevels) {
2712  $urlParameters['search_levels'] = ‪$this->searchLevels;
2713  }
2714  if ((!$exclList || !‪GeneralUtility::inList($exclList, 'pointer')) && $this->page) {
2715  $urlParameters['pointer'] = ‪$this->page;
2716  }
2717  if ((!$exclList || !‪GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) {
2718  $urlParameters['sortField'] = ‪$this->sortField;
2719  }
2720  if ((!$exclList || !‪GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) {
2721  $urlParameters['sortRev'] = ‪$this->sortRev;
2722  }
2723 
2724  return (string)$this->uriBuilder->buildUriFromRequest(
2725  $this->request,
2726  array_replace($urlParameters, $this->overrideUrlParameters)
2727  );
2728  }
2729 
2735  public function ‪setOverrideUrlParameters(array $urlParameters, ServerRequestInterface ‪$request)
2736  {
2737  $currentUrlParameter = ‪$request->getParsedBody()['curUrl'] ?? ‪$request->getQueryParams()['curUrl'] ?? '';
2738  if (isset($currentUrlParameter['url'])) {
2739  $urlParameters['P']['currentValue'] = $currentUrlParameter['url'];
2740  }
2741  $this->overrideUrlParameters = $urlParameters;
2742  }
2743 
2756  public function ‪setTableDisplayOrder(array $orderInformation)
2757  {
2758  foreach ($orderInformation as $tableName => &$configuration) {
2759  if (isset($configuration['before'])) {
2760  if (is_string($configuration['before'])) {
2761  $configuration['before'] = ‪GeneralUtility::trimExplode(',', $configuration['before'], true);
2762  } elseif (!is_array($configuration['before'])) {
2763  throw new \UnexpectedValueException(
2764  'The specified "before" order configuration for table "' . $tableName . '" is invalid.',
2765  1504793406
2766  );
2767  }
2768  }
2769  if (isset($configuration['after'])) {
2770  if (is_string($configuration['after'])) {
2771  $configuration['after'] = ‪GeneralUtility::trimExplode(',', $configuration['after'], true);
2772  } elseif (!is_array($configuration['after'])) {
2773  throw new \UnexpectedValueException(
2774  'The specified "after" order configuration for table "' . $tableName . '" is invalid.',
2775  1504793407
2776  );
2777  }
2778  }
2779  }
2780  $this->tableDisplayOrder = $orderInformation;
2781  }
2782 
2783  public function ‪getOverridePageIdList(): array
2784  {
2786  }
2787 
2791  public function ‪setOverridePageIdList(array ‪$overridePageIdList)
2792  {
2793  $this->overridePageIdList = array_map(intval(...), ‪$overridePageIdList);
2794  }
2795 
2803  protected function ‪getSearchableWebmounts(int ‪$id, int $depth): array
2804  {
2805  $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
2806  $hash = 'webmounts_list' . md5(‪$id . '-' . $depth . '-' . $this->perms_clause);
2807  $idList = $runtimeCache->get($hash);
2808  if ($idList === false) {
2809  $backendUser = $this->‪getBackendUserAuthentication();
2810 
2811  if (!$backendUser->isAdmin() && ‪$id === 0) {
2812  $mountPoints = array_map(intval(...), $backendUser->returnWebmounts());
2813  $mountPoints = array_unique($mountPoints);
2814  } else {
2815  $mountPoints = [‪$id];
2816  }
2817  // Add the initial mount points to the pids
2818  $idList = $mountPoints;
2819  $repository = GeneralUtility::makeInstance(PageTreeRepository::class);
2820  $repository->setAdditionalWhereClause($this->perms_clause);
2821  $pages = $repository->getFlattenedPages($mountPoints, $depth);
2822  foreach ($pages as ‪$page) {
2823  $idList[] = (int)‪$page['uid'];
2824  }
2825  $idList = array_unique($idList);
2826  $runtimeCache->set($hash, $idList);
2827  }
2828 
2829  return $idList;
2830  }
2831 
2838  protected function ‪addPageIdConstraint(string $tableName, QueryBuilder $queryBuilder, int ‪$searchLevels): QueryBuilder
2839  {
2840  // Set search levels to 999 instead of -1 as the following methods
2841  // do not support -1 as valid value for infinite search.
2842  if (‪$searchLevels === -1) {
2843  ‪$searchLevels = 999;
2844  }
2845 
2846  // When querying translated pages, the PID of the translated pages should be the same as the
2847  // the PID of the current page
2848  if ($tableName === 'pages' && $this->‪showOnlyTranslatedRecords) {
2849  $pageRecord = BackendUtility::getRecordWSOL('pages', $this->id);
2850  $queryBuilder->andWhere(
2851  $queryBuilder->expr()->eq(
2852  $tableName . '.pid',
2853  $queryBuilder->createNamedParameter($pageRecord['pid'] ?? 0, ‪Connection::PARAM_INT)
2854  )
2855  );
2856  } elseif (‪$searchLevels === 0) {
2857  $queryBuilder->andWhere(
2858  $queryBuilder->expr()->eq(
2859  $tableName . '.pid',
2860  $queryBuilder->createNamedParameter($this->id, ‪Connection::PARAM_INT)
2861  )
2862  );
2863  } elseif (‪$searchLevels > 0) {
2864  $allowedMounts = $this->‪getSearchableWebmounts($this->id, $searchLevels);
2865  $queryBuilder->andWhere(
2866  $queryBuilder->expr()->in(
2867  $tableName . '.pid',
2868  $queryBuilder->createNamedParameter($allowedMounts, ‪Connection::PARAM_INT_ARRAY)
2869  )
2870  );
2871  }
2872 
2873  if (!empty($this->‪getOverridePageIdList())) {
2874  $queryBuilder->andWhere(
2875  $queryBuilder->expr()->in(
2876  $tableName . '.pid',
2877  $queryBuilder->createNamedParameter($this->getOverridePageIdList(), ‪Connection::PARAM_INT_ARRAY)
2878  )
2879  );
2880  }
2881 
2882  return $queryBuilder;
2883  }
2884 
2886  {
2887  return ‪$GLOBALS['BE_USER'];
2888  }
2889 
2900  protected function ‪addElement($data, $rowParams = '', $colType = 'td')
2901  {
2902  $colType = ($colType === 'th') ? 'th' : 'td';
2903  $dataUid = ($colType === 'td') ? ($data['uid'] ?? 0) : 0;
2904  $l10nParent = $data['_l10nparent_'] ?? 0;
2905  $out = '<tr ' . $rowParams . ' data-uid="' . $dataUid . '" data-l10nparent="' . $l10nParent . '" data-multi-record-selection-element="true">';
2906 
2907  // Init rendering.
2908  $colsp = '';
2909  $lastKey = '';
2910  $c = 0;
2911  // __label is used as the label key to circumvent problems with uid used as label (see #67756)
2912  // as it was introduced later on, check if it really exists before using it
2914  if ($colType === 'td' && isset($data['__label'])) {
2915  // The title label column does always follow the icon column. Since
2916  // in some cases the first column - "_SELECTOR_" - might not be rendered,
2917  // we always have to calculate the key by searching for the icon column.
2918  $titleLabelKey = (int)(array_search('icon', ‪$fields, true)) + 1;
2919  ‪$fields[$titleLabelKey] = '__label';
2920  }
2921  // Traverse field array which contains the data to present:
2922  foreach (‪$fields as $vKey) {
2923  if (isset($data[$vKey])) {
2924  if ($lastKey) {
2925  $cssClass = $this->addElement_tdCssClass[$lastKey] ?? '';
2926  $out .= '
2927  <' . $colType . ' class="' . $cssClass . ' nowrap' . '"' . $colsp . '>' . $data[$lastKey] . '</' . $colType . '>';
2928  }
2929  $lastKey = $vKey;
2930  $c = 1;
2931  } else {
2932  if (!$lastKey) {
2933  $lastKey = $vKey;
2934  }
2935  $c++;
2936  }
2937  if ($c > 1) {
2938  $colsp = ' colspan="' . $c . '"';
2939  } else {
2940  $colsp = '';
2941  }
2942  }
2943  if ($lastKey) {
2944  $cssClass = $this->addElement_tdCssClass[$lastKey] ?? '';
2945  $out .= '
2946  <' . $colType . ' class="' . $cssClass . ' nowrap' . '"' . $colsp . '>' . $data[$lastKey] . '</' . $colType . '>';
2947  }
2948  // End row
2949  $out .= '
2950  </tr>';
2951  return $out;
2952  }
2953 
2963  protected function ‪getPossibleTranslations(int $pageUid): array
2964  {
2965  // Store languages that are included in the site configuration for the current page.
2966  $availableSystemLanguageUids = array_keys($this->translateTools->getSystemLanguages($pageUid));
2967  if ($availableSystemLanguageUids === []) {
2968  return [];
2969  }
2970  // Look up page overlays:
2971  $localizationParentField = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? '';
2972  $languageField = ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? '';
2973  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2974  ->getQueryBuilderForTable('pages');
2975  $queryBuilder->getRestrictions()
2976  ->removeAll()
2977  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2978  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUserAuthentication()->workspace));
2979  $result = $queryBuilder
2980  ->select('*')
2981  ->from('pages')
2982  ->where(
2983  $queryBuilder->expr()->and(
2984  $queryBuilder->expr()->eq($localizationParentField, $queryBuilder->createNamedParameter($pageUid, ‪Connection::PARAM_INT)),
2985  $queryBuilder->expr()->in($languageField, $queryBuilder->createNamedParameter($availableSystemLanguageUids, ‪Connection::PARAM_INT_ARRAY)),
2986  $queryBuilder->expr()->gt(
2987  $languageField,
2988  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
2989  )
2990  )
2991  )
2992  ->executeQuery();
2993  $allowedTranslationsOnPage = [];
2994  while ($row = $result->fetchAssociative()) {
2995  $allowedTranslationsOnPage[] = (int)$row[$languageField];
2996  }
2997  return $allowedTranslationsOnPage;
2998  }
2999 
3005  protected function ‪languageFlag(string ‪$table, array $row): string
3006  {
3007  $pageId = (int)(‪$table === 'pages' ? ($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField']] ?: $row['uid']) : $row['pid']);
3008  $languageUid = (int)($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'] ?? null] ?? 0);
3009  $languageInformation = $this->translateTools->getSystemLanguages($pageId);
3010  $title = htmlspecialchars($languageInformation[$languageUid]['title'] ?? '');
3011  $indent = $this->‪isLocalized($table, $row) ? '<span class="indent indent-inline-block" style="--indent-level: 1"></span> ' : '';
3012  if ($languageInformation[$languageUid]['flagIcon'] ?? false) {
3013  return $indent . $this->iconFactory
3014  ->getIcon($languageInformation[$languageUid]['flagIcon'], IconSize::SMALL)
3015  ->setTitle($title)
3016  ->render() . ' ' . $title;
3017  }
3018  return $title;
3019  }
3020 
3025  protected function ‪generateReferenceToolTip(string ‪$table, int ‪$uid): string
3026  {
3027  $numberOfReferences = $this->‪getReferenceCount($table, ‪$uid);
3028  if (!$numberOfReferences) {
3029  $htmlCode = '<button type="button" class="btn btn-default" disabled><span style="display:inline-block;min-width:16px">-</span></button>';
3030  } else {
3031  $showReferences = $this->‪getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references');
3032  $htmlCode = '<button type="button"'
3033  . ' class="btn btn-default"'
3034  . ' aria-haspopup="dialog"'
3035  . ' ' . $this->‪createShowItemTagAttributes($table . ',' . ‪$uid)
3036  . ' title="' . htmlspecialchars($showReferences) . ' (' . $numberOfReferences . ')' . '">'
3037  . '<span style="display:inline-block;min-width:16px">'
3038  . $numberOfReferences
3039  . '<span class="visually-hidden">' . $showReferences . '</span>'
3040  . '</span>'
3041  . '</button>';
3042  }
3043  return $htmlCode;
3044  }
3045 
3051  protected function ‪renderCheckboxActions(): string
3052  {
3053  $lang = $this->‪getLanguageService();
3054 
3055  $dropdownItems['checkAll'] = '
3056  <li>
3057  <button type="button" class="dropdown-item disabled" data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">
3058  ' . $this->iconFactory->getIcon('actions-selection-elements-all', IconSize::SMALL)->render() . '
3059  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '
3060  </button>
3061  </li>';
3062 
3063  $dropdownItems['checkNone'] = '
3064  <li>
3065  <button type="button" class="dropdown-item disabled" data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">
3066  ' . $this->iconFactory->getIcon('actions-selection-elements-none', IconSize::SMALL)->render() . '
3067  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '
3068  </button>
3069  </li>';
3071  $dropdownItems['toggleSelection'] = '
3072  <li>
3073  <button type="button" class="dropdown-item disabled" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">
3074  ' . $this->iconFactory->getIcon('actions-selection-elements-invert', IconSize::SMALL)->render() . '
3075  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '
3076  </button>
3077  </li>';
3078 
3079  return '
3080  <div class="btn-group dropdown">
3081  <button type="button" class="dropdown-toggle dropdown-toggle-link t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">
3082  ' . $this->iconFactory->getIcon('actions-selection', IconSize::SMALL) . '
3083  </button>
3084  <ul class="dropdown-menu t3js-multi-record-selection-check-actions">
3085  ' . implode(PHP_EOL, $dropdownItems) . '
3086  </ul>
3087  </div>';
3088  }
3089 
3093  protected function ‪renderMultiRecordSelectionActions(string ‪$table, array $currentIdList): string
3094  {
3095  $actions = [];
3096  $lang = $this->‪getLanguageService();
3097  $userTsConfig = $this->‪getBackendUserAuthentication()->getTSConfig();
3098  $addClipboardActions = $this->showClipboardActions && $this->‪isClipboardFunctionalityEnabled($table);
3099  $editPermission = (
3100  (‪$table === 'pages') ? $this->calcPerms->editPagePermissionIsGranted() : $this->calcPerms->editContentPermissionIsGranted()
3101  ) && $this->‪overlayEditLockPermissions($table);
3102 
3103  // Add actions in case table can be modified by the current user
3104  if ($editPermission && $this->‪isEditable($table)) {
3105  $editActionConfiguration = GeneralUtility::jsonEncodeForHtmlAttribute([
3106  'idField' => 'uid',
3107  'tableName' => ‪$table,
3108  'returnUrl' => $this->‪listURL(),
3109  ], true);
3110  $actions['edit'] = '
3111  <button
3112  type="button"
3113  title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit')) . '"
3114  class="btn btn-sm btn-default"
3115  data-multi-record-selection-action="edit"
3116  data-multi-record-selection-action-config="' . $editActionConfiguration . '"
3117  >
3118  ' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . '
3119  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit')) . '
3120  </button>';
3121 
3122  if (!(bool)trim((string)($userTsConfig['options.']['disableDelete.'][‪$table] ?? $userTsConfig['options.']['disableDelete'] ?? ''))) {
3123  $deleteActionConfiguration = GeneralUtility::jsonEncodeForHtmlAttribute([
3124  'idField' => 'uid',
3125  'ok' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete'),
3126  'title' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_deleteMarked'),
3127  'content' => sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_deleteMarkedWarning'), $lang->sL(‪$GLOBALS['TCA'][‪$table]['ctrl']['title'])),
3128  ], true);
3129  $actions['delete'] = '
3130  <button
3131  type="button"
3132  title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete')) . '"
3133  class="btn btn-sm btn-default"
3134  data-multi-record-selection-action="delete"
3135  data-multi-record-selection-action-config="' . $deleteActionConfiguration . '"
3136  aria-haspopup="dialog"
3137  >
3138  ' . $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->render() . '
3139  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete')) . '
3140  </button>';
3141  }
3142  }
3143 
3144  // Add clipboard actions in case they are enabled and clipboard is not deactivated
3145  if ($addClipboardActions && (string)($this->modTSconfig['enableClipBoard'] ?? '') !== 'deactivated') {
3146  $copyMarked = '
3147  <button type="button"
3148  class="btn btn-sm btn-default ' . ($this->clipObj->current === 'normal' ? 'disabled' : '') . '"
3149  title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.transferToClipboard')) . '"
3150  data-multi-record-selection-action="copyMarked"
3151  >
3152  ' . $this->iconFactory->getIcon('actions-edit-copy', IconSize::SMALL)->render() . '
3153  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.transferToClipboard')) . '
3154  </button>';
3155  $removeMarked = '
3156  <button type="button"
3157  class="btn btn-sm btn-default ' . ($this->clipObj->current === 'normal' ? 'disabled' : '') . '"
3158  title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.removeFromClipboard')) . '"
3159  data-multi-record-selection-action="removeMarked"
3160  >
3161  ' . $this->iconFactory->getIcon('actions-minus', IconSize::SMALL)->render() . '
3162  ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.removeFromClipboard')) . '
3163  </button>';
3164  // Add "copy marked" after "edit", or in case "edit" is not set, as first item
3165  if (!isset($actions['edit'])) {
3166  $actions = array_merge(['copyMarked' => $copyMarked], $actions);
3167  } else {
3168  $end = array_splice($actions, (int)(array_search('edit', array_keys($actions), true)) + 1);
3169  $actions = array_merge($actions, ['copyMarked' => $copyMarked, 'removeMarked' => $removeMarked], $end);
3170  }
3171  }
3172 
3173  $event = $this->eventDispatcher->dispatch(
3174  new ‪ModifyRecordListTableActionsEvent($actions, ‪$table, $currentIdList, $this)
3175  );
3177  $actions = $event->getActions();
3178 
3179  if ($actions === []) {
3180  // In case the user does not have permissions to execute on of the above
3181  // actions or a hook removed all remaining actions, inform the user about this.
3182  return '
3183  <span class="badge badge-info">
3184  ' . htmlspecialchars($lang->sL($event->getNoActionLabel() ?: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noActionAvailable')) . '
3185  </span>
3186  ';
3187  }
3188 
3189  return implode(LF, $actions);
3190  }
3191 
3197  {
3199  }
3200 
3204  protected function ‪createShowItemTagAttributes(string $arguments): string
3205  {
3206  return GeneralUtility::implodeAttributes([
3207  'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
3208  'data-dispatch-args-list' => $arguments,
3209  ], true);
3210  }
3211 
3212  protected function ‪getLanguageService(): LanguageService
3213  {
3214  return ‪$GLOBALS['LANG'];
3215  }
3216 
3218  {
3219  $this->languagesAllowedForUser = ‪$languagesAllowedForUser;
3220  return $this;
3221  }
3222 
3226  protected function ‪isLocalized(string ‪$table, array $row): bool
3227  {
3228  $languageField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['languageField'] ?? '';
3229  $transOrigPointerField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? '';
3230 
3231  return ($row[$languageField] ?? false) && ($row[$transOrigPointerField] ?? false);
3232  }
3233 
3238  protected function ‪getNoViewWithDokTypes(array $tsConfig): array
3239  {
3240  if (isset($tsConfig['noViewWithDokTypes'])) {
3241  $noViewDokTypes = ‪GeneralUtility::intExplode(',', (string)$tsConfig['noViewWithDokTypes'], true);
3242  } else {
3243  $noViewDokTypes = [
3246  ];
3247  }
3248 
3249  return $noViewDokTypes;
3250  }
3257  protected function ‪isClipboardFunctionalityEnabled(string ‪$table, array $row = []): bool
3258  {
3259  return $this->clipObj !== null
3260  && (‪$table !== 'pages' || !‪$this->showOnlyTranslatedRecords)
3261  && (
3262  $row === []
3263  || (
3264  !$this->‪isRecordDeletePlaceholder($row)
3265  && (int)($row[‪$GLOBALS['TCA'][‪$table]['ctrl']['transOrigPointerField'] ?? null] ?? 0) === 0
3266  )
3267  )
3268  && (BackendUtility::isTableWorkspaceEnabled(‪$table) || $this->‪getBackendUserAuthentication()->workspaceAllowsLiveEditingInTable($table));
3269  }
3270 
3274  protected function ‪addDividerToCellGroup(array &$cells): void
3275  {
3276  if (!($cells['secondary']['divider'] ?? false)) {
3277  $this->‪addActionToCellGroup($cells, '<hr class="dropdown-divider">', 'divider');
3278  }
3279  }
3280 
3281  protected function ‪isTextFieldType(string $fieldType): bool
3282  {
3283  $textFieldTypes = [
3284  'input',
3285  'text',
3286  'json',
3287  'flex',
3288  'email',
3289  'link',
3290  'slug',
3291  'color',
3292  'uuid',
3293  ];
3294 
3295  return in_array($fieldType, $textFieldTypes, true);
3296  }
3297 }
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$showLocalizeColumn
‪array $showLocalizeColumn
Definition: DatabaseRecordList.php:374
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$currentTable
‪int[][] $currentTable
Definition: DatabaseRecordList.php:182
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$showOnlyTranslatedRecords
‪bool $showOnlyTranslatedRecords
Definition: DatabaseRecordList.php:349
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\makeSearchString
‪string makeSearchString(string $table, int $currentPid, QueryBuilder $queryBuilder)
Definition: DatabaseRecordList.php:2448
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$overridePageIdList
‪array $overridePageIdList
Definition: DatabaseRecordList.php:333
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$disableSingleTableView
‪bool $disableSingleTableView
Definition: DatabaseRecordList.php:114
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isLocalized
‪isLocalized(string $table, array $row)
Definition: DatabaseRecordList.php:3203
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$duplicateField
‪string $duplicateField
Definition: DatabaseRecordList.php:272
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\overlayEditLockPermissions
‪bool overlayEditLockPermissions($table, $row=[], $editPermission=true)
Definition: DatabaseRecordList.php:2163
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\addElement
‪string addElement($data, $rowParams='', $colType='td')
Definition: DatabaseRecordList.php:2877
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getNoViewWithDokTypes
‪getNoViewWithDokTypes(array $tsConfig)
Definition: DatabaseRecordList.php:3215
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getFieldsToSelect
‪string[] getFieldsToSelect(string $table, array $columnsToRender)
Definition: DatabaseRecordList.php:453
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isEditable
‪isEditable(string $table)
Definition: DatabaseRecordList.php:2144
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\addSortLink
‪string addSortLink($label, $field, $table)
Definition: DatabaseRecordList.php:2013
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getTablesToRender
‪array getTablesToRender()
Definition: DatabaseRecordList.php:2266
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$calcPerms
‪Permission $calcPerms
Definition: DatabaseRecordList.php:223
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$displayRecordDownload
‪bool $displayRecordDownload
Definition: DatabaseRecordList.php:304
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getSearchableWebmounts
‪int[] getSearchableWebmounts(int $id, int $depth)
Definition: DatabaseRecordList.php:2780
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getQueryBuilder
‪getQueryBuilder(string $table, array $fields=[' *'], bool $addSorting=true, int $firstResult=0, int $maxResult=0)
Definition: DatabaseRecordList.php:2330
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setModuleData
‪setModuleData(ModuleData $moduleData)
Definition: DatabaseRecordList.php:2180
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:224
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$tableDisplayOrder
‪array $tableDisplayOrder
Definition: DatabaseRecordList.php:328
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getTable
‪string getTable($table)
Definition: DatabaseRecordList.php:504
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_NEW
‪const PAGE_NEW
Definition: Permission.php:50
‪TYPO3\CMS\Backend\Clipboard\Clipboard
Definition: Clipboard.php:48
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\start
‪start($id, $table, $pointer, $search='', $levels=0, $showLimit=0)
Definition: DatabaseRecordList.php:2195
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$request
‪ServerRequestInterface $request
Definition: DatabaseRecordList.php:376
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\linkWrapItems
‪string linkWrapItems($table, $uid, $code, $row)
Definition: DatabaseRecordList.php:2560
‪TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton
Definition: GenericButton.php:34
‪TYPO3\CMS\Backend\View\BackendViewFactory
Definition: BackendViewFactory.php:35
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$referenceCount
‪array $referenceCount
Definition: DatabaseRecordList.php:311
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\listURL
‪string listURL($altId='', $table='-1', $exclList='')
Definition: DatabaseRecordList.php:2669
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\createActionButtonNewRecord
‪createActionButtonNewRecord(string $table)
Definition: DatabaseRecordList.php:806
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$allowedNewTables
‪string[] $allowedNewTables
Definition: DatabaseRecordList.php:78
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$page
‪int $page
Definition: DatabaseRecordList.php:254
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$languagesAllowedForUser
‪array $languagesAllowedForUser
Definition: DatabaseRecordList.php:368
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:40
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$table
‪string $table
Definition: DatabaseRecordList.php:217
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\showNewRecLink
‪bool showNewRecLink($table)
Definition: DatabaseRecordList.php:2083
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\renderListRow
‪string renderListRow($table, array $row, int $indent, array $translations, bool $translationEnabled)
Definition: DatabaseRecordList.php:1047
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$CBnames
‪string[] $CBnames
Definition: DatabaseRecordList.php:290
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setIsEditable
‪setIsEditable(bool $isEditable)
Definition: DatabaseRecordList.php:2136
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\linkWrapTable
‪string linkWrapTable(string $table, string $label)
Definition: DatabaseRecordList.php:2541
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\showOnlyTranslatedRecords
‪showOnlyTranslatedRecords(bool $showOnlyTranslatedRecords)
Definition: DatabaseRecordList.php:3173
‪TYPO3\CMS\Backend\Module\ModuleData
Definition: ModuleData.php:30
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$sortField
‪string $sortField
Definition: DatabaseRecordList.php:143
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\addDividerToCellGroup
‪addDividerToCellGroup(array &$cells)
Definition: DatabaseRecordList.php:3251
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isRowListingConditionFulfilled
‪bool isRowListingConditionFulfilled($table, $row)
Definition: DatabaseRecordList.php:1030
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$pagePermsCache
‪array $pagePermsCache
Definition: DatabaseRecordList.php:373
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setOverrideUrlParameters
‪setOverrideUrlParameters(array $urlParameters, ServerRequestInterface $request)
Definition: DatabaseRecordList.php:2712
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\createActionButtonCollapse
‪createActionButtonCollapse(string $table)
Definition: DatabaseRecordList.php:972
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isTextFieldType
‪isTextFieldType(string $fieldType)
Definition: DatabaseRecordList.php:3258
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$addElement_tdCssClass
‪array $addElement_tdCssClass
Definition: DatabaseRecordList.php:176
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\generateList
‪string generateList()
Definition: DatabaseRecordList.php:2250
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList
Definition: DatabaseRecordList.php:68
‪TYPO3\CMS\Backend\Module\ModuleProvider
Definition: ModuleProvider.php:29
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getPossibleTranslations
‪int[] getPossibleTranslations(int $pageUid)
Definition: DatabaseRecordList.php:2940
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Filelist\Type\fieldArray
‪@ fieldArray
Definition: Mode.php:28
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$sortRev
‪bool $sortRev
Definition: DatabaseRecordList.php:267
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$listOnlyInSingleTableMode
‪bool $listOnlyInSingleTableMode
Definition: DatabaseRecordList.php:167
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$clickTitleMode
‪string $clickTitleMode
Definition: DatabaseRecordList.php:229
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\makeControl
‪string makeControl($table, $row)
Definition: DatabaseRecordList.php:1392
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$tableTSconfigOverTCA
‪array $tableTSconfigOverTCA
Definition: DatabaseRecordList.php:241
‪TYPO3\CMS\Backend\RecordList
Definition: DatabaseRecordList.php:18
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\linkUrlMail
‪string linkUrlMail(string $code, string $testString)
Definition: DatabaseRecordList.php:2644
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\isLoaded
‪static isLoaded(string $key)
Definition: ExtensionManagementUtility.php:55
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\renderMultiRecordSelectionActions
‪renderMultiRecordSelectionActions(string $table, array $currentIdList)
Definition: DatabaseRecordList.php:3070
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\makeCheckbox
‪string makeCheckbox(string $table, array $row)
Definition: DatabaseRecordList.php:1910
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$searchLevels
‪int $searchLevels
Definition: DatabaseRecordList.php:234
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:32
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$duplicateStack
‪array $duplicateStack
Definition: DatabaseRecordList.php:161
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\createActionButtonColumnSelector
‪createActionButtonColumnSelector(string $table)
Definition: DatabaseRecordList.php:922
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isRecordCurrentBackendUser
‪bool isRecordCurrentBackendUser($table, $row)
Definition: DatabaseRecordList.php:2122
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getPreviewUriBuilder
‪getPreviewUriBuilder(string $table, array $row)
Definition: DatabaseRecordList.php:1002
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\makeClip
‪makeClip(string $table, array $row, array &$cells)
Definition: DatabaseRecordList.php:1823
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$hideTables
‪string $hideTables
Definition: DatabaseRecordList.php:199
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$currentLink
‪array $currentLink
Definition: DatabaseRecordList.php:344
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\createShowItemTagAttributes
‪createShowItemTagAttributes(string $arguments)
Definition: DatabaseRecordList.php:3181
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isClipboardFunctionalityEnabled
‪isClipboardFunctionalityEnabled(string $table, array $row=[])
Definition: DatabaseRecordList.php:3234
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$showClipboardActions
‪bool $showClipboardActions
Definition: DatabaseRecordList.php:91
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getReferenceCount
‪int getReferenceCount($tableName, $uid)
Definition: DatabaseRecordList.php:1190
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\renderListHeader
‪string renderListHeader($table, $currentIdList)
Definition: DatabaseRecordList.php:1210
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\createActionButtonDownload
‪createActionButtonDownload(string $table, int $totalItems)
Definition: DatabaseRecordList.php:869
‪TYPO3\CMS\Core\Database\Query\QueryHelper\getDateTimeTypes
‪static array getDateTimeTypes()
Definition: QueryHelper.php:211
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$deniedNewTables
‪string[] $deniedNewTables
Definition: DatabaseRecordList.php:85
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\create
‪static static create(int $pageId)
Definition: PreviewUriBuilder.php:65
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$id
‪int $id
Definition: DatabaseRecordList.php:154
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$recPath_cache
‪array $recPath_cache
Definition: DatabaseRecordList.php:138
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SPACER
‪const DOKTYPE_SPACER
Definition: PageRepository.php:103
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getLanguageId
‪getLanguageId()
Definition: SiteLanguage.php:169
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\makeLocalizationPanel
‪makeLocalizationPanel($table, $row, array $translations)
Definition: DatabaseRecordList.php:1950
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$clickMenuEnabled
‪bool $clickMenuEnabled
Definition: DatabaseRecordList.php:103
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$displayColumnSelector
‪bool $displayColumnSelector
Definition: DatabaseRecordList.php:297
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent
Definition: ModifyRecordListRecordActionsEvent.php:27
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$clipObj
‪Clipboard $clipObj
Definition: DatabaseRecordList.php:284
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SYSFOLDER
‪const DOKTYPE_SYSFOLDER
Definition: PageRepository.php:104
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getColumnsToRender
‪getColumnsToRender(string $table, bool $includeMetaColumns)
Definition: DatabaseRecordList.php:399
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$hideTranslations
‪string $hideTranslations
Definition: DatabaseRecordList.php:131
‪$output
‪$output
Definition: annotationChecker.php:114
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\renderCheckboxActions
‪string renderCheckboxActions()
Definition: DatabaseRecordList.php:3028
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$editable
‪bool $editable
Definition: DatabaseRecordList.php:316
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\languageFlag
‪string languageFlag(string $table, array $row)
Definition: DatabaseRecordList.php:2982
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$returnUrl
‪string $returnUrl
Definition: DatabaseRecordList.php:211
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\__construct
‪__construct(protected readonly IconFactory $iconFactory, protected readonly UriBuilder $uriBuilder, protected readonly TranslationConfigurationProvider $translateTools, protected readonly EventDispatcherInterface $eventDispatcher, protected readonly BackendViewFactory $backendViewFactory, protected readonly ModuleProvider $moduleProvider,)
Definition: DatabaseRecordList.php:378
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$overrideUrlParameters
‪array $overrideUrlParameters
Definition: DatabaseRecordList.php:339
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$spaceIcon
‪string $spaceIcon
Definition: DatabaseRecordList.php:108
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListTableActionsEvent
Definition: ModifyRecordListTableActionsEvent.php:27
‪TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider
Definition: TranslationConfigurationProvider.php:39
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$pageRow
‪string[] $pageRow
Definition: DatabaseRecordList.php:124
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$showLimit
‪int $showLimit
Definition: DatabaseRecordList.php:187
‪TYPO3\CMS\Core\Type\Bitmask\Permission\CONTENT_EDIT
‪const CONTENT_EDIT
Definition: Permission.php:55
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$setFields
‪array $setFields
Definition: DatabaseRecordList.php:248
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder
Definition: PreviewUriBuilder.php:44
‪TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository
Definition: PageTreeRepository.php:41
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getPagePermissionsForRecord
‪getPagePermissionsForRecord(string $table, array $row)
Definition: DatabaseRecordList.php:2066
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$fieldArray
‪array $fieldArray
Definition: DatabaseRecordList.php:193
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$tableList
‪string $tableList
Definition: DatabaseRecordList.php:278
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\generateReferenceToolTip
‪generateReferenceToolTip(string $table, int $uid)
Definition: DatabaseRecordList.php:3002
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setTableDisplayOrder
‪setTableDisplayOrder(array $orderInformation)
Definition: DatabaseRecordList.php:2733
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setLanguagesAllowedForUser
‪setLanguagesAllowedForUser(array $languagesAllowedForUser)
Definition: DatabaseRecordList.php:3194
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setRequest
‪setRequest(ServerRequestInterface $request)
Definition: DatabaseRecordList.php:390
‪TYPO3\CMS\Core\Utility\GeneralUtility\inList
‪static bool inList($list, $item)
Definition: GeneralUtility.php:422
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\isRecordDeletePlaceholder
‪isRecordDeletePlaceholder(array $row)
Definition: DatabaseRecordList.php:2130
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\recPath
‪mixed[] recPath($pid)
Definition: DatabaseRecordList.php:2054
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:69
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\setOverridePageIdList
‪setOverridePageIdList(array $overridePageIdList)
Definition: DatabaseRecordList.php:2768
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$noControlPanels
‪bool $noControlPanels
Definition: DatabaseRecordList.php:97
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange(mixed $theInt, int $min, int $max=2000000000, int $defaultValue=0)
Definition: MathUtility.php:34
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getBackendUserAuthentication
‪getBackendUserAuthentication()
Definition: DatabaseRecordList.php:2862
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$possibleTranslations
‪array $possibleTranslations
Definition: DatabaseRecordList.php:361
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getOverridePageIdList
‪getOverridePageIdList()
Definition: DatabaseRecordList.php:2760
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$perms_clause
‪string $perms_clause
Definition: DatabaseRecordList.php:205
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\renderListNavigation
‪string renderListNavigation(string $table, int $totalItems, int $itemsPerPage)
Definition: DatabaseRecordList.php:1352
‪TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListHeaderColumnsEvent
Definition: ModifyRecordListHeaderColumnsEvent.php:26
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\addActionToCellGroup
‪addActionToCellGroup(&$cells, $action, $actionKey)
Definition: DatabaseRecordList.php:2100
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\addPageIdConstraint
‪QueryBuilder addPageIdConstraint(string $tableName, QueryBuilder $queryBuilder, int $searchLevels)
Definition: DatabaseRecordList.php:2815
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$modTSconfig
‪array[] $modTSconfig
Definition: DatabaseRecordList.php:171
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$searchString
‪string $searchString
Definition: DatabaseRecordList.php:260
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForRecordListingEvent
Definition: ModifyDatabaseQueryForRecordListingEvent.php:29
‪TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface
Definition: ButtonInterface.php:22
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\$moduleData
‪ModuleData $moduleData
Definition: DatabaseRecordList.php:148
‪TYPO3\CMS\Backend\RecordList\DatabaseRecordList\getLanguageService
‪getLanguageService()
Definition: DatabaseRecordList.php:3189
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39