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