1e167066489b51008250bee0fc274ca2f3e1e6a3
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaFlex.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\Form\FormDataCompiler;
18 use TYPO3\CMS\Backend\Form\FormDataGroup\FlexFormSegment;
19 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22 use TYPO3\CMS\Core\Migrations\TcaMigration;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25 /**
26 * Resolve and prepare flex structure data and add resolved data structure to TCA in
27 * $fieldName['config']['ds'] as well as resolved value array in $databaseRow
28 */
29 class TcaFlex extends AbstractItemProvider implements FormDataProviderInterface {
30
31 /**
32 * Resolve flex data structures and prepare flex data values.
33 *
34 * First, resolve ds pointer stuff and parse both ds and dv. Then normalize some
35 * details to have aligned array nesting for the rest of the method and the render
36 * engine. Next determine possible pageTsConfig overrides and apply them to ds.
37 * Determine available languages and sanitize dv for further processing. Then kick
38 * and validate further details like excluded fields. Finally for each possible
39 * value and ds call FormDataCompiler with set FlexFormSegment group to resolve
40 * single field stuff like item processor functions.
41 *
42 * @param array $result
43 * @return array
44 */
45 public function addData(array $result) {
46 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
47 if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
48 continue;
49 }
50
51 // @todo: It would probably be better to split this provider into multiple provider. At least it
52 // @todo: would be nice to have the data structure and value fetching and parsing available as
53 // @todo: stand alone provider.
54 $result = $this->initializeDataStructure($result, $fieldName);
55 $result = $this->initializeDataValues($result, $fieldName);
56 $result = $this->createDefaultSheetInDataStructureIfNotGiven($result, $fieldName);
57 $result = $this->removeTceFormsArrayKeyFromDataStructureElements($result, $fieldName);
58 $flexIdentifier = $this->getFlexIdentifier($result, $fieldName);
59 $pageTsConfigOfFlex = $this->getPageTsOfFlex($result, $fieldName, $flexIdentifier);
60 $result = $this->modifyOuterDataStructure($result, $fieldName, $pageTsConfigOfFlex);
61 $result = $this->removeExcludeFieldsFromDataStructure($result, $fieldName, $flexIdentifier);
62 $result = $this->removeDisabledFieldsFromDataStructure($result, $fieldName, $pageTsConfigOfFlex);
63 $result = $this->prepareLanguageHandlingInDataValues($result, $fieldName);
64 $result = $this->migrateFlexformTcaDataStructureElements($result, $fieldName);
65 $result = $this->modifyDataStructureAndDataValuesByFlexFormSegmentGroup($result, $fieldName, $pageTsConfigOfFlex);
66 }
67
68 return $result;
69 }
70
71 /**
72 * Fetch / initialize data structure
73 *
74 * @param array $result Result array
75 * @param $fieldName string Currently handled field name
76 * @return array Modified result
77 * @throws \UnexpectedValueException
78 */
79 protected function initializeDataStructure(array $result, $fieldName) {
80 // Fetch / initialize data structure
81 $dataStructureArray = BackendUtility::getFlexFormDS(
82 $result['processedTca']['columns'][$fieldName]['config'],
83 $result['databaseRow'],
84 $result['tableName'],
85 $fieldName
86 );
87 // If data structure can't be parsed, this is a developer error, so throw a non catchable exception
88 // @todo: It might be ok to have a non parsable ds if field is not displayed anyway
89 // @todo: Parse flex only for showitem validated fields and after displayCondition evaluation?
90 if (!is_array($dataStructureArray)) {
91 throw new \UnexpectedValueException(
92 'Data structure error: ' . $dataStructureArray,
93 1440506893
94 );
95 }
96 if (!isset($dataStructureArray['meta']) || !is_array($dataStructureArray['meta'])) {
97 $dataStructureArray['meta'] = array();
98 }
99 // This kicks one array depth: config['ds']['matchingIdentifier'] becomes config['ds']
100 $result['processedTca']['columns'][$fieldName]['config']['ds'] = $dataStructureArray;
101 return $result;
102 }
103
104 /**
105 * Parse / initialize value from xml string to array
106 *
107 * @param array $result Result array
108 * @param $fieldName string Currently handled field name
109 * @return array Modified result
110 */
111 protected function initializeDataValues(array $result, $fieldName) {
112 if (!array_key_exists($fieldName, $result['databaseRow'])) {
113 $result['databaseRow'][$fieldName] = '';
114 }
115 $valueArray = [];
116 if (isset($result['databaseRow'][$fieldName])) {
117 $valueArray = $result['databaseRow'][$fieldName];
118 }
119 if (!is_array($result['databaseRow'][$fieldName])) {
120 $valueArray = GeneralUtility::xml2array($result['databaseRow'][$fieldName]);
121 }
122 if (!is_array($valueArray)) {
123 $valueArray = [];
124 }
125 if (!isset($valueArray['data'])) {
126 $valueArray['data'] = [];
127 }
128 if (!isset($valueArray['meta'])) {
129 $valueArray['meta'] = [];
130 }
131 $result['databaseRow'][$fieldName] = $valueArray;
132 return $result;
133 }
134
135 /**
136 * Add a sheet structure if data structure has none yet to simplify further handling.
137 *
138 * Example TCA field config:
139 * ['config']['ds']['ROOT'] becomes
140 * ['config']['ds']['sheets']['sDEF']['ROOT']
141 *
142 * @param array $result Result array
143 * @param string $fieldName Currently handled field name
144 * @return array Modified result
145 * @throws \UnexpectedValueException
146 */
147 protected function createDefaultSheetInDataStructureIfNotGiven(array $result, $fieldName) {
148 $modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
149 if (isset($modifiedDataStructure['ROOT']) && isset($modifiedDataStructure['sheets'])) {
150 throw new \UnexpectedValueException(
151 'Parsed data structure has both ROOT and sheets on top level',
152 1440676540
153 );
154 }
155 if (isset($modifiedDataStructure['ROOT']) && is_array($modifiedDataStructure['ROOT'])) {
156 $modifiedDataStructure['sheets']['sDEF']['ROOT'] = $modifiedDataStructure['ROOT'];
157 unset($modifiedDataStructure['ROOT']);
158 }
159 $result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
160 return $result;
161 }
162
163 /**
164 * Remove "TCEforms" key from all elements in data structure to simplify further parsing.
165 *
166 * Example config:
167 * ['config']['ds']['sheets']['sDEF']['ROOT']['el']['anElement']['TCEforms']['label'] becomes
168 * ['config']['ds']['sheets']['sDEF']['ROOT']['el']['anElement']['label']
169 *
170 * @param array $result Result array
171 * @param string $fieldName Currently handled field name
172 * @return array Modified result
173 */
174 protected function removeTceFormsArrayKeyFromDataStructureElements(array $result, $fieldName) {
175 $modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
176 $modifiedDataStructure = $this->removeElementTceFormsRecursive($modifiedDataStructure);
177 $result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
178 return $result;
179 }
180
181 /**
182 * Move ['el']['something']['TCEforms'] to ['el']['something'] recursive
183 *
184 * @param array $structure Given hierarchy
185 * @return array Modified hierarchy
186 */
187 protected function removeElementTceFormsRecursive(array $structure) {
188 $newStructure = [];
189 foreach ($structure as $key => $value) {
190 if ($key === 'el' && is_array($value)) {
191 $newSubStructure = [];
192 foreach ($value as $subKey => $subValue) {
193 if (is_array($subValue) && count($subValue) === 1 && isset($subValue['TCEforms'])) {
194 $newSubStructure[$subKey] = $subValue['TCEforms'];
195 } else {
196 $newSubStructure[$subKey] = $subValue;
197 }
198 }
199 $value = $newSubStructure;
200 }
201 if (is_array($value)) {
202 $value = $this->removeElementTceFormsRecursive($value);
203 }
204 $newStructure[$key] = $value;
205 }
206 return $newStructure;
207 }
208
209 /**
210 * Take care of ds_pointerField and friends to determine the correct sub array within
211 * TCA config ds.
212 *
213 * Gets extension identifier. Use second pointer field if it's value is not empty, "list" or "*",
214 * else it must be a plugin and first one will be used.
215 * This code basically determines the sub key of ds field:
216 * config = array(
217 * ds => array(
218 * 'aFlexConfig' => '<flexXml ...
219 * ^^^^^^^^^^^
220 * $flexformIdentifier contains "aFlexConfig" after this operation.
221 *
222 * @todo: This method is only implemented half. It basically should do all the
223 * @todo: pointer handling that is done within BackendUtility::getFlexFormDS() to $srcPointer.
224 *
225 * @param array $result Result array
226 * @param string $fieldName Current handle field name
227 * @return string Pointer
228 */
229 protected function getFlexIdentifier(array $result, $fieldName) {
230 // @todo: Current implementation with the "list_type, CType" fallback is rather limited and customized for
231 // @todo: tt_content, also it forces a ds_pointerField to be defined and a casual "default" sub array does not work
232 $pointerFields = !empty($result['processedTca']['columns'][$fieldName]['config']['ds_pointerField'])
233 ? $result['processedTca']['columns'][$fieldName]['config']['ds_pointerField']
234 : 'list_type,CType';
235 $pointerFields = GeneralUtility::trimExplode(',', $pointerFields);
236 $flexformIdentifier = !empty($result['databaseRow'][$pointerFields[0]]) ? $result['databaseRow'][$pointerFields[0]] : '';
237 if (!empty($result['databaseRow'][$pointerFields[1]])
238 && $result['databaseRow'][$pointerFields[1]] !== 'list'
239 && $result['databaseRow'][$pointerFields[1]] !== '*'
240 ) {
241 $flexformIdentifier = $result['databaseRow'][$pointerFields[1]];
242 }
243 if (empty($flexformIdentifier)) {
244 $flexformIdentifier = 'default';
245 }
246
247 return $flexformIdentifier;
248 }
249
250 /**
251 * Determine TCEFORM.aTable.aField.matchingIdentifier
252 *
253 * @param array $result Result array
254 * @param string $fieldName Handled field name
255 * @param string $flexIdentifier Determined identifier
256 * @return array PageTsConfig for this flex
257 */
258 protected function getPageTsOfFlex(array $result, $fieldName, $flexIdentifier) {
259 $table = $result['tableName'];
260 $pageTs = [];
261 if (!empty($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'])
262 && is_array($result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'])) {
263 $pageTs = $result['pageTsConfigMerged']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'];
264 }
265 return $pageTs;
266 }
267
268 /**
269 * Handle "outer" flex data structure changes like language and sheet
270 * description. Does not change "TCA" or values of single elements
271 *
272 * @param array $result Result array
273 * @param string $fieldName Current handle field name
274 * @param array $pageTsConfig Given pageTsConfig of this flex form
275 * @return array Modified item array
276 */
277 protected function modifyOuterDataStructure(array $result, $fieldName, $pageTsConfig) {
278 $modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
279
280 if (isset($pageTsConfig['langDisable'])) {
281 $modifiedDataStructure['meta']['langDisable'] = $pageTsConfig['langDisable'];
282 }
283 if (isset($pageTsConfig['langChildren'])) {
284 $modifiedDataStructure['meta']['langChildren'] = $pageTsConfig['langChildren'];
285 }
286
287 if (isset($modifiedDataStructure['sheets']) && is_array($modifiedDataStructure['sheets'])) {
288 // Handling multiple sheets
289 foreach ($modifiedDataStructure['sheets'] as $sheetName => $sheetStructure) {
290 $modifiedDataStructure['sheets'][$sheetName] = $this->resolvePossibleExternalFile($sheetStructure);
291
292 if (isset($pageTsConfig[$sheetName . '.']) && is_array($pageTsConfig[$sheetName . '.'])) {
293 $pageTsOfSheet = $pageTsConfig[$sheetName . '.'];
294
295 // Remove whole sheet if disabled
296 if (!empty($pageTsOfSheet['disabled'])) {
297 unset($modifiedDataStructure['sheets'][$sheetName]);
298 continue;
299 }
300
301 // sheetTitle, sheetDescription, sheetShortDescr
302 $modifiedDataStructure['sheets'][$sheetName] = $this->modifySingleSheetInformation($sheetStructure, $pageTsOfSheet);
303 }
304 }
305 }
306
307 $modifiedDataStructure['meta']['langDisable'] = isset($modifiedDataStructure['meta']['langDisable'])
308 ? (bool)$modifiedDataStructure['meta']['langDisable']
309 : FALSE;
310 $modifiedDataStructure['meta']['langChildren'] = isset($modifiedDataStructure['meta']['langChildren'])
311 ? (bool)$modifiedDataStructure['meta']['langChildren']
312 : FALSE;
313
314 $result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
315
316 return $result;
317 }
318
319 /**
320 * Removes fields from data structure the user has no access to
321 *
322 * @param array $result Result array
323 * @param string $fieldName Current handle field name
324 * @param string $flexIdentifier Determined identifier
325 * @return array Modified result
326 */
327 protected function removeExcludeFieldsFromDataStructure(array $result, $fieldName, $flexIdentifier) {
328 $dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
329 $backendUser = $this->getBackendUser();
330 if ($backendUser->isAdmin() || !isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
331 return $result;
332 }
333
334 $userNonExcludeFields = GeneralUtility::trimExplode(',', $backendUser->groupData['non_exclude_fields']);
335 $excludeFieldsPrefix = $result['tableName'] . ':' . $fieldName . ';' . $flexIdentifier . ';';
336 $nonExcludeFields = [];
337 foreach ($userNonExcludeFields as $userNonExcludeField) {
338 if (strpos($userNonExcludeField, $excludeFieldsPrefix) !== FALSE) {
339 $exploded = explode(';', $userNonExcludeField);
340 $sheetName = $exploded[2];
341 $fieldName = $exploded[3];
342 $nonExcludeFields[$sheetName] = $fieldName;
343 }
344 }
345
346 foreach ($dataStructure['sheets'] as $sheetName => $sheetDefinition) {
347 if (!isset($sheetDefinition['ROOT']['el']) || !is_array($sheetDefinition['ROOT']['el'])) {
348 continue;
349 }
350 foreach ($sheetDefinition['ROOT']['el'] as $flexFieldName => $fieldDefinition) {
351 if (!empty($fieldDefinition['exclude']) && empty($nonExcludeFields[$sheetName])) {
352 unset($result['processedTca']['columns'][$fieldName]['config']['ds']['sheets'][$sheetName]['ROOT']['el'][$flexFieldName]);
353 }
354 }
355 }
356
357 return $result;
358 }
359
360 /**
361 * Handle "outer" flex data structure changes like language and sheet
362 * description. Does not change "TCA" or values of single elements
363 *
364 * @param array $result Result array
365 * @param string $fieldName Current handle field name
366 * @param array $pageTsConfig Given pageTsConfig of this flex form
367 * @return array Modified item array
368 */
369 protected function removeDisabledFieldsFromDataStructure(array $result, $fieldName, $pageTsConfig) {
370 $dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
371 if (!isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
372 return $result;
373 }
374 foreach ($dataStructure['sheets'] as $sheetName => $sheetDefinition) {
375 if (!isset($sheetDefinition['ROOT']['el']) || !is_array($sheetDefinition['ROOT']['el'])
376 || !isset($pageTsConfig[$sheetName . '.'])) {
377 continue;
378 }
379 foreach ($sheetDefinition['ROOT']['el'] as $flexFieldName => $fieldDefinition) {
380 if (!empty($pageTsConfig[$sheetName . '.'][$flexFieldName . '.']['disabled'])) {
381 unset($result['processedTca']['columns'][$fieldName]['config']['ds']['sheets'][$sheetName]['ROOT']['el'][$flexFieldName]);
382 }
383 }
384 }
385
386 return $result;
387 }
388
389 /**
390 * Remove data values in languages the user has no access to and add dummy entries
391 * for languages that are available but do not exist in data values yet.
392 *
393 * @param array $result Result array
394 * @param string $fieldName Current handle field name
395 * @return array Modified item array
396 */
397 protected function prepareLanguageHandlingInDataValues(array $result, $fieldName) {
398 $backendUser = $this->getBackendUser();
399 $dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
400
401 $langDisabled = $dataStructure['meta']['langDisable'];
402 $langChildren = $dataStructure['meta']['langChildren'];
403
404 // Existing page language overlays are only considered if options.checkPageLanguageOverlay is set in userTs
405 $checkPageLanguageOverlay = FALSE;
406 if (isset($result['userTsConfig']['options.']) && is_array($result['userTsConfig']['options.'])
407 && array_key_exists('checkPageLanguageOverlay', $result['userTsConfig']['options.'])
408 ) {
409 $checkPageLanguageOverlay = (bool)$result['userTsConfig']['options.']['checkPageLanguageOverlay'];
410 }
411
412 $systemLanguageRows = $result['systemLanguageRows'];
413
414 // Contains all language iso code that are valid and user has access to
415 $availableLanguageCodes = [];
416 foreach ($systemLanguageRows as $systemLanguageRow) {
417 $isoCode = $systemLanguageRow['iso'];
418 $isAvailable = TRUE;
419 if ($langDisabled && $isoCode !== 'DEF') {
420 $isAvailable = FALSE;
421 }
422 // @todo: Is it possible a user has no write access to default lang? If so, what to do?
423 if (!$backendUser->checkLanguageAccess($systemLanguageRow['uid'])) {
424 $isAvailable = FALSE;
425 }
426 if ($checkPageLanguageOverlay && $systemLanguageRow['uid'] > 0) {
427 $found = FALSE;
428 foreach ($result['pageLanguageOverlayRows'] as $overlayRow) {
429 if ((int)$overlayRow['sys_language_uid'] === (int)$systemLanguageRow['uid']) {
430 $found = TRUE;
431 break;
432 }
433 }
434 if (!$found) {
435 $isAvailable = FALSE;
436 }
437 }
438 if ($isAvailable) {
439 $availableLanguageCodes[] = $isoCode;
440 }
441 }
442 // Set the list of available languages in the data structure "meta" section to have it
443 // available for the render engine to iterate over it.
444 $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['availableLanguageCodes'] = $availableLanguageCodes;
445
446 if (!$langChildren) {
447 $allowedLanguageSheetKeys = [];
448 foreach ($availableLanguageCodes as $isoCode) {
449 $allowedLanguageSheetKeys['l' . $isoCode] = [];
450 }
451 $result = $this->setLanguageSheetsInDataValues($result, $fieldName, $allowedLanguageSheetKeys);
452
453 // With $langChildren = 0, values must only contain vDEF prefixed keys
454 $allowedValueLevelLanguageKeys = [];
455 $allowedValueLevelLanguageKeys['vDEF'] = [];
456 $allowedValueLevelLanguageKeys['vDEF.vDEFbase'] = [];
457 // A richtext special
458 $allowedValueLevelLanguageKeys['_TRANSFORM_vDEF.vDEFbase'] = [];
459 $result = $this->setLanguageValueLevelValues($result, $fieldName, $allowedValueLevelLanguageKeys);
460 } else {
461 // langChildren is set - only lDEF as sheet language is allowed, but more fields on value field level
462 $allowedLanguageSheetKeys = [
463 'lDEF' => [],
464 ];
465 $result = $this->setLanguageSheetsInDataValues($result, $fieldName, $allowedLanguageSheetKeys);
466
467 $allowedValueLevelLanguageKeys = [];
468 foreach ($availableLanguageCodes as $isoCode) {
469 $allowedValueLevelLanguageKeys['v' . $isoCode] = [];
470 $allowedValueLevelLanguageKeys['v' . $isoCode . '.vDEFbase'] = [];
471 $allowedValueLevelLanguageKeys['_TRANSFORM_v' . $isoCode . '.vDEFbase'] = [];
472 }
473 $result = $this->setLanguageValueLevelValues($result, $fieldName, $allowedValueLevelLanguageKeys);
474 }
475
476 return $result;
477 }
478
479 /**
480 * Feed single flex field and data to FlexFormSegment FormData compiler and merge result.
481 * This one is nasty. Goal is to have processed TCA stuff in DS and also have validated / processed data values.
482 *
483 * Three main parts in this method:
484 * * Process values of existing section container for default values
485 * * Process values and TCA of possible section container and create a default value row for each
486 * * Process TCA of "normal" fields and have default values in data ['templateRows']['containerName'] parallel to section ['el']
487 *
488 * @param array $result Result array
489 * @param string $fieldName Current handle field name
490 * @param array $pageTsConfig Given pageTsConfig of this flex form
491 * @return array Modified item array
492 */
493 protected function modifyDataStructureAndDataValuesByFlexFormSegmentGroup(array $result, $fieldName, $pageTsConfig) {
494 $dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
495 $dataValues = $result['databaseRow'][$fieldName];
496
497 $availableLanguageCodes = $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['availableLanguageCodes'];
498 if ($dataStructure['meta']['langChildren']) {
499 $languagesOnSheetLevel = [ 'DEF' ];
500 $languagesOnElementLevel = $availableLanguageCodes;
501 } else {
502 $languagesOnSheetLevel = $availableLanguageCodes;
503 $languagesOnElementLevel = [ 'DEF' ];
504 }
505
506 $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['languagesOnSheetLevel'] = $languagesOnSheetLevel;
507 $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['languagesOnElement'] = $languagesOnElementLevel;
508
509 if (!isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
510 return $result;
511 }
512
513 /** @var FlexFormSegment $formDataGroup */
514 $formDataGroup = GeneralUtility::makeInstance(FlexFormSegment::class);
515 /** @var FormDataCompiler $formDataCompiler */
516 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
517
518 foreach ($dataStructure['sheets'] as $dataStructureSheetName => $dataStructureSheetDefinition) {
519 if (!isset($dataStructureSheetDefinition['ROOT']['el']) || !is_array($dataStructureSheetDefinition['ROOT']['el'])) {
520 continue;
521 }
522 $dataStructureSheetElements = $dataStructureSheetDefinition['ROOT']['el'];
523
524 // Prepare pageTsConfig of this sheet
525 $pageTsConfigMerged['TCEFORM.']['flexDummyTable.'] = [];
526 if (isset($pageTsConfig[$dataStructureSheetName . '.']) && is_array($pageTsConfig[$dataStructureSheetName . '.'])) {
527 $pageTsConfigMerged['TCEFORM.']['flexDummyTable.'] = $pageTsConfig[$dataStructureSheetName . '.'];
528 }
529
530 foreach ($languagesOnSheetLevel as $isoSheetLevel) {
531 $langSheetLevel = 'l' . $isoSheetLevel;
532 foreach ($dataStructureSheetElements as $dataStructureSheetElementName => $dataStructureSheetElementDefinition) {
533 if (isset($dataStructureSheetElementDefinition['type']) && $dataStructureSheetElementDefinition['type'] === 'array'
534 && isset($dataStructureSheetElementDefinition['section']) && $dataStructureSheetElementDefinition['section'] === '1'
535 ) {
536 // A section
537
538 // Existing section container elements
539 if (isset($dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['el'])
540 && is_array($dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['el'])
541 ) {
542 $containerArray = $dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['el'];
543 foreach ($containerArray as $aContainerNumber => $aContainerArray) {
544 if (is_array($aContainerArray)) {
545 foreach ($aContainerArray as $aContainerName => $aContainerElementArray) {
546 if ($aContainerName === '_TOGGLE') {
547 // Don't handle internal toggle state field
548 continue;
549 }
550 if (!isset($dataStructureSheetElements[$dataStructureSheetElementName]['el'][$aContainerName])) {
551 // Container not defined in ds
552 continue;
553 }
554 foreach ($dataStructureSheetElements[$dataStructureSheetElementName]['el'][$aContainerName]['el'] as $singleFieldName => $singleFieldConfiguration) {
555 // $singleFieldValueArray = ['data']['sSections']['lDEF']['section_1']['el']['1']['container_1']['el']['element_1']
556 $singleFieldValueArray = [];
557 if (isset($aContainerElementArray['el'][$singleFieldName])
558 && is_array($aContainerElementArray['el'][$singleFieldName])
559 ) {
560 $singleFieldValueArray = $aContainerElementArray['el'][$singleFieldName];
561 }
562 foreach ($languagesOnElementLevel as $isoElementLevel) {
563 $langElementLevel = 'v' . $isoElementLevel;
564 $valueArray = [];
565 $command = 'new';
566 if (array_key_exists($langElementLevel, $singleFieldValueArray)) {
567 $command = 'edit';
568 $valueArray[$singleFieldName] = $singleFieldValueArray[$langElementLevel];
569 }
570 $inputToFlexFormSegment = [
571 'tableName' => 'flexDummyTable',
572 'command' => $command,
573 // It is currently not possible to have pageTsConfig for section container
574 'pageTsConfigMerged' => [],
575 'databaseRow' => $valueArray,
576 'vanillaTableTca' => [
577 'ctrl' => [],
578 'columns' => [
579 $singleFieldName => $singleFieldConfiguration,
580 ],
581 ],
582 'processedTca' => [
583 'ctrl' => [],
584 'columns' => [
585 $singleFieldName => $singleFieldConfiguration,
586 ],
587 ],
588 ];
589 $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
590 // Set data value result
591 if (array_key_exists($singleFieldName, $flexSegmentResult['databaseRow'])) {
592 $result['databaseRow'][$fieldName]
593 ['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['el']
594 [$aContainerNumber][$aContainerName]['el']
595 [$singleFieldName][$langElementLevel]
596 = $flexSegmentResult['databaseRow'][$singleFieldName];
597 }
598 // Set TCA structure result, actually, this call *might* be obsolete since the "dummy"
599 // handling below will set it again.
600 $result['processedTca']['columns'][$fieldName]['config']['ds']
601 ['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureSheetElementName]['el']
602 [$aContainerName]['el'][$singleFieldName]
603 = $flexSegmentResult['processedTca']['columns'][$singleFieldName];
604 }
605 }
606 }
607 }
608 }
609 } // End of existing data value handling
610
611 // Prepare "fresh" row for every possible container
612 if (isset($dataStructureSheetElements[$dataStructureSheetElementName]['el']) && is_array($dataStructureSheetElements[$dataStructureSheetElementName]['el'])) {
613 foreach ($dataStructureSheetElements[$dataStructureSheetElementName]['el'] as $possibleContainerName => $possibleContainerConfiguration) {
614 if (isset($possibleContainerConfiguration['el']) && is_array($possibleContainerConfiguration['el'])) {
615 // Initialize result data array templateRows
616 $result['databaseRow'][$fieldName]
617 ['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['templateRows']
618 [$possibleContainerName]['el']
619 = [];
620 foreach ($possibleContainerConfiguration['el'] as $singleFieldName => $singleFieldConfiguration) {
621 foreach ($languagesOnElementLevel as $isoElementLevel) {
622 $langElementLevel = 'v' . $isoElementLevel;
623 $inputToFlexFormSegment = [
624 'tableName' => 'flexDummyTable',
625 'command' => 'new',
626 'pageTsConfigMerged' => [],
627 'databaseRow' => [],
628 'vanillaTableTca' => [
629 'ctrl' => [],
630 'columns' => [
631 $singleFieldName => $singleFieldConfiguration,
632 ],
633 ],
634 'processedTca' => [
635 'ctrl' => [],
636 'columns' => [
637 $singleFieldName => $singleFieldConfiguration,
638 ],
639 ],
640 ];
641 $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
642 if (array_key_exists($singleFieldName, $flexSegmentResult['databaseRow'])) {
643 $result['databaseRow'][$fieldName]
644 ['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName]['templateRows']
645 [$possibleContainerName]['el'][$singleFieldName][$langElementLevel]
646 = $flexSegmentResult['databaseRow'][$singleFieldName];
647 }
648 $result['processedTca']['columns'][$fieldName]['config']['ds']
649 ['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureSheetElementName]['el']
650 [$possibleContainerName]['el'][$singleFieldName]
651 = $flexSegmentResult['processedTca']['columns'][$singleFieldName];
652 }
653 }
654 }
655 }
656 } // End of preparation for each possible container
657
658 // type without section is not ok
659 } elseif (isset($dataStructureSheetElementDefinition['type']) || isset($dataStructureSheetElementDefinition['section'])) {
660 throw new \UnexpectedValueException(
661 'Broken data structure on field name ' . $fieldName . '. section without type or vice versa is not allowed',
662 1440685208
663 );
664
665 // A "normal" TCA element
666 } else {
667 foreach ($languagesOnElementLevel as $isoElementLevel) {
668 $langElementLevel = 'v' . $isoElementLevel;
669 $valueArray = [];
670 $command = 'new';
671 if (isset($dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName])
672 && array_key_exists($langElementLevel, $dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName])
673 ) {
674 $command = 'edit';
675 $valueArray[$dataStructureSheetElementName] = $dataValues['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName][$langElementLevel];
676 }
677 $inputToFlexFormSegment = [
678 'tableName' => 'flexDummyTable',
679 'command' => $command,
680 'pageTsConfigMerged' => $pageTsConfigMerged,
681 'databaseRow' => $valueArray,
682 'vanillaTableTca' => [
683 'ctrl' => [],
684 'columns' => [
685 $dataStructureSheetElementName => $dataStructureSheetElementDefinition,
686 ],
687 ],
688 'processedTca' => [
689 'ctrl' => [],
690 'columns' => [
691 $dataStructureSheetElementName => $dataStructureSheetElementDefinition,
692 ],
693 ],
694 ];
695 $flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment);
696 // Set data value result
697 if (array_key_exists($dataStructureSheetElementName, $flexSegmentResult['databaseRow'])) {
698 $result['databaseRow'][$fieldName]
699 ['data'][$dataStructureSheetName][$langSheetLevel][$dataStructureSheetElementName][$langElementLevel]
700 = $flexSegmentResult['databaseRow'][$dataStructureSheetElementName];
701 }
702 // Set TCA structure result
703 $result['processedTca']['columns'][$fieldName]['config']['ds']
704 ['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureSheetElementName]
705 = $flexSegmentResult['processedTca']['columns'][$dataStructureSheetElementName];
706 }
707 } // End of single element handling
708
709 }
710 }
711 }
712
713 return $result;
714 }
715
716 /**
717 * Single fields can be extracted to files again. This is resolved here.
718 *
719 * @todo: Why is this not done in BackendUtility::getFlexFormDS() directly? If done there, the two methods
720 * @todo: GeneralUtility::resolveSheetDefInDS() and GeneralUtility::resolveAllSheetsInDS() could be killed
721 * @todo: since this resolving is basically the only really useful thing they actually do.
722 *
723 * @param string|array $sheetStructure Not resolved structure
724 * @return array Parsed sheet structure
725 */
726 protected function resolvePossibleExternalFile($sheetStructure) {
727 if (!is_array($sheetStructure)) {
728 $file = GeneralUtility::getFileAbsFileName($sheetStructure);
729 if ($file && @is_file($file)) {
730 $sheetStructure = GeneralUtility::xml2array(GeneralUtility::getUrl($file));
731 }
732 }
733 return $sheetStructure;
734 }
735
736 /**
737 * Modify data structure of a single "sheet"
738 * Sets "secondary" data like sheet names and so on, but does NOT modify single elements
739 *
740 * @param array $dataStructure Given data structure
741 * @param array $pageTsOfSheet Page Ts config of given field
742 * @return array Modified data structure
743 */
744 protected function modifySingleSheetInformation(array $dataStructure, array $pageTsOfSheet) {
745 // Return if no elements defined
746 if (!isset($dataStructure['ROOT']['el']) || !is_array($dataStructure['ROOT']['el'])) {
747 return $dataStructure;
748 }
749
750 // Rename sheet (tab)
751 if (!empty($pageTsOfSheet['sheetTitle'])) {
752 $dataStructure['ROOT']['TCEforms']['sheetTitle'] = $pageTsOfSheet['sheetTitle'];
753 }
754 // Set sheet description (tab)
755 if (!empty($pageTsOfSheet['sheetDescription'])) {
756 $dataStructure['ROOT']['TCEforms']['sheetDescription'] = $pageTsOfSheet['sheetDescription'];
757 }
758 // Set sheet short description (tab)
759 if (!empty($pageTsOfSheet['sheetShortDescr'])) {
760 $dataStructure['ROOT']['TCEforms']['sheetShortDescr'] = $pageTsOfSheet['sheetShortDescr'];
761 }
762
763 return $dataStructure;
764 }
765
766 /**
767 * Add new sheet languages not yet in data values and remove invalid ones
768 *
769 * databaseRow['aFlex']['data']['sDEF'] = array('lDEF', 'lNotAllowed');
770 * allowedLanguageKeys = array('lDEF', 'lNEW')
771 * -> databaseRow['aFlex']['data']['sDEF'] = array('lDEF', 'lNEW');
772 *
773 * @param array $result Result array
774 * @param string $fieldName Current handle field name
775 * @param array $allowedKeys List of allowed keys
776 * @return array Modified result
777 */
778 protected function setLanguageSheetsInDataValues(array $result, $fieldName, array $allowedKeys) {
779 $availableLanguageCodes = $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['availableLanguageCodes'];
780 $valueArray = [];
781 if (isset($result['databaseRow'][$fieldName]['data']) && is_array($result['databaseRow'][$fieldName]['data'])) {
782 $valueArray = $result['databaseRow'][$fieldName]['data'];
783 }
784 foreach ($valueArray as $sheetName => $sheetLanguages) {
785 // Add iso code with empty array if it does not yet exist in data
786 // and remove codes from data that do not exist in $allowed
787 $result['databaseRow'][$fieldName]['data'][$sheetName]
788 = array_intersect_key(array_merge($allowedKeys, $sheetLanguages), $allowedKeys);
789 }
790 return $result;
791 }
792
793 /**
794 * Remove invalid keys from data value array the user has no access to
795 * or that were removed or similar to prevent any rendering of this stuff
796 *
797 * Handles this for "normal" fields and also for section container element values.
798 *
799 * @param array $result Result array
800 * @param string $fieldName Current handle field name
801 * @param array $allowedKeys List of allowed keys
802 * @return array Modified result
803 */
804 protected function setLanguageValueLevelValues(array $result, $fieldName, $allowedKeys) {
805 $valueArray = [];
806 if (isset($result['databaseRow'][$fieldName]['data']) && is_array($result['databaseRow'][$fieldName]['data'])) {
807 $valueArray = $result['databaseRow'][$fieldName]['data'];
808 }
809 foreach ($valueArray as $sheetName => $sheetLanguages) {
810 if (!is_array($sheetLanguages)) {
811 continue;
812 }
813 foreach ($sheetLanguages as $languageName => $languageFields) {
814 if (!is_array($languageFields)) {
815 continue;
816 }
817 foreach ($languageFields as $flexFieldName => $fieldValues) {
818 if (!is_array($fieldValues)) {
819 continue;
820 }
821 $allowedSingleValues = [];
822 foreach ($fieldValues as $fieldValueName => $fieldValueValue) {
823 if (is_array($fieldValueValue) && $fieldValueName === 'el') {
824 // A section container
825 foreach ($fieldValueValue as $sectionNumber => $sectionElementArray) {
826 if (is_array($sectionElementArray)) {
827 $allowedSingleValues['el'][$sectionNumber] = [];
828 foreach ($sectionElementArray as $sectionElementName => $containerElementArray) {
829 if (isset($containerElementArray['el']) && is_array($containerElementArray['el']) && !empty($containerElementArray['el'])) {
830 foreach ($containerElementArray['el'] as $aContainerElementName => $aContainerElementValues) {
831 if (is_array($aContainerElementValues)) {
832 foreach ($aContainerElementValues as $aContainerElementValueKey => $aContainerElementValueValue) {
833 if (array_key_exists($aContainerElementValueKey, $allowedKeys)) {
834 $allowedSingleValues['el'][$sectionNumber][$sectionElementName]
835 ['el'][$aContainerElementName][$aContainerElementValueKey] = $aContainerElementValueValue;
836 }
837 }
838 } else {
839 $allowedSingleValues['el'][$sectionNumber][$sectionElementName]['el']
840 [$aContainerElementName] = $aContainerElementValues;
841 }
842 }
843 } else {
844 $allowedSingleValues['el'][$sectionNumber][$sectionElementName] = $containerElementArray;
845 }
846 }
847 } else {
848 $allowedSingleValues = $sectionElementArray;
849 }
850 }
851 } else {
852 // "normal" value field
853 if (array_key_exists($fieldValueName, $allowedKeys)) {
854 $allowedSingleValues[$fieldValueName] = $fieldValueValue;
855 }
856 }
857 }
858 $result['databaseRow'][$fieldName]['data'][$sheetName][$languageName][$flexFieldName] = $allowedSingleValues;
859 }
860 }
861 }
862 return $result;
863 }
864
865 /**
866 * On-the-fly migration for flex form "TCA"
867 *
868 * @param array $result Result array
869 * @param string $fieldName Currently handled field name
870 * @return array Modified result
871 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8. This can be removed *if* no additional TCA migration is added with CMS 8, see class TcaMigration
872 */
873 protected function migrateFlexformTcaDataStructureElements(array $result, $fieldName) {
874 $modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
875 $modifiedDataStructure = $this->migrateFlexformTcaRecursive($modifiedDataStructure, $result['tableName'], $fieldName);
876 $result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
877 return $result;
878 }
879
880 /**
881 * Recursively migrate flex form TCA
882 *
883 * @param array $structure Given hierarchy
884 * @param string $table
885 * @param string $fieldName
886 * @return array Modified hierarchy
887 */
888
889 protected function migrateFlexformTcaRecursive($structure, $table, $fieldName) {
890 $newStructure = [];
891 foreach ($structure as $key => $value) {
892 if ($key === 'el' && is_array($value)) {
893 $newSubStructure = [];
894 $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class);
895 foreach ($value as $subKey => $subValue) {
896 // On-the-fly migration for flex form "TCA"
897 // @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8. This can be removed *if* no additional TCA migration is added with CMS 8, see class TcaMigration
898 $dummyTca = array(
899 'dummyTable' => array(
900 'columns' => array(
901 'dummyField' => $subValue,
902 ),
903 ),
904 );
905 $migratedTca = $tcaMigration->migrate($dummyTca);
906 $messages = $tcaMigration->getMessages();
907 if (!empty($messages)) {
908 $context = 'FormEngine did an on-the-fly migration of a flex form data structure. This is deprecated and will be removed'
909 . ' with TYPO3 CMS 8. Merge the following changes into the flex form definition of table "' . $table . '"" in field "' . $fieldName . '"":';
910 array_unshift($messages, $context);
911 GeneralUtility::deprecationLog(implode(LF, $messages));
912 }
913 $newSubStructure[$subKey] = $migratedTca['dummyTable']['columns']['dummyField'];
914 }
915 $value = $newSubStructure;
916 }
917 if (is_array($value)) {
918 $value = $this->migrateFlexformTcaRecursive($value, $table, $fieldName);
919 }
920 $newStructure[$key] = $value;
921 }
922 return $newStructure;
923 }
924
925 /**
926 * @return BackendUserAuthentication
927 */
928 protected function getBackendUser() {
929 return $GLOBALS['BE_USER'];
930 }
931
932 }