1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Backend\Form\FormDataProvider; 17 18use Doctrine\DBAL\DBALException; 19use TYPO3\CMS\Backend\Module\ModuleLoader; 20use TYPO3\CMS\Backend\Utility\BackendUtility; 21use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 22use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException; 23use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; 24use TYPO3\CMS\Core\Database\ConnectionPool; 25use TYPO3\CMS\Core\Database\Query\QueryBuilder; 26use TYPO3\CMS\Core\Database\Query\QueryHelper; 27use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 28use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; 29use TYPO3\CMS\Core\Database\RelationHandler; 30use TYPO3\CMS\Core\Imaging\IconFactory; 31use TYPO3\CMS\Core\Imaging\IconRegistry; 32use TYPO3\CMS\Core\Localization\LanguageService; 33use TYPO3\CMS\Core\Messaging\FlashMessage; 34use TYPO3\CMS\Core\Messaging\FlashMessageQueue; 35use TYPO3\CMS\Core\Messaging\FlashMessageService; 36use TYPO3\CMS\Core\Resource\FileRepository; 37use TYPO3\CMS\Core\Resource\ResourceStorage; 38use TYPO3\CMS\Core\Site\SiteFinder; 39use TYPO3\CMS\Core\Type\Bitmask\Permission; 40use TYPO3\CMS\Core\Utility\ArrayUtility; 41use TYPO3\CMS\Core\Utility\GeneralUtility; 42use TYPO3\CMS\Core\Utility\MathUtility; 43use TYPO3\CMS\Core\Versioning\VersionState; 44 45/** 46 * Contains methods used by Data providers that handle elements 47 * with single items like select, radio and some more. 48 */ 49abstract class AbstractItemProvider 50{ 51 /** 52 * Resolve "itemProcFunc" of elements. 53 * 54 * @param array $result Main result array 55 * @param string $fieldName Field name to handle item list for 56 * @param array $items Existing items array 57 * @return array New list of item elements 58 */ 59 protected function resolveItemProcessorFunction(array $result, $fieldName, array $items) 60 { 61 $table = $result['tableName']; 62 $config = $result['processedTca']['columns'][$fieldName]['config']; 63 64 $pageTsProcessorParameters = null; 65 if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) { 66 $pageTsProcessorParameters = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.']; 67 } 68 $processorParameters = [ 69 // Function manipulates $items directly and return nothing 70 'items' => &$items, 71 'config' => $config, 72 'TSconfig' => $pageTsProcessorParameters, 73 'table' => $table, 74 'row' => $result['databaseRow'], 75 'field' => $fieldName, 76 // IMPORTANT: Below fields are only available in FormEngine context. 77 // They are not used by the DataHandler when processing itemsProcFunc 78 // for checking if a submitted value is valid. This means, in case 79 // an item is added based on one of these fields, it won't be persisted 80 // by the DataHandler. This currently(!) only concerns columns of type "check" 81 // and type "radio", see checkValueForCheck() and checkValueForRadio(). 82 // Therefore, no limitations when using those fields with other types 83 // like "select", but this may change in the future. 84 'inlineParentUid' => $result['inlineParentUid'], 85 'inlineParentTableName' => $result['inlineParentTableName'], 86 'inlineParentFieldName' => $result['inlineParentFieldName'], 87 'inlineParentConfig' => $result['inlineParentConfig'], 88 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'], 89 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'], 90 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'], 91 ]; 92 if (!empty($result['flexParentDatabaseRow'])) { 93 $processorParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow']; 94 } 95 96 try { 97 GeneralUtility::callUserFunction($config['itemsProcFunc'], $processorParameters, $this); 98 } catch (\Exception $exception) { 99 // The itemsProcFunc method may throw an exception, create a flash message if so 100 $languageService = $this->getLanguageService(); 101 $fieldLabel = $fieldName; 102 if (!empty($result['processedTca']['columns'][$fieldName]['label'])) { 103 $fieldLabel = $languageService->sL($result['processedTca']['columns'][$fieldName]['label']); 104 } 105 $message = sprintf( 106 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.items_proc_func_error'), 107 $fieldLabel, 108 $exception->getMessage() 109 ); 110 /** @var FlashMessage $flashMessage */ 111 $flashMessage = GeneralUtility::makeInstance( 112 FlashMessage::class, 113 $message, 114 '', 115 FlashMessage::ERROR, 116 true 117 ); 118 /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */ 119 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); 120 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); 121 $defaultFlashMessageQueue->enqueue($flashMessage); 122 } 123 124 return $items; 125 } 126 127 /** 128 * PageTsConfig addItems: 129 * 130 * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel, 131 * with type specific options merged by pageTsConfig already 132 * 133 * Used by TcaSelectItems and TcaSelectTreeItems data providers 134 * 135 * @param array $result result array 136 * @param string $fieldName Current handle field name 137 * @param array $items Incoming items 138 * @return array Modified item array 139 */ 140 protected function addItemsFromPageTsConfig(array $result, $fieldName, array $items) 141 { 142 $table = $result['tableName']; 143 if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.']) 144 && is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.']) 145 ) { 146 $addItemsArray = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.']; 147 foreach ($addItemsArray as $value => $label) { 148 // If the value ends with a dot, it is a subelement like "34.icon = mylabel.png", skip it 149 if (substr($value, -1) === '.') { 150 continue; 151 } 152 // Check if value "34 = mylabel" also has a "34.icon = myImage.png" 153 $iconIdentifier = null; 154 if (isset($addItemsArray[$value . '.']) 155 && is_array($addItemsArray[$value . '.']) 156 && !empty($addItemsArray[$value . '.']['icon']) 157 ) { 158 $iconIdentifier = $addItemsArray[$value . '.']['icon']; 159 } 160 $items[] = [$label, $value, $iconIdentifier]; 161 } 162 } 163 return $items; 164 } 165 166 /** 167 * TCA config "special" evaluation. Add them to $items 168 * 169 * Used by TcaSelectItems and TcaSelectTreeItems data providers 170 * 171 * @param array $result Result array 172 * @param string $fieldName Current handle field name 173 * @param array $items Incoming items 174 * @return array Modified item array 175 * @throws \UnexpectedValueException 176 */ 177 protected function addItemsFromSpecial(array $result, $fieldName, array $items) 178 { 179 // Guard 180 if (empty($result['processedTca']['columns'][$fieldName]['config']['special']) 181 || !is_string($result['processedTca']['columns'][$fieldName]['config']['special']) 182 ) { 183 return $items; 184 } 185 186 $languageService = $this->getLanguageService(); 187 $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class); 188 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 189 190 $special = $result['processedTca']['columns'][$fieldName]['config']['special']; 191 switch (true) { 192 case $special === 'tables': 193 foreach ($GLOBALS['TCA'] as $currentTable => $_) { 194 if (!empty($GLOBALS['TCA'][$currentTable]['ctrl']['adminOnly'])) { 195 // Hide "admin only" tables 196 continue; 197 } 198 $label = !empty($GLOBALS['TCA'][$currentTable]['ctrl']['title']) ? $GLOBALS['TCA'][$currentTable]['ctrl']['title'] : ''; 199 $icon = $iconFactory->mapRecordTypeToIconIdentifier($currentTable, []); 200 $languageService->loadSingleTableDescription($currentTable); 201 $helpText = (string)($GLOBALS['TCA_DESCR'][$currentTable]['columns']['']['description'] ?? ''); 202 $items[] = [$label, $currentTable, $icon, null, $helpText]; 203 } 204 break; 205 case $special === 'pagetypes': 206 if (isset($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items']) 207 && is_array($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items']) 208 ) { 209 $specialItems = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items']; 210 foreach ($specialItems as $specialItem) { 211 if (!is_array($specialItem) || $specialItem[1] === '--div--') { 212 // Skip non arrays and divider items 213 continue; 214 } 215 $label = $specialItem[0]; 216 $value = $specialItem[1]; 217 $icon = $iconFactory->mapRecordTypeToIconIdentifier('pages', ['doktype' => $specialItem[1]]); 218 $items[] = [$label, $value, $icon]; 219 } 220 } 221 break; 222 case $special === 'exclude': 223 $excludeArrays = $this->getExcludeFields(); 224 foreach ($excludeArrays as $excludeArray) { 225 // If the field comes from a FlexForm, the syntax is more complex 226 if ($excludeArray['origin'] === 'flexForm') { 227 // The field comes from a plugins FlexForm 228 // Add header if not yet set for plugin section 229 if (!isset($items[$excludeArray['sectionHeader']])) { 230 // there is no icon handling for plugins - we take the icon from the table 231 $icon = $iconFactory->mapRecordTypeToIconIdentifier($excludeArray['table'], []); 232 $items[$excludeArray['sectionHeader']] = [ 233 $excludeArray['sectionHeader'], 234 '--div--', 235 $icon 236 ]; 237 } 238 } else { 239 // Add header if not yet set for table 240 if (!isset($items[$excludeArray['table']])) { 241 $icon = $iconFactory->mapRecordTypeToIconIdentifier($excludeArray['table'], []); 242 $items[$excludeArray['table']] = [ 243 $GLOBALS['TCA'][$excludeArray['table']]['ctrl']['title'], 244 '--div--', 245 $icon 246 ]; 247 } 248 } 249 // Add help text 250 $languageService->loadSingleTableDescription($excludeArray['table']); 251 $helpText = (string)($GLOBALS['TCA_DESCR'][$excludeArray['table']]['columns'][$excludeArray['fullField']]['description'] ?? ''); 252 // Item configuration: 253 $items[] = [ 254 rtrim($excludeArray['origin'] === 'flexForm' ? $excludeArray['fieldLabel'] : $languageService->sL($GLOBALS['TCA'][$excludeArray['table']]['columns'][$excludeArray['fieldName']]['label']), ':') . ' (' . $excludeArray['fieldName'] . ')', 255 $excludeArray['table'] . ':' . $excludeArray['fullField'], 256 'empty-empty', 257 null, 258 $helpText 259 ]; 260 } 261 break; 262 case $special === 'explicitValues': 263 $theTypes = $this->getExplicitAuthFieldValues(); 264 $icons = [ 265 'ALLOW' => 'status-status-permission-granted', 266 'DENY' => 'status-status-permission-denied' 267 ]; 268 // Traverse types: 269 foreach ($theTypes as $tableFieldKey => $theTypeArrays) { 270 if (!empty($theTypeArrays['items'])) { 271 // Add header: 272 $items[] = [ 273 $theTypeArrays['tableFieldLabel'], 274 '--div--', 275 ]; 276 // Traverse options for this field: 277 foreach ($theTypeArrays['items'] as $itemValue => $itemContent) { 278 // Add item to be selected: 279 $items[] = [ 280 '[' . $itemContent[2] . '] ' . $itemContent[1], 281 $tableFieldKey . ':' . preg_replace('/[:|,]/', '', $itemValue) . ':' . $itemContent[0], 282 $icons[$itemContent[0]] 283 ]; 284 } 285 } 286 } 287 break; 288 case $special === 'languages': 289 $allLanguages = []; 290 if (($result['effectivePid'] ?? 0) === 0) { 291 // This provides a list of all languages available for ALL sites 292 // Due to the nature of the "sys_language_uid" field having no meaning currently, 293 // We preserve the language ID and make a list of all languages 294 $sites = $this->getAllSites(); 295 foreach ($sites as $site) { 296 foreach ($site->getAllLanguages() as $language) { 297 $languageId = $language->getLanguageId(); 298 if (isset($allLanguages[$languageId])) { 299 // Language already provided by another site, just add the label separately 300 $allLanguages[$languageId][0] .= ', ' . $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']'; 301 } else { 302 $allLanguages[$languageId] = [ 303 0 => $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']', 304 1 => $languageId, 305 2 => $language->getFlagIdentifier() 306 ]; 307 } 308 } 309 } 310 ksort($allLanguages); 311 } 312 if (!empty($allLanguages)) { 313 foreach ($allLanguages as $item) { 314 $items[] = $item; 315 } 316 } else { 317 // Happens for non-pid=0 records (e.g. "tt_content"), or when no site was configured 318 foreach ($result['systemLanguageRows'] as $language) { 319 if ($language['uid'] !== -1) { 320 $items[] = [ 321 0 => $language['title'], 322 1 => $language['uid'], 323 2 => $language['flagIconIdentifier'] 324 ]; 325 } 326 } 327 } 328 329 break; 330 case $special === 'custom': 331 $customOptions = $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions']; 332 if (is_array($customOptions)) { 333 foreach ($customOptions as $coKey => $coValue) { 334 if (is_array($coValue['items'])) { 335 // Add header: 336 $items[] = [ 337 $languageService->sL($coValue['header']), 338 '--div--' 339 ]; 340 // Traverse items: 341 foreach ($coValue['items'] as $itemKey => $itemCfg) { 342 $icon = 'empty-empty'; 343 $helpText = ''; 344 if (!empty($itemCfg[1])) { 345 if ($iconRegistry->isRegistered($itemCfg[1])) { 346 // Use icon identifier when registered 347 $icon = $itemCfg[1]; 348 } 349 } 350 if (!empty($itemCfg[2])) { 351 $helpText = $languageService->sL($itemCfg[2]); 352 } 353 $items[] = [ 354 $languageService->sL($itemCfg[0]), 355 $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey), 356 $icon, 357 null, 358 $helpText 359 ]; 360 } 361 } 362 } 363 } 364 break; 365 case $special === 'modListGroup' || $special === 'modListUser': 366 /** @var ModuleLoader $loadModules */ 367 $loadModules = GeneralUtility::makeInstance(ModuleLoader::class); 368 $loadModules->load($GLOBALS['TBE_MODULES']); 369 $modList = $special === 'modListUser' ? $loadModules->modListUser : $loadModules->modListGroup; 370 if (is_array($modList)) { 371 foreach ($modList as $theMod) { 372 $moduleLabels = $loadModules->getLabelsForModule($theMod); 373 $moduleArray = GeneralUtility::trimExplode('_', $theMod, true); 374 $mainModule = $moduleArray[0] ?? ''; 375 $subModule = $moduleArray[1] ?? ''; 376 // Icon: 377 if (!empty($subModule)) { 378 $icon = $loadModules->modules[$mainModule]['sub'][$subModule]['iconIdentifier']; 379 } else { 380 $icon = $loadModules->modules[$theMod]['iconIdentifier']; 381 } 382 // Add help text 383 $helpText = [ 384 'title' => $languageService->sL($moduleLabels['shortdescription']), 385 'description' => $languageService->sL($moduleLabels['description']) 386 ]; 387 388 $label = ''; 389 // Add label for main module if this is a submodule 390 if (!empty($subModule)) { 391 $mainModuleLabels = $loadModules->getLabelsForModule($mainModule); 392 $label .= $languageService->sL($mainModuleLabels['title']) . '>'; 393 } 394 // Add modules own label now 395 $label .= $languageService->sL($moduleLabels['title']); 396 397 // Item configuration 398 $items[] = [$label, $theMod, $icon, null, $helpText]; 399 } 400 } 401 break; 402 default: 403 throw new \UnexpectedValueException( 404 'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'], 405 1439298496 406 ); 407 } 408 return $items; 409 } 410 411 /** 412 * TCA config "fileFolder" evaluation. Add them to $items 413 * 414 * Used by TcaSelectItems and TcaSelectTreeItems data providers 415 * 416 * @param array $result Result array 417 * @param string $fieldName Current handle field name 418 * @param array $items Incoming items 419 * @return array Modified item array 420 * @throws \RuntimeException 421 */ 422 protected function addItemsFromFolder(array $result, $fieldName, array $items) 423 { 424 if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder']) 425 || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder']) 426 ) { 427 return $items; 428 } 429 430 $fileFolderRaw = $result['processedTca']['columns'][$fieldName]['config']['fileFolder']; 431 $fileFolder = GeneralUtility::getFileAbsFileName($fileFolderRaw); 432 if ($fileFolder === '') { 433 throw new \RuntimeException( 434 'Invalid folder given for item processing: ' . $fileFolderRaw . ' for table ' . $result['tableName'] . ', field ' . $fieldName, 435 1479399227 436 ); 437 } 438 $fileFolder = rtrim($fileFolder, '/') . '/'; 439 440 if (@is_dir($fileFolder)) { 441 $fileExtensionList = ''; 442 if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList']) 443 && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList']) 444 ) { 445 $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList']; 446 } 447 $recursionLevels = isset($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions']) 448 ? MathUtility::forceIntegerInRange($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'], 0, 99) 449 : 99; 450 $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $fileFolder, $fileExtensionList, false, $recursionLevels); 451 $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder); 452 foreach ($fileArray as $fileReference) { 453 $fileInformation = pathinfo($fileReference); 454 $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension'])) 455 ? $fileFolder . $fileReference 456 : ''; 457 $items[] = [ 458 $fileReference, 459 $fileReference, 460 $icon 461 ]; 462 } 463 } 464 465 return $items; 466 } 467 468 /** 469 * TCA config "foreign_table" evaluation. Add them to $items 470 * 471 * Used by TcaSelectItems and TcaSelectTreeItems data providers 472 * 473 * @param array $result Result array 474 * @param string $fieldName Current handle field name 475 * @param array $items Incoming items 476 * @return array Modified item array 477 * @throws \UnexpectedValueException 478 */ 479 protected function addItemsFromForeignTable(array $result, $fieldName, array $items) 480 { 481 $databaseError = null; 482 $queryResult = null; 483 // Guard 484 if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table']) 485 || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table']) 486 ) { 487 return $items; 488 } 489 490 $languageService = $this->getLanguageService(); 491 492 $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table']; 493 494 if (!isset($GLOBALS['TCA'][$foreignTable]) || !is_array($GLOBALS['TCA'][$foreignTable])) { 495 throw new \UnexpectedValueException( 496 'Field ' . $fieldName . ' of table ' . $result['tableName'] . ' reference to foreign table ' 497 . $foreignTable . ', but this table is not defined in TCA', 498 1439569743 499 ); 500 } 501 502 $queryBuilder = $this->buildForeignTableQueryBuilder($result, $fieldName); 503 try { 504 $queryResult = $queryBuilder->execute(); 505 } catch (DBALException $e) { 506 $databaseError = $e->getPrevious()->getMessage(); 507 } 508 509 // Early return on error with flash message 510 if (!empty($databaseError)) { 511 $msg = $databaseError . '. '; 512 $msg .= $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch'); 513 $msgTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch_title'); 514 /** @var FlashMessage $flashMessage */ 515 $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true); 516 /** @var FlashMessageService $flashMessageService */ 517 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); 518 /** @var FlashMessageQueue $defaultFlashMessageQueue */ 519 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); 520 $defaultFlashMessageQueue->enqueue($flashMessage); 521 return $items; 522 } 523 524 $labelPrefix = ''; 525 if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) { 526 $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix']; 527 $labelPrefix = $languageService->sL($labelPrefix); 528 } 529 530 $fileRepository = GeneralUtility::makeInstance(FileRepository::class); 531 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 532 533 while ($foreignRow = $queryResult->fetch()) { 534 BackendUtility::workspaceOL($foreignTable, $foreignRow); 535 // Only proceed in case the row was not unset and we don't deal with a delete placeholder 536 if (is_array($foreignRow) 537 && !VersionState::cast($foreignRow['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER) 538 ) { 539 // If the foreign table sets selicon_field, this field can contain an image 540 // that represents this specific row. 541 $iconFieldName = ''; 542 $isReferenceField = false; 543 if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'])) { 544 $iconFieldName = $GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field']; 545 if (isset($GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type']) 546 && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'] === 'inline' 547 && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['foreign_table'] === 'sys_file_reference' 548 ) { 549 $isReferenceField = true; 550 } 551 } 552 $icon = ''; 553 if ($isReferenceField) { 554 $references = $fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']); 555 if (is_array($references) && !empty($references)) { 556 $icon = reset($references); 557 $icon = $icon->getPublicUrl(); 558 } 559 } else { 560 // Else, determine icon based on record type, or a generic fallback 561 $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow); 562 } 563 // Add the item 564 $items[] = [ 565 $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow), 566 $foreignRow['uid'], 567 $icon 568 ]; 569 } 570 } 571 572 return $items; 573 } 574 575 /** 576 * Remove items using "keepItems" pageTsConfig 577 * 578 * Used by TcaSelectItems and TcaSelectTreeItems data providers 579 * 580 * @param array $result Result array 581 * @param string $fieldName Current handle field name 582 * @param array $items Incoming items 583 * @return array Modified item array 584 */ 585 protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items) 586 { 587 $table = $result['tableName']; 588 if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems']) 589 || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems']) 590 ) { 591 return $items; 592 } 593 594 // If keepItems is set but is an empty list all current items get removed 595 if ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'] === '') { 596 return []; 597 } 598 599 return ArrayUtility::keepItemsInArray( 600 $items, 601 $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'], 602 function ($value) { 603 return $value[1]; 604 } 605 ); 606 } 607 608 /** 609 * Remove items using "removeItems" pageTsConfig 610 * 611 * Used by TcaSelectItems and TcaSelectTreeItems data providers 612 * 613 * @param array $result Result array 614 * @param string $fieldName Current handle field name 615 * @param array $items Incoming items 616 * @return array Modified item array 617 */ 618 protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items) 619 { 620 $table = $result['tableName']; 621 if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems']) 622 || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems']) 623 || $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'] === '' 624 ) { 625 return $items; 626 } 627 628 $removeItems = array_flip(GeneralUtility::trimExplode( 629 ',', 630 $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'], 631 true 632 )); 633 foreach ($items as $key => $itemValues) { 634 if (isset($removeItems[$itemValues[1]])) { 635 unset($items[$key]); 636 } 637 } 638 639 return $items; 640 } 641 642 /** 643 * Remove items user restriction on language field 644 * 645 * Used by TcaSelectItems and TcaSelectTreeItems data providers 646 * 647 * @param array $result Result array 648 * @param string $fieldName Current handle field name 649 * @param array $items Incoming items 650 * @return array Modified item array 651 */ 652 protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items) 653 { 654 // Guard clause returns if not a language field is handled 655 if (empty($result['processedTca']['ctrl']['languageField']) 656 || $result['processedTca']['ctrl']['languageField'] !== $fieldName 657 ) { 658 return $items; 659 } 660 661 $backendUser = $this->getBackendUser(); 662 foreach ($items as $key => $itemValues) { 663 if (!$backendUser->checkLanguageAccess($itemValues[1])) { 664 unset($items[$key]); 665 } 666 } 667 668 return $items; 669 } 670 671 /** 672 * Remove items by user restriction on authMode items 673 * 674 * Used by TcaSelectItems and TcaSelectTreeItems data providers 675 * 676 * @param array $result Result array 677 * @param string $fieldName Current handle field name 678 * @param array $items Incoming items 679 * @return array Modified item array 680 */ 681 protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items) 682 { 683 // Guard clause returns early if no authMode field is configured 684 if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode']) 685 || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode']) 686 ) { 687 return $items; 688 } 689 690 $backendUser = $this->getBackendUser(); 691 $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode']; 692 foreach ($items as $key => $itemValues) { 693 // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this 694 if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) { 695 unset($items[$key]); 696 } 697 } 698 699 return $items; 700 } 701 702 /** 703 * Remove items if doktype is handled for non admin users 704 * 705 * Used by TcaSelectItems and TcaSelectTreeItems data providers 706 * 707 * @param array $result Result array 708 * @param string $fieldName Current handle field name 709 * @param array $items Incoming items 710 * @return array Modified item array 711 */ 712 protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items) 713 { 714 $table = $result['tableName']; 715 $backendUser = $this->getBackendUser(); 716 // Guard clause returns if not correct table and field or if user is admin 717 if ($table !== 'pages' || $fieldName !== 'doktype' || $backendUser->isAdmin() 718 ) { 719 return $items; 720 } 721 722 $allowedPageTypes = $backendUser->groupData['pagetypes_select']; 723 foreach ($items as $key => $itemValues) { 724 if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) { 725 unset($items[$key]); 726 } 727 } 728 729 return $items; 730 } 731 732 /** 733 * Remove items if sys_file_storage is not allowed for non-admin users. 734 * 735 * Used by TcaSelectItems data providers 736 * 737 * @param array $result Result array 738 * @param string $fieldName Current handle field name 739 * @param array $items Incoming items 740 * @return array Modified item array 741 */ 742 protected function removeItemsByUserStorageRestriction(array $result, $fieldName, array $items) 743 { 744 $referencedTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'] ?? null; 745 if ($referencedTableName !== 'sys_file_storage') { 746 return $items; 747 } 748 749 $allowedStorageIds = array_map( 750 function (ResourceStorage $storage) { 751 return $storage->getUid(); 752 }, 753 $this->getBackendUser()->getFileStorages() 754 ); 755 756 return array_filter( 757 $items, 758 function (array $item) use ($allowedStorageIds) { 759 $itemValue = $item[1] ?? null; 760 return empty($itemValue) 761 || in_array((int)$itemValue, $allowedStorageIds, true); 762 } 763 ); 764 } 765 766 /** 767 * Returns an array with the exclude fields as defined in TCA and FlexForms 768 * Used for listing the exclude fields in be_groups forms. 769 * 770 * @return array Array of arrays with excludeFields (fieldName, table:fieldName) from TCA 771 * and FlexForms (fieldName, table:extKey;sheetName;fieldName) 772 */ 773 protected function getExcludeFields() 774 { 775 $languageService = $this->getLanguageService(); 776 $finalExcludeArray = []; 777 778 // Fetch translations for table names 779 $tableToTranslation = []; 780 // All TCA keys 781 foreach ($GLOBALS['TCA'] as $table => $conf) { 782 $tableToTranslation[$table] = $languageService->sL($conf['ctrl']['title']); 783 } 784 /** @var array<string, string> $tableToTranslation */ 785 // Sort by translations 786 asort($tableToTranslation); 787 foreach ($tableToTranslation as $table => $translatedTable) { 788 $excludeArrayTable = []; 789 790 // All field names configured and not restricted to admins 791 if (is_array($GLOBALS['TCA'][$table]['columns']) 792 && empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly']) 793 && (empty($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) || !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction'])) 794 ) { 795 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) { 796 $isExcludeField = (bool)($GLOBALS['TCA'][$table]['columns'][$field]['exclude'] ?? false); 797 $isOnlyVisibleForAdmins = ($GLOBALS['TCA'][$table]['columns'][$field]['displayCond'] ?? '') === 'HIDE_FOR_NON_ADMINS'; 798 // Only show fields that can be excluded for editors, or are hidden for non-admins 799 if ($isExcludeField && !$isOnlyVisibleForAdmins) { 800 // Get human readable names of fields 801 $translatedField = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$field]['label']); 802 // Add entry, key 'labels' needed for sorting 803 $excludeArrayTable[] = [ 804 'labels' => $translatedTable . ':' . $translatedField, 805 'sectionHeader' => $translatedTable, 806 'table' => $table, 807 'tableField' => $field, 808 'fieldName' => $field, 809 'fullField' => $field, 810 'fieldLabel' => $translatedField, 811 'origin' => 'tca', 812 ]; 813 } 814 } 815 } 816 // All FlexForm fields 817 $flexFormArray = $this->getRegisteredFlexForms($table); 818 foreach ($flexFormArray as $tableField => $flexForms) { 819 // Prefix for field label, e.g. "Plugin Options:" 820 $labelPrefix = ''; 821 if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) { 822 $labelPrefix = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$tableField]['label']); 823 } 824 // Get all sheets 825 foreach ($flexForms as $extIdent => $extConf) { 826 // Get all fields in sheet 827 foreach ($extConf['sheets'] as $sheetName => $sheet) { 828 if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) { 829 continue; 830 } 831 foreach ($sheet['ROOT']['el'] as $pluginFieldName => $field) { 832 // Use only fields that have exclude flag set 833 if (empty($field['TCEforms']['exclude'])) { 834 continue; 835 } 836 $fieldLabel = !empty($field['TCEforms']['label']) 837 ? $languageService->sL($field['TCEforms']['label']) 838 : $pluginFieldName; 839 $excludeArrayTable[] = [ 840 'labels' => trim($translatedTable . ' ' . $labelPrefix . ' ' . $extIdent, ': ') . ':' . $fieldLabel, 841 'sectionHeader' => trim($translatedTable . ' ' . $labelPrefix . ' ' . $extIdent, ':'), 842 'table' => $table, 843 'tableField' => $tableField, 844 'extIdent' => $extIdent, 845 'fieldName' => $pluginFieldName, 846 'fullField' => $tableField . ';' . $extIdent . ';' . $sheetName . ';' . $pluginFieldName, 847 'fieldLabel' => $fieldLabel, 848 'origin' => 'flexForm', 849 ]; 850 } 851 } 852 } 853 } 854 // Sort fields by the translated value 855 if (!empty($excludeArrayTable)) { 856 usort($excludeArrayTable, function (array $array1, array $array2) { 857 $array1 = reset($array1); 858 $array2 = reset($array2); 859 if (is_string($array1) && is_string($array2)) { 860 return strcasecmp($array1, $array2); 861 } 862 return 0; 863 }); 864 $finalExcludeArray = array_merge($finalExcludeArray, $excludeArrayTable); 865 } 866 } 867 868 return $finalExcludeArray; 869 } 870 871 /** 872 * Returns FlexForm data structures it finds. Used in select "special" for be_groups 873 * to set "exclude" flags for single flex form fields. 874 * 875 * This only finds flex forms registered in 'ds' config sections. 876 * This does not resolve other sophisticated flex form data structure references. 877 * 878 * @todo: This approach is limited and doesn't find everything. It works for casual tt_content plugins, though: 879 * @todo: The data structure identifier determination depends on data row, but we don't have all rows at hand here. 880 * @todo: The code thus "guesses" some standard data structure identifier scenarios and tries to resolve those. 881 * @todo: This guessing can not be solved in a good way. A general registry of "all" possible data structures is 882 * @todo: probably not wanted, since that wouldn't work for truly dynamic DS calculations. Probably the only 883 * @todo: thing we could do here is a hook to allow extensions declaring specific data structures to 884 * @todo: allow backend admins to set exclude flags for certain fields in those cases. 885 * 886 * @param string $table Table to handle 887 * @return array Data structures 888 */ 889 protected function getRegisteredFlexForms($table) 890 { 891 if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) { 892 return []; 893 } 894 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class); 895 $flexForms = []; 896 foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) { 897 if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] === 'flex') { 898 $flexForms[$tableField] = []; 899 foreach (array_keys($fieldConf['config']['ds']) as $flexFormKey) { 900 $flexFormKey = (string)$flexFormKey; 901 // Get extension identifier (uses second value if it's not empty, "list" or "*", else first one) 902 $identFields = GeneralUtility::trimExplode(',', $flexFormKey); 903 $extIdent = $identFields[0]; 904 if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') { 905 $extIdent = $identFields[1]; 906 } 907 $flexFormDataStructureIdentifier = json_encode([ 908 'type' => 'tca', 909 'tableName' => $table, 910 'fieldName' => $tableField, 911 'dataStructureKey' => $flexFormKey, 912 ]); 913 try { 914 $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexFormDataStructureIdentifier); 915 $flexForms[$tableField][$extIdent] = $dataStructure; 916 } catch (InvalidIdentifierException $e) { 917 // Deliberately empty: The DS identifier is guesswork and the flex ds parser throws 918 // this exception if it can not resolve to a valid data structure. This is "ok" here 919 // and the exception is just eaten. 920 } 921 } 922 } 923 } 924 return $flexForms; 925 } 926 927 /** 928 * Returns an array with explicit Allow/Deny fields. 929 * Used for listing these field/value pairs in be_groups forms 930 * 931 * @return array Array with information from all of $GLOBALS['TCA'] 932 */ 933 protected function getExplicitAuthFieldValues() 934 { 935 $languageService = static::getLanguageService(); 936 $adLabel = [ 937 'ALLOW' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.allow'), 938 'DENY' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deny') 939 ]; 940 $allowDenyOptions = []; 941 foreach ($GLOBALS['TCA'] as $table => $_) { 942 // All field names configured: 943 if (is_array($GLOBALS['TCA'][$table]['columns'])) { 944 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $__) { 945 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config']; 946 if ($fieldConfig['type'] === 'select' && $fieldConfig['authMode']) { 947 // Check for items 948 if (is_array($fieldConfig['items'])) { 949 // Get Human Readable names of fields and table: 950 $allowDenyOptions[$table . ':' . $field]['tableFieldLabel'] = 951 $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']) . ': ' 952 . $languageService->sL($GLOBALS['TCA'][$table]['columns'][$field]['label']); 953 foreach ($fieldConfig['items'] as $iVal) { 954 $itemIdentifier = (string)$iVal[1]; 955 // Values '' and '--div--' are not controlled by this setting. 956 if ($itemIdentifier === '' || $itemIdentifier === '--div--') { 957 continue; 958 } 959 // Find iMode 960 $iMode = ''; 961 switch ((string)$fieldConfig['authMode']) { 962 case 'explicitAllow': 963 $iMode = 'ALLOW'; 964 break; 965 case 'explicitDeny': 966 $iMode = 'DENY'; 967 break; 968 case 'individual': 969 if ($iVal[5] ?? false) { 970 if ($iVal[5] === 'EXPL_ALLOW') { 971 $iMode = 'ALLOW'; 972 } elseif ($iVal[5] === 'EXPL_DENY') { 973 $iMode = 'DENY'; 974 } 975 } 976 break; 977 } 978 // Set iMode 979 if ($iMode) { 980 $allowDenyOptions[$table . ':' . $field]['items'][$itemIdentifier] = [ 981 $iMode, 982 $languageService->sL($iVal[0]), 983 $adLabel[$iMode] 984 ]; 985 } 986 } 987 } 988 } 989 } 990 } 991 } 992 return $allowDenyOptions; 993 } 994 995 /** 996 * Build query to fetch foreign records. Helper method of 997 * addItemsFromForeignTable(), do not call otherwise. 998 * 999 * @param array $result Result array 1000 * @param string $localFieldName Current handle field name 1001 * @return QueryBuilder 1002 */ 1003 protected function buildForeignTableQueryBuilder(array $result, string $localFieldName): QueryBuilder 1004 { 1005 $backendUser = $this->getBackendUser(); 1006 1007 $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table']; 1008 $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName); 1009 1010 $fieldList = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.'); 1011 /** @var QueryBuilder $queryBuilder */ 1012 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1013 ->getQueryBuilderForTable($foreignTableName); 1014 1015 $queryBuilder->getRestrictions() 1016 ->removeAll() 1017 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 1018 ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); 1019 1020 $queryBuilder 1021 ->select(...GeneralUtility::trimExplode(',', $fieldList, true)) 1022 ->from($foreignTableName) 1023 ->where($foreignTableClauseArray['WHERE']); 1024 1025 if (!empty($foreignTableClauseArray['GROUPBY'])) { 1026 $queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']); 1027 } 1028 1029 if (!empty($foreignTableClauseArray['ORDERBY'])) { 1030 foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) { 1031 [$fieldName, $order] = $orderPair; 1032 $queryBuilder->addOrderBy($fieldName, $order); 1033 } 1034 } elseif (!empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby'])) { 1035 $orderByClauses = QueryHelper::parseOrderBy($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby']); 1036 foreach ($orderByClauses as $orderByClause) { 1037 if (!empty($orderByClause[0])) { 1038 $queryBuilder->addOrderBy($foreignTableName . '.' . $orderByClause[0], $orderByClause[1]); 1039 } 1040 } 1041 } 1042 1043 if (!empty($foreignTableClauseArray['LIMIT'])) { 1044 if (!empty($foreignTableClauseArray['LIMIT'][1])) { 1045 $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][1]); 1046 $queryBuilder->setFirstResult($foreignTableClauseArray['LIMIT'][0]); 1047 } elseif (!empty($foreignTableClauseArray['LIMIT'][0])) { 1048 $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][0]); 1049 } 1050 } 1051 1052 // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1) 1053 // rootLevel = 0 means that elements are not allowed on root level 1054 // rootLevel = 1 means that elements are only on the root level (pid=0) 1055 $rootLevel = 0; 1056 if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) { 1057 $rootLevel = (int)$GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel']; 1058 } 1059 1060 if ($rootLevel === -1) { 1061 $queryBuilder->andWhere( 1062 $queryBuilder->expr()->neq( 1063 $foreignTableName . '.pid', 1064 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) 1065 ) 1066 ); 1067 } elseif ($rootLevel === 1) { 1068 $queryBuilder->andWhere( 1069 $queryBuilder->expr()->eq( 1070 $foreignTableName . '.pid', 1071 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 1072 ) 1073 ); 1074 } else { 1075 $queryBuilder->andWhere($backendUser->getPagePermsClause(Permission::PAGE_SHOW)); 1076 if ($foreignTableName !== 'pages') { 1077 $queryBuilder 1078 ->from('pages') 1079 ->andWhere( 1080 $queryBuilder->expr()->eq( 1081 'pages.uid', 1082 $queryBuilder->quoteIdentifier($foreignTableName . '.pid') 1083 ) 1084 ); 1085 } 1086 } 1087 1088 // @todo what about PID restriction? 1089 if ($this->getBackendUser()->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreignTableName)) { 1090 $queryBuilder 1091 ->andWhere( 1092 $queryBuilder->expr()->neq( 1093 $foreignTableName . '.t3ver_state', 1094 $queryBuilder->createNamedParameter(VersionState::MOVE_PLACEHOLDER, \PDO::PARAM_INT) 1095 ) 1096 ); 1097 } 1098 1099 return $queryBuilder; 1100 } 1101 1102 /** 1103 * Replace markers in a where clause from TCA foreign_table_where 1104 * 1105 * ###REC_FIELD_[field name]### 1106 * ###THIS_UID### - is current element uid (zero if new). 1107 * ###CURRENT_PID### - is the current page id (pid of the record). 1108 * ###SITEROOT### 1109 * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically. 1110 * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically. 1111 * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically. 1112 * 1113 * @param array $result Result array 1114 * @param string $foreignTableName Name of foreign table 1115 * @param string $localFieldName Current handle field name 1116 * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT 1117 */ 1118 protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName) 1119 { 1120 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($foreignTableName); 1121 $localTable = $result['tableName']; 1122 $effectivePid = $result['effectivePid']; 1123 1124 $foreignTableClause = ''; 1125 if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where']) 1126 && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where']) 1127 ) { 1128 $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where']; 1129 // Replace possible markers in query 1130 if (strpos($foreignTableClause, '###REC_FIELD_') !== false) { 1131 // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...") 1132 $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause); 1133 foreach ($whereClauseParts as $key => $value) { 1134 if ($key !== 0) { 1135 // "field1###' AND ..." -> array("field1", "' AND ...") 1136 $whereClauseSubParts = explode('###', $value, 2); 1137 // @todo: Throw exception if there is no value? What happens for NEW records? 1138 $databaseRowKey = empty($result['flexParentDatabaseRow']) ? 'databaseRow' : 'flexParentDatabaseRow'; 1139 $rowFieldValue = $result[$databaseRowKey][$whereClauseSubParts[0]] ?? ''; 1140 if (is_array($rowFieldValue)) { 1141 // If a select or group field is used here, it may have been processed already and 1142 // is now an array containing uid + table + title + row. 1143 // See TcaGroup data provider for details. 1144 // Pick the first one (always on 0), and use uid only. 1145 $rowFieldValue = $rowFieldValue[0]['uid'] ?? $rowFieldValue[0]; 1146 } 1147 if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') { 1148 $whereClauseParts[0] = substr($whereClauseParts[0], 0, -1); 1149 $whereClauseSubParts[1] = substr($whereClauseSubParts[1], 1); 1150 } 1151 $whereClauseParts[$key] = $connection->quote($rowFieldValue) . $whereClauseSubParts[1]; 1152 } 1153 } 1154 $foreignTableClause = implode('', $whereClauseParts); 1155 } 1156 if (strpos($foreignTableClause, '###CURRENT_PID###') !== false) { 1157 // Use pid from parent page clause if in flex form context 1158 if (!empty($result['flexParentDatabaseRow']['pid'])) { 1159 $effectivePid = $result['flexParentDatabaseRow']['pid']; 1160 } elseif (!$effectivePid && !empty($result['databaseRow']['pid'])) { 1161 // Use pid from database row if in inline context 1162 $effectivePid = $result['databaseRow']['pid']; 1163 } 1164 } 1165 1166 $siteRootUid = 0; 1167 foreach ($result['rootline'] as $rootlinePage) { 1168 if (!empty($rootlinePage['is_siteroot'])) { 1169 $siteRootUid = (int)$rootlinePage['uid']; 1170 break; 1171 } 1172 } 1173 1174 $pageTsConfigId = 0; 1175 if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']) 1176 && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'] 1177 ) { 1178 $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']; 1179 } 1180 1181 $pageTsConfigIdList = 0; 1182 if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']) 1183 && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'] 1184 ) { 1185 $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']; 1186 } 1187 $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true); 1188 $pageTsConfigIdList = []; 1189 foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) { 1190 if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) { 1191 $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement; 1192 } 1193 } 1194 $pageTsConfigIdList = implode(',', $pageTsConfigIdList); 1195 1196 $pageTsConfigString = ''; 1197 if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']) 1198 && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'] 1199 ) { 1200 $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']; 1201 $pageTsConfigString = $connection->quote($pageTsConfigString); 1202 } 1203 1204 $foreignTableClause = str_replace( 1205 [ 1206 '###CURRENT_PID###', 1207 '###THIS_UID###', 1208 '###SITEROOT###', 1209 '###PAGE_TSCONFIG_ID###', 1210 '###PAGE_TSCONFIG_IDLIST###', 1211 '\'###PAGE_TSCONFIG_STR###\'', 1212 '###PAGE_TSCONFIG_STR###' 1213 ], 1214 [ 1215 (int)$effectivePid, 1216 (int)$result['databaseRow']['uid'], 1217 $siteRootUid, 1218 $pageTsConfigId, 1219 $pageTsConfigIdList, 1220 $pageTsConfigString, 1221 $pageTsConfigString 1222 ], 1223 $foreignTableClause 1224 ); 1225 } 1226 1227 // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT 1228 // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element. 1229 $foreignTableClause = ' ' . $foreignTableClause; 1230 $foreignTableClauseArray = [ 1231 'WHERE' => '', 1232 'GROUPBY' => '', 1233 'ORDERBY' => '', 1234 'LIMIT' => '', 1235 ]; 1236 // Find LIMIT 1237 $reg = []; 1238 if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/is', $foreignTableClause, $reg)) { 1239 $foreignTableClauseArray['LIMIT'] = GeneralUtility::intExplode(',', trim($reg[2]), true); 1240 $foreignTableClause = $reg[1]; 1241 } 1242 // Find ORDER BY 1243 $reg = []; 1244 if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) { 1245 $foreignTableClauseArray['ORDERBY'] = QueryHelper::parseOrderBy(trim($reg[2])); 1246 $foreignTableClause = $reg[1]; 1247 } 1248 // Find GROUP BY 1249 $reg = []; 1250 if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) { 1251 $foreignTableClauseArray['GROUPBY'] = QueryHelper::parseGroupBy(trim($reg[2])); 1252 $foreignTableClause = $reg[1]; 1253 } 1254 // Rest is assumed to be "WHERE" clause 1255 $foreignTableClauseArray['WHERE'] = QueryHelper::stripLogicalOperatorPrefix($foreignTableClause); 1256 1257 return $foreignTableClauseArray; 1258 } 1259 1260 /** 1261 * Convert the current database values into an array 1262 * 1263 * @param array $row database row 1264 * @param string $fieldName fieldname to process 1265 * @return array 1266 */ 1267 protected function processDatabaseFieldValue(array $row, $fieldName) 1268 { 1269 $currentDatabaseValues = array_key_exists($fieldName, $row) 1270 ? $row[$fieldName] 1271 : ''; 1272 if (!is_array($currentDatabaseValues)) { 1273 $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true); 1274 } 1275 return $currentDatabaseValues; 1276 } 1277 1278 /** 1279 * Validate and sanitize database row values of the select field with the given name. 1280 * Creates an array out of databaseRow[selectField] values. 1281 * 1282 * Used by TcaSelectItems and TcaSelectTreeItems data providers 1283 * 1284 * @param array $result The current result array. 1285 * @param string $fieldName Name of the current select field. 1286 * @param array $staticValues Array with statically defined items, item value is used as array key. 1287 * @return array 1288 */ 1289 protected function processSelectFieldValue(array $result, $fieldName, array $staticValues) 1290 { 1291 $fieldConfig = $result['processedTca']['columns'][$fieldName]; 1292 1293 $currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : []; 1294 $newDatabaseValueArray = []; 1295 1296 // Add all values that were defined by static methods and do not come from the relation 1297 // e.g. TCA, TSconfig, itemProcFunc etc. 1298 foreach ($currentDatabaseValueArray as $value) { 1299 if (isset($staticValues[$value])) { 1300 $newDatabaseValueArray[] = $value; 1301 } 1302 } 1303 1304 if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) { 1305 /** @var RelationHandler $relationHandler */ 1306 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); 1307 $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']); 1308 if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') { 1309 // MM relation 1310 $relationHandler->start( 1311 implode(',', $currentDatabaseValueArray), 1312 $fieldConfig['config']['foreign_table'], 1313 $fieldConfig['config']['MM'], 1314 $result['databaseRow']['uid'], 1315 $result['tableName'], 1316 $fieldConfig['config'] 1317 ); 1318 $relationHandler->processDeletePlaceholder(); 1319 $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray()); 1320 } else { 1321 // Non MM relation 1322 // If not dealing with MM relations, use default live uid, not versioned uid for record relations 1323 $relationHandler->start( 1324 implode(',', $currentDatabaseValueArray), 1325 $fieldConfig['config']['foreign_table'], 1326 '', 1327 $this->getLiveUid($result), 1328 $result['tableName'], 1329 $fieldConfig['config'] 1330 ); 1331 $relationHandler->processDeletePlaceholder(); 1332 $databaseIds = array_merge($newDatabaseValueArray, $relationHandler->getValueArray()); 1333 // remove all items from the current DB values if not available as relation or static value anymore 1334 $newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $databaseIds)); 1335 } 1336 } 1337 1338 if ($fieldConfig['config']['multiple'] ?? false) { 1339 return $newDatabaseValueArray; 1340 } 1341 return array_unique($newDatabaseValueArray); 1342 } 1343 1344 /** 1345 * Translate the item labels 1346 * 1347 * Used by TcaSelectItems and TcaSelectTreeItems data providers 1348 * 1349 * @param array $result Result array 1350 * @param array $itemArray Items 1351 * @param string $table 1352 * @param string $fieldName 1353 * @return array 1354 */ 1355 public function translateLabels(array $result, array $itemArray, $table, $fieldName) 1356 { 1357 $languageService = $this->getLanguageService(); 1358 1359 foreach ($itemArray as $key => $item) { 1360 if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]) 1361 && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]) 1362 ) { 1363 $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]); 1364 } else { 1365 $label = $languageService->sL(trim($item[0])); 1366 } 1367 $value = strlen((string)$item[1]) > 0 ? $item[1] : ''; 1368 $icon = !empty($item[2]) ? $item[2] : null; 1369 $groupId = $item[3] ?? null; 1370 $helpText = null; 1371 if (!empty($item[4])) { 1372 if (\is_string($item[4])) { 1373 $helpText = $languageService->sL($item[4]); 1374 } else { 1375 $helpText = $item[4]; 1376 } 1377 } 1378 $itemArray[$key] = [ 1379 $label, 1380 $value, 1381 $icon, 1382 $groupId, 1383 $helpText 1384 ]; 1385 } 1386 1387 return $itemArray; 1388 } 1389 1390 /** 1391 * Add alternative icon using "altIcons" TSconfig 1392 * 1393 * @param array $result 1394 * @param array $items 1395 * @param string $table 1396 * @param string $fieldName 1397 * 1398 * @return array 1399 */ 1400 public function addIconFromAltIcons(array $result, array $items, string $table, string $fieldName): array 1401 { 1402 foreach ($items as &$item) { 1403 if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]]) 1404 && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]]) 1405 ) { 1406 $item[2] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]]; 1407 } 1408 } 1409 1410 return $items; 1411 } 1412 1413 /** 1414 * Sanitize incoming item array 1415 * 1416 * Used by TcaSelectItems and TcaSelectTreeItems data providers 1417 * 1418 * @param mixed $itemArray 1419 * @param string $tableName 1420 * @param string $fieldName 1421 * @throws \UnexpectedValueException 1422 * @return array 1423 */ 1424 public function sanitizeItemArray($itemArray, $tableName, $fieldName) 1425 { 1426 if (!is_array($itemArray)) { 1427 $itemArray = []; 1428 } 1429 foreach ($itemArray as $item) { 1430 if (!is_array($item)) { 1431 throw new \UnexpectedValueException( 1432 'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected', 1433 1439288036 1434 ); 1435 } 1436 } 1437 1438 return $itemArray; 1439 } 1440 1441 /** 1442 * Gets the record uid of the live default record. If already 1443 * pointing to the live record, the submitted record uid is returned. 1444 * 1445 * @param array $result Result array 1446 * @return int 1447 * @throws \UnexpectedValueException 1448 */ 1449 protected function getLiveUid(array $result) 1450 { 1451 $table = $result['tableName']; 1452 $row = $result['databaseRow']; 1453 $uid = $row['uid']; 1454 if (BackendUtility::isTableWorkspaceEnabled($table) && (int)$row['t3ver_oid'] > 0) { 1455 $uid = $row['t3ver_oid']; 1456 } 1457 return $uid; 1458 } 1459 1460 protected function getAllSites(): array 1461 { 1462 return GeneralUtility::makeInstance(SiteFinder::class)->getAllSites(); 1463 } 1464 1465 /** 1466 * @return LanguageService 1467 */ 1468 protected function getLanguageService() 1469 { 1470 return $GLOBALS['LANG']; 1471 } 1472 1473 /** 1474 * @return BackendUserAuthentication 1475 */ 1476 protected function getBackendUser() 1477 { 1478 return $GLOBALS['BE_USER']; 1479 } 1480} 1481