1<?php 2namespace TYPO3\CMS\Recordlist\RecordList; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider; 18use TYPO3\CMS\Backend\Module\BaseScriptClass; 19use TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface; 20use TYPO3\CMS\Backend\Routing\UriBuilder; 21use TYPO3\CMS\Backend\Template\Components\ButtonBar; 22use TYPO3\CMS\Backend\Template\DocumentTemplate; 23use TYPO3\CMS\Backend\Template\ModuleTemplate; 24use TYPO3\CMS\Backend\Tree\View\PageTreeView; 25use TYPO3\CMS\Backend\Utility\BackendUtility; 26use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 27use TYPO3\CMS\Core\Cache\CacheManager; 28use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait; 29use TYPO3\CMS\Core\Database\Connection; 30use TYPO3\CMS\Core\Database\ConnectionPool; 31use TYPO3\CMS\Core\Database\Query\QueryBuilder; 32use TYPO3\CMS\Core\Database\Query\QueryHelper; 33use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction; 34use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 35use TYPO3\CMS\Core\Database\ReferenceIndex; 36use TYPO3\CMS\Core\Imaging\Icon; 37use TYPO3\CMS\Core\Imaging\IconFactory; 38use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException; 39use TYPO3\CMS\Core\LinkHandling\LinkService; 40use TYPO3\CMS\Core\Localization\LanguageService; 41use TYPO3\CMS\Core\Log\LogManager; 42use TYPO3\CMS\Core\Messaging\FlashMessage; 43use TYPO3\CMS\Core\Messaging\FlashMessageService; 44use TYPO3\CMS\Core\Service\DependencyOrderingService; 45use TYPO3\CMS\Core\Type\Bitmask\Permission; 46use TYPO3\CMS\Core\Utility\CsvUtility; 47use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 48use TYPO3\CMS\Core\Utility\GeneralUtility; 49use TYPO3\CMS\Core\Utility\HttpUtility; 50use TYPO3\CMS\Core\Utility\MathUtility; 51use TYPO3\CMS\Core\Versioning\VersionState; 52use TYPO3\CMS\Frontend\Page\PageRepository; 53 54/** 55 * Class for rendering of Web>List module 56 * @internal This class is a specific TYPO3 Backend implementation and is not part of the TYPO3's Core API. 57 */ 58class DatabaseRecordList 59{ 60 use PublicPropertyDeprecationTrait; 61 62 /** 63 * @var array 64 */ 65 protected $deprecatedPublicProperties = [ 66 'newWizards' => 'Using $newWizards of class DatabaseRecordList from outside is discouraged, property will be removed in TYPO3 v10.0.', 67 ]; 68 69 // ********* 70 // External: 71 // ********* 72 73 /** 74 * Used to indicate which tables (values in the array) that can have a 75 * create-new-record link. If the array is empty, all tables are allowed. 76 * 77 * @var string[] 78 */ 79 public $allowedNewTables = []; 80 81 /** 82 * Used to indicate which tables (values in the array) that cannot have a 83 * create-new-record link. If the array is empty, all tables are allowed. 84 * 85 * @var string[] 86 */ 87 public $deniedNewTables = []; 88 89 /** 90 * If TRUE, the control panel will contain links to the create-new wizards for 91 * pages and tt_content elements (normally, the link goes to just creating a new 92 * element without the wizards!). 93 * 94 * @var bool 95 * @deprecated and unused since TYPO3 v9, will be removed in TYPO3 v10.0 96 */ 97 public $newWizards = false; 98 99 /** 100 * If TRUE, will disable the rendering of clipboard + control panels. 101 * 102 * @var bool 103 */ 104 public $dontShowClipControlPanels = false; 105 106 /** 107 * If TRUE, will show the clipboard in the field list. 108 * 109 * @var bool 110 */ 111 public $showClipboard = false; 112 113 /** 114 * If TRUE, will DISABLE all control panels in lists. (Takes precedence) 115 * 116 * @var bool 117 */ 118 public $noControlPanels = false; 119 120 /** 121 * If TRUE, clickmenus will be rendered 122 * 123 * @var bool 124 */ 125 public $clickMenuEnabled = true; 126 127 /** 128 * Count of record rows in view 129 * 130 * @var int 131 */ 132 public $totalRowCount; 133 134 /** 135 * Space icon used for alignment 136 * 137 * @var string 138 */ 139 public $spaceIcon; 140 141 /** 142 * Disable single table view 143 * 144 * @var bool 145 */ 146 public $disableSingleTableView = false; 147 148 // ********* 149 // Internal: 150 // ********* 151 152 /** 153 * Set to the page record (see writeTop()) 154 * 155 * @var string[] 156 */ 157 public $pageRow = []; 158 159 /** 160 * Shared module configuration, used by localization features 161 * 162 * @var array 163 */ 164 public $modSharedTSconfig = []; 165 166 /** 167 * Contains page translation languages 168 * 169 * @var array 170 */ 171 public $pageOverlays = []; 172 173 /** 174 * Contains sys language icons and titles 175 * 176 * @var array 177 */ 178 public $languageIconTitles = []; 179 180 /** 181 * Tables which should not list their translations 182 * 183 * @var string 184 */ 185 public $hideTranslations = ''; 186 187 /** 188 * If set, the listing is returned as CSV instead. 189 * 190 * @var bool 191 */ 192 public $csvOutput = false; 193 194 /** 195 * Cache for record path 196 * 197 * @var mixed[] 198 */ 199 public $recPath_cache = []; 200 201 /** 202 * Field, to sort list by 203 * 204 * @var string 205 */ 206 public $sortField; 207 208 /** 209 * default Max items shown per table in "multi-table mode", may be overridden by tables.php 210 * 211 * @var int 212 */ 213 public $itemsLimitPerTable = 20; 214 215 /** 216 * Keys are fieldnames and values are td-parameters to add in addElement(), please use $addElement_tdCSSClass for CSS-classes; 217 * 218 * @var array 219 */ 220 public $addElement_tdParams = []; 221 222 /** 223 * Page id 224 * 225 * @var int 226 */ 227 public $id; 228 229 /** 230 * @var int 231 */ 232 public $no_noWrap = 0; 233 234 /** 235 * Set to zero, if you don't want a left-margin with addElement function 236 * 237 * @var int 238 */ 239 public $setLMargin = 1; 240 241 /** 242 * Used for tracking duplicate values of fields 243 * 244 * @var string[] 245 */ 246 public $duplicateStack = []; 247 248 /** 249 * Current script name 250 * 251 * @var string 252 */ 253 public $script = 'index.php'; 254 255 /** 256 * If TRUE, records are listed only if a specific table is selected. 257 * 258 * @var bool 259 */ 260 public $listOnlyInSingleTableMode = false; 261 262 /** 263 * Script URL 264 * 265 * @var string 266 */ 267 public $thisScript = ''; 268 269 /** 270 * JavaScript code accumulation 271 * 272 * @var string 273 */ 274 public $JScode = ''; 275 276 /** 277 * @var TranslationConfigurationProvider 278 */ 279 public $translateTools; 280 281 /** 282 * default Max items shown per table in "single-table mode", may be overridden by tables.php 283 * 284 * @var int 285 */ 286 public $itemsLimitSingleTable = 100; 287 288 /** 289 * Array of collapsed / uncollapsed tables in multi table view 290 * 291 * @var int[][] 292 */ 293 public $tablesCollapsed = []; 294 295 /** 296 * @var array[] Module configuration 297 */ 298 public $modTSconfig; 299 300 /** 301 * String with accumulated HTML content 302 * 303 * @var string 304 */ 305 public $HTMLcode = ''; 306 307 /** 308 * Keys are fieldnames and values are td-css-classes to add in addElement(); 309 * 310 * @var array 311 */ 312 public $addElement_tdCssClass = []; 313 314 /** 315 * Thumbnails on records containing files (pictures) 316 * 317 * @var bool 318 */ 319 public $thumbs = 0; 320 321 /** 322 * Used for tracking next/prev uids 323 * 324 * @var int[][] 325 */ 326 public $currentTable = []; 327 328 /** 329 * Indicates if all available fields for a user should be selected or not. 330 * 331 * @var int 332 */ 333 public $allFields = 0; 334 335 /** 336 * Number of records to show 337 * 338 * @var int 339 */ 340 public $showLimit = 0; 341 342 /** 343 * Decides the columns shown. Filled with values that refers to the keys of the data-array. $this->fieldArray[0] is the title column. 344 * 345 * @var array 346 */ 347 public $fieldArray = []; 348 349 /** 350 * Tables which should not get listed 351 * 352 * @var string 353 */ 354 public $hideTables = ''; 355 356 /** 357 * Containing which fields to display in extended mode 358 * 359 * @var string[] 360 */ 361 public $displayFields; 362 363 /** 364 * If set this is <td> CSS-classname for odd columns in addElement. Used with db_layout / pages section 365 * 366 * @var string 367 */ 368 public $oddColumnsCssClass = ''; 369 370 /** 371 * Not used in this class - but maybe extension classes... 372 * Max length of strings 373 * 374 * @var int 375 */ 376 public $fixedL = 30; 377 378 /** 379 * Page select permissions 380 * 381 * @var string 382 */ 383 public $perms_clause = ''; 384 385 /** 386 * Return URL 387 * 388 * @var string 389 */ 390 public $returnUrl = ''; 391 392 /** 393 * Tablename if single-table mode 394 * 395 * @var string 396 */ 397 public $table = ''; 398 399 /** 400 * Some permissions... 401 * 402 * @var int 403 */ 404 public $calcPerms = 0; 405 406 /** 407 * Mode for what happens when a user clicks the title of a record. 408 * 409 * @var string 410 */ 411 public $clickTitleMode = ''; 412 413 /** 414 * @var int 415 */ 416 public $showIcon = 1; 417 418 /** 419 * Levels to search down. 420 * 421 * @var int 422 */ 423 public $searchLevels = ''; 424 425 /** 426 * "LIMIT " in SQL... 427 * 428 * @var int 429 */ 430 public $iLimit = 0; 431 432 /** 433 * Set to the total number of items for a table when selecting. 434 * 435 * @var string 436 */ 437 public $totalItems = ''; 438 439 /** 440 * OBSOLETE - NOT USED ANYMORE. leftMargin 441 * 442 * @var int 443 */ 444 public $leftMargin = 0; 445 446 /** 447 * TSconfig which overwrites TCA-Settings 448 * 449 * @var mixed[][] 450 */ 451 public $tableTSconfigOverTCA = []; 452 453 /** 454 * Loaded with page record with version overlay if any. 455 * 456 * @var string[] 457 */ 458 public $pageRecord = []; 459 460 /** 461 * Fields to display for the current table 462 * 463 * @var string[] 464 */ 465 public $setFields = []; 466 467 /** 468 * Counter increased for each element. Used to index elements for the JavaScript-code that transfers to the clipboard 469 * 470 * @var int 471 */ 472 public $counter = 0; 473 474 /** 475 * Pointer for browsing list 476 * 477 * @var int 478 */ 479 public $firstElementNumber = 0; 480 481 /** 482 * Counting the elements no matter what... 483 * 484 * @var int 485 */ 486 public $eCounter = 0; 487 488 /** 489 * Search string 490 * 491 * @var string 492 */ 493 public $searchString = ''; 494 495 /** 496 * Field, indicating to sort in reverse order. 497 * 498 * @var bool 499 */ 500 public $sortRev; 501 502 /** 503 * String, can contain the field name from a table which must have duplicate values marked. 504 * 505 * @var string 506 */ 507 public $duplicateField; 508 509 /** 510 * Specify a list of tables which are the only ones allowed to be displayed. 511 * 512 * @var string 513 */ 514 public $tableList = ''; 515 516 /** 517 * Used to accumulate CSV lines for CSV export. 518 * 519 * @var string[] 520 */ 521 protected $csvLines = []; 522 523 /** 524 * Clipboard object 525 * 526 * @var \TYPO3\CMS\Backend\Clipboard\Clipboard 527 */ 528 public $clipObj; 529 530 /** 531 * Tracking names of elements (for clipboard use) 532 * 533 * @var string[] 534 */ 535 public $CBnames = []; 536 537 /** 538 * [$tablename][$uid] = number of references to this record 539 * 540 * @var int[][] 541 */ 542 protected $referenceCount = []; 543 544 /** 545 * Translations of the current record 546 * 547 * @var string[] 548 */ 549 public $translations; 550 551 /** 552 * select fields for the query which fetches the translations of the current 553 * record 554 * 555 * @var string 556 */ 557 public $selFieldList; 558 559 /** 560 * @var mixed[] 561 */ 562 public $pageinfo; 563 564 /** 565 * Injected by RecordList 566 * 567 * @var string[] 568 */ 569 public $MOD_MENU; 570 571 /** 572 * If defined the records are editable 573 * 574 * @var bool 575 */ 576 protected $editable = true; 577 578 /** 579 * @var IconFactory 580 */ 581 protected $iconFactory; 582 583 /** 584 * Array with before/after setting for tables 585 * Structure: 586 * 'tableName' => [ 587 * 'before' => ['A', ...] 588 * 'after' => [] 589 * ] 590 * 591 * @var array[] 592 */ 593 protected $tableDisplayOrder = []; 594 595 /** 596 * Override the page ids taken into account by getPageIdConstraint() 597 * 598 * @var array 599 */ 600 protected $overridePageIdList = []; 601 602 /** 603 * Override/add urlparameters in listUrl() method 604 * @var mixed[] 605 */ 606 protected $overrideUrlParameters = []; 607 608 /** 609 * Current link: array with table names and uid 610 * 611 * @var array 612 */ 613 protected $currentLink = []; 614 615 /** 616 * Only used to render translated records, used in list module to show page translations 617 * 618 * @var bool 619 */ 620 protected $showOnlyTranslatedRecords = false; 621 622 /** 623 * All languages that are included in the site configuration 624 * for the current page. New records can only be created in those 625 * languages. 626 * 627 * @var array 628 */ 629 protected $systemLanguagesOnPage; 630 631 /** 632 * All languages that are allowed by the user 633 * 634 * @var array 635 */ 636 protected $languagesAllowedForUser = []; 637 638 /** 639 * Constructor 640 */ 641 public function __construct() 642 { 643 if (isset($GLOBALS['BE_USER']->uc['titleLen']) && $GLOBALS['BE_USER']->uc['titleLen'] > 0) { 644 $this->fixedL = $GLOBALS['BE_USER']->uc['titleLen']; 645 } 646 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); 647 $this->getTranslateTools(); 648 $this->determineScriptUrl(); 649 650 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); 651 } 652 653 /** 654 * Create the panel of buttons for submitting the form or otherwise perform 655 * operations. 656 * 657 * @return string[] All available buttons as an assoc. array 658 */ 659 public function getButtons() 660 { 661 $module = $this->getModule(); 662 $backendUser = $this->getBackendUserAuthentication(); 663 $lang = $this->getLanguageService(); 664 $buttons = [ 665 'csh' => '', 666 'view' => '', 667 'edit' => '', 668 'hide_unhide' => '', 669 'move' => '', 670 'new_record' => '', 671 'paste' => '', 672 'level_up' => '', 673 'cache' => '', 674 'reload' => '', 675 'shortcut' => '', 676 'back' => '', 677 'csv' => '', 678 'export' => '' 679 ]; 680 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 681 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); 682 // Get users permissions for this page record: 683 $localCalcPerms = $backendUser->calcPerms($this->pageRow); 684 // CSH 685 if ((string)$this->id === '') { 686 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_noId'); 687 } elseif (!$this->id) { 688 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module_root'); 689 } else { 690 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'list_module'); 691 } 692 if (isset($this->id)) { 693 // View Exclude doktypes 254,255 Configuration: 694 // mod.web_list.noViewWithDokTypes = 254,255 695 if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) { 696 $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true); 697 } else { 698 //default exclusion: doktype 254 (folder), 255 (recycler) 699 $noViewDokTypes = [ 700 PageRepository::DOKTYPE_SYSFOLDER, 701 PageRepository::DOKTYPE_RECYCLER 702 ]; 703 } 704 if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) { 705 $onClick = htmlspecialchars(BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id))); 706 $buttons['view'] = '<a href="#" onclick="' . $onClick . '" title="' 707 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">' 708 . $this->iconFactory->getIcon('actions-document-view', Icon::SIZE_SMALL)->render() . '</a>'; 709 } 710 // New record on pages that are not locked by editlock 711 if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) { 712 $onClick = htmlspecialchars('return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');'); 713 $buttons['new_record'] = '<a href="#" onclick="' . $onClick . '" title="' 714 . htmlspecialchars($lang->getLL('newRecordGeneral')) . '">' 715 . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . '</a>'; 716 } 717 // If edit permissions are set, see 718 // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication 719 if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions() && $this->getBackendUserAuthentication()->checkLanguageAccess(0)) { 720 // Edit 721 $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit'; 722 $onClick = htmlspecialchars(BackendUtility::editOnClick($params, '', -1)); 723 $buttons['edit'] = '<a href="#" onclick="' . $onClick . '" title="' . htmlspecialchars($lang->getLL('editPage')) . '">' 724 . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render() 725 . '</a>'; 726 } 727 // Paste 728 if (($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) { 729 $elFromTable = $this->clipObj->elFromTable(''); 730 if (!empty($elFromTable)) { 731 $confirmText = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable); 732 $buttons['paste'] = '<a' 733 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $this->id)) . '"' 734 . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"' 735 . ' class="t3js-modal-trigger"' 736 . ' data-severity="warning"' 737 . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"' 738 . ' data-content="' . htmlspecialchars($confirmText) . '"' 739 . '>' 740 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() 741 . '</a>'; 742 } 743 } 744 // Cache 745 $buttons['cache'] = '<a href="' . htmlspecialchars($this->listURL() . '&clear_cache=1') . '" title="' 746 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache')) . '">' 747 . $this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL)->render() . '</a>'; 748 if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks']) 749 || (isset($module->modTSconfig['properties']['noExportRecordsLinks']) 750 && !$module->modTSconfig['properties']['noExportRecordsLinks'])) 751 ) { 752 // CSV 753 $buttons['csv'] = '<a href="' . htmlspecialchars($this->listURL() . '&csv=1') . '" title="' 754 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv')) . '">' 755 . $this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL)->render() . '</a>'; 756 // Export 757 if (ExtensionManagementUtility::isLoaded('impexp')) { 758 $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']); 759 $buttons['export'] = '<a href="' . htmlspecialchars($url . '&tx_impexp[list][]=' 760 . rawurlencode($this->table . ':' . $this->id)) . '" title="' 761 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export')) . '">' 762 . $this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL)->render() . '</a>'; 763 } 764 } 765 // Reload 766 $buttons['reload'] = '<a href="' . htmlspecialchars($this->listURL()) . '" title="' 767 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload')) . '">' 768 . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a>'; 769 // Shortcut 770 if ($backendUser->mayMakeShortcut()) { 771 $buttons['shortcut'] = $this->getDocumentTemplate()->makeShortcutIcon( 772 'id, M, imagemode, pointer, table, search_field, search_levels, showLimit, sortField, sortRev', 773 implode(',', array_keys($this->MOD_MENU)), 774 'web_list' 775 ); 776 } 777 // Back 778 if ($this->returnUrl) { 779 $href = htmlspecialchars(GeneralUtility::linkThisUrl($this->returnUrl, ['id' => $this->id])); 780 $buttons['back'] = '<a href="' . $href . '" class="typo3-goBack" title="' 781 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack')) . '">' 782 . $this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL)->render() . '</a>'; 783 } 784 } 785 return $buttons; 786 } 787 788 /** 789 * Create the panel of buttons for submitting the form or otherwise perform 790 * operations. 791 * 792 * @param ModuleTemplate $moduleTemplate 793 */ 794 public function getDocHeaderButtons(ModuleTemplate $moduleTemplate) 795 { 796 $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar(); 797 $module = $this->getModule(); 798 $backendUser = $this->getBackendUserAuthentication(); 799 $lang = $this->getLanguageService(); 800 // Get users permissions for this page record: 801 $localCalcPerms = $backendUser->calcPerms($this->pageRow); 802 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 803 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); 804 // CSH 805 if ((string)$this->id === '') { 806 $fieldName = 'list_module_noId'; 807 } elseif (!$this->id) { 808 $fieldName = 'list_module_root'; 809 } else { 810 $fieldName = 'list_module'; 811 } 812 $cshButton = $buttonBar->makeHelpButton() 813 ->setModuleName('xMOD_csh_corebe') 814 ->setFieldName($fieldName); 815 $buttonBar->addButton($cshButton); 816 if (isset($this->id)) { 817 // View Exclude doktypes 254,255 Configuration: 818 // mod.web_list.noViewWithDokTypes = 254,255 819 if (isset($module->modTSconfig['properties']['noViewWithDokTypes'])) { 820 $noViewDokTypes = GeneralUtility::trimExplode(',', $module->modTSconfig['properties']['noViewWithDokTypes'], true); 821 } else { 822 //default exclusion: doktype 254 (folder), 255 (recycler) 823 $noViewDokTypes = [ 824 PageRepository::DOKTYPE_SYSFOLDER, 825 PageRepository::DOKTYPE_RECYCLER 826 ]; 827 } 828 // New record on pages that are not locked by editlock 829 if (!$module->modTSconfig['properties']['noCreateRecordsLink'] && $this->editLockPermissions()) { 830 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id])) . ');'; 831 $newRecordButton = $buttonBar->makeLinkButton() 832 ->setHref('#') 833 ->setOnClick($onClick) 834 ->setTitle($lang->getLL('newRecordGeneral')) 835 ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)); 836 $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 10); 837 } 838 if (!in_array($this->pageRow['doktype'], $noViewDokTypes)) { 839 $onClick = BackendUtility::viewOnClick($this->id, '', BackendUtility::BEgetRootLine($this->id)); 840 $viewButton = $buttonBar->makeLinkButton() 841 ->setHref('#') 842 ->setOnClick($onClick) 843 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) 844 ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)); 845 $buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 20); 846 } 847 // If edit permissions are set, see 848 // \TYPO3\CMS\Core\Authentication\BackendUserAuthentication 849 if ($localCalcPerms & Permission::PAGE_EDIT && !empty($this->id) && $this->editLockPermissions() && $backendUser->checkLanguageAccess(0)) { 850 // Edit 851 $params = '&edit[pages][' . $this->pageRow['uid'] . ']=edit'; 852 $onClick = BackendUtility::editOnClick($params, '', -1); 853 $editButton = $buttonBar->makeLinkButton() 854 ->setHref('#') 855 ->setOnClick($onClick) 856 ->setTitle($lang->getLL('editPage')) 857 ->setIcon($this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)); 858 $buttonBar->addButton($editButton, ButtonBar::BUTTON_POSITION_LEFT, 20); 859 } 860 // Paste 861 if ($this->showClipboard && ($localCalcPerms & Permission::PAGE_NEW || $localCalcPerms & Permission::CONTENT_EDIT) && $this->editLockPermissions()) { 862 $elFromTable = $this->clipObj->elFromTable(''); 863 if (!empty($elFromTable)) { 864 $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable); 865 $pasteButton = $buttonBar->makeLinkButton() 866 ->setHref($this->clipObj->pasteUrl('', $this->id)) 867 ->setTitle($lang->getLL('clip_paste')) 868 ->setClasses('t3js-modal-trigger') 869 ->setDataAttributes([ 870 'severity' => 'warning', 871 'content' => $confirmMessage, 872 'title' => $lang->getLL('clip_paste') 873 ]) 874 ->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)); 875 $buttonBar->addButton($pasteButton, ButtonBar::BUTTON_POSITION_LEFT, 40); 876 } 877 } 878 // Cache 879 $clearCacheButton = $buttonBar->makeLinkButton() 880 ->setHref($this->listURL() . '&clear_cache=1') 881 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache')) 882 ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', Icon::SIZE_SMALL)); 883 $buttonBar->addButton($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT); 884 if ($this->table && (!isset($module->modTSconfig['properties']['noExportRecordsLinks']) 885 || (isset($module->modTSconfig['properties']['noExportRecordsLinks']) 886 && !$module->modTSconfig['properties']['noExportRecordsLinks'])) 887 ) { 888 // CSV 889 $csvButton = $buttonBar->makeLinkButton() 890 ->setHref($this->listURL() . '&csv=1') 891 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.csv')) 892 ->setIcon($this->iconFactory->getIcon('actions-document-export-csv', Icon::SIZE_SMALL)) 893 ->setShowLabelText(true); 894 $buttonBar->addButton($csvButton, ButtonBar::BUTTON_POSITION_LEFT, 40); 895 // Export 896 if (ExtensionManagementUtility::isLoaded('impexp')) { 897 $url = (string)$uriBuilder->buildUriFromRoute('xMOD_tximpexp', ['tx_impexp[action]' => 'export']); 898 $exportButton = $buttonBar->makeLinkButton() 899 ->setHref($url . '&tx_impexp[list][]=' . rawurlencode($this->table . ':' . $this->id)) 900 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.export')) 901 ->setIcon($this->iconFactory->getIcon('actions-document-export-t3d', Icon::SIZE_SMALL)) 902 ->setShowLabelText(true); 903 $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_LEFT, 40); 904 } 905 } 906 // Reload 907 $reloadButton = $buttonBar->makeLinkButton() 908 ->setHref($this->listURL()) 909 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload')) 910 ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)); 911 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT); 912 // Shortcut 913 if ($backendUser->mayMakeShortcut()) { 914 $shortCutButton = $buttonBar->makeShortcutButton() 915 ->setModuleName('web_list') 916 ->setGetVariables([ 917 'id', 918 'route', 919 'imagemode', 920 'pointer', 921 'table', 922 'search_field', 923 'search_levels', 924 'showLimit', 925 'sortField', 926 'sortRev' 927 ]) 928 ->setSetVariables(array_keys($this->MOD_MENU)); 929 $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT); 930 } 931 // Back 932 if ($this->returnUrl) { 933 $backButton = $buttonBar->makeLinkButton() 934 ->setHref($this->returnUrl) 935 ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack')) 936 ->setIcon($this->iconFactory->getIcon('actions-view-go-back', Icon::SIZE_SMALL)); 937 $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT); 938 } 939 } 940 } 941 942 /** 943 * Creates the listing of records from a single table 944 * 945 * @param string $table Table name 946 * @param int $id Page id 947 * @param string $rowList List of fields to show in the listing. Pseudo fields will be added including the record header. 948 * @throws \UnexpectedValueException 949 * @return string HTML table with the listing for the record. 950 */ 951 public function getTable($table, $id, $rowList = '') 952 { 953 $rowListArray = GeneralUtility::trimExplode(',', $rowList, true); 954 // if no columns have been specified, show description (if configured) 955 if (!empty($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']) && empty($rowListArray)) { 956 $rowListArray[] = $GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']; 957 } 958 $backendUser = $this->getBackendUserAuthentication(); 959 $lang = $this->getLanguageService(); 960 // Init 961 $addWhere = ''; 962 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 963 $titleCol = $GLOBALS['TCA'][$table]['ctrl']['label']; 964 $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail']; 965 $l10nEnabled = $GLOBALS['TCA'][$table]['ctrl']['languageField'] 966 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; 967 $tableCollapsed = (bool)$this->tablesCollapsed[$table]; 968 // prepare space icon 969 $this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'; 970 // Cleaning rowlist for duplicates and place the $titleCol as the first column always! 971 $this->fieldArray = []; 972 // title Column 973 // Add title column 974 $this->fieldArray[] = $titleCol; 975 // Control-Panel 976 if (!GeneralUtility::inList($rowList, '_CONTROL_')) { 977 $this->fieldArray[] = '_CONTROL_'; 978 } 979 // Clipboard 980 if ($this->showClipboard) { 981 $this->fieldArray[] = '_CLIPBOARD_'; 982 } 983 // Ref 984 if (!$this->dontShowClipControlPanels) { 985 $this->fieldArray[] = '_REF_'; 986 } 987 // Path 988 if ($this->searchLevels) { 989 $this->fieldArray[] = '_PATH_'; 990 } 991 // Localization 992 if ($l10nEnabled) { 993 $this->fieldArray[] = '_LOCALIZATION_'; 994 // Do not show the "Localize to:" field when only translated records should be shown 995 if (!$this->showOnlyTranslatedRecords) { 996 $this->fieldArray[] = '_LOCALIZATION_b'; 997 } 998 // Only restrict to the default language if no search request is in place 999 // And if only translations should be shown 1000 if ($this->searchString === '' && !$this->showOnlyTranslatedRecords) { 1001 $addWhere = (string)$queryBuilder->expr()->orX( 1002 $queryBuilder->expr()->lte($GLOBALS['TCA'][$table]['ctrl']['languageField'], 0), 1003 $queryBuilder->expr()->eq($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 0) 1004 ); 1005 } 1006 } 1007 // Cleaning up: 1008 $this->fieldArray = array_unique(array_merge($this->fieldArray, $rowListArray)); 1009 if ($this->noControlPanels) { 1010 $tempArray = array_flip($this->fieldArray); 1011 unset($tempArray['_CONTROL_']); 1012 unset($tempArray['_CLIPBOARD_']); 1013 $this->fieldArray = array_keys($tempArray); 1014 } 1015 // Creating the list of fields to include in the SQL query: 1016 $selectFields = $this->fieldArray; 1017 $selectFields[] = 'uid'; 1018 $selectFields[] = 'pid'; 1019 // adding column for thumbnails 1020 if ($thumbsCol) { 1021 $selectFields[] = $thumbsCol; 1022 } 1023 if ($table === 'pages') { 1024 $selectFields[] = 'module'; 1025 $selectFields[] = 'extendToSubpages'; 1026 $selectFields[] = 'nav_hide'; 1027 $selectFields[] = 'doktype'; 1028 $selectFields[] = 'shortcut'; 1029 $selectFields[] = 'shortcut_mode'; 1030 $selectFields[] = 'mount_pid'; 1031 } 1032 if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) { 1033 $selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']); 1034 } 1035 foreach (['type', 'typeicon_column', 'editlock'] as $field) { 1036 if ($GLOBALS['TCA'][$table]['ctrl'][$field]) { 1037 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl'][$field]; 1038 } 1039 } 1040 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { 1041 $selectFields[] = 't3ver_id'; 1042 $selectFields[] = 't3ver_state'; 1043 $selectFields[] = 't3ver_wsid'; 1044 } 1045 if ($l10nEnabled) { 1046 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['languageField']; 1047 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; 1048 } 1049 if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) { 1050 $selectFields = array_merge( 1051 $selectFields, 1052 GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true) 1053 ); 1054 } 1055 // Unique list! 1056 $selectFields = array_unique($selectFields); 1057 $fieldListFields = $this->makeFieldList($table, 1); 1058 if (empty($fieldListFields) && $GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) { 1059 $message = sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessage'), $table, $table); 1060 $messageTitle = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:missingTcaColumnsMessageTitle'); 1061 /** @var FlashMessage $flashMessage */ 1062 $flashMessage = GeneralUtility::makeInstance( 1063 FlashMessage::class, 1064 $message, 1065 $messageTitle, 1066 FlashMessage::WARNING, 1067 true 1068 ); 1069 /** @var FlashMessageService $flashMessageService */ 1070 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); 1071 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */ 1072 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); 1073 $defaultFlashMessageQueue->enqueue($flashMessage); 1074 } 1075 // Making sure that the fields in the field-list ARE in the field-list from TCA! 1076 $selectFields = array_intersect($selectFields, $fieldListFields); 1077 // Implode it into a list of fields for the SQL-statement. 1078 $selFieldList = implode(',', $selectFields); 1079 $this->selFieldList = $selFieldList; 1080 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable'] ?? [] as $className) { 1081 $hookObject = GeneralUtility::makeInstance($className); 1082 if (!$hookObject instanceof RecordListGetTableHookInterface) { 1083 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListGetTableHookInterface::class, 1195114460); 1084 } 1085 $hookObject->getDBlistQuery($table, $id, $addWhere, $selFieldList, $this); 1086 } 1087 1088 if ($table == 'pages' && $this->showOnlyTranslatedRecords) { 1089 $addWhere .= ' AND ' . $GLOBALS['TCA']['pages']['ctrl']['languageField'] . ' IN(' . implode(',', array_keys($this->languagesAllowedForUser)) . ')'; 1090 } 1091 1092 $additionalConstraints = empty($addWhere) ? [] : [QueryHelper::stripLogicalOperatorPrefix($addWhere)]; 1093 $selFieldList = GeneralUtility::trimExplode(',', $selFieldList, true); 1094 1095 // Create the SQL query for selecting the elements in the listing: 1096 // do not do paging when outputting as CSV 1097 if ($this->csvOutput) { 1098 $this->iLimit = 0; 1099 } 1100 if ($this->firstElementNumber > 2 && $this->iLimit > 0) { 1101 // Get the two previous rows for sorting if displaying page > 1 1102 $this->firstElementNumber -= 2; 1103 $this->iLimit += 2; 1104 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList) 1105 $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints); 1106 $this->firstElementNumber += 2; 1107 $this->iLimit -= 2; 1108 } else { 1109 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList) 1110 $queryBuilder = $this->getQueryBuilder($table, $id, $additionalConstraints); 1111 } 1112 1113 // Finding the total amount of records on the page 1114 // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList) 1115 $this->setTotalItems($table, $id, $additionalConstraints); 1116 1117 // Init: 1118 $queryResult = $queryBuilder->execute(); 1119 $dbCount = 0; 1120 $out = ''; 1121 $tableHeader = ''; 1122 $listOnlyInSingleTableMode = $this->listOnlyInSingleTableMode && !$this->table; 1123 // If the count query returned any number of records, we perform the real query, 1124 // selecting records. 1125 if ($this->totalItems) { 1126 // Fetch records only if not in single table mode 1127 if ($listOnlyInSingleTableMode) { 1128 $dbCount = $this->totalItems; 1129 } else { 1130 // Set the showLimit to the number of records when outputting as CSV 1131 if ($this->csvOutput) { 1132 $this->showLimit = $this->totalItems; 1133 $this->iLimit = $this->totalItems; 1134 $dbCount = $this->totalItems; 1135 } else { 1136 if ($this->firstElementNumber + $this->showLimit <= $this->totalItems) { 1137 $dbCount = $this->showLimit + 2; 1138 } else { 1139 $dbCount = $this->totalItems - $this->firstElementNumber + 2; 1140 } 1141 } 1142 } 1143 } 1144 // If any records was selected, render the list: 1145 if ($dbCount) { 1146 $tableIdentifier = $table; 1147 // Use a custom table title for translated pages 1148 if ($table == 'pages' && $this->showOnlyTranslatedRecords) { 1149 // pages records in list module are split into two own sections, one for pages with 1150 // sys_language_uid = 0 "Page" and an own section for sys_language_uid > 0 "Page Translation". 1151 // This if sets the different title for the page translation case and a unique table identifier 1152 // which is used in DOM as id. 1153 $tableTitle = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:pageTranslation')); 1154 $tableIdentifier = 'pages_translated'; 1155 } else { 1156 $tableTitle = htmlspecialchars($lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'])); 1157 if ($tableTitle === '') { 1158 $tableTitle = $table; 1159 } 1160 } 1161 // Header line is drawn 1162 $theData = []; 1163 if ($this->disableSingleTableView) { 1164 $theData[$titleCol] = '<span class="c-table">' . BackendUtility::wrapInHelp($table, '', $tableTitle) 1165 . '</span> (<span class="t3js-table-total-items">' . $this->totalItems . '</span>)'; 1166 } else { 1167 $icon = $this->table 1168 ? '<span title="' . htmlspecialchars($lang->getLL('contractView')) . '">' . $this->iconFactory->getIcon('actions-view-table-collapse', Icon::SIZE_SMALL)->render() . '</span>' 1169 : '<span title="' . htmlspecialchars($lang->getLL('expandView')) . '">' . $this->iconFactory->getIcon('actions-view-table-expand', Icon::SIZE_SMALL)->render() . '</span>'; 1170 $theData[$titleCol] = $this->linkWrapTable($table, $tableTitle . ' (<span class="t3js-table-total-items">' . $this->totalItems . '</span>) ' . $icon); 1171 } 1172 if ($listOnlyInSingleTableMode) { 1173 $tableHeader .= BackendUtility::wrapInHelp($table, '', $theData[$titleCol]); 1174 } else { 1175 // Render collapse button if in multi table mode 1176 $collapseIcon = ''; 1177 if (!$this->table) { 1178 $href = htmlspecialchars($this->listURL() . '&collapse[' . $table . ']=' . ($tableCollapsed ? '0' : '1')); 1179 $title = $tableCollapsed 1180 ? htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.expandTable')) 1181 : htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.collapseTable')); 1182 $icon = '<span class="collapseIcon">' . $this->iconFactory->getIcon(($tableCollapsed ? 'actions-view-list-expand' : 'actions-view-list-collapse'), Icon::SIZE_SMALL)->render() . '</span>'; 1183 $collapseIcon = '<a href="' . $href . '" title="' . $title . '" class="pull-right t3js-toggle-recordlist" data-table="' . htmlspecialchars($tableIdentifier) . '" data-toggle="collapse" data-target="#recordlist-' . htmlspecialchars($tableIdentifier) . '">' . $icon . '</a>'; 1184 } 1185 $tableHeader .= $theData[$titleCol] . $collapseIcon; 1186 } 1187 // Render table rows only if in multi table view or if in single table view 1188 $rowOutput = ''; 1189 if (!$listOnlyInSingleTableMode || $this->table) { 1190 // Fixing an order table for sortby tables 1191 $this->currentTable = []; 1192 $currentIdList = []; 1193 $doSort = $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField; 1194 $prevUid = 0; 1195 $prevPrevUid = 0; 1196 // Get first two rows and initialize prevPrevUid and prevUid if on page > 1 1197 if ($this->firstElementNumber > 2 && $this->iLimit > 0) { 1198 $row = $queryResult->fetch(); 1199 $prevPrevUid = -((int)$row['uid']); 1200 $row = $queryResult->fetch(); 1201 $prevUid = $row['uid']; 1202 } 1203 $accRows = []; 1204 // Accumulate rows here 1205 while ($row = $queryResult->fetch()) { 1206 if (!$this->isRowListingConditionFulfilled($table, $row)) { 1207 continue; 1208 } 1209 // In offline workspace, look for alternative record: 1210 BackendUtility::workspaceOL($table, $row, $backendUser->workspace, true); 1211 if (is_array($row)) { 1212 $accRows[] = $row; 1213 $currentIdList[] = $row['uid']; 1214 if ($doSort) { 1215 if ($prevUid) { 1216 $this->currentTable['prev'][$row['uid']] = $prevPrevUid; 1217 $this->currentTable['next'][$prevUid] = '-' . $row['uid']; 1218 $this->currentTable['prevUid'][$row['uid']] = $prevUid; 1219 } 1220 $prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid']; 1221 $prevUid = $row['uid']; 1222 } 1223 } 1224 } 1225 $this->totalRowCount = count($accRows); 1226 // CSV initiated 1227 if ($this->csvOutput) { 1228 $this->initCSV(); 1229 } 1230 // Render items: 1231 $this->CBnames = []; 1232 $this->duplicateStack = []; 1233 $this->eCounter = $this->firstElementNumber; 1234 $cc = 0; 1235 foreach ($accRows as $row) { 1236 // Render item row if counter < limit 1237 if ($cc < $this->iLimit) { 1238 $cc++; 1239 $this->translations = false; 1240 $rowOutput .= $this->renderListRow($table, $row, $cc, $titleCol, $thumbsCol); 1241 // If no search happened it means that the selected 1242 // records are either default or All language and here we will not select translations 1243 // which point to the main record: 1244 if ($l10nEnabled && $this->searchString === '' && !($this->hideTranslations === '*' || GeneralUtility::inList($this->hideTranslations, $table))) { 1245 // For each available translation, render the record: 1246 if (is_array($this->translations)) { 1247 foreach ($this->translations as $lRow) { 1248 // $lRow isn't always what we want - if record was moved we've to work with the 1249 // placeholder records otherwise the list is messed up a bit 1250 if ($row['_MOVE_PLH_uid'] && $row['_MOVE_PLH_pid']) { 1251 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1252 ->getQueryBuilderForTable($table); 1253 $queryBuilder->getRestrictions() 1254 ->removeAll() 1255 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1256 $predicates = [ 1257 $queryBuilder->expr()->eq( 1258 't3ver_move_id', 1259 $queryBuilder->createNamedParameter((int)$lRow['uid'], \PDO::PARAM_INT) 1260 ), 1261 $queryBuilder->expr()->eq( 1262 'pid', 1263 $queryBuilder->createNamedParameter((int)$row['_MOVE_PLH_pid'], \PDO::PARAM_INT) 1264 ), 1265 $queryBuilder->expr()->eq( 1266 't3ver_wsid', 1267 $queryBuilder->createNamedParameter((int)$row['t3ver_wsid'], \PDO::PARAM_INT) 1268 ), 1269 ]; 1270 1271 $tmpRow = $queryBuilder 1272 ->select(...$selFieldList) 1273 ->from($table) 1274 ->andWhere(...$predicates) 1275 ->execute() 1276 ->fetch(); 1277 1278 $lRow = is_array($tmpRow) ? $tmpRow : $lRow; 1279 } 1280 if (!$this->isRowListingConditionFulfilled($table, $lRow)) { 1281 continue; 1282 } 1283 // In offline workspace, look for alternative record: 1284 BackendUtility::workspaceOL($table, $lRow, $backendUser->workspace, true); 1285 if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) { 1286 $currentIdList[] = $lRow['uid']; 1287 $rowOutput .= $this->renderListRow($table, $lRow, $cc, $titleCol, $thumbsCol, 18); 1288 } 1289 } 1290 } 1291 } 1292 } 1293 // Counter of total rows incremented: 1294 $this->eCounter++; 1295 } 1296 // Record navigation is added to the beginning and end of the table if in single 1297 // table mode 1298 if ($this->table) { 1299 $rowOutput = $this->renderListNavigation('top') . $rowOutput . $this->renderListNavigation('bottom'); 1300 } else { 1301 // Show that there are more records than shown 1302 if ($this->totalItems > $this->itemsLimitPerTable) { 1303 $countOnFirstPage = $this->totalItems > $this->itemsLimitSingleTable ? $this->itemsLimitSingleTable : $this->totalItems; 1304 $hasMore = $this->totalItems > $this->itemsLimitSingleTable; 1305 $colspan = $this->showIcon ? count($this->fieldArray) + 1 : count($this->fieldArray); 1306 $rowOutput .= '<tr><td colspan="' . $colspan . '"> 1307 <a href="' . htmlspecialchars($this->listURL() . '&table=' . rawurlencode($tableIdentifier)) . '" class="btn btn-default">' 1308 . '<span class="t3-icon fa fa-chevron-down"></span> <i>[1 - ' . $countOnFirstPage . ($hasMore ? '+' : '') . ']</i></a> 1309 </td></tr>'; 1310 } 1311 } 1312 // The header row for the table is now created: 1313 $out .= $this->renderListHeader($table, $currentIdList); 1314 } 1315 1316 $collapseClass = $tableCollapsed && !$this->table ? 'collapse' : 'collapse in'; 1317 $dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded'; 1318 1319 // The list of records is added after the header: 1320 $out .= $rowOutput; 1321 // ... and it is all wrapped in a table: 1322 $out = ' 1323 1324 1325 1326 <!-- 1327 DB listing of elements: "' . htmlspecialchars($tableIdentifier) . '" 1328 --> 1329 <div class="panel panel-space panel-default recordlist"> 1330 <div class="panel-heading"> 1331 ' . $tableHeader . ' 1332 </div> 1333 <div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($tableIdentifier) . '"> 1334 <div class="table-fit"> 1335 <table data-table="' . htmlspecialchars($tableIdentifier) . '" class="table table-striped table-hover' . ($listOnlyInSingleTableMode ? ' typo3-dblist-overview' : '') . '"> 1336 ' . $out . ' 1337 </table> 1338 </div> 1339 </div> 1340 </div> 1341 '; 1342 // Output csv if... 1343 // This ends the page with exit. 1344 if ($this->csvOutput) { 1345 $this->outputCSV($table); 1346 } 1347 } 1348 // Return content: 1349 return $out; 1350 } 1351 1352 /** 1353 * Get viewOnClick link for pages or tt_content records 1354 * 1355 * @param string $table 1356 * @param array $row 1357 * 1358 * @return string 1359 */ 1360 protected function getOnClickForRow(string $table, array $row): string 1361 { 1362 if ($table === 'tt_content') { 1363 // Link to a content element, possibly translated and with anchor 1364 $additionalParams = ''; 1365 $language = (int)$row[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']]; 1366 if ($language > 0) { 1367 $additionalParams = '&L=' . $language; 1368 } 1369 $onClick = BackendUtility::viewOnClick( 1370 $this->id, 1371 '', 1372 null, 1373 '#c' . $row['uid'], 1374 '', 1375 $additionalParams 1376 ); 1377 } elseif ($table === 'pages' && $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] > 0) { 1378 // Link to a page translation needs uid of default language page as id 1379 $onClick = BackendUtility::viewOnClick( 1380 $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']], 1381 '', 1382 null, 1383 '', 1384 '', 1385 '&L=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']] 1386 ); 1387 } else { 1388 // Link to a page in the default language 1389 $onClick = BackendUtility::viewOnClick($row['uid']); 1390 } 1391 return $onClick; 1392 } 1393 1394 /** 1395 * Check if all row listing conditions are fulfilled. 1396 * 1397 * This function serves as a dummy method to be overridden in extending classes. 1398 * 1399 * @param string $table Table name 1400 * @param string[] $row Record 1401 * @return bool True, if all conditions are fulfilled. 1402 */ 1403 protected function isRowListingConditionFulfilled($table, $row) 1404 { 1405 return true; 1406 } 1407 1408 /** 1409 * Rendering a single row for the list 1410 * 1411 * @param string $table Table name 1412 * @param mixed[] $row Current record 1413 * @param int $cc Counter, counting for each time an element is rendered (used for alternating colors) 1414 * @param string $titleCol Table field (column) where header value is found 1415 * @param string $thumbsCol Table field (column) where (possible) thumbnails can be found 1416 * @param int $indent Indent from left. 1417 * @return string Table row for the element 1418 * @internal 1419 * @see getTable() 1420 */ 1421 public function renderListRow($table, $row, $cc, $titleCol, $thumbsCol, $indent = 0) 1422 { 1423 if (!is_array($row)) { 1424 return ''; 1425 } 1426 $rowOutput = ''; 1427 $id_orig = null; 1428 // If in search mode, make sure the preview will show the correct page 1429 if ((string)$this->searchString !== '') { 1430 $id_orig = $this->id; 1431 $this->id = $row['pid']; 1432 } 1433 1434 $tagAttributes = [ 1435 'class' => [], 1436 'data-table' => $table, 1437 'title' => 'id=' . $row['uid'], 1438 ]; 1439 1440 // Add active class to record of current link 1441 if ( 1442 isset($this->currentLink['tableNames']) 1443 && (int)$this->currentLink['uid'] === (int)$row['uid'] 1444 && GeneralUtility::inList($this->currentLink['tableNames'], $table) 1445 ) { 1446 $tagAttributes['class'][] = 'active'; 1447 } 1448 // Add special classes for first and last row 1449 if ($cc == 1 && $indent == 0) { 1450 $tagAttributes['class'][] = 'firstcol'; 1451 } 1452 if ($cc == $this->totalRowCount || $cc == $this->iLimit) { 1453 $tagAttributes['class'][] = 'lastcol'; 1454 } 1455 // Overriding with versions background color if any: 1456 if (!empty($row['_CSSCLASS'])) { 1457 $tagAttributes['class'] = [$row['_CSSCLASS']]; 1458 } 1459 1460 $tagAttributes['class'][] = 't3js-entity'; 1461 1462 // Incr. counter. 1463 $this->counter++; 1464 // The icon with link 1465 $toolTip = BackendUtility::getRecordToolTip($row, $table); 1466 $additionalStyle = $indent ? ' style="margin-left: ' . $indent . 'px;"' : ''; 1467 $iconImg = '<span ' . $toolTip . ' ' . $additionalStyle . '>' 1468 . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() 1469 . '</span>'; 1470 $theIcon = $this->clickMenuEnabled ? BackendUtility::wrapClickMenuOnIcon($iconImg, $table, $row['uid']) : $iconImg; 1471 // Preparing and getting the data-array 1472 $theData = []; 1473 $localizationMarkerClass = ''; 1474 foreach ($this->fieldArray as $fCol) { 1475 if ($fCol == $titleCol) { 1476 $recTitle = BackendUtility::getRecordTitle($table, $row, false, true); 1477 $warning = ''; 1478 // If the record is edit-locked by another user, we will show a little warning sign: 1479 $lockInfo = BackendUtility::isRecordLocked($table, $row['uid']); 1480 if ($lockInfo) { 1481 $warning = '<span data-toggle="tooltip" data-placement="right" data-title="' . htmlspecialchars($lockInfo['msg']) . '">' 1482 . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>'; 1483 } 1484 $theData[$fCol] = $theData['__label'] = $warning . $this->linkWrapItems($table, $row['uid'], $recTitle, $row); 1485 // Render thumbnails, if: 1486 // - a thumbnail column exists 1487 // - there is content in it 1488 // - the thumbnail column is visible for the current type 1489 $type = 0; 1490 if (isset($GLOBALS['TCA'][$table]['ctrl']['type'])) { 1491 $typeColumn = $GLOBALS['TCA'][$table]['ctrl']['type']; 1492 $type = $row[$typeColumn]; 1493 } 1494 // If current type doesn't exist, set it to 0 (or to 1 for historical reasons, 1495 // if 0 doesn't exist) 1496 if (!isset($GLOBALS['TCA'][$table]['types'][$type])) { 1497 $type = isset($GLOBALS['TCA'][$table]['types'][0]) ? 0 : 1; 1498 } 1499 1500 $visibleColumns = $this->getVisibleColumns($GLOBALS['TCA'][$table], $type); 1501 1502 if ($this->thumbs && 1503 trim($row[$thumbsCol]) && 1504 preg_match('/(^|(.*(;|,)?))' . $thumbsCol . '(((;|,).*)|$)/', $visibleColumns) === 1 1505 ) { 1506 $thumbCode = '<br />' . $this->thumbCode($row, $table, $thumbsCol); 1507 $theData[$fCol] .= $thumbCode; 1508 $theData['__label'] .= $thumbCode; 1509 } 1510 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) 1511 && $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] != 0 1512 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0 1513 ) { 1514 // It's a translated record with a language parent 1515 $localizationMarkerClass = ' localization'; 1516 } 1517 } elseif ($fCol === 'pid') { 1518 $theData[$fCol] = $row[$fCol]; 1519 } elseif ($fCol === '_PATH_') { 1520 $theData[$fCol] = $this->recPath($row['pid']); 1521 } elseif ($fCol === '_REF_') { 1522 $theData[$fCol] = $this->createReferenceHtml($table, $row['uid']); 1523 } elseif ($fCol === '_CONTROL_') { 1524 $theData[$fCol] = $this->makeControl($table, $row); 1525 } elseif ($fCol === '_CLIPBOARD_') { 1526 $theData[$fCol] = $this->makeClip($table, $row); 1527 } elseif ($fCol === '_LOCALIZATION_') { 1528 [$lC1, $lC2] = $this->makeLocalizationPanel($table, $row); 1529 $theData[$fCol] = $lC1; 1530 $theData[$fCol . 'b'] = '<div class="btn-group">' . $lC2 . '</div>'; 1531 } elseif ($fCol === '_LOCALIZATION_b') { 1532 // deliberately empty 1533 } else { 1534 $pageId = $table === 'pages' ? $row['uid'] : $row['pid']; 1535 $tmpProc = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 100, $row['uid'], true, $pageId); 1536 $theData[$fCol] = $this->linkUrlMail(htmlspecialchars($tmpProc), $row[$fCol]); 1537 if ($this->csvOutput) { 1538 $row[$fCol] = BackendUtility::getProcessedValueExtra($table, $fCol, $row[$fCol], 0, $row['uid']); 1539 } 1540 } 1541 } 1542 // Reset the ID if it was overwritten 1543 if ((string)$this->searchString !== '') { 1544 $this->id = $id_orig; 1545 } 1546 // Add row to CSV list: 1547 if ($this->csvOutput) { 1548 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['customizeCsvRow'] ?? []; 1549 if (!empty($hooks)) { 1550 $hookParameters = [ 1551 'databaseRow' => &$row, 1552 'tableName' => $table, 1553 'pageId' => $this->id 1554 ]; 1555 foreach ($hooks as $hookFunction) { 1556 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this); 1557 } 1558 } 1559 $this->addToCSV($row); 1560 } 1561 // Add classes to table cells 1562 $this->addElement_tdCssClass[$titleCol] = 'col-title col-responsive' . $localizationMarkerClass; 1563 $this->addElement_tdCssClass['__label'] = $this->addElement_tdCssClass[$titleCol]; 1564 $this->addElement_tdCssClass['_CONTROL_'] = 'col-control'; 1565 if ($this->getModule()->MOD_SETTINGS['clipBoard']) { 1566 $this->addElement_tdCssClass['_CLIPBOARD_'] = 'col-clipboard'; 1567 } 1568 $this->addElement_tdCssClass['_PATH_'] = 'col-path'; 1569 $this->addElement_tdCssClass['_LOCALIZATION_'] = 'col-localizationa'; 1570 $this->addElement_tdCssClass['_LOCALIZATION_b'] = 'col-localizationb'; 1571 // Create element in table cells: 1572 $theData['uid'] = $row['uid']; 1573 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) 1574 && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) 1575 ) { 1576 $theData['_l10nparent_'] = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; 1577 } 1578 1579 $tagAttributes = array_map( 1580 function ($attributeValue) { 1581 if (is_array($attributeValue)) { 1582 return implode(' ', $attributeValue); 1583 } 1584 return $attributeValue; 1585 }, 1586 $tagAttributes 1587 ); 1588 1589 $rowOutput .= $this->addElement(1, $theIcon, $theData, GeneralUtility::implodeAttributes($tagAttributes, true)); 1590 // Finally, return table row element: 1591 return $rowOutput; 1592 } 1593 1594 /** 1595 * Gets the number of records referencing the record with the UID $uid in 1596 * the table $tableName. 1597 * 1598 * @param string $tableName 1599 * @param int $uid 1600 * @return int The number of references to record $uid in table 1601 */ 1602 protected function getReferenceCount($tableName, $uid) 1603 { 1604 if (!isset($this->referenceCount[$tableName][$uid])) { 1605 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); 1606 $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid); 1607 $this->referenceCount[$tableName][$uid] = $numberOfReferences; 1608 } 1609 return $this->referenceCount[$tableName][$uid]; 1610 } 1611 1612 /** 1613 * Rendering the header row for a table 1614 * 1615 * @param string $table Table name 1616 * @param int[] $currentIdList Array of the currently displayed uids of the table 1617 * @throws \UnexpectedValueException 1618 * @return string Header table row 1619 * @internal 1620 * @see getTable() 1621 */ 1622 public function renderListHeader($table, $currentIdList) 1623 { 1624 $tsConfig = BackendUtility::getPagesTSconfig($this->id); 1625 $tsConfigOfTable = is_array($tsConfig['TCEFORM.'][$table . '.']) ? $tsConfig['TCEFORM.'][$table . '.'] : null; 1626 1627 $lang = $this->getLanguageService(); 1628 // Init: 1629 $theData = []; 1630 $icon = ''; 1631 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 1632 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); 1633 // Traverse the fields: 1634 foreach ($this->fieldArray as $fCol) { 1635 // Calculate users permissions to edit records in the table: 1636 $permsEdit = $this->calcPerms & ($table === 'pages' ? 2 : 16) && $this->overlayEditLockPermissions($table); 1637 switch ((string)$fCol) { 1638 case '_PATH_': 1639 // Path 1640 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_')) . ']</i>'; 1641 break; 1642 case '_REF_': 1643 // References 1644 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:c__REF_')) . ']</i>'; 1645 break; 1646 case '_LOCALIZATION_': 1647 // Path 1648 $theData[$fCol] = '<i>[' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._LOCALIZATION_')) . ']</i>'; 1649 break; 1650 case '_LOCALIZATION_b': 1651 // Path 1652 $theData[$fCol] = htmlspecialchars($lang->getLL('Localize')); 1653 break; 1654 case '_CLIPBOARD_': 1655 if (!$this->getModule()->MOD_SETTINGS['clipBoard']) { 1656 break; 1657 } 1658 // Clipboard: 1659 $cells = []; 1660 // If there are elements on the clipboard for this table, and the parent page is not locked by editlock 1661 // then display the "paste into" icon: 1662 $elFromTable = $this->clipObj->elFromTable($table); 1663 if (!empty($elFromTable) && $this->overlayEditLockPermissions($table)) { 1664 $href = htmlspecialchars($this->clipObj->pasteUrl($table, $this->id)); 1665 $confirmMessage = $this->clipObj->confirmMsgText('pages', $this->pageRow, 'into', $elFromTable); 1666 $cells['pasteAfter'] = '<a class="btn btn-default t3js-modal-trigger"' 1667 . ' href="' . $href . '"' 1668 . ' title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"' 1669 . ' data-title="' . htmlspecialchars($lang->getLL('clip_paste')) . '"' 1670 . ' data-content="' . htmlspecialchars($confirmMessage) . '"' 1671 . ' data-severity="warning">' 1672 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() 1673 . '</a>'; 1674 } 1675 // If the numeric clipboard pads are enabled, display the control icons for that: 1676 if ($this->clipObj->current !== 'normal') { 1677 // The "select" link: 1678 $spriteIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render(); 1679 $cells['copyMarked'] = $this->linkClipboardHeaderIcon($spriteIcon, $table, 'setCB', '', $lang->getLL('clip_selectMarked')); 1680 // The "edit marked" link: 1681 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit') 1682 . '&edit[' . $table . '][{entityIdentifiers:editList}]=edit' 1683 . '&returnUrl={T3_THIS_LOCATION}'; 1684 $cells['edit'] = '<a class="btn btn-default t3js-record-edit-multiple" href="#"' 1685 . ' data-uri="' . htmlspecialchars($editUri) . '"' 1686 . ' title="' . htmlspecialchars($lang->getLL('clip_editMarked')) . '">' 1687 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>'; 1688 // The "Delete marked" link: 1689 $cells['delete'] = $this->linkClipboardHeaderIcon( 1690 $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(), 1691 $table, 1692 'delete', 1693 sprintf($lang->getLL('clip_deleteMarkedWarning'), $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'])), 1694 $lang->getLL('clip_deleteMarked') 1695 ); 1696 // The "Select all" link: 1697 $onClick = htmlspecialchars('checkOffCB(' . GeneralUtility::quoteJSvalue(implode(',', $this->CBnames)) . ', this); return false;'); 1698 $cells['markAll'] = '<a class="btn btn-default" rel="" href="#" onclick="' . $onClick . '" title="' 1699 . htmlspecialchars($lang->getLL('clip_markRecords')) . '">' 1700 . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>'; 1701 } else { 1702 $cells['empty'] = ''; 1703 } 1704 /* 1705 * hook: renderListHeaderActions: Allows to change the clipboard icons of the Web>List table headers 1706 * usage: Above each listed table in Web>List a header row is shown. 1707 * This hook allows to modify the icons responsible for the clipboard functions 1708 * (shown above the clipboard checkboxes when a clipboard other than "Normal" is selected), 1709 * or other "Action" functions which perform operations on the listed records. 1710 */ 1711 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) { 1712 $hookObject = GeneralUtility::makeInstance($className); 1713 if (!$hookObject instanceof RecordListHookInterface) { 1714 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567850); 1715 } 1716 $cells = $hookObject->renderListHeaderActions($table, $currentIdList, $cells, $this); 1717 } 1718 $theData[$fCol] = ''; 1719 if (isset($cells['edit']) && isset($cells['delete'])) { 1720 $theData[$fCol] .= '<div class="btn-group" role="group">' . $cells['edit'] . $cells['delete'] . '</div>'; 1721 unset($cells['edit'], $cells['delete']); 1722 } 1723 $theData[$fCol] .= '<div class="btn-group" role="group">' . implode('', $cells) . '</div>'; 1724 break; 1725 case '_CONTROL_': 1726 // Control panel: 1727 if ($this->isEditable($table)) { 1728 // If new records can be created on this page, add links: 1729 $permsAdditional = ($table === 'pages' ? 8 : 16); 1730 if ($this->calcPerms & $permsAdditional && $this->showNewRecLink($table)) { 1731 $spriteIcon = $table === 'pages' 1732 ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL) 1733 : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL); 1734 if ($table === 'tt_content') { 1735 // If mod.newContentElementWizard.override is set, use that extension's create new content wizard instead: 1736 $newContentElementWizard = BackendUtility::getPagesTSconfig($this->pageinfo['uid'])['mod.']['newContentElementWizard.']['override'] 1737 ?? 'new_content_element_wizard'; 1738 $url = (string)$uriBuilder->buildUriFromRoute( 1739 $newContentElementWizard, 1740 [ 1741 'id' => $this->id, 1742 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'), 1743 ] 1744 ); 1745 $icon = '<a href="' . htmlspecialchars($url) . '" ' 1746 . 'data-title="' . htmlspecialchars($lang->getLL('new')) . '"' 1747 . 'class="btn btn-default t3js-toggle-new-content-element-wizard disabled"">' 1748 . $spriteIcon->render() 1749 . '</a>'; 1750 } elseif ($table === 'pages') { 1751 $parameters = ['id' => $this->id, 'pagesOnly' => 1, 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')]; 1752 $href = (string)$uriBuilder->buildUriFromRoute('db_new', $parameters); 1753 $icon = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="' . htmlspecialchars($lang->getLL('new')) . '">' 1754 . $spriteIcon->render() . '</a>'; 1755 } else { 1756 $params = '&edit[' . $table . '][' . $this->id . ']=new'; 1757 if ($table === 'pages') { 1758 $params .= '&overrideVals[pages][doktype]=' . (int)$this->pageRow['doktype']; 1759 } 1760 $icon = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1)) 1761 . '" title="' . htmlspecialchars($lang->getLL('new')) . '">' . $spriteIcon->render() . '</a>'; 1762 } 1763 } 1764 // If the table can be edited, add link for editing ALL SHOWN fields for all listed records: 1765 if ($permsEdit && $this->table && is_array($currentIdList)) { 1766 $entityIdentifiers = 'entityIdentifiers'; 1767 if ($this->clipNumPane()) { 1768 $entityIdentifiers .= ':editList'; 1769 } 1770 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit') 1771 . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit' 1772 . '&columnsOnly=' . implode(',', $this->fieldArray) 1773 . '&returnUrl={T3_THIS_LOCATION}'; 1774 $icon .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"' 1775 . ' data-uri="' . htmlspecialchars($editUri) . '"' 1776 . ' title="' . htmlspecialchars($lang->getLL('editShownColumns')) . '">' 1777 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>'; 1778 $icon = '<div class="btn-group" role="group">' . $icon . '</div>'; 1779 } 1780 // Add an empty entry, so column count fits again after moving this into $icon 1781 $theData[$fCol] = ' '; 1782 } else { 1783 $icon = $this->spaceIcon; 1784 } 1785 break; 1786 default: 1787 // Regular fields header: 1788 $theData[$fCol] = ''; 1789 1790 // Check if $fCol is really a field and get the label and remove the colons 1791 // at the end 1792 $sortLabel = BackendUtility::getItemLabel($table, $fCol); 1793 if ($sortLabel !== null) { 1794 $sortLabel = rtrim(trim($lang->sL($sortLabel)), ':'); 1795 1796 // Field label 1797 $fieldTSConfig = []; 1798 if (isset($tsConfigOfTable[$fCol . '.']) 1799 && is_array($tsConfigOfTable[$fCol . '.']) 1800 ) { 1801 $fieldTSConfig = $tsConfigOfTable[$fCol . '.']; 1802 } 1803 if (!empty($fieldTSConfig['label'])) { 1804 $sortLabel = $lang->sL($fieldTSConfig['label']); 1805 } 1806 if (!empty($fieldTSConfig['label.'][$lang->lang])) { 1807 $sortLabel = $lang->sL($fieldTSConfig['label.'][$lang->lang]); 1808 } 1809 $sortLabel = htmlspecialchars($sortLabel); 1810 } else { 1811 // No TCA field, only output the $fCol variable with square brackets [] 1812 $sortLabel = htmlspecialchars($fCol); 1813 $sortLabel = '<i>[' . rtrim(trim($sortLabel), ':') . ']</i>'; 1814 } 1815 1816 if ($this->table && is_array($currentIdList)) { 1817 // If the numeric clipboard pads are selected, show duplicate sorting link: 1818 if ($this->clipNumPane()) { 1819 $theData[$fCol] .= '<a class="btn btn-default" href="' . htmlspecialchars($this->listURL('', '-1') . '&duplicateField=' . $fCol) 1820 . '" title="' . htmlspecialchars($lang->getLL('clip_duplicates')) . '">' 1821 . $this->iconFactory->getIcon('actions-document-duplicates-select', Icon::SIZE_SMALL)->render() . '</a>'; 1822 } 1823 // If the table can be edited, add link for editing THIS field for all 1824 // listed records: 1825 if ($this->isEditable($table) && $permsEdit && $GLOBALS['TCA'][$table]['columns'][$fCol]) { 1826 $entityIdentifiers = 'entityIdentifiers'; 1827 if ($this->clipNumPane()) { 1828 $entityIdentifiers .= ':editList'; 1829 } 1830 $editUri = (string)$uriBuilder->buildUriFromRoute('record_edit') 1831 . '&edit[' . $table . '][{' . $entityIdentifiers . '}]=edit' 1832 . '&columnsOnly=' . $fCol 1833 . '&returnUrl={T3_THIS_LOCATION}'; 1834 $iTitle = sprintf($lang->getLL('editThisColumn'), $sortLabel); 1835 $theData[$fCol] .= '<a class="btn btn-default t3js-record-edit-multiple" href="#"' 1836 . ' data-uri="' . htmlspecialchars($editUri) . '"' 1837 . ' title="' . htmlspecialchars($iTitle) . '">' 1838 . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>'; 1839 } 1840 if (strlen($theData[$fCol]) > 0) { 1841 $theData[$fCol] = '<div class="btn-group" role="group">' . $theData[$fCol] . '</div> '; 1842 } 1843 } 1844 $theData[$fCol] .= $this->addSortLink($sortLabel, $fCol, $table); 1845 } 1846 } 1847 /* 1848 * hook: renderListHeader: Allows to change the contents of columns/cells of the Web>List table headers 1849 * usage: Above each listed table in Web>List a header row is shown. 1850 * Containing the labels of all shown fields and additional icons to create new records for this 1851 * table or perform special clipboard tasks like mark and copy all listed records to clipboard, etc. 1852 */ 1853 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) { 1854 $hookObject = GeneralUtility::makeInstance($className); 1855 if (!$hookObject instanceof RecordListHookInterface) { 1856 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567855); 1857 } 1858 $theData = $hookObject->renderListHeader($table, $currentIdList, $theData, $this); 1859 } 1860 1861 // Create and return header table row: 1862 return '<thead>' . $this->addElement(1, $icon, $theData, '', '', '', 'th') . '</thead>'; 1863 } 1864 1865 /** 1866 * Get pointer for first element on the page 1867 * 1868 * @param int $page Page number starting with 1 1869 * @return int Pointer to first element on the page (starting with 0) 1870 */ 1871 protected function getPointerForPage($page) 1872 { 1873 return ($page - 1) * $this->iLimit; 1874 } 1875 1876 /** 1877 * Creates a page browser for tables with many records 1878 * 1879 * @param string $renderPart Distinguish between 'top' and 'bottom' part of the navigation (above or below the records) 1880 * @return string Navigation HTML 1881 */ 1882 protected function renderListNavigation($renderPart = 'top') 1883 { 1884 $totalPages = ceil($this->totalItems / $this->iLimit); 1885 // Show page selector if not all records fit into one page 1886 if ($totalPages <= 1) { 1887 return ''; 1888 } 1889 $content = ''; 1890 $listURL = $this->listURL('', $this->table, 'firstElementNumber'); 1891 // 1 = first page 1892 // 0 = first element 1893 $currentPage = floor($this->firstElementNumber / $this->iLimit) + 1; 1894 // Compile first, previous, next, last and refresh buttons 1895 if ($currentPage > 1) { 1896 $labelFirst = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:first')); 1897 $labelPrevious = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:previous')); 1898 $first = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage(1) . '" title="' . $labelFirst . '">' 1899 . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</a></li>'; 1900 $previous = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage - 1) . '" title="' . $labelPrevious . '">' 1901 . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</a></li>'; 1902 } else { 1903 $first = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-first', Icon::SIZE_SMALL)->render() . '</span></li>'; 1904 $previous = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-previous', Icon::SIZE_SMALL)->render() . '</span></li>'; 1905 } 1906 if ($currentPage < $totalPages) { 1907 $labelNext = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:next')); 1908 $labelLast = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:last')); 1909 $next = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($currentPage + 1) . '" title="' . $labelNext . '">' 1910 . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</a></li>'; 1911 $last = '<li><a href="' . $listURL . '&pointer=' . $this->getPointerForPage($totalPages) . '" title="' . $labelLast . '">' 1912 . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</a></li>'; 1913 } else { 1914 $next = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-next', Icon::SIZE_SMALL)->render() . '</span></li>'; 1915 $last = '<li class="disabled"><span>' . $this->iconFactory->getIcon('actions-view-paging-last', Icon::SIZE_SMALL)->render() . '</span></li>'; 1916 } 1917 $reload = '<li><a href="#" onclick="document.dblistForm.action=' . GeneralUtility::quoteJSvalue($listURL 1918 . '&pointer=') . '+calculatePointer(document.getElementById(' . GeneralUtility::quoteJSvalue('jumpPage-' . $renderPart) 1919 . ').value); document.dblistForm.submit(); return true;" title="' 1920 . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:reload')) . '">' 1921 . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</a></li>'; 1922 if ($renderPart === 'top') { 1923 // Add js to traverse a page select input to a pointer value 1924 $content = ' 1925<script type="text/javascript"> 1926/*<![CDATA[*/ 1927 function calculatePointer(page) { 1928 if (page > ' . $totalPages . ') { 1929 page = ' . $totalPages . '; 1930 } 1931 if (page < 1) { 1932 page = 1; 1933 } 1934 return (page - 1) * ' . $this->iLimit . '; 1935 } 1936/*]]>*/ 1937</script> 1938'; 1939 } 1940 $pageNumberInput = ' 1941 <input type="number" min="1" max="' . $totalPages . '" value="' . $currentPage . '" size="3" class="form-control input-sm paginator-input" id="jumpPage-' . $renderPart . '" name="jumpPage-' 1942 . $renderPart . '" onkeyup="if (event.keyCode == 13) { document.dblistForm.action=' . htmlspecialchars(GeneralUtility::quoteJSvalue($listURL . '&pointer=')) 1943 . '+calculatePointer(this.value); document.dblistForm.submit(); } return true;" /> 1944 '; 1945 $pageIndicatorText = sprintf( 1946 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:pageIndicator'), 1947 $pageNumberInput, 1948 $totalPages 1949 ); 1950 $pageIndicator = '<li><span>' . $pageIndicatorText . '</span></li>'; 1951 if ($this->totalItems > $this->firstElementNumber + $this->iLimit) { 1952 $lastElementNumber = $this->firstElementNumber + $this->iLimit; 1953 } else { 1954 $lastElementNumber = $this->totalItems; 1955 } 1956 $rangeIndicator = '<li><span>' . sprintf( 1957 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:rangeIndicator'), 1958 $this->firstElementNumber + 1, 1959 $lastElementNumber 1960 ) . '</span></li>'; 1961 1962 $titleColumn = $this->fieldArray[0]; 1963 $data = [ 1964 $titleColumn => $content . ' 1965 <nav class="pagination-wrap"> 1966 <ul class="pagination pagination-block"> 1967 ' . $first . ' 1968 ' . $previous . ' 1969 ' . $rangeIndicator . ' 1970 ' . $pageIndicator . ' 1971 ' . $next . ' 1972 ' . $last . ' 1973 ' . $reload . ' 1974 </ul> 1975 </nav> 1976 ' 1977 ]; 1978 return $this->addElement(1, '', $data); 1979 } 1980 1981 /********************************* 1982 * 1983 * Rendering of various elements 1984 * 1985 *********************************/ 1986 1987 /** 1988 * Creates the control panel for a single record in the listing. 1989 * 1990 * @param string $table The table 1991 * @param mixed[] $row The record for which to make the control panel. 1992 * @throws \UnexpectedValueException 1993 * @return string HTML table with the control panel (unless disabled) 1994 */ 1995 public function makeControl($table, $row) 1996 { 1997 $backendUser = $this->getBackendUserAuthentication(); 1998 $userTsConfig = $backendUser->getTSConfig(); 1999 $module = $this->getModule(); 2000 $rowUid = $row['uid']; 2001 if (ExtensionManagementUtility::isLoaded('workspaces') && isset($row['_ORIG_uid'])) { 2002 $rowUid = $row['_ORIG_uid']; 2003 } 2004 $cells = [ 2005 'primary' => [], 2006 'secondary' => [] 2007 ]; 2008 // Enables to hide the move elements for localized records - doesn't make much sense to perform these options for them 2009 // For page translations these icons should never be shown 2010 $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0; 2011 if ($table === 'pages') { 2012 // If the listed table is 'pages' we have to request the permission settings for each page. 2013 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $row['uid'])); 2014 } else { 2015 // If the listed table is not 'pages' we have to request the permission settings from the parent page 2016 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $row['pid'])); 2017 } 2018 $permsEdit = $table === 'pages' 2019 && $backendUser->checkLanguageAccess((int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]) 2020 && $localCalcPerms & Permission::PAGE_EDIT 2021 || $table !== 'pages' 2022 && $localCalcPerms & Permission::CONTENT_EDIT 2023 && $backendUser->recordEditAccessInternals($table, $row); 2024 $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit); 2025 // "Show" link (only pages and tt_content elements) 2026 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 2027 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); 2028 2029 if ($table === 'pages' || $table === 'tt_content') { 2030 $onClick = $this->getOnClickForRow($table, $row); 2031 $viewAction = '<a class="btn btn-default" href="#" onclick="' 2032 . htmlspecialchars( 2033 $onClick 2034 ) . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">'; 2035 if ($table === 'pages') { 2036 $viewAction .= $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render(); 2037 } else { 2038 $viewAction .= $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render(); 2039 } 2040 $viewAction .= '</a>'; 2041 $this->addActionToCellGroup($cells, $viewAction, 'view'); 2042 } 2043 // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id) 2044 if ($permsEdit && $this->isEditable($table)) { 2045 $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit'; 2046 $iconIdentifier = 'actions-open'; 2047 if ($table === 'pages') { 2048 // Disallow manual adjustment of the language field for pages 2049 $params .= '&overrideVals[pages][sys_language_uid]=' . (int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]; 2050 $iconIdentifier = 'actions-page-open'; 2051 } 2052 $editAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1)) 2053 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render() . '</a>'; 2054 } else { 2055 $editAction = $this->spaceIcon; 2056 } 2057 $this->addActionToCellGroup($cells, $editAction, 'edit'); 2058 // "Info": (All records) 2059 $onClick = 'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;'; 2060 $viewBigAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('showInfo')) . '">' 2061 . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '</a>'; 2062 $this->addActionToCellGroup($cells, $viewBigAction, 'viewBig'); 2063 // "Move" wizard link for pages/tt_content elements: 2064 if ($permsEdit && ($table === 'tt_content' || $table === 'pages') && $this->isEditable($table)) { 2065 if ($isL10nOverlay) { 2066 $moveAction = $this->spaceIcon; 2067 } else { 2068 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('move_element') . '&table=' . $table . '&uid=' . $row['uid']) . ');'; 2069 $linkTitleLL = htmlspecialchars($this->getLanguageService()->getLL('move_' . ($table === 'tt_content' ? 'record' : 'page'))); 2070 $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-move', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-document-move', Icon::SIZE_SMALL)); 2071 $moveAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $linkTitleLL . '">' . $icon->render() . '</a>'; 2072 } 2073 $this->addActionToCellGroup($cells, $moveAction, 'move'); 2074 } 2075 // If the table is NOT a read-only table, then show these links: 2076 if ($this->isEditable($table)) { 2077 // "Revert" link (history/undo) 2078 if ((bool)\trim($userTsConfig['options.']['showHistory.'][$table] ?? $userTsConfig['options.']['showHistory'] ?? '1')) { 2079 $moduleUrl = (string)$uriBuilder->buildUriFromRoute('record_history', ['element' => $table . ':' . $row['uid']]); 2080 $onClick = 'return jumpExt(' . GeneralUtility::quoteJSvalue($moduleUrl) . ',\'#latest\');'; 2081 $historyAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' 2082 . htmlspecialchars($this->getLanguageService()->getLL('history')) . '">' 2083 . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render() . '</a>'; 2084 $this->addActionToCellGroup($cells, $historyAction, 'history'); 2085 } 2086 // "Edit Perms" link: 2087 if ($table === 'pages' && $backendUser->check('modules', 'system_BeuserTxPermission') && ExtensionManagementUtility::isLoaded('beuser')) { 2088 if ($isL10nOverlay) { 2089 $permsAction = $this->spaceIcon; 2090 } else { 2091 $href = (string)$uriBuilder->buildUriFromRoute('system_BeuserTxPermission') . '&id=' . $row['uid'] . '&tx_beuser_system_beusertxpermission[action]=edit' . $this->makeReturnUrl(); 2092 $permsAction = '<a class="btn btn-default" href="' . htmlspecialchars($href) . '" title="' 2093 . htmlspecialchars($this->getLanguageService()->getLL('permissions')) . '">' 2094 . $this->iconFactory->getIcon('actions-lock', Icon::SIZE_SMALL)->render() . '</a>'; 2095 } 2096 $this->addActionToCellGroup($cells, $permsAction, 'perms'); 2097 } 2098 // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row 2099 // or if default values can depend on previous record): 2100 if (($GLOBALS['TCA'][$table]['ctrl']['sortby'] || $GLOBALS['TCA'][$table]['ctrl']['useColumnsForDefaultValues']) && $permsEdit) { 2101 if ($table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT || $table === 'pages' && $this->calcPerms & Permission::PAGE_NEW) { 2102 if ($table === 'pages' && $isL10nOverlay) { 2103 $this->addActionToCellGroup($cells, $this->spaceIcon, 'new'); 2104 } elseif ($this->showNewRecLink($table)) { 2105 $params = '&edit[' . $table . '][' . -($row['_MOVE_PLH'] ? $row['_MOVE_PLH_uid'] : $row['uid']) . ']=new'; 2106 $icon = ($table === 'pages' ? $this->iconFactory->getIcon('actions-page-new', Icon::SIZE_SMALL) : $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)); 2107 $titleLabel = 'new'; 2108 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { 2109 $titleLabel .= ($table === 'pages' ? 'Page' : 'Record'); 2110 } 2111 $newAction = '<a class="btn btn-default" href="#" onclick="' . htmlspecialchars(BackendUtility::editOnClick($params, '', -1)) 2112 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($titleLabel)) . '">' 2113 . $icon->render() . '</a>'; 2114 $this->addActionToCellGroup($cells, $newAction, 'new'); 2115 } 2116 } 2117 } 2118 // "Up/Down" links 2119 if ($permsEdit && $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField && !$this->searchLevels) { 2120 if (!$isL10nOverlay && isset($this->currentTable['prev'][$row['uid']])) { 2121 // Up 2122 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prev'][$row['uid']]; 2123 $moveUpAction = '<a class="btn btn-default" href="#" onclick="' 2124 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');') 2125 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">' 2126 . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>'; 2127 } else { 2128 $moveUpAction = $this->spaceIcon; 2129 } 2130 $this->addActionToCellGroup($cells, $moveUpAction, 'moveUp'); 2131 2132 if (!$isL10nOverlay && $this->currentTable['next'][$row['uid']]) { 2133 // Down 2134 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['next'][$row['uid']]; 2135 $moveDownAction = '<a class="btn btn-default" href="#" onclick="' 2136 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');') 2137 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">' 2138 . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>'; 2139 } else { 2140 $moveDownAction = $this->spaceIcon; 2141 } 2142 $this->addActionToCellGroup($cells, $moveDownAction, 'moveDown'); 2143 } 2144 // "Hide/Unhide" links: 2145 $hiddenField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']; 2146 2147 if ( 2148 !empty($GLOBALS['TCA'][$table]['columns'][$hiddenField]) 2149 && (empty($GLOBALS['TCA'][$table]['columns'][$hiddenField]['exclude']) 2150 || $backendUser->check('non_exclude_fields', $table . ':' . $hiddenField)) 2151 ) { 2152 if (!$permsEdit || $this->isRecordCurrentBackendUser($table, $row)) { 2153 $hideAction = $this->spaceIcon; 2154 } else { 2155 $hideTitle = htmlspecialchars($this->getLanguageService()->getLL('hide' . ($table === 'pages' ? 'Page' : ''))); 2156 $unhideTitle = htmlspecialchars($this->getLanguageService()->getLL('unHide' . ($table === 'pages' ? 'Page' : ''))); 2157 if ($row[$hiddenField]) { 2158 $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=0'; 2159 $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="hidden" href="#"' 2160 . ' data-params="' . htmlspecialchars($params) . '"' 2161 . ' title="' . $unhideTitle . '"' 2162 . ' data-toggle-title="' . $hideTitle . '">' 2163 . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '</a>'; 2164 } else { 2165 $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=1'; 2166 $hideAction = '<a class="btn btn-default t3js-record-hide" data-state="visible" href="#"' 2167 . ' data-params="' . htmlspecialchars($params) . '"' 2168 . ' title="' . $hideTitle . '"' 2169 . ' data-toggle-title="' . $unhideTitle . '">' 2170 . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '</a>'; 2171 } 2172 } 2173 $this->addActionToCellGroup($cells, $hideAction, 'hide'); 2174 } 2175 // "Delete" link: 2176 $disableDelete = (bool)\trim($userTsConfig['options.']['disableDelete.'][$table] ?? $userTsConfig['options.']['disableDelete'] ?? '0'); 2177 if ($permsEdit && !$disableDelete && ($table === 'pages' && $localCalcPerms & Permission::PAGE_DELETE || $table !== 'pages' && $this->calcPerms & Permission::CONTENT_EDIT)) { 2178 // Check if the record version is in "deleted" state, because that will switch the action to "restore" 2179 if ($backendUser->workspace > 0 && isset($row['t3ver_state']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { 2180 $actionName = 'restore'; 2181 $refCountMsg = ''; 2182 } else { 2183 $actionName = 'delete'; 2184 $refCountMsg = BackendUtility::referenceCount( 2185 $table, 2186 $row['uid'], 2187 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'), 2188 $this->getReferenceCount($table, $row['uid']) 2189 ) . BackendUtility::translationCount( 2190 $table, 2191 $row['uid'], 2192 ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord') 2193 ); 2194 } 2195 2196 if ($this->isRecordCurrentBackendUser($table, $row)) { 2197 $deleteAction = $this->spaceIcon; 2198 } else { 2199 $title = BackendUtility::getRecordTitle($table, $row); 2200 $warningText = $this->getLanguageService()->getLL($actionName . 'Warning') . ' "' . $title . '" ' . '[' . $table . ':' . $row['uid'] . ']' . $refCountMsg; 2201 2202 $params = 'cmd[' . $table . '][' . $row['uid'] . '][delete]=1'; 2203 $icon = $this->iconFactory->getIcon('actions-edit-' . $actionName, Icon::SIZE_SMALL)->render(); 2204 $linkTitle = htmlspecialchars($this->getLanguageService()->getLL($actionName)); 2205 $l10nParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? ''; 2206 $deleteAction = '<a class="btn btn-default t3js-record-delete" href="#" ' 2207 . ' data-button-ok-text="' . htmlspecialchars($linkTitle) . '"' 2208 . ' data-l10parent="' . ($l10nParentField ? htmlspecialchars($row[$l10nParentField]) : '') . '"' 2209 . ' data-params="' . htmlspecialchars($params) . '" data-title="' . htmlspecialchars($title) . '"' 2210 . ' data-message="' . htmlspecialchars($warningText) . '" title="' . $linkTitle . '"' 2211 . '>' . $icon . '</a>'; 2212 } 2213 } else { 2214 $deleteAction = $this->spaceIcon; 2215 } 2216 $this->addActionToCellGroup($cells, $deleteAction, 'delete'); 2217 // "Levels" links: Moving pages into new levels... 2218 if ($permsEdit && $table === 'pages' && !$this->searchLevels) { 2219 // Up (Paste as the page right after the current parent page) 2220 if ($this->calcPerms & Permission::PAGE_NEW) { 2221 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . -$this->id; 2222 $moveLeftAction = '<a class="btn btn-default" href="#" onclick="' 2223 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');') 2224 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('prevLevel')) . '">' 2225 . $this->iconFactory->getIcon('actions-move-left', Icon::SIZE_SMALL)->render() . '</a>'; 2226 $this->addActionToCellGroup($cells, $isL10nOverlay ? $this->spaceIcon : $moveLeftAction, 'moveLeft'); 2227 } 2228 // Down (Paste as subpage to the page right above) 2229 if (!$isL10nOverlay && $this->currentTable['prevUid'][$row['uid']]) { 2230 $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $this->currentTable['prevUid'][$row['uid']])); 2231 if ($localCalcPerms & Permission::PAGE_NEW) { 2232 $params = '&cmd[' . $table . '][' . $row['uid'] . '][move]=' . $this->currentTable['prevUid'][$row['uid']]; 2233 $moveRightAction = '<a class="btn btn-default" href="#" onclick="' 2234 . htmlspecialchars('return jumpToUrl(' . BackendUtility::getLinkToDataHandlerAction($params, -1) . ');') 2235 . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('nextLevel')) . '">' 2236 . $this->iconFactory->getIcon('actions-move-right', Icon::SIZE_SMALL)->render() . '</a>'; 2237 } else { 2238 $moveRightAction = $this->spaceIcon; 2239 } 2240 } else { 2241 $moveRightAction = $this->spaceIcon; 2242 } 2243 $this->addActionToCellGroup($cells, $moveRightAction, 'moveRight'); 2244 } 2245 } 2246 /* 2247 * hook: recStatInfoHooks: Allows to insert HTML before record icons on various places 2248 */ 2249 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? []; 2250 if (!empty($hooks)) { 2251 $stat = ''; 2252 $_params = [$table, $row['uid']]; 2253 foreach ($hooks as $_funcRef) { 2254 $stat .= GeneralUtility::callUserFunction($_funcRef, $_params, $this); 2255 } 2256 $this->addActionToCellGroup($cells, $stat, 'stat'); 2257 } 2258 /* 2259 * hook: makeControl: Allows to change control icons of records in list-module 2260 * usage: This hook method gets passed the current $cells array as third parameter. 2261 * This array contains values for the icons/actions generated for each record in Web>List. 2262 * Each array entry is accessible by an index-key. 2263 * The order of the icons is depending on the order of those array entries. 2264 */ 2265 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? false)) { 2266 // for compatibility reason, we move all icons to the rootlevel 2267 // before calling the hooks 2268 foreach ($cells as $section => $actions) { 2269 foreach ($actions as $actionKey => $action) { 2270 $cells[$actionKey] = $action; 2271 } 2272 } 2273 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] as $className) { 2274 $hookObject = GeneralUtility::makeInstance($className); 2275 if (!$hookObject instanceof RecordListHookInterface) { 2276 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567840); 2277 } 2278 $cells = $hookObject->makeControl($table, $row, $cells, $this); 2279 } 2280 // now sort icons again into primary and secondary sections 2281 // after all hooks are processed 2282 $hookCells = $cells; 2283 foreach ($hookCells as $key => $value) { 2284 if ($key === 'primary' || $key === 'secondary') { 2285 continue; 2286 } 2287 $this->addActionToCellGroup($cells, $value, $key); 2288 } 2289 } 2290 $output = '<!-- CONTROL PANEL: ' . $table . ':' . $row['uid'] . ' -->'; 2291 foreach ($cells as $classification => $actions) { 2292 $visibilityClass = ($classification !== 'primary' && !$module->MOD_SETTINGS['bigControlPanel'] ? 'collapsed' : 'expanded'); 2293 if ($visibilityClass === 'collapsed') { 2294 $cellOutput = ''; 2295 foreach ($actions as $action) { 2296 $cellOutput .= $action; 2297 } 2298 $output .= ' <div class="btn-group">' . 2299 '<span id="actions_' . $table . '_' . $row['uid'] . '" class="btn-group collapse collapse-horizontal width">' . $cellOutput . '</span>' . 2300 '<a href="#actions_' . $table . '_' . $row['uid'] . '" class="btn btn-default collapsed" data-toggle="collapse" aria-expanded="false"><span class="t3-icon fa fa-ellipsis-h"></span></a>' . 2301 '</div>'; 2302 } else { 2303 $output .= ' <div class="btn-group" role="group">' . implode('', $actions) . '</div>'; 2304 } 2305 } 2306 return $output; 2307 } 2308 2309 /** 2310 * Creates the clipboard panel for a single record in the listing. 2311 * 2312 * @param string $table The table 2313 * @param mixed[] $row The record for which to make the clipboard panel. 2314 * @throws \UnexpectedValueException 2315 * @return string HTML table with the clipboard panel (unless disabled) 2316 */ 2317 public function makeClip($table, $row) 2318 { 2319 // Return blank, if disabled: 2320 if (!$this->getModule()->MOD_SETTINGS['clipBoard']) { 2321 return ''; 2322 } 2323 if (!$this->isEditable($table)) { 2324 return ''; 2325 } 2326 $cells = []; 2327 $cells['pasteAfter'] = ($cells['pasteInto'] = $this->spaceIcon); 2328 // Enables to hide the copy, cut and paste icons for localized records - doesn't make much sense to perform these options for them 2329 // For page translations these icons should never be shown 2330 $isL10nOverlay = $table === 'pages' && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0; 2331 // Return blank, if disabled: 2332 // Whether a numeric clipboard pad is active or the normal pad we will see different content of the panel: 2333 // For the "Normal" pad: 2334 if ($this->clipObj->current === 'normal') { 2335 // Show copy/cut icons: 2336 $isSel = (string)$this->clipObj->isSelected($table, $row['uid']); 2337 if ($isL10nOverlay || !$this->overlayEditLockPermissions($table, $row)) { 2338 $cells['copy'] = $this->spaceIcon; 2339 $cells['cut'] = $this->spaceIcon; 2340 } else { 2341 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL); 2342 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut', Icon::SIZE_SMALL); 2343 2344 if ($isSel === 'copy') { 2345 $copyIcon = $this->iconFactory->getIcon('actions-edit-copy-release', Icon::SIZE_SMALL); 2346 } elseif ($isSel === 'cut') { 2347 $cutIcon = $this->iconFactory->getIcon('actions-edit-cut-release', Icon::SIZE_SMALL); 2348 } 2349 2350 $cells['copy'] = '<a class="btn btn-default" href="#" onclick="' 2351 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB( 2352 $table, 2353 $row['uid'], 2354 1, 2355 $isSel === 'copy', 2356 ['returnUrl' => ''] 2357 )) . ');') 2358 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy')) . '">' 2359 . $copyIcon->render() . '</a>'; 2360 2361 // Check permission to cut page or content 2362 if ($table === 'pages') { 2363 $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms(BackendUtility::getRecord('pages', $row['uid'])); 2364 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT; 2365 } else { 2366 $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT; 2367 } 2368 $permsEdit = $this->overlayEditLockPermissions($table, $row, $permsEdit); 2369 2370 // If the listed table is 'pages' we have to request the permission settings for each page: 2371 if ($table === 'pages') { 2372 if ($permsEdit) { 2373 $cells['cut'] = '<a class="btn btn-default" href="#" onclick="' 2374 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB( 2375 $table, 2376 $row['uid'], 2377 0, 2378 $isSel === 'cut', 2379 ['returnUrl' => ''] 2380 )) . ');') 2381 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">' 2382 . $cutIcon->render() . '</a>'; 2383 } else { 2384 $cells['cut'] = $this->spaceIcon; 2385 } 2386 } else { 2387 if ($this->calcPerms & Permission::CONTENT_EDIT) { 2388 $cells['cut'] = '<a class="btn btn-default" href="#" onclick="' 2389 . htmlspecialchars('return jumpSelf(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB( 2390 $table, 2391 $row['uid'], 2392 0, 2393 $isSel === 'cut', 2394 ['returnUrl' => ''] 2395 )) . ');') 2396 . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut')) . '">' 2397 . $cutIcon->render() . '</a>'; 2398 } else { 2399 $cells['cut'] = $this->spaceIcon; 2400 } 2401 } 2402 } 2403 } else { 2404 // For the numeric clipboard pads (showing checkboxes where one can select elements on/off) 2405 // Setting name of the element in ->CBnames array: 2406 $n = $table . '|' . $row['uid']; 2407 $this->CBnames[] = $n; 2408 // Check if the current element is selected and if so, prepare to set the checkbox as selected: 2409 $checked = $this->clipObj->isSelected($table, $row['uid']) ? 'checked="checked" ' : ''; 2410 // If the "duplicateField" value is set then select all elements which are duplicates... 2411 if ($this->duplicateField && isset($row[$this->duplicateField])) { 2412 $checked = ''; 2413 if (in_array($row[$this->duplicateField], $this->duplicateStack)) { 2414 $checked = 'checked="checked" '; 2415 } 2416 $this->duplicateStack[] = $row[$this->duplicateField]; 2417 } 2418 // Adding the checkbox to the panel: 2419 $cells['select'] = $isL10nOverlay 2420 ? $this->spaceIcon 2421 : '<input type="hidden" name="CBH[' . $n . ']" value="0" /><label class="btn btn-default btn-checkbox"><input type="checkbox"' 2422 . ' name="CBC[' . $n . ']" value="1" ' . $checked . '/><span class="t3-icon fa"></span></label>'; 2423 } 2424 // Now, looking for selected elements from the current table: 2425 $elFromTable = $this->clipObj->elFromTable($table); 2426 if (!empty($elFromTable) && $GLOBALS['TCA'][$table]['ctrl']['sortby']) { 2427 // IF elements are found, they can be individually ordered and are not locked by editlock, then add a "paste after" icon: 2428 $cells['pasteAfter'] = $isL10nOverlay || !$this->overlayEditLockPermissions($table, $row) 2429 ? $this->spaceIcon 2430 : '<a class="btn btn-default t3js-modal-trigger"' 2431 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl($table, -$row['uid'])) . '"' 2432 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"' 2433 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteAfter')) . '"' 2434 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'after', $elFromTable)) . '"' 2435 . ' data-severity="warning">' 2436 . $this->iconFactory->getIcon('actions-document-paste-after', Icon::SIZE_SMALL)->render() . '</a>'; 2437 } 2438 // Now, looking for elements in general: 2439 $elFromTable = $this->clipObj->elFromTable(''); 2440 if ($table === 'pages' && !$isL10nOverlay && !empty($elFromTable)) { 2441 $cells['pasteInto'] = '<a class="btn btn-default t3js-modal-trigger"' 2442 . ' href="' . htmlspecialchars($this->clipObj->pasteUrl('', $row['uid'])) . '"' 2443 . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"' 2444 . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_pasteInto')) . '"' 2445 . ' data-content="' . htmlspecialchars($this->clipObj->confirmMsgText($table, $row, 'into', $elFromTable)) . '"' 2446 . ' data-severity="warning">' 2447 . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() . '</a>'; 2448 } 2449 /* 2450 * hook: makeClip: Allows to change clip-icons of records in list-module 2451 * usage: This hook method gets passed the current $cells array as third parameter. 2452 * This array contains values for the clipboard icons generated for each record in Web>List. 2453 * Each array entry is accessible by an index-key. 2454 * The order of the icons is depending on the order of those array entries. 2455 */ 2456 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions'] ?? [] as $className) { 2457 $hookObject = GeneralUtility::makeInstance($className); 2458 if (!$hookObject instanceof RecordListHookInterface) { 2459 throw new \UnexpectedValueException($className . ' must implement interface ' . RecordListHookInterface::class, 1195567845); 2460 } 2461 $cells = $hookObject->makeClip($table, $row, $cells, $this); 2462 } 2463 return '<div class="btn-group" role="group">' . implode('', $cells) . '</div>'; 2464 } 2465 2466 /** 2467 * Creates the HTML for a reference count for the record with the UID $uid 2468 * in the table $tableName. 2469 * 2470 * @param string $tableName 2471 * @param int $uid 2472 * @return string HTML of reference a link, will be empty if there are no 2473 */ 2474 protected function createReferenceHtml($tableName, $uid) 2475 { 2476 $referenceCount = GeneralUtility::makeInstance(ConnectionPool::class) 2477 ->getConnectionForTable('sys_refindex') 2478 ->count( 2479 '*', 2480 'sys_refindex', 2481 [ 2482 'ref_table' => $tableName, 2483 'ref_uid' => (int)$uid, 2484 'deleted' => 0, 2485 ] 2486 ); 2487 2488 return $this->generateReferenceToolTip( 2489 $referenceCount, 2490 GeneralUtility::quoteJSvalue($tableName) . ', ' . GeneralUtility::quoteJSvalue($uid) 2491 ); 2492 } 2493 2494 /** 2495 * Creates the localization panel 2496 * 2497 * @param string $table The table 2498 * @param mixed[] $row The record for which to make the localization panel. 2499 * @return string[] Array with key 0/1 with content for column 1 and 2 2500 */ 2501 public function makeLocalizationPanel($table, $row) 2502 { 2503 $out = [ 2504 0 => '', 2505 1 => '' 2506 ]; 2507 // Reset translations 2508 $this->translations = []; 2509 2510 // Language title and icon: 2511 $out[0] = $this->languageFlag($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]); 2512 // Guard clause so we can quickly return if a record is localized to "all languages" 2513 // It should only be possible to localize a record off default (uid 0) 2514 // Reasoning: The Parent is for ALL languages... why overlay with a localization? 2515 if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === -1) { 2516 return $out; 2517 } 2518 2519 $translations = $this->translateTools->translationInfo($table, $row['uid'], 0, $row, $this->selFieldList); 2520 if (is_array($translations)) { 2521 $this->translations = $translations['translations']; 2522 // Traverse page translations and add icon for each language that does NOT yet exist and is included in site configuration: 2523 $lNew = ''; 2524 foreach ($this->pageOverlays as $lUid_OnPage => $lsysRec) { 2525 if (isset($this->systemLanguagesOnPage[$lUid_OnPage]) && $this->isEditable($table) && !isset($translations['translations'][$lUid_OnPage]) && $this->getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)) { 2526 $url = $this->listURL(); 2527 $href = BackendUtility::getLinkToDataHandlerAction( 2528 '&cmd[' . $table . '][' . $row['uid'] . '][localize]=' . $lUid_OnPage, 2529 $url . '&justLocalized=' . rawurlencode($table . ':' . $row['uid'] . ':' . $lUid_OnPage) 2530 ); 2531 $language = BackendUtility::getRecord('sys_language', $lUid_OnPage, 'title'); 2532 if ($this->languageIconTitles[$lUid_OnPage]['flagIcon']) { 2533 $lC = $this->iconFactory->getIcon($this->languageIconTitles[$lUid_OnPage]['flagIcon'], Icon::SIZE_SMALL)->render(); 2534 } else { 2535 $lC = $this->languageIconTitles[$lUid_OnPage]['title']; 2536 } 2537 $lC = '<a href="' . htmlspecialchars($href) . '" title="' 2538 . htmlspecialchars($language['title']) . '" class="btn btn-default t3js-action-localize">' 2539 . $lC . '</a> '; 2540 $lNew .= $lC; 2541 } 2542 } 2543 if ($lNew) { 2544 $out[1] .= $lNew; 2545 } 2546 } elseif ($row['l18n_parent']) { 2547 $out[0] = ' ' . $out[0]; 2548 } 2549 return $out; 2550 } 2551 2552 /** 2553 * Creates a checkbox list for selecting fields to display from a table: 2554 * 2555 * @param string $table Table name 2556 * @param bool $formFields If TRUE, form-fields will be wrapped around the table. 2557 * @return string HTML table with the selector check box (name: displayFields['.$table.'][]) 2558 */ 2559 public function fieldSelectBox($table, $formFields = true) 2560 { 2561 $lang = $this->getLanguageService(); 2562 // Init: 2563 $formElements = ['', '']; 2564 if ($formFields) { 2565 $formElements = ['<form action="' . htmlspecialchars($this->listURL()) . '" method="post" name="fieldSelectBox">', '</form>']; 2566 } 2567 // Load already selected fields, if any: 2568 $setFields = is_array($this->setFields[$table]) ? $this->setFields[$table] : []; 2569 // Request fields from table: 2570 $fields = $this->makeFieldList($table, false, true); 2571 // Add pseudo "control" fields 2572 $fields[] = '_PATH_'; 2573 $fields[] = '_REF_'; 2574 $fields[] = '_LOCALIZATION_'; 2575 $fields[] = '_CONTROL_'; 2576 $fields[] = '_CLIPBOARD_'; 2577 // Create a checkbox for each field: 2578 $checkboxes = []; 2579 $checkAllChecked = true; 2580 $tsConfig = BackendUtility::getPagesTSconfig($this->id); 2581 $tsConfigOfTable = is_array($tsConfig['TCEFORM.'][$table . '.']) ? $tsConfig['TCEFORM.'][$table . '.'] : null; 2582 foreach ($fields as $fieldName) { 2583 // Hide field if hidden 2584 if ($tsConfigOfTable && is_array($tsConfigOfTable[$fieldName . '.']) && isset($tsConfigOfTable[$fieldName . '.']['disabled']) && (int)$tsConfigOfTable[$fieldName . '.']['disabled'] === 1) { 2585 continue; 2586 } 2587 // Determine, if checkbox should be checked 2588 if (in_array($fieldName, $setFields, true) || $fieldName === $this->fieldArray[0]) { 2589 $checked = ' checked="checked"'; 2590 } else { 2591 $checkAllChecked = false; 2592 $checked = ''; 2593 } 2594 2595 // Field label 2596 $fieldTSConfig = []; 2597 $fieldLabel = ''; 2598 if (isset($tsConfigOfTable[$fieldName . '.']) 2599 && is_array($tsConfigOfTable[$fieldName . '.']) 2600 ) { 2601 $fieldTSConfig = $tsConfigOfTable[$fieldName . '.']; 2602 } 2603 if (!empty($fieldTSConfig['label'])) { 2604 $fieldLabel = $fieldTSConfig['label']; 2605 } 2606 if (!empty($fieldTSConfig['label.'][$lang->lang])) { 2607 $fieldLabel = $fieldTSConfig['label.'][$lang->lang]; 2608 } 2609 2610 $fieldLabel = $fieldLabel ?: BackendUtility::getItemLabel($table, $fieldName); 2611 2612 $checkboxes[] = '<tr><td class="col-checkbox"><input type="checkbox" id="check-' . $fieldName . '" name="displayFields[' 2613 . $table . '][]" value="' . $fieldName . '" ' . $checked 2614 . ($fieldName === $this->fieldArray[0] ? ' disabled="disabled"' : '') . '></td><td class="col-title">' 2615 . '<label class="label-block" for="check-' . $fieldName . '">' . htmlspecialchars($lang->sL($fieldLabel)) . ' <span class="text-muted text-monospace">[' . htmlspecialchars($fieldName) . ']</span></label></td></tr>'; 2616 } 2617 // Table with the field selector:: 2618 $content = $formElements[0] . ' 2619 <input type="hidden" name="displayFields[' . $table . '][]" value=""> 2620 <div class="table-fit table-scrollable"> 2621 <table border="0" cellpadding="0" cellspacing="0" class="table table-transparent table-hover"> 2622 <thead> 2623 <tr> 2624 <th class="col-checkbox checkbox" colspan="2"> 2625 <label><input type="checkbox" class="checkbox checkAll" ' . ($checkAllChecked ? ' checked="checked"' : '') . '> 2626 ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall')) . '</label> 2627 </th> 2628 </tr> 2629 </thead> 2630 <tbody> 2631 ' . implode('', $checkboxes) . ' 2632 </tbody> 2633 </table> 2634 </div> 2635 <input type="submit" name="search" class="btn btn-default" value="' 2636 . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.setFields')) . '"/> 2637 ' . $formElements[1]; 2638 return '<div class="fieldSelectBox">' . $content . '</div>'; 2639 } 2640 2641 /********************************* 2642 * 2643 * Helper functions 2644 * 2645 *********************************/ 2646 /** 2647 * Creates a link around $string. The link contains an onclick action 2648 * which submits the script with some clipboard action. 2649 * Currently, this is used for setting elements / delete elements. 2650 * 2651 * @param string $string The HTML content to link (image/text) 2652 * @param string $table Table name 2653 * @param string $cmd Clipboard command (eg. "setCB" or "delete") 2654 * @param string $warning Warning text, if any ("delete" uses this for confirmation 2655 * @param string $title title attribute for the anchor 2656 * @return string <a> tag wrapped link. 2657 */ 2658 public function linkClipboardHeaderIcon($string, $table, $cmd, $warning = '', $title = '') 2659 { 2660 $jsCode = 'document.dblistForm.cmd.value=' . GeneralUtility::quoteJSvalue($cmd) 2661 . ';document.dblistForm.cmd_table.value=' 2662 . GeneralUtility::quoteJSvalue($table) 2663 . ';document.dblistForm.submit();'; 2664 2665 $attributes = []; 2666 if ($title !== '') { 2667 $attributes['title'] = $title; 2668 } 2669 if ($warning) { 2670 $attributes['class'] = 'btn btn-default t3js-modal-trigger'; 2671 $attributes['data-href'] = 'javascript:' . $jsCode; 2672 $attributes['data-severity'] = 'warning'; 2673 $attributes['data-title'] = $title; 2674 $attributes['data-content'] = $warning; 2675 } else { 2676 $attributes['class'] = 'btn btn-default'; 2677 $attributes['onclick'] = $jsCode . 'return false;'; 2678 } 2679 2680 $attributesString = ''; 2681 foreach ($attributes as $key => $value) { 2682 $attributesString .= ' ' . $key . '="' . htmlspecialchars($value) . '"'; 2683 } 2684 return '<a href="#" ' . $attributesString . '>' . $string . '</a>'; 2685 } 2686 2687 /** 2688 * Returns TRUE if a numeric clipboard pad is selected/active 2689 * 2690 * @return bool 2691 */ 2692 public function clipNumPane() 2693 { 2694 return in_array('_CLIPBOARD_', $this->fieldArray) && $this->clipObj->current !== 'normal'; 2695 } 2696 2697 /** 2698 * Creates a sort-by link on the input string ($code). 2699 * It will automatically detect if sorting should be ascending or descending depending on $this->sortRev. 2700 * Also some fields will not be possible to sort (including if single-table-view is disabled). 2701 * 2702 * @param string $code The string to link (text) 2703 * @param string $field The fieldname represented by the title ($code) 2704 * @param string $table Table name 2705 * @return string Linked $code variable 2706 */ 2707 public function addSortLink($code, $field, $table) 2708 { 2709 // Certain circumstances just return string right away (no links): 2710 if ($field === '_CONTROL_' || $field === '_LOCALIZATION_' || $field === '_CLIPBOARD_' || $field === '_REF_' || $this->disableSingleTableView) { 2711 return $code; 2712 } 2713 // If "_PATH_" (showing record path) is selected, force sorting by pid field (will at least group the records!) 2714 if ($field === '_PATH_') { 2715 $field = 'pid'; 2716 } 2717 // Create the sort link: 2718 $sortUrl = $this->listURL('', '-1', 'sortField,sortRev,table,firstElementNumber') . '&table=' . $table 2719 . '&sortField=' . $field . '&sortRev=' . ($this->sortRev || $this->sortField != $field ? 0 : 1); 2720 $sortArrow = $this->sortField === $field 2721 ? $this->iconFactory->getIcon('status-status-sorting-' . ($this->sortRev ? 'desc' : 'asc'), Icon::SIZE_SMALL)->render() 2722 : ''; 2723 // Return linked field: 2724 return '<a href="' . htmlspecialchars($sortUrl) . '">' . $code . $sortArrow . '</a>'; 2725 } 2726 2727 /** 2728 * Returns the path for a certain pid 2729 * The result is cached internally for the session, thus you can call 2730 * this function as much as you like without performance problems. 2731 * 2732 * @param int $pid The page id for which to get the path 2733 * @return mixed[] The path. 2734 */ 2735 public function recPath($pid) 2736 { 2737 if (!isset($this->recPath_cache[$pid])) { 2738 $this->recPath_cache[$pid] = BackendUtility::getRecordPath($pid, $this->perms_clause, 20); 2739 } 2740 return $this->recPath_cache[$pid]; 2741 } 2742 2743 /** 2744 * Returns TRUE if a link for creating new records should be displayed for $table 2745 * 2746 * @param string $table Table name 2747 * @return bool Returns TRUE if a link for creating new records should be displayed for $table 2748 * @see \TYPO3\CMS\Backend\Controller\NewRecordController::showNewRecLink 2749 */ 2750 public function showNewRecLink($table) 2751 { 2752 // No deny/allow tables are set: 2753 if (empty($this->allowedNewTables) && empty($this->deniedNewTables)) { 2754 return true; 2755 } 2756 return !in_array($table, $this->deniedNewTables) 2757 && (empty($this->allowedNewTables) || in_array($table, $this->allowedNewTables)); 2758 } 2759 2760 /** 2761 * Creates the "&returnUrl" parameter for links - this is used when the script links 2762 * to other scripts and passes its own URL with the link so other scripts can return to the listing again. 2763 * Uses REQUEST_URI as value. 2764 * 2765 * @return string 2766 */ 2767 public function makeReturnUrl() 2768 { 2769 return '&returnUrl=' . rawurlencode(GeneralUtility::getIndpEnv('REQUEST_URI')); 2770 } 2771 2772 /************************************ 2773 * 2774 * CSV related functions 2775 * 2776 ************************************/ 2777 /** 2778 * Initializes internal csvLines array with the header of field names 2779 */ 2780 protected function initCSV() 2781 { 2782 $this->addHeaderRowToCSV(); 2783 } 2784 2785 /** 2786 * Add header line with field names as CSV line 2787 */ 2788 protected function addHeaderRowToCSV() 2789 { 2790 $fieldArray = array_combine($this->fieldArray, $this->fieldArray); 2791 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][__CLASS__]['customizeCsvHeader'] ?? []; 2792 if (!empty($hooks)) { 2793 $hookParameters = [ 2794 'fields' => &$fieldArray 2795 ]; 2796 foreach ($hooks as $hookFunction) { 2797 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this); 2798 } 2799 } 2800 // Add header row, control fields will be reduced inside addToCSV() 2801 $this->addToCSV($fieldArray); 2802 } 2803 2804 /** 2805 * Adds selected columns of one table row as CSV line. 2806 * 2807 * @param mixed[] $row Record array, from which the values of fields found in $this->fieldArray will be listed in the CSV output. 2808 */ 2809 protected function addToCSV(array $row = []) 2810 { 2811 $rowReducedByControlFields = self::removeControlFieldsFromFieldRow($row); 2812 // Get an field array without control fields but in the expected order 2813 $fieldArray = array_intersect_key(array_flip($this->fieldArray), $rowReducedByControlFields); 2814 // Overwrite fieldArray to keep the order with an array of needed fields 2815 $rowReducedToSelectedColumns = array_replace($fieldArray, array_intersect_key($rowReducedByControlFields, $fieldArray)); 2816 $this->setCsvRow($rowReducedToSelectedColumns); 2817 } 2818 2819 /** 2820 * Remove control fields from row for CSV export 2821 * 2822 * @param mixed[] $row fieldNames => fieldValues 2823 * @return mixed[] Input array reduces by control fields 2824 */ 2825 protected static function removeControlFieldsFromFieldRow(array $row = []) 2826 { 2827 // Possible control fields in a list row 2828 $controlFields = [ 2829 '_PATH_', 2830 '_REF_', 2831 '_CONTROL_', 2832 '_CLIPBOARD_', 2833 '_LOCALIZATION_', 2834 '_LOCALIZATION_b' 2835 ]; 2836 return array_diff_key($row, array_flip($controlFields)); 2837 } 2838 2839 /** 2840 * Adds input row of values to the internal csvLines array as a CSV formatted line 2841 * 2842 * @param mixed[] $csvRow Array with values to be listed. 2843 */ 2844 public function setCsvRow($csvRow) 2845 { 2846 $csvDelimiter = $this->modTSconfig['properties']['csvDelimiter'] ?? ','; 2847 $csvQuote = $this->modTSconfig['properties']['csvQuote'] ?? '"'; 2848 2849 $this->csvLines[] = CsvUtility::csvValues($csvRow, $csvDelimiter, $csvQuote); 2850 } 2851 2852 /** 2853 * Compiles the internal csvLines array to a csv-string and outputs it to the browser. 2854 * This function exits! 2855 * 2856 * @param string $prefix Filename prefix: 2857 */ 2858 public function outputCSV($prefix) 2859 { 2860 // Setting filename: 2861 $filename = $prefix . '_' . date('dmy-Hi') . '.csv'; 2862 // Creating output header: 2863 header('Content-Type: application/octet-stream'); 2864 header('Content-Disposition: attachment; filename=' . $filename); 2865 // Cache-Control header is needed here to solve an issue with browser IE and 2866 // versions lower than 9. See for more information: http://support.microsoft.com/kb/323308 2867 header("Cache-Control: ''"); 2868 // Printing the content of the CSV lines: 2869 echo implode(CRLF, $this->csvLines); 2870 // Exits: 2871 die; 2872 } 2873 2874 /** 2875 * add action into correct section 2876 * 2877 * @param array $cells 2878 * @param string $action 2879 * @param string $actionKey 2880 */ 2881 public function addActionToCellGroup(&$cells, $action, $actionKey) 2882 { 2883 $cellsMap = [ 2884 'primary' => [ 2885 'view', 'edit', 'hide', 'delete', 'stat' 2886 ], 2887 'secondary' => [ 2888 'viewBig', 'history', 'perms', 'new', 'move', 'moveUp', 'moveDown', 'moveLeft', 'moveRight', 'version' 2889 ] 2890 ]; 2891 $classification = in_array($actionKey, $cellsMap['primary']) ? 'primary' : 'secondary'; 2892 $cells[$classification][$actionKey] = $action; 2893 unset($cells[$actionKey]); 2894 } 2895 2896 /** 2897 * Check if the record represents the current backend user 2898 * 2899 * @param string $table 2900 * @param array $row 2901 * @return bool 2902 */ 2903 protected function isRecordCurrentBackendUser($table, $row) 2904 { 2905 return $table === 'be_users' && (int)$row['uid'] === $this->getBackendUserAuthentication()->user['uid']; 2906 } 2907 2908 /** 2909 * @param bool $isEditable 2910 */ 2911 public function setIsEditable($isEditable) 2912 { 2913 $this->editable = $isEditable; 2914 } 2915 2916 /** 2917 * Check if the table is readonly or editable 2918 * @param string $table 2919 * @return bool 2920 */ 2921 public function isEditable($table) 2922 { 2923 $backendUser = $this->getBackendUserAuthentication(); 2924 return !$GLOBALS['TCA'][$table]['ctrl']['readOnly'] 2925 && $this->editable 2926 && ($backendUser->isAdmin() || $backendUser->check('tables_modify', $table)); 2927 } 2928 2929 /** 2930 * Check if the current record is locked by editlock. Pages are locked if their editlock flag is set, 2931 * records are if they are locked themselves or if the page they are on is locked (a page’s editlock 2932 * is transitive for its content elements). 2933 * 2934 * @param string $table 2935 * @param array $row 2936 * @param bool $editPermission 2937 * @return bool 2938 */ 2939 protected function overlayEditLockPermissions($table, $row = [], $editPermission = true) 2940 { 2941 if ($editPermission && !$this->getBackendUserAuthentication()->isAdmin()) { 2942 // If no $row is submitted we only check for general edit lock of current page (except for table "pages") 2943 if (empty($row)) { 2944 return $table === 'pages' ? true : !$this->pageRow['editlock']; 2945 } 2946 if (($table === 'pages' && $row['editlock']) || ($table !== 'pages' && $this->pageRow['editlock'])) { 2947 $editPermission = false; 2948 } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['editlock']) && $row[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) { 2949 $editPermission = false; 2950 } 2951 } 2952 return $editPermission; 2953 } 2954 2955 /** 2956 * Check whether or not the current backend user is an admin or the current page is 2957 * locked by editlock. 2958 * 2959 * @return bool 2960 */ 2961 protected function editLockPermissions() 2962 { 2963 return $this->getBackendUserAuthentication()->isAdmin() || !$this->pageRow['editlock']; 2964 } 2965 2966 /** 2967 * @return BaseScriptClass 2968 */ 2969 protected function getModule() 2970 { 2971 return $GLOBALS['SOBE']; 2972 } 2973 2974 /** 2975 * @return DocumentTemplate 2976 */ 2977 protected function getDocumentTemplate() 2978 { 2979 return $GLOBALS['TBE_TEMPLATE']; 2980 } 2981 2982 /** 2983 * Initializes the list generation 2984 * 2985 * @param int $id Page id for which the list is rendered. Must be >= 0 2986 * @param string $table Tablename - if extended mode where only one table is listed at a time. 2987 * @param int $pointer Browsing pointer. 2988 * @param string $search Search word, if any 2989 * @param int $levels Number of levels to search down the page tree 2990 * @param int $showLimit Limit of records to be listed. 2991 */ 2992 public function start($id, $table, $pointer, $search = '', $levels = 0, $showLimit = 0) 2993 { 2994 $backendUser = $this->getBackendUserAuthentication(); 2995 // Setting internal variables: 2996 // sets the parent id 2997 $this->id = (int)$id; 2998 // Store languages that are included in the site configuration for the current page. 2999 $this->systemLanguagesOnPage = $this->translateTools->getSystemLanguages($this->id); 3000 if ($GLOBALS['TCA'][$table]) { 3001 // Setting single table mode, if table exists: 3002 $this->table = $table; 3003 } 3004 $this->firstElementNumber = $pointer; 3005 $this->searchString = trim($search); 3006 $this->searchLevels = (int)$levels; 3007 $this->showLimit = MathUtility::forceIntegerInRange($showLimit, 0, 10000); 3008 // Setting GPvars: 3009 $this->csvOutput = (bool)GeneralUtility::_GP('csv'); 3010 $this->sortField = GeneralUtility::_GP('sortField'); 3011 $this->sortRev = GeneralUtility::_GP('sortRev'); 3012 $this->displayFields = GeneralUtility::_GP('displayFields'); 3013 $this->duplicateField = GeneralUtility::_GP('duplicateField'); 3014 if (GeneralUtility::_GP('justLocalized')) { 3015 $this->localizationRedirect(GeneralUtility::_GP('justLocalized')); 3016 } 3017 // Init dynamic vars: 3018 $this->counter = 0; 3019 $this->JScode = ''; 3020 $this->HTMLcode = ''; 3021 // Limits 3022 if (isset($this->modTSconfig['properties']['itemsLimitPerTable'])) { 3023 $this->itemsLimitPerTable = MathUtility::forceIntegerInRange( 3024 (int)$this->modTSconfig['properties']['itemsLimitPerTable'], 3025 1, 3026 10000 3027 ); 3028 } 3029 if (isset($this->modTSconfig['properties']['itemsLimitSingleTable'])) { 3030 $this->itemsLimitSingleTable = MathUtility::forceIntegerInRange( 3031 (int)$this->modTSconfig['properties']['itemsLimitSingleTable'], 3032 1, 3033 10000 3034 ); 3035 } 3036 3037 // If there is a current link to a record, set the current link uid and get the table name from the link handler configuration 3038 $currentLinkValue = isset($this->overrideUrlParameters['P']['currentValue']) ? trim($this->overrideUrlParameters['P']['currentValue']) : ''; 3039 if ($currentLinkValue) { 3040 $linkService = GeneralUtility::makeInstance(LinkService::class); 3041 try { 3042 $currentLinkParts = $linkService->resolve($currentLinkValue); 3043 if ($currentLinkParts['type'] === 'record' && isset($currentLinkParts['identifier'])) { 3044 $this->currentLink['tableNames'] = $this->tableList; 3045 $this->currentLink['uid'] = (int)$currentLinkParts['uid']; 3046 } 3047 } catch (UnknownLinkHandlerException $e) { 3048 } 3049 } 3050 3051 // $table might be NULL at this point in the code. As the expressionBuilder 3052 // is used to limit returned records based on the page permissions and the 3053 // uid field of the pages it can hardcoded to work on the pages table. 3054 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 3055 ->getQueryBuilderForTable('pages') 3056 ->expr(); 3057 $permsClause = $expressionBuilder->andX($backendUser->getPagePermsClause(Permission::PAGE_SHOW)); 3058 // This will hide records from display - it has nothing to do with user rights!! 3059 $pidList = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '', true); 3060 if (!empty($pidList)) { 3061 $permsClause->add($expressionBuilder->notIn('pages.uid', $pidList)); 3062 } 3063 $this->perms_clause = (string)$permsClause; 3064 3065 // Get configuration of collapsed tables from user uc and merge with sanitized GP vars 3066 $this->tablesCollapsed = is_array($backendUser->uc['moduleData']['list']) 3067 ? $backendUser->uc['moduleData']['list'] 3068 : []; 3069 $collapseOverride = GeneralUtility::_GP('collapse'); 3070 if (is_array($collapseOverride)) { 3071 foreach ($collapseOverride as $collapseTable => $collapseValue) { 3072 if (is_array($GLOBALS['TCA'][$collapseTable]) && ($collapseValue == 0 || $collapseValue == 1)) { 3073 $this->tablesCollapsed[$collapseTable] = $collapseValue; 3074 } 3075 } 3076 // Save modified user uc 3077 $backendUser->uc['moduleData']['list'] = $this->tablesCollapsed; 3078 $backendUser->writeUC($backendUser->uc); 3079 $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl')); 3080 if ($returnUrl !== '') { 3081 HttpUtility::redirect($returnUrl); 3082 } 3083 } 3084 $this->initializeLanguages(); 3085 } 3086 3087 /** 3088 * Traverses the table(s) to be listed and renders the output code for each: 3089 * The HTML is accumulated in $this->HTMLcode 3090 * Finishes off with a stopper-gif 3091 */ 3092 public function generateList() 3093 { 3094 // Set page record in header 3095 $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id); 3096 $hideTablesArray = GeneralUtility::trimExplode(',', $this->hideTables); 3097 3098 $backendUser = $this->getBackendUserAuthentication(); 3099 3100 // pre-process tables and add sorting instructions 3101 $tableNames = array_flip(array_keys($GLOBALS['TCA'])); 3102 foreach ($tableNames as $tableName => &$config) { 3103 $hideTable = false; 3104 3105 // Checking if the table should be rendered: 3106 // Checks that we see only permitted/requested tables: 3107 if ($this->table && $tableName !== $this->table 3108 || $this->tableList && !GeneralUtility::inList($this->tableList, $tableName) 3109 || !$backendUser->check('tables_select', $tableName) 3110 ) { 3111 $hideTable = true; 3112 } 3113 3114 if (!$hideTable) { 3115 // Don't show table if hidden by TCA ctrl section 3116 // Don't show table if hidden by pageTSconfig mod.web_list.hideTables 3117 $hideTable = $hideTable 3118 || !empty($GLOBALS['TCA'][$tableName]['ctrl']['hideTable']) 3119 || in_array($tableName, $hideTablesArray, true) 3120 || in_array('*', $hideTablesArray, true); 3121 // Override previous selection if table is enabled or hidden by TSconfig TCA override mod.web_list.table 3122 if (isset($this->tableTSconfigOverTCA[$tableName . '.']['hideTable'])) { 3123 $hideTable = (bool)$this->tableTSconfigOverTCA[$tableName . '.']['hideTable']; 3124 } 3125 } 3126 if ($hideTable) { 3127 unset($tableNames[$tableName]); 3128 } else { 3129 if (isset($this->tableDisplayOrder[$tableName])) { 3130 // Copy display order information 3131 $tableNames[$tableName] = $this->tableDisplayOrder[$tableName]; 3132 } else { 3133 $tableNames[$tableName] = []; 3134 } 3135 } 3136 } 3137 unset($config); 3138 3139 $orderedTableNames = GeneralUtility::makeInstance(DependencyOrderingService::class) 3140 ->orderByDependencies($tableNames); 3141 3142 foreach ($orderedTableNames as $tableName => $_) { 3143 // check if we are in single- or multi-table mode 3144 if ($this->table) { 3145 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems']) 3146 ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxSingleDBListItems'] 3147 : $this->itemsLimitSingleTable; 3148 } else { 3149 // if there are no records in table continue current foreach 3150 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 3151 ->getQueryBuilderForTable($tableName); 3152 $queryBuilder->getRestrictions() 3153 ->removeAll() 3154 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 3155 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); 3156 $queryBuilder = $this->addPageIdConstraint($tableName, $queryBuilder); 3157 $firstRow = $queryBuilder->select('uid') 3158 ->from($tableName) 3159 ->setMaxResults(1) 3160 ->execute() 3161 ->fetch(); 3162 if (!is_array($firstRow)) { 3163 continue; 3164 } 3165 $this->iLimit = isset($GLOBALS['TCA'][$tableName]['interface']['maxDBListItems']) 3166 ? (int)$GLOBALS['TCA'][$tableName]['interface']['maxDBListItems'] 3167 : $this->itemsLimitPerTable; 3168 } 3169 if ($this->showLimit) { 3170 $this->iLimit = $this->showLimit; 3171 } 3172 // Setting fields to select: 3173 if ($this->allFields) { 3174 $fields = $this->makeFieldList($tableName); 3175 $fields[] = 'tstamp'; 3176 $fields[] = 'crdate'; 3177 $fields[] = '_PATH_'; 3178 $fields[] = '_CONTROL_'; 3179 if (is_array($this->setFields[$tableName])) { 3180 $fields = array_intersect($fields, $this->setFields[$tableName]); 3181 } else { 3182 $fields = []; 3183 } 3184 } else { 3185 $fields = []; 3186 } 3187 3188 // Finally, render the list: 3189 $this->HTMLcode .= $this->getTable($tableName, $this->id, implode(',', $fields)); 3190 } 3191 } 3192 3193 /** 3194 * Creates the search box 3195 * 3196 * @param bool $formFields If TRUE, the search box is wrapped in its own form-tags 3197 * @return string HTML for the search box 3198 */ 3199 public function getSearchBox($formFields = true) 3200 { 3201 /** @var IconFactory $iconFactory */ 3202 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 3203 $lang = $this->getLanguageService(); 3204 // Setting form-elements, if applicable: 3205 $formElements = ['', '']; 3206 if ($formFields) { 3207 $formElements = [ 3208 '<form action="' . htmlspecialchars( 3209 $this->listURL('', '-1', 'firstElementNumber,search_field') 3210 ) . '" method="post">', 3211 '</form>' 3212 ]; 3213 } 3214 // Make level selector: 3215 $opt = []; 3216 3217 // "New" generation of search levels ... based on TS config 3218 $config = BackendUtility::getPagesTSconfig($this->id); 3219 $searchLevelsFromTSconfig = $config['mod.']['web_list.']['searchLevel.']['items.']; 3220 $searchLevelItems = []; 3221 3222 // get translated labels for search levels from pagets 3223 foreach ($searchLevelsFromTSconfig as $keySearchLevel => $labelConfigured) { 3224 $label = $lang->sL('LLL:' . $labelConfigured); 3225 if ($label === '') { 3226 $label = $labelConfigured; 3227 } 3228 $searchLevelItems[$keySearchLevel] = $label; 3229 } 3230 3231 foreach ($searchLevelItems as $kv => $label) { 3232 $opt[] = '<option value="' . $kv . '"' . ($kv === $this->searchLevels ? ' selected="selected"' : '') . '>' . htmlspecialchars( 3233 $label 3234 ) . '</option>'; 3235 } 3236 $lMenu = '<select class="form-control" name="search_levels" title="' . htmlspecialchars( 3237 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search_levels') 3238 ) . '" id="search_levels">' . implode('', $opt) . '</select>'; 3239 // Table with the search box: 3240 $content = '<div class="db_list-searchbox-form db_list-searchbox-toolbar module-docheader-bar module-docheader-bar-search t3js-module-docheader-bar t3js-module-docheader-bar-search" id="db_list-searchbox-toolbar" style="display: ' . ($this->searchString == '' ? 'none' : 'block') . ';"> 3241 ' . $formElements[0] . ' 3242 <div id="typo3-dblist-search"> 3243 <div class="panel panel-default"> 3244 <div class="panel-body"> 3245 <div class="row"> 3246 <div class="col-sm-6 col-xs-12"> 3247 <label for="search_field">' . htmlspecialchars( 3248 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.searchString') 3249 ) . '</label> 3250 <input class="form-control" type="search" placeholder="' . htmlspecialchars( 3251 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enterSearchString') 3252 ) . '" title="' . htmlspecialchars( 3253 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.searchString') 3254 ) . '" name="search_field" id="search_field" value="' . htmlspecialchars($this->searchString) . '" /> 3255 </div> 3256 <div class="col-xs-12 col-sm-3"> 3257 <label for="search_levels">' . htmlspecialchars( 3258 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.search_levels') 3259 ) . '</label> 3260 ' . $lMenu . ' 3261 </div> 3262 <div class="col-xs-12 col-sm-3"> 3263 <label for="showLimit">' . htmlspecialchars( 3264 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.label.limit') 3265 ) . '</label> 3266 <input class="form-control" type="number" min="0" max="10000" placeholder="10" title="' . htmlspecialchars( 3267 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.limit') 3268 ) . '" name="showLimit" id="showLimit" value="' . htmlspecialchars( 3269 ($this->showLimit ? $this->showLimit : '') 3270 ) . '" /> 3271 </div> 3272 <div class="col-xs-12"> 3273 <div class="form-control-wrap"> 3274 <button type="submit" class="btn btn-default" name="search" title="' . htmlspecialchars( 3275 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.title.search') 3276 ) . '"> 3277 ' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render( 3278 ) . ' ' . htmlspecialchars( 3279 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.search') 3280 ) . ' 3281 </button> 3282 </div> 3283 </div> 3284 </div> 3285 </div> 3286 </div> 3287 </div> 3288 ' . $formElements[1] . '</div>'; 3289 return $content; 3290 } 3291 3292 /** 3293 * Setting the field names to display in extended list. 3294 * Sets the internal variable $this->setFields 3295 */ 3296 public function setDispFields() 3297 { 3298 $backendUser = $this->getBackendUserAuthentication(); 3299 // Getting from session: 3300 $dispFields = $backendUser->getModuleData('list/displayFields'); 3301 // If fields has been inputted, then set those as the value and push it to session variable: 3302 if (is_array($this->displayFields)) { 3303 reset($this->displayFields); 3304 $tKey = key($this->displayFields); 3305 $dispFields[$tKey] = $this->displayFields[$tKey]; 3306 $backendUser->pushModuleData('list/displayFields', $dispFields); 3307 } 3308 // Setting result: 3309 $this->setFields = $dispFields; 3310 } 3311 3312 /** 3313 * Create thumbnail code for record/field 3314 * 3315 * @param mixed[] $row Record array 3316 * @param string $table Table (record is from) 3317 * @param string $field Field name for which thumbnail are to be rendered. 3318 * @return string HTML for thumbnails, if any. 3319 */ 3320 public function thumbCode($row, $table, $field) 3321 { 3322 return BackendUtility::thumbCode($row, $table, $field); 3323 } 3324 3325 /** 3326 * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted 3327 * depending on the current searchlevel setting. 3328 * 3329 * @param string $table Table name 3330 * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions 3331 * @param string[] $additionalConstraints Additional part for where clause 3332 * @param string[] $fields Field list to select, * for all 3333 * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder 3334 */ 3335 public function getQueryBuilder( 3336 string $table, 3337 int $pageId, 3338 array $additionalConstraints = [], 3339 array $fields = ['*'] 3340 ): QueryBuilder { 3341 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 3342 ->getQueryBuilderForTable($table); 3343 $queryBuilder->getRestrictions() 3344 ->removeAll() 3345 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 3346 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); 3347 $queryBuilder 3348 ->select(...$fields) 3349 ->from($table); 3350 3351 if (!empty($additionalConstraints)) { 3352 $queryBuilder->andWhere(...$additionalConstraints); 3353 } 3354 3355 $queryBuilder = $this->prepareQueryBuilder($table, $pageId, $fields, $additionalConstraints, $queryBuilder); 3356 3357 return $queryBuilder; 3358 } 3359 3360 /** 3361 * Return the modified QueryBuilder object ($queryBuilder) which will be 3362 * used to select the records from a table $table with pid = $this->pidList 3363 * 3364 * @param string $table Table name 3365 * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions 3366 * @param string[] $fieldList List of fields to select from the table 3367 * @param string[] $additionalConstraints Additional part for where clause 3368 * @param QueryBuilder $queryBuilder 3369 * @param bool $addSorting 3370 * @return QueryBuilder 3371 */ 3372 protected function prepareQueryBuilder( 3373 string $table, 3374 int $pageId, 3375 array $fieldList = ['*'], 3376 array $additionalConstraints = [], 3377 QueryBuilder $queryBuilder, 3378 bool $addSorting = true 3379 ): QueryBuilder { 3380 $parameters = [ 3381 'table' => $table, 3382 'fields' => $fieldList, 3383 'groupBy' => null, 3384 'orderBy' => null, 3385 'firstResult' => $this->firstElementNumber ?: null, 3386 'maxResults' => $this->iLimit ?: null 3387 ]; 3388 3389 if ($this->iLimit > 0) { 3390 $queryBuilder->setMaxResults($this->iLimit); 3391 } 3392 3393 if ($this->firstElementNumber > 0) { 3394 $queryBuilder->setFirstResult($this->firstElementNumber); 3395 } 3396 3397 if ($addSorting) { 3398 if ($this->sortField && in_array($this->sortField, $this->makeFieldList($table, 1))) { 3399 $queryBuilder->orderBy($this->sortField, $this->sortRev ? 'DESC' : 'ASC'); 3400 } else { 3401 $orderBy = $GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby']; 3402 $orderBys = QueryHelper::parseOrderBy((string)$orderBy); 3403 foreach ($orderBys as $orderBy) { 3404 $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]); 3405 } 3406 } 3407 } 3408 3409 // Build the query constraints 3410 $queryBuilder = $this->addPageIdConstraint($table, $queryBuilder); 3411 $searchWhere = $this->makeSearchString($table, $pageId); 3412 if (!empty($searchWhere)) { 3413 $queryBuilder->andWhere($searchWhere); 3414 } 3415 3416 // Filtering on displayable pages (permissions): 3417 if ($table === 'pages' && $this->perms_clause) { 3418 $queryBuilder->andWhere($this->perms_clause); 3419 } 3420 3421 // Filter out records that are translated, if TSconfig mod.web_list.hideTranslations is set 3422 if (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) 3423 && (GeneralUtility::inList($this->hideTranslations, $table) || $this->hideTranslations === '*') 3424 ) { 3425 $queryBuilder->andWhere( 3426 $queryBuilder->expr()->eq( 3427 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 3428 0 3429 ) 3430 ); 3431 } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) && $this->showOnlyTranslatedRecords) { 3432 // When only translated records should be shown, it is necessary to use l10n_parent=pageId, instead of 3433 // a check to the PID 3434 $queryBuilder->andWhere( 3435 $queryBuilder->expr()->eq( 3436 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 3437 $queryBuilder->createNamedParameter( 3438 $this->id, 3439 \PDO::PARAM_INT 3440 ) 3441 ) 3442 ); 3443 } 3444 3445 $hookName = DatabaseRecordList::class; 3446 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'])) { 3447 // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead. 3448 trigger_error('The hook ($GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][' . $hookName . '][\'buildQueryParameters\']) will be removed in TYPO3 v10.0, the modifyQuery hook should be used instead.', E_USER_DEPRECATED); 3449 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['buildQueryParameters'] as $className) { 3450 $hookObject = GeneralUtility::makeInstance($className); 3451 if (method_exists($hookObject, 'buildQueryParametersPostProcess')) { 3452 $hookObject->buildQueryParametersPostProcess( 3453 $parameters, 3454 $table, 3455 $pageId, 3456 $additionalConstraints, 3457 $fieldList, 3458 $this, 3459 $queryBuilder 3460 ); 3461 } 3462 } 3463 } 3464 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][$hookName]['modifyQuery'] ?? [] as $className) { 3465 $hookObject = GeneralUtility::makeInstance($className); 3466 if (method_exists($hookObject, 'modifyQuery')) { 3467 $hookObject->modifyQuery( 3468 $parameters, 3469 $table, 3470 $pageId, 3471 $additionalConstraints, 3472 $fieldList, 3473 $queryBuilder 3474 ); 3475 } 3476 } 3477 3478 // array_unique / array_filter used to eliminate empty and duplicate constraints 3479 // the array keys are eliminated by this as well to facilitate argument unpacking 3480 // when used with the querybuilder. 3481 // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 3482 if (!empty($parameters['where'])) { 3483 $parameters['where'] = array_unique(array_filter(array_values($parameters['where']))); 3484 } 3485 if (!empty($parameters['where'])) { 3486 $this->logDeprecation('where'); 3487 $queryBuilder->where(...$parameters['where']); 3488 } 3489 if (!empty($parameters['orderBy'])) { 3490 $this->logDeprecation('orderBy'); 3491 foreach ($parameters['orderBy'] as $fieldNameAndSorting) { 3492 [$fieldName, $sorting] = $fieldNameAndSorting; 3493 $queryBuilder->addOrderBy($fieldName, $sorting); 3494 } 3495 } 3496 if (!empty($parameters['firstResult']) && $parameters['firstResult'] !== $this->firstElementNumber) { 3497 $this->logDeprecation('firstResult'); 3498 $queryBuilder->setFirstResult((int)$parameters['firstResult']); 3499 } 3500 if (!empty($parameters['maxResults']) && $parameters['maxResults'] !== $this->iLimit) { 3501 $this->logDeprecation('maxResults'); 3502 $queryBuilder->setMaxResults((int)$parameters['maxResults']); 3503 } 3504 if (!empty($parameters['groupBy'])) { 3505 $this->logDeprecation('groupBy'); 3506 $queryBuilder->groupBy($parameters['groupBy']); 3507 } 3508 3509 return $queryBuilder; 3510 } 3511 3512 /** 3513 * Executed a query to set $this->totalItems to the number of total 3514 * items, eg. for pagination 3515 * 3516 * @param string $table Table name 3517 * @param int $pageId Only used to build the search constraints, $this->pidList is used for restrictions 3518 * @param array $constraints Additional constraints for where clause 3519 */ 3520 public function setTotalItems(string $table, int $pageId, array $constraints) 3521 { 3522 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 3523 ->getQueryBuilderForTable($table); 3524 3525 $queryBuilder->getRestrictions() 3526 ->removeAll() 3527 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 3528 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); 3529 $queryBuilder 3530 ->from($table); 3531 3532 if (!empty($constraints)) { 3533 $queryBuilder->andWhere(...$constraints); 3534 } 3535 3536 $queryBuilder = $this->prepareQueryBuilder($table, $pageId, ['*'], $constraints, $queryBuilder, false); 3537 // Reset limit and offset for full count query 3538 $queryBuilder->setFirstResult(0); 3539 $queryBuilder->setMaxResults(1); 3540 3541 $this->totalItems = (int)$queryBuilder->count('*') 3542 ->execute() 3543 ->fetchColumn(); 3544 } 3545 3546 /** 3547 * Creates part of query for searching after a word ($this->searchString) 3548 * fields in input table. 3549 * 3550 * @param string $table Table, in which the fields are being searched. 3551 * @param int $currentPid Page id for the possible search limit. -1 only if called from an old XCLASS. 3552 * @return string Returns part of WHERE-clause for searching, if applicable. 3553 */ 3554 public function makeSearchString($table, $currentPid = -1) 3555 { 3556 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 3557 $expressionBuilder = $queryBuilder->expr(); 3558 $constraints = []; 3559 $currentPid = (int)$currentPid; 3560 $tablePidField = $table === 'pages' ? 'uid' : 'pid'; 3561 // Make query only if table is valid and a search string is actually defined 3562 if (empty($this->searchString)) { 3563 return ''; 3564 } 3565 3566 $searchableFields = $this->getSearchFields($table); 3567 if (MathUtility::canBeInterpretedAsInteger($this->searchString)) { 3568 $constraints[] = $expressionBuilder->eq('uid', (int)$this->searchString); 3569 foreach ($searchableFields as $fieldName) { 3570 if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) { 3571 continue; 3572 } 3573 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']; 3574 $fieldType = $fieldConfig['type']; 3575 $evalRules = $fieldConfig['eval'] ?: ''; 3576 if ($fieldType === 'input' && $evalRules && GeneralUtility::inList($evalRules, 'int')) { 3577 if (!isset($fieldConfig['search']['pidonly']) 3578 || ($fieldConfig['search']['pidonly'] && $currentPid > 0) 3579 ) { 3580 $constraints[] = $expressionBuilder->andX( 3581 $expressionBuilder->eq($fieldName, (int)$this->searchString), 3582 $expressionBuilder->eq($tablePidField, (int)$currentPid) 3583 ); 3584 } 3585 } elseif ($fieldType === 'text' 3586 || $fieldType === 'flex' 3587 || ($fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules))) 3588 ) { 3589 $constraints[] = $expressionBuilder->like( 3590 $fieldName, 3591 $queryBuilder->quote('%' . (int)$this->searchString . '%') 3592 ); 3593 } 3594 } 3595 } elseif (!empty($searchableFields)) { 3596 $like = $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($this->searchString) . '%'); 3597 foreach ($searchableFields as $fieldName) { 3598 if (!isset($GLOBALS['TCA'][$table]['columns'][$fieldName])) { 3599 continue; 3600 } 3601 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']; 3602 $fieldType = $fieldConfig['type']; 3603 $evalRules = $fieldConfig['eval'] ?: ''; 3604 $searchConstraint = $expressionBuilder->andX( 3605 $expressionBuilder->comparison( 3606 'LOWER(' . $queryBuilder->castFieldToTextType($fieldName) . ')', 3607 'LIKE', 3608 'LOWER(' . $like . ')' 3609 ) 3610 ); 3611 if (is_array($fieldConfig['search'])) { 3612 $searchConfig = $fieldConfig['search']; 3613 if (in_array('case', $searchConfig)) { 3614 // Replace case insensitive default constraint 3615 $searchConstraint = $expressionBuilder->andX($expressionBuilder->like($fieldName, $like)); 3616 } 3617 if (in_array('pidonly', $searchConfig) && $currentPid > 0) { 3618 $searchConstraint->add($expressionBuilder->eq($tablePidField, (int)$currentPid)); 3619 } 3620 if ($searchConfig['andWhere']) { 3621 $searchConstraint->add( 3622 QueryHelper::stripLogicalOperatorPrefix($fieldConfig['search']['andWhere']) 3623 ); 3624 } 3625 } 3626 if ($fieldType === 'text' 3627 || $fieldType === 'flex' 3628 || $fieldType === 'input' && (!$evalRules || !preg_match('/\b(?:date|time|int)\b/', $evalRules)) 3629 ) { 3630 if ($searchConstraint->count() !== 0) { 3631 $constraints[] = $searchConstraint; 3632 } 3633 } 3634 } 3635 } 3636 // Call hook to add or change the list 3637 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][DatabaseRecordList::class]['makeSearchStringConstraints'] ?? [] as $className) { 3638 $hookObject = GeneralUtility::makeInstance($className); 3639 if (method_exists($hookObject, 'makeSearchStringConstraints')) { 3640 $constraints = $hookObject->makeSearchStringConstraints( 3641 $queryBuilder, 3642 $constraints, 3643 $this->searchString, 3644 $table, 3645 $currentPid 3646 ); 3647 } 3648 } 3649 // If no search field conditions have been built ensure no results are returned 3650 if (empty($constraints)) { 3651 return '0=1'; 3652 } 3653 3654 return $expressionBuilder->orX(...$constraints); 3655 } 3656 3657 /** 3658 * Fetches a list of fields to use in the Backend search for the given table. 3659 * 3660 * @param string $tableName 3661 * @return string[] 3662 */ 3663 protected function getSearchFields($tableName) 3664 { 3665 $fieldArray = []; 3666 $fieldListWasSet = false; 3667 // Get fields from ctrl section of TCA first 3668 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) { 3669 $fieldArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true); 3670 $fieldListWasSet = true; 3671 } 3672 // Call hook to add or change the list 3673 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'])) { 3674 $hookParameters = [ 3675 'tableHasSearchConfiguration' => $fieldListWasSet, 3676 'tableName' => $tableName, 3677 'searchFields' => &$fieldArray, 3678 'searchString' => $this->searchString 3679 ]; 3680 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['mod_list']['getSearchFieldList'] as $hookFunction) { 3681 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this); 3682 } 3683 } 3684 return $fieldArray; 3685 } 3686 3687 /** 3688 * Returns the title (based on $code) of a table ($table) with the proper link around. For headers over tables. 3689 * The link will cause the display of all extended mode or not for the table. 3690 * 3691 * @param string $table Table name 3692 * @param string $code Table label 3693 * @return string The linked table label 3694 */ 3695 public function linkWrapTable($table, $code) 3696 { 3697 if ($this->table !== $table) { 3698 return '<a href="' . htmlspecialchars( 3699 $this->listURL('', $table, 'firstElementNumber') 3700 ) . '">' . $code . '</a>'; 3701 } 3702 return '<a href="' . htmlspecialchars( 3703 $this->listURL('', '', 'sortField,sortRev,table,firstElementNumber') 3704 ) . '">' . $code . '</a>'; 3705 } 3706 3707 /** 3708 * Returns the title (based on $code) of a record (from table $table) with the proper link around (that is for 'pages'-records a link to the level of that record...) 3709 * 3710 * @param string $table Table name 3711 * @param int $uid Item uid 3712 * @param string $code Item title (not htmlspecialchars()'ed yet) 3713 * @param mixed[] $row Item row 3714 * @return string The item title. Ready for HTML output (is htmlspecialchars()'ed) 3715 */ 3716 public function linkWrapItems($table, $uid, $code, $row) 3717 { 3718 $lang = $this->getLanguageService(); 3719 $origCode = $code; 3720 // If the title is blank, make a "no title" label: 3721 if ((string)$code === '') { 3722 $code = '<i>[' . htmlspecialchars( 3723 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') 3724 ) . ']</i> - ' 3725 . htmlspecialchars(BackendUtility::getRecordTitle($table, $row)); 3726 } else { 3727 $code = htmlspecialchars($code, ENT_QUOTES, 'UTF-8', false); 3728 if ($code != htmlspecialchars($origCode)) { 3729 $code = '<span title="' . htmlspecialchars( 3730 $origCode, 3731 ENT_QUOTES, 3732 'UTF-8', 3733 false 3734 ) . '">' . $code . '</span>'; 3735 } else { 3736 $code = '<span title="' . $code . '">' . $code . '</span>'; 3737 } 3738 } 3739 switch ((string)$this->clickTitleMode) { 3740 case 'edit': 3741 // If the listed table is 'pages' we have to request the permission settings for each page: 3742 if ($table === 'pages') { 3743 $localCalcPerms = $this->getBackendUserAuthentication()->calcPerms( 3744 BackendUtility::getRecord('pages', $row['uid']) 3745 ); 3746 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT; 3747 } else { 3748 $backendUser = $this->getBackendUserAuthentication(); 3749 $permsEdit = $this->calcPerms & Permission::CONTENT_EDIT && $backendUser->recordEditAccessInternals($table, $row); 3750 } 3751 // "Edit" link: ( Only if permissions to edit the page-record of the content of the parent page ($this->id) 3752 if ($permsEdit && $this->isEditable($table)) { 3753 $params = '&edit[' . $table . '][' . $row['uid'] . ']=edit'; 3754 $code = '<a href="#" onclick="' . htmlspecialchars( 3755 BackendUtility::editOnClick($params, '', -1) 3756 ) . '" title="' . htmlspecialchars($lang->getLL('edit')) . '">' . $code . '</a>'; 3757 } 3758 break; 3759 case 'show': 3760 // "Show" link (only pages and tt_content elements) 3761 if ($table === 'pages' || $table === 'tt_content') { 3762 $onClick = $this->getOnClickForRow($table, $row); 3763 $code = '<a href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars( 3764 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage') 3765 ) . '">' . $code . '</a>'; 3766 } 3767 break; 3768 case 'info': 3769 // "Info": (All records) 3770 $code = '<a href="#" onclick="' . htmlspecialchars( 3771 'top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . (int)$row['uid'] . '); return false;' 3772 ) . '" title="' . htmlspecialchars($lang->getLL('showInfo')) . '">' . $code . '</a>'; 3773 break; 3774 default: 3775 // Output the label now: 3776 if ($table === 'pages') { 3777 $code = '<a href="' . htmlspecialchars( 3778 $this->listURL($uid, '', 'firstElementNumber') 3779 ) . '" onclick="setHighlight(' . (int)$uid . ')">' . $code . '</a>'; 3780 } else { 3781 $code = $this->linkUrlMail($code, $origCode); 3782 } 3783 } 3784 return $code; 3785 } 3786 3787 /** 3788 * Wrapping input code in link to URL or email if $testString is either. 3789 * 3790 * @param string $code code to wrap 3791 * @param string $testString String which is tested for being a URL or email and which will be used for the link if so. 3792 * @return string Link-Wrapped $code value, if $testString was URL or email. 3793 */ 3794 public function linkUrlMail($code, $testString) 3795 { 3796 // Check for URL: 3797 $scheme = parse_url($testString, PHP_URL_SCHEME); 3798 if ($scheme === 'http' || $scheme === 'https' || $scheme === 'ftp') { 3799 return '<a href="' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>'; 3800 } 3801 // Check for email: 3802 if (GeneralUtility::validEmail($testString)) { 3803 return '<a href="mailto:' . htmlspecialchars($testString) . '" target="_blank">' . $code . '</a>'; 3804 } 3805 // Return if nothing else... 3806 return $code; 3807 } 3808 3809 /** 3810 * Creates the URL to this script, including all relevant GPvars 3811 * Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit 3812 * The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $exclList variable. 3813 * 3814 * @param string $altId Alternative id value. Enter blank string for the current id ($this->id) 3815 * @param string $table Table name to display. Enter "-1" for the current table. 3816 * @param string $exclList Comma separated list of fields NOT to include ("sortField", "sortRev" or "firstElementNumber") 3817 * @return string URL 3818 */ 3819 public function listURL($altId = '', $table = '-1', $exclList = '') 3820 { 3821 $urlParameters = []; 3822 if ((string)$altId !== '') { 3823 $urlParameters['id'] = $altId; 3824 } else { 3825 $urlParameters['id'] = $this->id; 3826 } 3827 if ($table === '-1') { 3828 $urlParameters['table'] = $this->table; 3829 } else { 3830 $urlParameters['table'] = $table; 3831 } 3832 if ($this->thumbs) { 3833 $urlParameters['imagemode'] = $this->thumbs; 3834 } 3835 if ($this->returnUrl) { 3836 $urlParameters['returnUrl'] = $this->returnUrl; 3837 } 3838 if ((!$exclList || !GeneralUtility::inList($exclList, 'search_field')) && $this->searchString) { 3839 $urlParameters['search_field'] = $this->searchString; 3840 } 3841 if ($this->searchLevels) { 3842 $urlParameters['search_levels'] = $this->searchLevels; 3843 } 3844 if ($this->showLimit) { 3845 $urlParameters['showLimit'] = $this->showLimit; 3846 } 3847 if ((!$exclList || !GeneralUtility::inList($exclList, 'firstElementNumber')) && $this->firstElementNumber) { 3848 $urlParameters['pointer'] = $this->firstElementNumber; 3849 } 3850 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortField')) && $this->sortField) { 3851 $urlParameters['sortField'] = $this->sortField; 3852 } 3853 if ((!$exclList || !GeneralUtility::inList($exclList, 'sortRev')) && $this->sortRev) { 3854 $urlParameters['sortRev'] = $this->sortRev; 3855 } 3856 3857 $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters); 3858 3859 if ($routePath = GeneralUtility::_GP('route')) { 3860 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 3861 $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters); 3862 } else { 3863 $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . HttpUtility::buildQueryString($urlParameters, '?'); 3864 } 3865 return $url; 3866 } 3867 3868 /** 3869 * Returns "requestUri" - which is basically listURL 3870 * @return string Content of ->listURL() 3871 */ 3872 public function requestUri() 3873 { 3874 return $this->listURL(); 3875 } 3876 3877 /** 3878 * Makes the list of fields to select for a table 3879 * 3880 * @param string $table Table name 3881 * @param bool $dontCheckUser If set, users access to the field (non-exclude-fields) is NOT checked. 3882 * @param bool $addDateFields If set, also adds crdate and tstamp fields (note: they will also be added if user is admin or dontCheckUser is set) 3883 * @return string[] Array, where values are fieldnames to include in query 3884 */ 3885 public function makeFieldList($table, $dontCheckUser = false, $addDateFields = false) 3886 { 3887 $backendUser = $this->getBackendUserAuthentication(); 3888 // Init fieldlist array: 3889 $fieldListArr = []; 3890 // Check table: 3891 if (is_array($GLOBALS['TCA'][$table]) && isset($GLOBALS['TCA'][$table]['columns']) && is_array( 3892 $GLOBALS['TCA'][$table]['columns'] 3893 )) { 3894 if (isset($GLOBALS['TCA'][$table]['columns']) && is_array($GLOBALS['TCA'][$table]['columns'])) { 3895 // Traverse configured columns and add them to field array, if available for user. 3896 foreach ($GLOBALS['TCA'][$table]['columns'] as $fN => $fieldValue) { 3897 if ($fieldValue['config']['type'] === 'none') { 3898 // Never render or fetch type=none fields from db 3899 continue; 3900 } 3901 if ($dontCheckUser 3902 || (!$fieldValue['exclude'] || $backendUser->check('non_exclude_fields', $table . ':' . $fN)) && $fieldValue['config']['type'] !== 'passthrough' 3903 ) { 3904 $fieldListArr[] = $fN; 3905 } 3906 } 3907 3908 $fieldListArr[] = 'uid'; 3909 $fieldListArr[] = 'pid'; 3910 3911 // Add date fields 3912 if ($dontCheckUser || $backendUser->isAdmin() || $addDateFields) { 3913 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) { 3914 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['tstamp']; 3915 } 3916 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) { 3917 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['crdate']; 3918 } 3919 } 3920 // Add more special fields: 3921 if ($dontCheckUser || $backendUser->isAdmin()) { 3922 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) { 3923 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['cruser_id']; 3924 } 3925 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { 3926 $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; 3927 } 3928 if (ExtensionManagementUtility::isLoaded( 3929 'workspaces' 3930 ) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { 3931 $fieldListArr[] = 't3ver_id'; 3932 $fieldListArr[] = 't3ver_state'; 3933 $fieldListArr[] = 't3ver_wsid'; 3934 } 3935 } 3936 } else { 3937 GeneralUtility::makeInstance(LogManager::class) 3938 ->getLogger(__CLASS__) 3939 ->error('TCA is broken for the table "' . $table . '": no required "columns" entry in TCA.'); 3940 } 3941 } 3942 return $fieldListArr; 3943 } 3944 3945 /** 3946 * Redirects to FormEngine if a record is just localized. 3947 * 3948 * @param string $justLocalized String with table, orig uid and language separated by ": 3949 */ 3950 public function localizationRedirect($justLocalized) 3951 { 3952 [$table, $orig_uid, $language] = explode(':', $justLocalized); 3953 if ($GLOBALS['TCA'][$table] 3954 && $GLOBALS['TCA'][$table]['ctrl']['languageField'] 3955 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] 3956 ) { 3957 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 3958 $queryBuilder->getRestrictions() 3959 ->removeAll() 3960 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 3961 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); 3962 3963 $localizedRecordUid = $queryBuilder->select('uid') 3964 ->from($table) 3965 ->where( 3966 $queryBuilder->expr()->eq( 3967 $GLOBALS['TCA'][$table]['ctrl']['languageField'], 3968 $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT) 3969 ), 3970 $queryBuilder->expr()->eq( 3971 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 3972 $queryBuilder->createNamedParameter($orig_uid, \PDO::PARAM_INT) 3973 ) 3974 ) 3975 ->setMaxResults(1) 3976 ->execute() 3977 ->fetchColumn(); 3978 3979 if ($localizedRecordUid !== false) { 3980 // Create parameters and finally run the classic page module for creating a new page translation 3981 $url = $this->listURL(); 3982 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 3983 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class); 3984 3985 $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute( 3986 'record_edit', 3987 [ 3988 'edit[' . $table . '][' . $localizedRecordUid . ']' => 'edit', 3989 'returnUrl' => $url 3990 ] 3991 ); 3992 HttpUtility::redirect($editUserAccountUrl); 3993 } 3994 } 3995 } 3996 3997 /** 3998 * Set URL parameters to override or add in the listUrl() method. 3999 * 4000 * @param string[] $urlParameters 4001 */ 4002 public function setOverrideUrlParameters(array $urlParameters) 4003 { 4004 $currentUrlParameter = GeneralUtility::_GP('curUrl'); 4005 if (isset($currentUrlParameter['url'])) { 4006 $urlParameters['P']['currentValue'] = $currentUrlParameter['url']; 4007 } 4008 $this->overrideUrlParameters = $urlParameters; 4009 } 4010 4011 /** 4012 * Set table display order information 4013 * 4014 * Structure of $orderInformation: 4015 * 'tableName' => [ 4016 * 'before' => // comma-separated string list or array of table names 4017 * 'after' => // comma-separated string list or array of table names 4018 * ] 4019 * 4020 * @param array $orderInformation 4021 * @throws \UnexpectedValueException 4022 */ 4023 public function setTableDisplayOrder(array $orderInformation) 4024 { 4025 foreach ($orderInformation as $tableName => &$configuration) { 4026 if (isset($configuration['before'])) { 4027 if (is_string($configuration['before'])) { 4028 $configuration['before'] = GeneralUtility::trimExplode(',', $configuration['before'], true); 4029 } elseif (!is_array($configuration['before'])) { 4030 throw new \UnexpectedValueException( 4031 'The specified "before" order configuration for table "' . $tableName . '" is invalid.', 4032 1504793406 4033 ); 4034 } 4035 } 4036 if (isset($configuration['after'])) { 4037 if (is_string($configuration['after'])) { 4038 $configuration['after'] = GeneralUtility::trimExplode(',', $configuration['after'], true); 4039 } elseif (!is_array($configuration['after'])) { 4040 throw new \UnexpectedValueException( 4041 'The specified "after" order configuration for table "' . $tableName . '" is invalid.', 4042 1504793407 4043 ); 4044 } 4045 } 4046 } 4047 $this->tableDisplayOrder = $orderInformation; 4048 } 4049 4050 /** 4051 * @return array 4052 */ 4053 public function getOverridePageIdList(): array 4054 { 4055 return $this->overridePageIdList; 4056 } 4057 4058 /** 4059 * @param int[]|array $overridePageIdList 4060 */ 4061 public function setOverridePageIdList(array $overridePageIdList) 4062 { 4063 $this->overridePageIdList = array_map('intval', $overridePageIdList); 4064 } 4065 4066 /** 4067 * Get all allowed mount pages to be searched in. 4068 * 4069 * @param int $id Page id 4070 * @param int $depth Depth to go down 4071 * @param string $perms_clause select clause 4072 * @return int[] 4073 */ 4074 protected function getSearchableWebmounts($id, $depth, $perms_clause) 4075 { 4076 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime'); 4077 $hash = 'webmounts_list' . md5($id . '-' . $depth . '-' . $perms_clause); 4078 if ($runtimeCache->has($hash)) { 4079 $idList = $runtimeCache->get($hash); 4080 } else { 4081 $backendUser = $this->getBackendUserAuthentication(); 4082 /** @var PageTreeView $tree */ 4083 $tree = GeneralUtility::makeInstance(PageTreeView::class); 4084 $tree->init('AND ' . $perms_clause); 4085 $tree->makeHTML = 0; 4086 $tree->fieldArray = ['uid', 'php_tree_stop']; 4087 $idList = []; 4088 4089 $allowedMounts = !$backendUser->isAdmin() && $id === 0 4090 ? $backendUser->returnWebmounts() 4091 : [$id]; 4092 4093 foreach ($allowedMounts as $allowedMount) { 4094 $idList[] = $allowedMount; 4095 if ($depth) { 4096 $tree->getTree($allowedMount, $depth, ''); 4097 } 4098 $idList = array_merge($idList, $tree->ids); 4099 } 4100 $runtimeCache->set($hash, $idList); 4101 } 4102 4103 return $idList; 4104 } 4105 4106 /** 4107 * Add conditions to the QueryBuilder object ($queryBuilder) to limit a 4108 * query to a list of page IDs based on the current search level setting. 4109 * 4110 * @param string $tableName 4111 * @param QueryBuilder $queryBuilder 4112 * @return QueryBuilder Modified QueryBuilder object 4113 */ 4114 protected function addPageIdConstraint(string $tableName, QueryBuilder $queryBuilder): QueryBuilder 4115 { 4116 // Set search levels: 4117 $searchLevels = $this->searchLevels; 4118 4119 // Set search levels to 999 instead of -1 as the following methods 4120 // do not support -1 as valid value for infinite search. 4121 if ($searchLevels === -1) { 4122 $searchLevels = 999; 4123 } 4124 4125 // When querying translated pages, the PID of the translated pages should be the same as the 4126 // the PID of the current page 4127 if ($tableName === 'pages' && $this->showOnlyTranslatedRecords) { 4128 $queryBuilder->andWhere( 4129 $queryBuilder->expr()->eq( 4130 $tableName . '.pid', 4131 $queryBuilder->createNamedParameter($this->pageRecord['pid'], \PDO::PARAM_INT) 4132 ) 4133 ); 4134 } elseif ($searchLevels === 0) { 4135 $queryBuilder->andWhere( 4136 $queryBuilder->expr()->eq( 4137 $tableName . '.pid', 4138 $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) 4139 ) 4140 ); 4141 } elseif ($searchLevels > 0) { 4142 $allowedMounts = $this->getSearchableWebmounts($this->id, $searchLevels, $this->perms_clause); 4143 $queryBuilder->andWhere( 4144 $queryBuilder->expr()->in( 4145 $tableName . '.pid', 4146 $queryBuilder->createNamedParameter($allowedMounts, Connection::PARAM_INT_ARRAY) 4147 ) 4148 ); 4149 } 4150 4151 if (!empty($this->getOverridePageIdList())) { 4152 $queryBuilder->andWhere( 4153 $queryBuilder->expr()->in( 4154 $tableName . '.pid', 4155 $queryBuilder->createNamedParameter($this->getOverridePageIdList(), Connection::PARAM_INT_ARRAY) 4156 ) 4157 ); 4158 } 4159 4160 return $queryBuilder; 4161 } 4162 4163 /** 4164 * Method used to log deprecated usage of old buildQueryParametersPostProcess hook arguments 4165 * 4166 * @param string $index 4167 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 - see method usages 4168 */ 4169 protected function logDeprecation(string $index) 4170 { 4171 trigger_error('[index: ' . $index . '] $parameters in "buildQueryParameters"-Hook will be removed in TYPO3 v10.0, use $queryBuilder instead.', E_USER_DEPRECATED); 4172 } 4173 4174 /** 4175 * @return BackendUserAuthentication 4176 */ 4177 protected function getBackendUserAuthentication() 4178 { 4179 return $GLOBALS['BE_USER']; 4180 } 4181 4182 /** 4183 * Returns a table-row with the content from the fields in the input data array. 4184 * OBS: $this->fieldArray MUST be set! (represents the list of fields to display) 4185 * 4186 * @param int $h Is an integer >=0 and denotes how tall an element is. Set to '0' makes a halv line, -1 = full line, set to 1 makes a 'join' and above makes 'line' 4187 * @param string $icon Is the <img>+<a> of the record. If not supplied the first 'join'-icon will be a 'line' instead 4188 * @param array $data Is the dataarray, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags 4189 * @param string $rowParams Is insert in the <tr>-tags. Must carry a ' ' as first character 4190 * @param string $_ OBSOLETE - NOT USED ANYMORE. $lMargin is the leftMargin (int) 4191 * @param string $_2 OBSOLETE - NOT USED ANYMORE. Is the HTML <img>-tag for an alternative 'gfx/ol/line.gif'-icon (used in the top) 4192 * @param string $colType Defines the tag being used for the columns. Default is td. 4193 * 4194 * @return string HTML content for the table row 4195 */ 4196 public function addElement($h, $icon, $data, $rowParams = '', $_ = '', $_2 = '', $colType = 'td') 4197 { 4198 $colType = ($colType === 'th') ? 'th' : 'td'; 4199 $noWrap = $this->no_noWrap ? '' : ' nowrap'; 4200 // Start up: 4201 $l10nParent = isset($data['_l10nparent_']) ? (int)$data['_l10nparent_'] : 0; 4202 $out = ' 4203 <!-- Element, begin: --> 4204 <tr ' . $rowParams . ' data-uid="' . (int)$data['uid'] . '" data-l10nparent="' . $l10nParent . '">'; 4205 // Show icon and lines 4206 if ($this->showIcon) { 4207 $out .= ' 4208 <' . $colType . ' class="col-icon nowrap">'; 4209 if (!$h) { 4210 $out .= ' '; 4211 } else { 4212 for ($a = 0; $a < $h; $a++) { 4213 if (!$a) { 4214 if ($icon) { 4215 $out .= $icon; 4216 } 4217 } 4218 } 4219 } 4220 $out .= '</' . $colType . '> 4221 '; 4222 } 4223 // Init rendering. 4224 $colsp = ''; 4225 $lastKey = ''; 4226 $c = 0; 4227 $ccount = 0; 4228 // __label is used as the label key to circumvent problems with uid used as label (see #67756) 4229 // as it was introduced later on, check if it really exists before using it 4230 $fields = $this->fieldArray; 4231 if ($colType === 'td' && array_key_exists('__label', $data)) { 4232 $fields[0] = '__label'; 4233 } 4234 // Traverse field array which contains the data to present: 4235 foreach ($fields as $vKey) { 4236 if (isset($data[$vKey])) { 4237 if ($lastKey) { 4238 $cssClass = $this->addElement_tdCssClass[$lastKey]; 4239 if ($this->oddColumnsCssClass && $ccount % 2 == 0) { 4240 $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]); 4241 } 4242 $out .= ' 4243 <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . '</' . $colType . '>'; 4244 } 4245 $lastKey = $vKey; 4246 $c = 1; 4247 $ccount++; 4248 } else { 4249 if (!$lastKey) { 4250 $lastKey = $vKey; 4251 } 4252 $c++; 4253 } 4254 if ($c > 1) { 4255 $colsp = ' colspan="' . $c . '"'; 4256 } else { 4257 $colsp = ''; 4258 } 4259 } 4260 if ($lastKey) { 4261 $cssClass = $this->addElement_tdCssClass[$lastKey]; 4262 if ($this->oddColumnsCssClass) { 4263 $cssClass = implode(' ', [$this->addElement_tdCssClass[$lastKey], $this->oddColumnsCssClass]); 4264 } 4265 $out .= ' 4266 <' . $colType . ' class="' . $cssClass . $noWrap . '"' . $colsp . $this->addElement_tdParams[$lastKey] . '>' . $data[$lastKey] . '</' . $colType . '>'; 4267 } 4268 // End row 4269 $out .= ' 4270 </tr>'; 4271 // Return row. 4272 return $out; 4273 } 4274 4275 /** 4276 * Dummy function, used to write the top of a table listing. 4277 */ 4278 public function writeTop() 4279 { 4280 } 4281 4282 /** 4283 * Creates a forward/reverse button based on the status of ->eCounter, ->firstElementNumber, ->iLimit 4284 * 4285 * @param string $table Table name 4286 * @return array array([boolean], [HTML]) where [boolean] is 1 for reverse element, [HTML] is the table-row code for the element 4287 */ 4288 public function fwd_rwd_nav($table = '') 4289 { 4290 $code = ''; 4291 if ($this->eCounter >= $this->firstElementNumber && $this->eCounter < $this->firstElementNumber + $this->iLimit) { 4292 if ($this->firstElementNumber && $this->eCounter == $this->firstElementNumber) { 4293 // Reverse 4294 $theData = []; 4295 $titleCol = $this->fieldArray[0]; 4296 $theData[$titleCol] = $this->fwd_rwd_HTML('fwd', $this->eCounter, $table); 4297 $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"'); 4298 } 4299 return [1, $code]; 4300 } 4301 if ($this->eCounter == $this->firstElementNumber + $this->iLimit) { 4302 // Forward 4303 $theData = []; 4304 $titleCol = $this->fieldArray[0]; 4305 $theData[$titleCol] = $this->fwd_rwd_HTML('rwd', $this->eCounter, $table); 4306 $code = $this->addElement(1, '', $theData, 'class="fwd_rwd_nav"'); 4307 } 4308 return [0, $code]; 4309 } 4310 4311 /** 4312 * Creates the button with link to either forward or reverse 4313 * 4314 * @param string $type Type: "fwd" or "rwd 4315 * @param int $pointer Pointer 4316 * @param string $table Table name 4317 * @return string 4318 * @internal 4319 */ 4320 public function fwd_rwd_HTML($type, $pointer, $table = '') 4321 { 4322 $content = ''; 4323 $tParam = $table ? '&table=' . rawurlencode($table) : ''; 4324 switch ($type) { 4325 case 'fwd': 4326 $href = $this->listURL() . '&pointer=' . ($pointer - $this->iLimit) . $tParam; 4327 $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon( 4328 'actions-move-up', 4329 Icon::SIZE_SMALL 4330 )->render() . '</a> <i>[' . (max(0, $pointer - $this->iLimit) + 1) . ' - ' . $pointer . ']</i>'; 4331 break; 4332 case 'rwd': 4333 $href = $this->listURL() . '&pointer=' . $pointer . $tParam; 4334 $content = '<a href="' . htmlspecialchars($href) . '">' . $this->iconFactory->getIcon( 4335 'actions-move-down', 4336 Icon::SIZE_SMALL 4337 )->render() . '</a> <i>[' . ($pointer + 1) . ' - ' . $this->totalItems . ']</i>'; 4338 break; 4339 } 4340 return $content; 4341 } 4342 4343 /** 4344 * Returning JavaScript for ClipBoard functionality. 4345 * 4346 * @return string 4347 */ 4348 public function CBfunctions() 4349 { 4350 return ' 4351 // checkOffCB() 4352 function checkOffCB(listOfCBnames, link) { // 4353 var checkBoxes, flag, i; 4354 var checkBoxes = listOfCBnames.split(","); 4355 if (link.rel === "") { 4356 link.rel = "allChecked"; 4357 flag = true; 4358 } else { 4359 link.rel = ""; 4360 flag = false; 4361 } 4362 for (i = 0; i < checkBoxes.length; i++) { 4363 setcbValue(checkBoxes[i], flag); 4364 } 4365 } 4366 // cbValue() 4367 function cbValue(CBname) { // 4368 var CBfullName = "CBC["+CBname+"]"; 4369 return (document.dblistForm[CBfullName] && document.dblistForm[CBfullName].checked ? 1 : 0); 4370 } 4371 // setcbValue() 4372 function setcbValue(CBname,flag) { // 4373 CBfullName = "CBC["+CBname+"]"; 4374 if(document.dblistForm[CBfullName]) { 4375 document.dblistForm[CBfullName].checked = flag ? "on" : 0; 4376 } 4377 } 4378 4379 '; 4380 } 4381 4382 /** 4383 * Initializes page languages and icons 4384 */ 4385 public function initializeLanguages() 4386 { 4387 // Look up page overlays: 4388 $localizationParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']; 4389 $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField']; 4390 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 4391 ->getQueryBuilderForTable('pages'); 4392 $queryBuilder->getRestrictions() 4393 ->removeAll() 4394 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 4395 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); 4396 $result = $queryBuilder 4397 ->select('*') 4398 ->from('pages') 4399 ->where( 4400 $queryBuilder->expr()->andX( 4401 $queryBuilder->expr()->eq($localizationParentField, $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)), 4402 $queryBuilder->expr()->gt( 4403 $languageField, 4404 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 4405 ) 4406 ) 4407 ) 4408 ->execute(); 4409 4410 $this->pageOverlays = []; 4411 while ($row = $result->fetch()) { 4412 $this->pageOverlays[$row[$languageField]] = $row; 4413 } 4414 4415 $this->languageIconTitles = $this->getTranslateTools()->getSystemLanguages($this->id); 4416 } 4417 4418 /** 4419 * Return the icon for the language 4420 * 4421 * @param int $sys_language_uid Sys language uid 4422 * @param bool $addAsAdditionalText If set to true, only the flag is returned 4423 * @return string Language icon 4424 */ 4425 public function languageFlag($sys_language_uid, $addAsAdditionalText = true) 4426 { 4427 $out = ''; 4428 $title = htmlspecialchars($this->languageIconTitles[$sys_language_uid]['title']); 4429 if ($this->languageIconTitles[$sys_language_uid]['flagIcon']) { 4430 $out .= '<span title="' . $title . '">' . $this->iconFactory->getIcon( 4431 $this->languageIconTitles[$sys_language_uid]['flagIcon'], 4432 Icon::SIZE_SMALL 4433 )->render() . '</span>'; 4434 if (!$addAsAdditionalText) { 4435 return $out; 4436 } 4437 $out .= ' '; 4438 } 4439 $out .= $title; 4440 return $out; 4441 } 4442 4443 /** 4444 * Sets the script url depending on being a module or script request 4445 */ 4446 protected function determineScriptUrl() 4447 { 4448 if ($routePath = GeneralUtility::_GP('route')) { 4449 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 4450 $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath); 4451 } else { 4452 $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME'); 4453 } 4454 } 4455 4456 /** 4457 * @return string 4458 */ 4459 protected function getThisScript() 4460 { 4461 return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&'; 4462 } 4463 4464 /** 4465 * Gets an instance of TranslationConfigurationProvider 4466 * 4467 * @return TranslationConfigurationProvider 4468 */ 4469 protected function getTranslateTools() 4470 { 4471 if (!isset($this->translateTools)) { 4472 $this->translateTools = GeneralUtility::makeInstance(TranslationConfigurationProvider::class); 4473 } 4474 return $this->translateTools; 4475 } 4476 4477 /** 4478 * Generates HTML code for a Reference tooltip out of 4479 * sys_refindex records you hand over 4480 * 4481 * @param int $references number of records from sys_refindex table 4482 * @param string $launchViewParameter JavaScript String, which will be passed as parameters to top.TYPO3.InfoWindow.showItem 4483 * @return string 4484 */ 4485 protected function generateReferenceToolTip($references, $launchViewParameter = '') 4486 { 4487 if (!$references) { 4488 $htmlCode = '-'; 4489 } else { 4490 $htmlCode = '<a href="#"'; 4491 if ($launchViewParameter !== '') { 4492 $htmlCode .= ' onclick="' . htmlspecialchars( 4493 'top.TYPO3.InfoWindow.showItem(' . $launchViewParameter . '); return false;' 4494 ) . '"'; 4495 } 4496 $htmlCode .= ' title="' . htmlspecialchars( 4497 $this->getLanguageService()->sL( 4498 'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:show_references' 4499 ) . ' (' . $references . ')' 4500 ) . '">'; 4501 $htmlCode .= $references; 4502 $htmlCode .= '</a>'; 4503 } 4504 return $htmlCode; 4505 } 4506 4507 /** 4508 * If enabled, only translations are shown (= only with l10n_parent) 4509 * See the use case in RecordList class, where a list of page translations is rendered before. 4510 * 4511 * @param bool $showOnlyTranslatedRecords 4512 */ 4513 public function showOnlyTranslatedRecords(bool $showOnlyTranslatedRecords) 4514 { 4515 $this->showOnlyTranslatedRecords = $showOnlyTranslatedRecords; 4516 } 4517 4518 /** 4519 * Flatten palettes into types showitem 4520 * 4521 * By replacing the palettes in showitem of a TCA type with each palette content, the fields within a palette 4522 * can be considered as visible database columns for a backend form. 4523 * 4524 * @param array $tableTCA 4525 * @param string $type 4526 * @return string 4527 */ 4528 protected function getVisibleColumns(array $tableTCA, string $type) 4529 { 4530 $visibleColumns = $tableTCA['types'][$type]['showitem'] ?? ''; 4531 4532 if (strpos($visibleColumns, '--palette--') !== false) { 4533 $matches = []; 4534 preg_match_all('/--palette--\s*;[^;]*;\s*(\w+)/', $visibleColumns, $matches, PREG_SET_ORDER); 4535 if (!empty($matches)) { 4536 foreach ($matches as $palette) { 4537 $paletteColumns = $tableTCA['palettes'][$palette[1]]['showitem'] ?? ''; 4538 $paletteColumns = rtrim($paletteColumns, ", \t\r\n"); 4539 $visibleColumns = str_replace($palette[0], $paletteColumns, $visibleColumns); 4540 } 4541 } 4542 } 4543 4544 return $visibleColumns; 4545 } 4546 4547 /** 4548 * Returns the language service 4549 * @return LanguageService 4550 */ 4551 protected function getLanguageService() 4552 { 4553 return $GLOBALS['LANG']; 4554 } 4555 4556 /** 4557 * @param array $languagesAllowedForUser 4558 * @return DatabaseRecordList 4559 */ 4560 public function setLanguagesAllowedForUser(array $languagesAllowedForUser): DatabaseRecordList 4561 { 4562 $this->languagesAllowedForUser = $languagesAllowedForUser; 4563 return $this; 4564 } 4565} 4566