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