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