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