[TASK] Disallow empty values for multi select fields
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaSelectItems.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 TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
18 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
19 use TYPO3\CMS\Backend\Module\ModuleLoader;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Backend\Utility\IconUtility;
22 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23 use TYPO3\CMS\Core\Database\DatabaseConnection;
24 use TYPO3\CMS\Core\Database\RelationHandler;
25 use TYPO3\CMS\Core\Messaging\FlashMessage;
26 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
27 use TYPO3\CMS\Core\Messaging\FlashMessageService;
28 use TYPO3\CMS\Core\Utility\ArrayUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Core\Utility\MathUtility;
31 use TYPO3\CMS\Core\Utility\PathUtility;
32 use TYPO3\CMS\Lang\LanguageService;
33
34 /**
35 * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
36 */
37 class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInterface {
38
39 /**
40 * Resolve select items
41 *
42 * @param array $result
43 * @return array
44 * @throws \UnexpectedValueException
45 */
46 public function addData(array $result) {
47 $languageService = $this->getLanguageService();
48
49 $table = $result['tableName'];
50
51 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
52 if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
53 continue;
54 }
55
56 // Sanitize incoming item array
57 if (!is_array($fieldConfig['config']['items'])) {
58 $fieldConfig['config']['items'] = [];
59 }
60
61 // Make sure maxitems is always filled with a valid integer value.
62 if (
63 !empty($fieldConfig['config']['maxitems'])
64 && (int)$fieldConfig['config']['maxitems'] > 1
65 ) {
66 $fieldConfig['config']['maxitems'] = (int)$fieldConfig['config']['maxitems'];
67 } else {
68 $fieldConfig['config']['maxitems'] = 1;
69 }
70
71 foreach ($fieldConfig['config']['items'] as $item) {
72 if (!is_array($item)) {
73 throw new \UnexpectedValueException(
74 'An item in field ' . $fieldName . ' of table ' . $table . ' is not an array as expected',
75 1439288036
76 );
77 }
78 }
79
80 $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
81 $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
82 $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
83 $staticItems = $fieldConfig['config']['items'];
84
85 $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
86 $dynamicItems = array_diff_key($fieldConfig['config']['items'], $staticItems);
87
88 $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
89 $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
90 $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
91 $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
92 $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
93
94 // Resolve "itemsProcFunc"
95 if (!empty($fieldConfig['config']['itemsProcFunc'])) {
96 $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
97 // itemsProcFunc must not be used anymore
98 unset($fieldConfig['config']['itemsProcFunc']);
99 }
100
101 // Translate labels
102 $staticValues = [];
103 foreach ($fieldConfig['config']['items'] as $key => $item) {
104 if (!isset($dynamicItems[$key])) {
105 $staticValues[$item[1]] = $item;
106 }
107 if (isset($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
108 && !empty($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
109 ) {
110 $label = $languageService->sL($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
111 } else {
112 $label = $languageService->sL($item[0]);
113 }
114 $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
115 $icon = $item[2] ?: NULL;
116 $helpText = $item[3] ?: NULL;
117 $fieldConfig['config']['items'][$key] = [
118 $label,
119 $value,
120 $icon,
121 $helpText
122 ];
123 }
124 // Keys may contain table names, so a numeric array is created
125 $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
126
127 $result['processedTca']['columns'][$fieldName] = $fieldConfig;
128 $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $staticValues);
129 }
130
131 return $result;
132 }
133
134 /**
135 * TCA config "special" evaluation. Add them to $items
136 *
137 * @param array $result Result array
138 * @param string $fieldName Current handle field name
139 * @param array $items Incoming items
140 * @return array Modified item array
141 * @throws \UnexpectedValueException
142 */
143 protected function addItemsFromSpecial(array $result, $fieldName, array $items) {
144 $languageService = $this->getLanguageService();
145
146 // Guard
147 if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
148 || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
149 ) {
150 return $items;
151 }
152
153 $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
154 if ($special === 'tables') {
155 foreach ($GLOBALS['TCA'] as $currentTable => $_) {
156 if (!empty($GLOBALS['TCA'][$currentTable]['ctrl']['adminOnly'])) {
157 // Hide "admin only" tables
158 continue;
159 }
160 $label = !empty($GLOBALS['TCA'][$currentTable]['ctrl']['title']) ? $GLOBALS['TCA'][$currentTable]['ctrl']['title'] : '';
161 $icon = IconUtility::mapRecordTypeToSpriteIconName($currentTable, array());
162 $helpText = array();
163 $languageService->loadSingleTableDescription($currentTable);
164 // @todo: check if this actually works, currently help texts are missing
165 $helpTextArray = $GLOBALS['TCA_DESCR'][$currentTable]['columns'][''];
166 if (!empty($helpTextArray['description'])) {
167 $helpText['description'] = $helpTextArray['description'];
168 }
169 $items[] = array($label, $currentTable, $icon, $helpText);
170 }
171 } elseif ($special === 'pagetypes') {
172 if (isset($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
173 && is_array($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'])
174 ) {
175 $specialItems = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
176 foreach ($specialItems as $specialItem) {
177 if (!is_array($specialItem) || $specialItem[1] === '--div--') {
178 // Skip non arrays and divider items
179 continue;
180 }
181 $label = $specialItem[0];
182 $value = $specialItem[1];
183 $icon = IconUtility::mapRecordTypeToSpriteIconName('pages', array('doktype' => $specialItem[1]));
184 $items[] = array($label, $value, $icon);
185 }
186 }
187 } elseif ($special === 'exclude') {
188 $excludeArrays = $this->getExcludeFields();
189 foreach ($excludeArrays as $excludeArray) {
190 list($theTable, $theFullField) = explode(':', $excludeArray[1]);
191 // If the field comes from a FlexForm, the syntax is more complex
192 $theFieldParts = explode(';', $theFullField);
193 $theField = array_pop($theFieldParts);
194 // Add header if not yet set for table:
195 if (!array_key_exists($theTable, $items)) {
196 $icon = IconUtility::mapRecordTypeToSpriteIconName($theTable, array());
197 $items[$theTable] = array(
198 $GLOBALS['TCA'][$theTable]['ctrl']['title'],
199 '--div--',
200 $icon
201 );
202 }
203 // Add help text
204 $helpText = array();
205 $languageService->loadSingleTableDescription($theTable);
206 $helpTextArray = $GLOBALS['TCA_DESCR'][$theTable]['columns'][$theFullField];
207 if (!empty($helpTextArray['description'])) {
208 $helpText['description'] = $helpTextArray['description'];
209 }
210 // Item configuration:
211 // @todo: the title calculation does not work well for flex form fields, see unit tests
212 $items[] = array(
213 rtrim($languageService->sL($GLOBALS['TCA'][$theTable]['columns'][$theField]['label']), ':') . ' (' . $theField . ')',
214 $excludeArray[1],
215 'empty-empty',
216 $helpText
217 );
218 }
219 } elseif ($special === 'explicitValues') {
220 $theTypes = $this->getExplicitAuthFieldValues();
221 $icons = array(
222 'ALLOW' => 'status-status-permission-granted',
223 'DENY' => 'status-status-permission-denied'
224 );
225 // Traverse types:
226 foreach ($theTypes as $tableFieldKey => $theTypeArrays) {
227 if (is_array($theTypeArrays['items'])) {
228 // Add header:
229 $items[] = array(
230 $theTypeArrays['tableFieldLabel'],
231 '--div--',
232 );
233 // Traverse options for this field:
234 foreach ($theTypeArrays['items'] as $itemValue => $itemContent) {
235 // Add item to be selected:
236 $items[] = array(
237 '[' . $itemContent[2] . '] ' . $itemContent[1],
238 $tableFieldKey . ':' . preg_replace('/[:|,]/', '', $itemValue) . ':' . $itemContent[0],
239 $icons[$itemContent[0]]
240 );
241 }
242 }
243 }
244 } elseif ($special === 'languages') {
245 /** @var TranslationConfigurationProvider $translationConfigurationProvider */
246 $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
247 $languages = $translationConfigurationProvider->getSystemLanguages();
248 foreach ($languages as $language) {
249 if ($language['uid'] !== -1) {
250 $items[] = array(
251 0 => $language['title'] . ' [' . $language['uid'] . ']',
252 1 => $language['uid'],
253 2 => $language['flagIcon']
254 );
255 }
256 }
257 } elseif ($special === 'custom') {
258 $customOptions = $GLOBALS['TYPO3_CONF_VARS']['BE']['customPermOptions'];
259 if (is_array($customOptions)) {
260 foreach ($customOptions as $coKey => $coValue) {
261 if (is_array($coValue['items'])) {
262 // Add header:
263 $items[] = array(
264 $languageService->sL($coValue['header']),
265 '--div--'
266 );
267 // Traverse items:
268 foreach ($coValue['items'] as $itemKey => $itemCfg) {
269 $icon = 'empty-empty';
270 $helpText = array();
271 if (!empty($itemCfg[2])) {
272 $helpText['description'] = $languageService->sL($itemCfg[2]);
273 }
274 $items[] = array(
275 $languageService->sL($itemCfg[0]),
276 $coKey . ':' . preg_replace('/[:|,]/', '', $itemKey),
277 $icon,
278 $helpText
279 );
280 }
281 }
282 }
283 }
284 } elseif ($special === 'modListGroup' || $special === 'modListUser') {
285 $loadModules = GeneralUtility::makeInstance(ModuleLoader::class);
286 $loadModules->load($GLOBALS['TBE_MODULES']);
287 $modList = $special === 'modListUser' ? $loadModules->modListUser : $loadModules->modListGroup;
288 if (is_array($modList)) {
289 foreach ($modList as $theMod) {
290 // Icon:
291 $icon = $languageService->moduleLabels['tabs_images'][$theMod . '_tab'];
292 if ($icon) {
293 $icon = '../' . PathUtility::stripPathSitePrefix($icon);
294 }
295 // Add help text
296 $helpText = array(
297 'title' => $languageService->moduleLabels['labels'][$theMod . '_tablabel'],
298 'description' => $languageService->moduleLabels['labels'][$theMod . '_tabdescr']
299 );
300
301 $label = '';
302 // Add label for main module:
303 $pp = explode('_', $theMod);
304 if (count($pp) > 1) {
305 $label .= $languageService->moduleLabels['tabs'][($pp[0] . '_tab')] . '>';
306 }
307 // Add modules own label now:
308 $label .= $languageService->moduleLabels['tabs'][$theMod . '_tab'];
309
310 // Item configuration:
311 $items[] = array($label, $theMod, $icon, $helpText);
312 }
313 }
314 } else {
315 throw new \UnexpectedValueException(
316 'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
317 1439298496
318 );
319 }
320
321 return $items;
322 }
323
324 /**
325 * TCA config "fileFolder" evaluation. Add them to $items
326 *
327 * @param array $result Result array
328 * @param string $fieldName Current handle field name
329 * @param array $items Incoming items
330 * @return array Modified item array
331 */
332 protected function addItemsFromFolder(array $result, $fieldName, array $items) {
333 if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
334 || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
335 ) {
336 return $items;
337 }
338
339 $fileFolder = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
340 $fileFolder = GeneralUtility::getFileAbsFileName($fileFolder);
341 $fileFolder = rtrim($fileFolder, '/') . '/';
342
343 if (@is_dir($fileFolder)) {
344 $fileExtensionList = '';
345 if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
346 && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
347 ) {
348 $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
349 }
350 $recursionLevels = isset($fieldValue['config']['fileFolder_recursions'])
351 ? MathUtility::forceIntegerInRange($fieldValue['config']['fileFolder_recursions'], 0, 99)
352 : 99;
353 $fileArray = GeneralUtility::getAllFilesAndFoldersInPath(array(), $fileFolder, $fileExtensionList, 0, $recursionLevels);
354 $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
355 foreach ($fileArray as $fileReference) {
356 $fileInformation = pathinfo($fileReference);
357 $icon = GeneralUtility::inList('gif,png,jpeg,jpg', strtolower($fileInformation['extension']))
358 ? '../' . PathUtility::stripPathSitePrefix($fileFolder) . $fileReference
359 : '';
360 $items[] = array(
361 $fileReference,
362 $fileReference,
363 $icon
364 );
365 }
366 }
367
368 return $items;
369 }
370
371 /**
372 * TCA config "foreign_table" evaluation. Add them to $items
373 *
374 * @param array $result Result array
375 * @param string $fieldName Current handle field name
376 * @param array $items Incoming items
377 * @return array Modified item array
378 */
379 protected function addItemsFromForeignTable(array $result, $fieldName, array $items) {
380 // Guard
381 if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
382 || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
383 ) {
384 return $items;
385 }
386
387 $languageService = $this->getLanguageService();
388 $database = $this->getDatabaseConnection();
389
390 $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
391 $foreignTableQueryArray = $this->buildForeignTableQuery($result, $fieldName);
392 $queryResource = $database->exec_SELECT_queryArray($foreignTableQueryArray);
393
394 // Early return on error with flash message
395 $databaseError = $database->sql_error();
396 if (!empty($databaseError)) {
397 $msg = htmlspecialchars($databaseError) . '<br />' . LF;
398 $msg .= $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch');
399 $msgTitle = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:error.database_schema_mismatch_title');
400 /** @var $flashMessage FlashMessage */
401 $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, TRUE);
402 /** @var $flashMessageService FlashMessageService */
403 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
404 /** @var $defaultFlashMessageQueue FlashMessageQueue */
405 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
406 $defaultFlashMessageQueue->enqueue($flashMessage);
407 $database->sql_free_result($queryResource);
408 return $items;
409 }
410
411 $labelPrefix = '';
412 if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
413 $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
414 $labelPrefix = $languageService->sL($labelPrefix);
415 }
416 $iconFieldName = '';
417 if (!empty($result['processedTca']['ctrl']['selicon_field'])) {
418 $iconFieldName = $result['processedTca']['ctrl']['selicon_field'];
419 }
420 $iconPath = '';
421 if (!empty($result['processedTca']['ctrl']['selicon_field_path'])) {
422 $iconPath = $result['processedTca']['ctrl']['selicon_field_path'];
423 }
424
425 while ($foreignRow = $database->sql_fetch_assoc($queryResource)) {
426 BackendUtility::workspaceOL($foreignTable, $foreignRow);
427 if (is_array($foreignRow)) {
428 // Prepare the icon if available:
429 if ($iconFieldName && $iconPath && $foreignRow[$iconFieldName]) {
430 $iParts = GeneralUtility::trimExplode(',', $foreignRow[$iconFieldName], TRUE);
431 $icon = '../' . $iconPath . '/' . trim($iParts[0]);
432 } else {
433 $icon = IconUtility::mapRecordTypeToSpriteIconName($foreignTable, $foreignRow);
434 }
435 // Add the item
436 $items[] = array(
437 $labelPrefix . htmlspecialchars(BackendUtility::getRecordTitle($foreignTable, $foreignRow)),
438 $foreignRow['uid'],
439 $icon
440 );
441 }
442 }
443
444 $database->sql_free_result($queryResource);
445
446 return $items;
447 }
448
449 /**
450 * Remove items using "keepItems" pageTsConfig
451 *
452 * @param array $result Result array
453 * @param string $fieldName Current handle field name
454 * @param array $items Incoming items
455 * @return array Modified item array
456 */
457 protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items) {
458 $table = $result['tableName'];
459 if (empty($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
460 || !is_string($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
461 ) {
462 return $items;
463 }
464
465 return ArrayUtility::keepItemsInArray(
466 $items,
467 $result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
468 function ($value) {
469 return $value[1];
470 }
471 );
472 }
473
474 /**
475 * Remove items using "removeItems" pageTsConfig
476 *
477 * @param array $result Result array
478 * @param string $fieldName Current handle field name
479 * @param array $items Incoming items
480 * @return array Modified item array
481 */
482 protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items) {
483 $table = $result['tableName'];
484 if (empty($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
485 || !is_string($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
486 ) {
487 return $items;
488 }
489
490 $removeItems = GeneralUtility::trimExplode(
491 ',',
492 $result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
493 TRUE
494 );
495 foreach ($items as $key => $itemValues) {
496 if (in_array($itemValues[1], $removeItems)) {
497 unset($items[$key]);
498 }
499 }
500
501 return $items;
502 }
503
504 /**
505 * Remove items user restriction on language field
506 *
507 * @param array $result Result array
508 * @param string $fieldName Current handle field name
509 * @param array $items Incoming items
510 * @return array Modified item array
511 */
512 protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items) {
513 // Guard clause returns if not a language field is handled
514 if (empty($result['processedTca']['ctrl']['languageField'])
515 || $result['processedTca']['ctrl']['languageField'] !== $fieldName
516 ) {
517 return $items;
518 }
519
520 $backendUser = $this->getBackendUser();
521 foreach ($items as $key => $itemValues) {
522 if (!$backendUser->checkLanguageAccess($itemValues[1])) {
523 unset($items[$key]);
524 }
525 }
526
527 return $items;
528 }
529
530 /**
531 * Remove items by user restriction on authMode items
532 *
533 * @param array $result Result array
534 * @param string $fieldName Current handle field name
535 * @param array $items Incoming items
536 * @return array Modified item array
537 */
538 protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items) {
539 // Guard clause returns early if no authMode field is configured
540 if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
541 || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
542 ) {
543 return $items;
544 }
545
546 $backendUser = $this->getBackendUser();
547 $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
548 foreach ($items as $key => $itemValues) {
549 // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
550 if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
551 unset($items[$key]);
552 }
553 }
554
555 return $items;
556 }
557
558 /**
559 * Remove items if doktype is handled for non admin users
560 *
561 * @param array $result Result array
562 * @param string $fieldName Current handle field name
563 * @param array $items Incoming items
564 * @return array Modified item array
565 */
566 protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items) {
567 $table = $result['tableName'];
568 $backendUser = $this->getBackendUser();
569 // Guard clause returns if not correct table and field or if user is admin
570 if ($table !== 'pages' && $table !== 'pages_language_overlay'
571 || $fieldName !== 'doktype' || $backendUser->isAdmin()
572 ) {
573 return $items;
574 }
575
576 $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
577 foreach ($items as $key => $itemValues) {
578 if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
579 unset($items[$key]);
580 }
581 }
582
583 return $items;
584 }
585
586 /**
587 * Returns an array with the exclude fields as defined in TCA and FlexForms
588 * Used for listing the exclude fields in be_groups forms.
589 *
590 * @return array Array of arrays with excludeFields (fieldName, table:fieldName) from TCA
591 * and FlexForms (fieldName, table:extKey;sheetName;fieldName)
592 */
593 protected function getExcludeFields() {
594 $languageService = $this->getLanguageService();
595 $finalExcludeArray = array();
596
597 // Fetch translations for table names
598 $tableToTranslation = array();
599 // All TCA keys
600 foreach ($GLOBALS['TCA'] as $table => $conf) {
601 $tableToTranslation[$table] = $languageService->sl($conf['ctrl']['title']);
602 }
603 // Sort by translations
604 asort($tableToTranslation);
605 foreach ($tableToTranslation as $table => $translatedTable) {
606 $excludeArrayTable = array();
607
608 // All field names configured and not restricted to admins
609 if (is_array($GLOBALS['TCA'][$table]['columns'])
610 && empty($GLOBALS['TCA'][$table]['ctrl']['adminOnly'])
611 && (empty($GLOBALS['TCA'][$table]['ctrl']['rootLevel']) || !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']))
612 ) {
613 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
614 if ($GLOBALS['TCA'][$table]['columns'][$field]['exclude']) {
615 // Get human readable names of fields
616 $translatedField = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
617 // Add entry
618 $excludeArrayTable[] = array($translatedTable . ': ' . $translatedField, $table . ':' . $field);
619 }
620 }
621 }
622 // All FlexForm fields
623 $flexFormArray = $this->getRegisteredFlexForms($table);
624 foreach ($flexFormArray as $tableField => $flexForms) {
625 // Prefix for field label, e.g. "Plugin Options:"
626 $labelPrefix = '';
627 if (!empty($GLOBALS['TCA'][$table]['columns'][$tableField]['label'])) {
628 $labelPrefix = $languageService->sl($GLOBALS['TCA'][$table]['columns'][$tableField]['label']);
629 }
630 // Get all sheets and title
631 foreach ($flexForms as $extIdent => $extConf) {
632 $extTitle = $languageService->sl($extConf['title']);
633 // Get all fields in sheet
634 foreach ($extConf['ds']['sheets'] as $sheetName => $sheet) {
635 if (empty($sheet['ROOT']['el']) || !is_array($sheet['ROOT']['el'])) {
636 continue;
637 }
638 foreach ($sheet['ROOT']['el'] as $fieldName => $field) {
639 // Use only fields that have exclude flag set
640 if (empty($field['TCEforms']['exclude'])) {
641 continue;
642 }
643 $fieldLabel = !empty($field['TCEforms']['label']) ? $languageService->sl($field['TCEforms']['label']) : $fieldName;
644 $fieldIdent = $table . ':' . $tableField . ';' . $extIdent . ';' . $sheetName . ';' . $fieldName;
645 $excludeArrayTable[] = array(trim($labelPrefix . ' ' . $extTitle, ': ') . ': ' . $fieldLabel, $fieldIdent);
646 }
647 }
648 }
649 }
650 // Sort fields by the translated value
651 if (!empty($excludeArrayTable)) {
652 usort($excludeArrayTable, function (array $array1, array $array2) {
653 $array1 = reset($array1);
654 $array2 = reset($array2);
655 if (is_string($array1) && is_string($array2)) {
656 return strcasecmp($array1, $array2);
657 }
658 return 0;
659 });
660 $finalExcludeArray = array_merge($finalExcludeArray, $excludeArrayTable);
661 }
662 }
663
664 return $finalExcludeArray;
665 }
666
667 /**
668 * Returns all registered FlexForm definitions with title and fields
669 *
670 * @param string $table Table to handle
671 * @return array Data structures with speaking extension title
672 */
673 protected function getRegisteredFlexForms($table) {
674 if (empty($table) || empty($GLOBALS['TCA'][$table]['columns'])) {
675 return array();
676 }
677 $flexForms = array();
678 foreach ($GLOBALS['TCA'][$table]['columns'] as $tableField => $fieldConf) {
679 if (!empty($fieldConf['config']['type']) && !empty($fieldConf['config']['ds']) && $fieldConf['config']['type'] == 'flex') {
680 $flexForms[$tableField] = array();
681 unset($fieldConf['config']['ds']['default']);
682 // Get pointer fields
683 $pointerFields = !empty($fieldConf['config']['ds_pointerField']) ? $fieldConf['config']['ds_pointerField'] : 'list_type,CType';
684 $pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
685 // Get FlexForms
686 foreach ($fieldConf['config']['ds'] as $flexFormKey => $dataStructure) {
687 // Get extension identifier (uses second value if it's not empty, "list" or "*", else first one)
688 $identFields = GeneralUtility::trimExplode(',', $flexFormKey);
689 $extIdent = $identFields[0];
690 if (!empty($identFields[1]) && $identFields[1] !== 'list' && $identFields[1] !== '*') {
691 $extIdent = $identFields[1];
692 }
693 // Load external file references
694 if (!is_array($dataStructure)) {
695 $file = GeneralUtility::getFileAbsFileName(str_ireplace('FILE:', '', $dataStructure));
696 if ($file && @is_file($file)) {
697 $dataStructure = GeneralUtility::getUrl($file);
698 }
699 $dataStructure = GeneralUtility::xml2array($dataStructure);
700 if (!is_array($dataStructure)) {
701 continue;
702 }
703 }
704 // Get flexform content
705 $dataStructure = GeneralUtility::resolveAllSheetsInDS($dataStructure);
706 if (empty($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
707 continue;
708 }
709 // Use DS pointer to get extension title from TCA
710 // @todo: I don't understand this code ... does it make sense at all?
711 $title = $extIdent;
712 $keyFields = GeneralUtility::trimExplode(',', $flexFormKey);
713 foreach ($pointerFields as $pointerKey => $pointerName) {
714 if (empty($keyFields[$pointerKey]) || $keyFields[$pointerKey] === '*' || $keyFields[$pointerKey] === 'list') {
715 continue;
716 }
717 if (!empty($GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'])) {
718 $items = $GLOBALS['TCA'][$table]['columns'][$pointerName]['config']['items'];
719 if (!is_array($items)) {
720 continue;
721 }
722 foreach ($items as $itemConf) {
723 if (!empty($itemConf[0]) && !empty($itemConf[1]) && $itemConf[1] == $keyFields[$pointerKey]) {
724 $title = $itemConf[0];
725 break 2;
726 }
727 }
728 }
729 }
730 $flexForms[$tableField][$extIdent] = array(
731 'title' => $title,
732 'ds' => $dataStructure
733 );
734 }
735 }
736 }
737 return $flexForms;
738 }
739
740 /**
741 * Returns an array with explicit Allow/Deny fields.
742 * Used for listing these field/value pairs in be_groups forms
743 *
744 * @return array Array with information from all of $GLOBALS['TCA']
745 */
746 protected function getExplicitAuthFieldValues() {
747 $languageService = static::getLanguageService();
748 $adLabel = array(
749 'ALLOW' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.allow'),
750 'DENY' => $languageService->sl('LLL:EXT:lang/locallang_core.xlf:labels.deny')
751 );
752 $allowDenyOptions = array();
753 foreach ($GLOBALS['TCA'] as $table => $_) {
754 // All field names configured:
755 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
756 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $_) {
757 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
758 if ($fieldConfig['type'] === 'select' && $fieldConfig['authMode']) {
759 // Check for items
760 if (is_array($fieldConfig['items'])) {
761 // Get Human Readable names of fields and table:
762 $allowDenyOptions[$table . ':' . $field]['tableFieldLabel'] =
763 $languageService->sl($GLOBALS['TCA'][$table]['ctrl']['title']) . ': '
764 . $languageService->sl($GLOBALS['TCA'][$table]['columns'][$field]['label']);
765 foreach ($fieldConfig['items'] as $iVal) {
766 // Values '' is not controlled by this setting.
767 if ((string)$iVal[1] !== '') {
768 // Find iMode
769 $iMode = '';
770 switch ((string)$fieldConfig['authMode']) {
771 case 'explicitAllow':
772 $iMode = 'ALLOW';
773 break;
774 case 'explicitDeny':
775 $iMode = 'DENY';
776 break;
777 case 'individual':
778 if ($iVal[4] === 'EXPL_ALLOW') {
779 $iMode = 'ALLOW';
780 } elseif ($iVal[4] === 'EXPL_DENY') {
781 $iMode = 'DENY';
782 }
783 break;
784 }
785 // Set iMode
786 if ($iMode) {
787 $allowDenyOptions[$table . ':' . $field]['items'][$iVal[1]] = array($iMode, $languageService->sl($iVal[0]), $adLabel[$iMode]);
788 }
789 }
790 }
791 }
792 }
793 }
794 }
795 }
796 return $allowDenyOptions;
797 }
798
799 /**
800 * Build query to fetch foreign records
801 *
802 * @param array $result Result array
803 * @param string $localFieldName Current handle field name
804 * @return array Query array ready to be executed via Database->exec_SELECT_queryArray()
805 * @throws \UnexpectedValueException
806 */
807 protected function buildForeignTableQuery(array $result, $localFieldName) {
808 $backendUser = $this->getBackendUser();
809
810 $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
811
812 if (!is_array($GLOBALS['TCA'][$foreignTableName])) {
813 throw new \UnexpectedValueException(
814 'Field ' . $localFieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
815 . $foreignTableName . ', but this table is not defined in TCA',
816 1439569743
817 );
818 }
819
820 $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
821
822 $queryArray = array();
823 $queryArray['SELECT'] = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
824
825 // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
826 // rootLevel = 0 means that elements are not allowed on root level
827 // rootLevel = 1 means that elements are only on the root level (pid=0)
828 $rootLevel = 0;
829 if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
830 $rootLevel = $GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
831 }
832 $deleteClause = BackendUtility::deleteClause($foreignTableName);
833 if ($rootLevel == 1 || $rootLevel == -1) {
834 $pidWhere = $foreignTableName . '.pid' . (($rootLevel == -1) ? '<>-1' : '=0');
835 $queryArray['FROM'] = $foreignTableName;
836 $queryArray['WHERE'] = $pidWhere . $deleteClause . $foreignTableClauseArray['WHERE'];
837 } else {
838 $pageClause = $backendUser->getPagePermsClause(1);
839 if ($foreignTableName === 'pages') {
840 $queryArray['FROM'] = 'pages';
841 $queryArray['WHERE'] = '1=1' . $deleteClause . ' AND' . $pageClause . $foreignTableClauseArray['WHERE'];
842 } else {
843 $queryArray['FROM'] = $foreignTableName . ', pages';
844 $queryArray['WHERE'] = 'pages.uid=' . $foreignTableName . '.pid AND pages.deleted=0'
845 . $deleteClause . ' AND' . $pageClause . $foreignTableClauseArray['WHERE'];
846 }
847 }
848
849 $queryArray['GROUPBY'] = $foreignTableClauseArray['GROUPBY'];
850 $queryArray['ORDERBY'] = $foreignTableClauseArray['ORDERBY'];
851 $queryArray['LIMIT'] = $foreignTableClauseArray['LIMIT'];
852
853 return $queryArray;
854 }
855
856 /**
857 * Replace markers in a where clause from TCA foreign_table_where
858 *
859 * ###REC_FIELD_[field name]###
860 * ###THIS_UID### - is current element uid (zero if new).
861 * ###CURRENT_PID### - is the current page id (pid of the record).
862 * ###SITEROOT###
863 * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
864 * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
865 * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
866 *
867 * @param array $result Result array
868 * @param string $foreignTableName Name of foreign table
869 * @param string $localFieldName Current handle field name
870 * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
871 */
872 protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName) {
873 $database = $this->getDatabaseConnection();
874 $localTable = $result['tableName'];
875
876 $foreignTableClause = '';
877 if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
878 && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
879 ) {
880 $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
881 // Replace possible markers in query
882 if (strstr($foreignTableClause, '###REC_FIELD_')) {
883 // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
884 $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
885 foreach ($whereClauseParts as $key => $value) {
886 if ($key !== 0) {
887 // "field1###' AND ..." -> array("field1", "' AND ...")
888 $whereClauseSubParts = explode('###', $value, 2);
889 // @todo: Throw exception if there is no value? What happens for NEW records?
890 $rowFieldValue = $result['databaseRow'][$whereClauseSubParts[0]];
891 if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
892 $whereClauseParts[$key] = $database->quoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
893 } else {
894 $whereClauseParts[$key] = $database->fullQuoteStr($rowFieldValue, $foreignTableName) . $whereClauseSubParts[1];
895 }
896 }
897 }
898 $foreignTableClause = implode('', $whereClauseParts);
899 }
900
901 $siteRootUid = 0;
902 foreach ($result['rootline'] as $rootlinePage) {
903 if (!empty($rootlinePage['is_siteroot'])) {
904 $siteRootUid = (int)$rootlinePage['uid'];
905 break;
906 }
907 }
908 $pageTsConfigId = 0;
909 if ($result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']) {
910 $pageTsConfigId = (int)$result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
911 }
912 $pageTsConfigIdList = 0;
913 if ($result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']) {
914 $pageTsConfigIdList = $result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
915 $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, TRUE);
916 $pageTsConfigIdList = array();
917 foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
918 if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
919 $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
920 }
921 }
922 $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
923 }
924 $pageTsConfigString = '';
925 if ($result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']) {
926 $pageTsConfigString = $result['pageTsConfigMerged']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
927 $pageTsConfigString = $database->quoteStr($pageTsConfigString, $foreignTableName);
928 }
929
930 $foreignTableClause = str_replace(
931 array(
932 '###CURRENT_PID###',
933 '###THIS_UID###',
934 '###SITEROOT###',
935 '###PAGE_TSCONFIG_ID###',
936 '###PAGE_TSCONFIG_IDLIST###',
937 '###PAGE_TSCONFIG_STR###'
938 ),
939 array(
940 (int)$result['effectivePid'],
941 (int)$result['databaseRow']['uid'],
942 $siteRootUid,
943 $pageTsConfigId,
944 $pageTsConfigIdList,
945 $pageTsConfigString
946 ),
947 $foreignTableClause
948 );
949 }
950
951 // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
952 // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
953 $foreignTableClause = ' ' . $foreignTableClause;
954 $foreignTableClauseArray = array(
955 'WHERE' => '',
956 'GROUPBY' => '',
957 'ORDERBY' => '',
958 'LIMIT' => '',
959 );
960 // Find LIMIT
961 $reg = array();
962 if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
963 $foreignTableClauseArray['LIMIT'] = trim($reg[2]);
964 $foreignTableClause = $reg[1];
965 }
966 // Find ORDER BY
967 $reg = array();
968 if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
969 $foreignTableClauseArray['ORDERBY'] = trim($reg[2]);
970 $foreignTableClause = $reg[1];
971 }
972 // Find GROUP BY
973 $reg = array();
974 if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._]+)$/i', $foreignTableClause, $reg)) {
975 $foreignTableClauseArray['GROUPBY'] = trim($reg[2]);
976 $foreignTableClause = $reg[1];
977 }
978 // Rest is assumed to be "WHERE" clause
979 $foreignTableClauseArray['WHERE'] = $foreignTableClause;
980
981 return $foreignTableClauseArray;
982 }
983
984 /**
985 * Validate and sanitize database row values of the select field with the given name.
986 * Creates an array out of databaseRow[selectField] values.
987 *
988 * @param array $result The current result array.
989 * @param string $fieldName Name of the current select field.
990 * @param array $staticValues Array with statically defined items, item value is used as array key.
991 * @return array
992 */
993 protected function processSelectFieldValue(array $result, $fieldName, array $staticValues) {
994 $fieldConfig = $result['processedTca']['columns'][$fieldName];
995
996 // For single select fields we just keep the current value because the renderer
997 // will take care of showing the "Invalid value" text.
998 // For maxitems=1 select fields is is also possible to select empty values.
999 // @todo: move handling of invalid values to this data provider.
1000 if ($fieldConfig['config']['maxitems'] === 1) {
1001 return array($result['databaseRow'][$fieldName]);
1002 }
1003
1004 $currentDatabaseValues = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : '';
1005 // Selecting empty values does not make sense for fields that can contain more than one item
1006 // because it is impossible to determine if the empty value or nothing is selected.
1007 // This is why empty values will be removed for multi value fields.
1008 $currentDatabaseValuesArray = GeneralUtility::trimExplode(',', $currentDatabaseValues, TRUE);
1009 $newDatabaseValueArray = [];
1010
1011 // Add all values that were defined by static methods and do not come from the relation
1012 // e.g. TCA, TSconfig, itemProcFunc etc.
1013 foreach ($currentDatabaseValuesArray as $value) {
1014 if (isset($staticValues[$value])) {
1015 $newDatabaseValueArray[] = $value;
1016 }
1017 }
1018
1019 if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
1020 /** @var RelationHandler $relationHandler */
1021 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1022 $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
1023 if (isset($fieldConfig['config']['MM']) && !empty($fieldConfig['config']['MM'])) {
1024 // MM relation
1025 $relationHandler->start(
1026 $currentDatabaseValues,
1027 $fieldConfig['config']['foreign_table'],
1028 $fieldConfig['config']['MM'],
1029 $result['databaseRow']['uid'],
1030 $result['tableName'],
1031 $fieldConfig['config']
1032 );
1033 } else {
1034 // Non MM relation
1035 // If not dealing with MM relations, use default live uid, not versioned uid for record relations
1036 $relationHandler->start(
1037 $currentDatabaseValues,
1038 $fieldConfig['config']['foreign_table'],
1039 '',
1040 $this->getLiveUid($result),
1041 $result['tableName'],
1042 $fieldConfig['config']
1043 );
1044 }
1045 $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
1046 }
1047
1048 return array_unique($newDatabaseValueArray);
1049 }
1050
1051 /**
1052 * Gets the record uid of the live default record. If already
1053 * pointing to the live record, the submitted record uid is returned.
1054 *
1055 * @param array $result Result array
1056 * @return int
1057 * @throws \UnexpectedValueException
1058 */
1059 protected function getLiveUid(array $result) {
1060 $table = $result['tableName'];
1061 $row = $result['databaseRow'];
1062 $uid = $row['uid'];
1063 if (!empty($result['processedTca']['ctrl']['versioningWS'])
1064 && $result['pid'] === -1
1065 ) {
1066 if (empty($row['t3ver_oid'])) {
1067 throw new \UnexpectedValueException(
1068 'No t3ver_oid found for record ' . $row['uid'] . ' on table ' . $table,
1069 1440066481
1070 );
1071 }
1072 $uid = $row['t3ver_oid'];
1073 }
1074 return $uid;
1075 }
1076
1077 /**
1078 * @return LanguageService
1079 */
1080 protected function getLanguageService() {
1081 return $GLOBALS['LANG'];
1082 }
1083
1084 /**
1085 * @return DatabaseConnection
1086 */
1087 protected function getDatabaseConnection() {
1088 return $GLOBALS['TYPO3_DB'];
1089 }
1090
1091 /**
1092 * @return BackendUserAuthentication
1093 */
1094 protected function getBackendUser() {
1095 return $GLOBALS['BE_USER'];
1096 }
1097
1098 }