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