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