‪TYPO3CMS  11.5
FlexFormTools.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
35 
40 {
46  public ‪$reNumberIndexesOfSectionData = false;
47 
54  public ‪$flexArray2Xml_options = [
55  'parentTagMap' => [
56  'data' => 'sheet',
57  'sheet' => 'language',
58  'language' => 'field',
59  'el' => 'field',
60  'field' => 'value',
61  'field:el' => 'el',
62  'el:_IS_NUM' => 'section',
63  'section' => 'itemType',
64  ],
65  'disableTypeAttrib' => 2,
66  ];
67 
73  public ‪$callBackObj;
74 
80  public ‪$cleanFlexFormXML = [];
81 
120  public function ‪getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row): string
121  {
122  $dataStructureIdentifier = null;
123  // Hook to inject an own logic to point to a data structure elsewhere.
124  // A hook has to implement method getDataStructureIdentifierPreProcess() to be called here.
125  // All hooks are called in a row, each MUST return an array, and the FIRST one that
126  // returns a non-empty array is used as final identifier.
127  // It is important to restrict hooks as much as possible to give other hooks a chance to kick in.
128  // The returned identifier is later given to parseFlexFormDataStructureByIdentifier() and a hook in there MUST
129  // be used to handle this identifier again.
130  // Warning: If adding source record details like the uid or pid here, this may turn out to be fragile.
131  // Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may
132  // break in between different core versions.
133  // It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see
134  // the core internal 'tca' and 'record' return values below
135  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
136  && is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
137  $hookClasses = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
138  foreach ($hookClasses as $hookClass) {
139  $hookInstance = GeneralUtility::makeInstance($hookClass);
140  if (method_exists($hookClass, 'getDataStructureIdentifierPreProcess')) {
141  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPreProcess(
142  $fieldTca,
143  $tableName,
144  $fieldName,
145  $row
146  );
147  if (!is_array($dataStructureIdentifier)) {
148  throw new \RuntimeException(
149  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPreProcess must return an array',
150  1478096535
151  );
152  }
153  if (!empty($dataStructureIdentifier)) {
154  // Early break at first hook that returned something!
155  break;
156  }
157  }
158  }
159  }
160 
161  // If hooks didn't return something, kick in core logic
162  if (empty($dataStructureIdentifier)) {
163  $tcaDataStructureArray = $fieldTca['config']['ds'] ?? null;
164  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
165  if (!is_array($tcaDataStructureArray) && $tcaDataStructurePointerField) {
166  // "ds" is not an array, but "ds_pointerField" is set -> data structure is found in different table
167  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromRecord(
168  $fieldTca,
169  $tableName,
170  $fieldName,
171  $row
172  );
173  } elseif (is_array($tcaDataStructureArray)) {
174  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromTcaArray(
175  $fieldTca,
176  $tableName,
177  $fieldName,
178  $row
179  );
180  } else {
181  throw new \RuntimeException(
182  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
183  . ' The field is configured as type="flex" and no "ds_pointerField" is defined and "ds" is not an array.'
184  . ' Either configure a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
185  . ' that specifies the data structure',
186  1463826960
187  );
188  }
189  }
190 
191  // Second hook to manipulate identifier again. This can be used to add additional data to
192  // identifiers. Be careful here, especially if stuff from the source record like uid or pid
193  // is added! This may easily lead to issues with data handler details like copy or move records,
194  // localization and version overlays. Test this very well!
195  // Multiple hooks may add information to the same identifier here - take care to namespace array keys.
196  // Information added here can be later used in parseDataStructureByIdentifier post process hook again.
197  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
198  && is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
199  $hookClasses = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
200  foreach ($hookClasses as $hookClass) {
201  $hookInstance = GeneralUtility::makeInstance($hookClass);
202  if (method_exists($hookClass, 'getDataStructureIdentifierPostProcess')) {
203  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPostProcess(
204  $fieldTca,
205  $tableName,
206  $fieldName,
207  $row,
208  $dataStructureIdentifier
209  );
210  if (!is_array($dataStructureIdentifier) || empty($dataStructureIdentifier)) {
211  throw new \RuntimeException(
212  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPostProcess must return a non empty array',
213  1478350835
214  );
215  }
216  }
217  }
218  }
219 
220  return json_encode($dataStructureIdentifier);
221  }
222 
272  protected function ‪getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row): array
273  {
274  $pointerFieldName = $finalPointerFieldName = $fieldTca['config']['ds_pointerField'];
275  if (!array_key_exists($pointerFieldName, $row)) {
276  // Pointer field does not exist in row at all -> throw
277  throw new ‪InvalidTcaException(
278  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
279  . ' configured and given row does not have a field with ds_pointerField name "' . $pointerFieldName . '".',
280  1464115059
281  );
282  }
283  $pointerValue = $row[$pointerFieldName];
284  // If set, this is typically set to "pid"
285  $parentFieldName = $fieldTca['config']['ds_pointerField_searchParent'] ?? null;
286  $pointerSubFieldName = $fieldTca['config']['ds_pointerField_searchParent_subField'] ?? null;
287  if (!$pointerValue && $parentFieldName) {
288  // Fetch rootline until a valid pointer value is found
289  $handledUids = [];
290  while (!$pointerValue) {
291  $handledUids[$row['uid']] = 1;
292  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
293  $queryBuilder->getRestrictions()
294  ->removeAll()
295  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
296  $queryBuilder->select('uid', $parentFieldName, $pointerFieldName);
297  if (!empty($pointerSubFieldName)) {
298  $queryBuilder->addSelect($pointerSubFieldName);
299  }
300  $queryStatement = $queryBuilder->from($tableName)
301  ->where(
302  $queryBuilder->expr()->eq(
303  'uid',
304  $queryBuilder->createNamedParameter($row[$parentFieldName], ‪Connection::PARAM_INT)
305  )
306  )
307  ->executeQuery();
308  $rowCount = $queryBuilder
309  ->count('uid')
310  ->executeQuery()
311  ->fetchOne();
312  if ($rowCount !== 1) {
314  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
315  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
316  . ' with uid "' . $row[$parentFieldName] . '" was done. This row however does not exist or was deleted.',
317  1463833794
318  );
319  }
320  $row = $queryStatement->fetchAssociative();
321  if (isset($handledUids[$row[$parentFieldName]])) {
322  // Row has been fetched before already -> loop detected!
324  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
325  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
326  . ' with uid "' . $row[$parentFieldName] . '" was done. A loop of records was detected, the tree is broken.',
327  1464110956
328  );
329  }
330  BackendUtility::workspaceOL($tableName, $row);
331  // New pointer value: This is the "subField" value if given, else the field value
332  // ds_pointerField_searchParent_subField is the "template on next level" structure from templavoila
333  if ($pointerSubFieldName && $row[$pointerSubFieldName]) {
334  $finalPointerFieldName = $pointerSubFieldName;
335  $pointerValue = $row[$pointerSubFieldName];
336  } else {
337  $pointerValue = $row[$pointerFieldName];
338  }
339  if (!$pointerValue && ((int)$row[$parentFieldName] === 0 || $row[$parentFieldName] === null)) {
340  // If on root level and still no valid pointer found -> exception
342  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
343  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
344  . ' with uid "' . $row[$parentFieldName] . '" was done. Root node with uid "' . $row['uid'] . '"'
345  . ' was fetched and still no valid pointer field value was found.',
346  1464112555
347  );
348  }
349  }
350  }
351  if (!$pointerValue) {
352  // Still no valid pointer value -> exception, This still can be a data integrity issue, so throw a catchable exception
354  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
355  . ' configured and data structure could be found by resolving parents. This is probably a TCA misconfiguration.',
356  1464114011
357  );
358  }
359  // Ok, finally we have the field value. This is now either a data structure directly, or a pointer to a file,
360  // or the value can be interpreted as integer (is a uid) and "ds_tableField" is set, so this is the table, uid and field
361  // where the final data structure can be found.
362  if (‪MathUtility::canBeInterpretedAsInteger($pointerValue)) {
363  if (!isset($fieldTca['config']['ds_tableField'])) {
364  throw new ‪InvalidTcaException(
365  'Invalid data structure pointer for field "' . $fieldName . '" in table "' . $tableName . '", the value'
366  . 'resolved to "' . $pointerValue . '" . which is an integer, so "ds_tableField" must be configured',
367  1464115639
368  );
369  }
370  if (substr_count($fieldTca['config']['ds_tableField'], ':') !== 1) {
371  // ds_tableField must be of the form "table:field"
372  throw new ‪InvalidTcaException(
373  'Invalid TCA configuration for field "' . $fieldName . '" in table "' . $tableName . '", the setting'
374  . '"ds_tableField" must be of the form "tableName:fieldName"',
375  1464116002
376  );
377  }
378  [$foreignTableName, $foreignFieldName] = ‪GeneralUtility::trimExplode(':', $fieldTca['config']['ds_tableField']);
379  $dataStructureIdentifier = [
380  'type' => 'record',
381  'tableName' => $foreignTableName,
382  'uid' => (int)$pointerValue,
383  'fieldName' => $foreignFieldName,
384  ];
385  } else {
386  $dataStructureIdentifier = [
387  'type' => 'record',
388  'tableName' => $tableName,
389  'uid' => (int)$row['uid'],
390  'fieldName' => $finalPointerFieldName,
391  ];
392  }
393  return $dataStructureIdentifier;
394  }
395 
437  protected function ‪getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row): array
438  {
439  $dataStructureIdentifier = [
440  'type' => 'tca',
441  'tableName' => $tableName,
442  'fieldName' => $fieldName,
443  'dataStructureKey' => null,
444  ];
445  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
446  if ($tcaDataStructurePointerField === null) {
447  // No ds_pointerField set -> use 'default' as ds array key if exists.
448  if (isset($fieldTca['config']['ds']['default'])) {
449  $dataStructureIdentifier['dataStructureKey'] = 'default';
450  } else {
451  // A tca is configured as flex without ds_pointerField. A 'default' key must exist, otherwise
452  // this is a configuration error.
453  // May happen with an unloaded extension -> catchable
454  throw new ‪InvalidTcaException(
455  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
456  . ' The field is configured as type="flex" and no "ds_pointerField" is defined. Either configure'
457  . ' a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
458  . ' that specifies the data structure',
459  1463652560
460  );
461  }
462  } else {
463  // ds_pointerField is set, it can be a comma separated list of two fields, explode it.
464  $pointerFieldArray = ‪GeneralUtility::trimExplode(',', $tcaDataStructurePointerField, true);
465  // Obvious configuration error, either one or two fields must be declared
466  $pointerFieldsCount = count($pointerFieldArray);
467  if ($pointerFieldsCount !== 1 && $pointerFieldsCount !== 2) {
468  // If it's there, it must be correct -> not catchable
469  throw new \RuntimeException(
470  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
471  . ' ds_pointerField must be either a single field name, or a comma separated list of two fields,'
472  . ' the invalid configuration string provided was: "' . $tcaDataStructurePointerField . '"',
473  1463577497
474  );
475  }
476  // Verify first field exists in row array. If not, this is a hard error: Any extension that sets a
477  // ds_pointerField to some field name should take care that field does exist, too. They are a pair,
478  // so there shouldn't be a situation where the field does not exist. Throw an exception if that is violated.
479  if (!isset($row[$pointerFieldArray[0]])) {
480  // If it's declared, it must exist -> not catchable
481  throw new \RuntimeException(
482  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
483  . ' ds_pointerField "' . $pointerFieldArray[0] . '" points to a field name that does not exist.',
484  1463578899
485  );
486  }
487  // Similar situation for the second field: If it is set, the field must exist.
488  if (isset($pointerFieldArray[1]) && !isset($row[$pointerFieldArray[1]])) {
489  // If it's declared, it must exist -> not catchable
490  throw new \RuntimeException(
491  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
492  . ' Second part "' . $pointerFieldArray[1] . '" of ds_pointerField with full value "'
493  . $tcaDataStructurePointerField . '" points to a field name that does not exist.',
494  1463578900
495  );
496  }
497  if ($pointerFieldsCount === 1) {
498  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
499  // Field value points directly to an existing key in tca ds
500  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
501  } elseif (isset($fieldTca['config']['ds']['default'])) {
502  // Field value does not exit in tca ds, fall back to default key if exists
503  $dataStructureIdentifier['dataStructureKey'] = 'default';
504  } else {
505  // The value of the ds_pointerField field points to a key in the ds array that does
506  // not exist, and there is no fallback either. This can happen if an extension brings
507  // new flex form definitions and that extension is unloaded later. "Old" records of the
508  // extension could then still point to the no longer existing key in ds. We throw a
509  // specific exception here to give controllers an opportunity to catch this case.
511  'Field value of field "' . $pointerFieldArray[0] . '" of database record with uid "'
512  . $row['uid'] . '" from table "' . $tableName . '" points to a "ds" key ' . $row[$pointerFieldArray[0]]
513  . ' but this key does not exist and there is no "default" fallback.',
514  1463653197
515  );
516  }
517  } else {
518  // Two comma separated field names
519  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]]])) {
520  // firstValue,secondValue
521  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]];
522  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',*'])) {
523  // firstValue,*
524  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',*';
525  } elseif (isset($fieldTca['config']['ds']['*,' . $row[$pointerFieldArray[1]]])) {
526  // *,secondValue
527  $dataStructureIdentifier['dataStructureKey'] = '*,' . $row[$pointerFieldArray[1]];
528  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
529  // firstValue
530  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
531  } elseif (isset($fieldTca['config']['ds']['default'])) {
532  // Fall back to default
533  $dataStructureIdentifier['dataStructureKey'] = 'default';
534  } else {
535  // No ds_pointerField value could be determined and 'default' does not exist as
536  // fallback. This is the same case as the above scenario, throw a
537  // InvalidCombinedPointerFieldException here, too.
539  'Field combination of fields "' . $pointerFieldArray[0] . '" and "' . $pointerFieldArray[1] . '" of database'
540  . 'record with uid "' . $row['uid'] . '" from table "' . $tableName . '" with values "' . $row[$pointerFieldArray[0]] . '"'
541  . ' and "' . $row[$pointerFieldArray[1]] . '" could not be resolved to any registered data structure and '
542  . ' no "default" fallback exists.',
543  1463678524
544  );
545  }
546  }
547  }
548  return $dataStructureIdentifier;
549  }
550 
579  public function ‪parseDataStructureByIdentifier(string $identifier): array
580  {
581  // Throw an exception for an empty string. This might be a valid use case for new
582  // records in some situations, so this is catchable to give callers a chance to deal with that.
583  if (empty($identifier)) {
585  'Empty string given to parseFlexFormDataStructureByIdentifier(). This exception might '
586  . ' be caught to handle some new record situations properly',
587  1478100828
588  );
589  }
590 
591  $identifier = json_decode($identifier, true);
592 
593  if (!is_array($identifier) || empty($identifier)) {
594  // If there is some identifier and it can't be decoded, programming error -> not catchable
595  throw new \RuntimeException(
596  'Identifier could not be decoded to an array.',
597  1478345642
598  );
599  }
600 
601  $dataStructure = '';
602 
603  // Hook to fetch data structure by given identifier.
604  // Method parseFlexFormDataStructureByIdentifier() must be implemented and returns either an
605  // empty string "not my business", or a string with the resolved data structure string, or FILE: reference,
606  // or a fully parsed data structure as array.
607  // Result of the FIRST hook that gives a non-empty string is used, namespace your identifiers in
608  // a way that there is little chance they overlap (eg. prefix with extension name).
609  // If implemented, this hook should be paired with a hook in getDataStructureIdentifier() above.
610  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'] ?? [] as $hookClass) {
611  $hookInstance = GeneralUtility::makeInstance($hookClass);
612  if (method_exists($hookClass, 'parseDataStructureByIdentifierPreProcess')) {
613  $dataStructure = $hookInstance->parseDataStructureByIdentifierPreProcess($identifier);
614  if (!is_string($dataStructure) && !is_array($dataStructure)) {
615  // Programming error -> not catchable
616  throw new \RuntimeException(
617  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must either'
618  . ' return an empty string or a data structure string or a parsed data structure array.',
619  1478168512
620  );
621  }
622  if (!empty($dataStructure)) {
623  // Early break if a hook resolved to something!
624  break;
625  }
626  }
627  }
628 
629  // If hooks didn't resolve, try own methods
630  if (empty($dataStructure)) {
631  if ($identifier['type'] === 'record') {
632  // Handle "record" type, see getDataStructureIdentifierFromRecord()
633  if (empty($identifier['tableName']) || empty($identifier['uid']) || empty($identifier['fieldName'])) {
634  throw new \RuntimeException(
635  'Incomplete "record" based identifier: ' . json_encode($identifier),
636  1478113873
637  );
638  }
639  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($identifier['tableName']);
640  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
641  $dataStructure = $queryBuilder
642  ->select($identifier['fieldName'])
643  ->from($identifier['tableName'])
644  ->where(
645  $queryBuilder->expr()->eq(
646  'uid',
647  $queryBuilder->createNamedParameter($identifier['uid'], ‪Connection::PARAM_INT)
648  )
649  )
650  ->executeQuery()
651  ->fetchOne();
652  } elseif ($identifier['type'] === 'tca') {
653  // Handle "tca" type, see getDataStructureIdentifierFromTcaArray
654  if (empty($identifier['tableName']) || empty($identifier['fieldName']) || empty($identifier['dataStructureKey'])) {
655  throw new \RuntimeException(
656  'Incomplete "tca" based identifier: ' . json_encode($identifier),
657  1478113471
658  );
659  }
660  $table = $identifier['tableName'];
661  $field = $identifier['fieldName'];
662  $dataStructureKey = $identifier['dataStructureKey'];
663  if (!isset(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
664  || !is_string(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
665  ) {
666  // This may happen for elements pointing to an unloaded extension -> catchable
668  'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid'
669  . ' TCA array value',
670  1478105491
671  );
672  }
673  $dataStructure = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey];
674  } else {
676  'Identifier ' . json_encode($identifier) . ' could not be resolved',
677  1478104554
678  );
679  }
680  }
681 
682  // Hooks may have parsed the data structure already to an array. If that is not the case, parse it now.
683  if (is_string($dataStructure)) {
684  // Resolve FILE: prefix pointing to a DS in a file
685  if (strpos(trim($dataStructure), 'FILE:') === 0) {
686  $file = GeneralUtility::getFileAbsFileName(substr(trim($dataStructure), 5));
687  if (empty($file) || !@is_file($file)) {
688  throw new \RuntimeException(
689  'Data structure file ' . $file . ' could not be resolved to an existing file',
690  1478105826
691  );
692  }
693  $dataStructure = (string)file_get_contents($file);
694  }
695 
696  // Parse main structure
697  $dataStructure = ‪GeneralUtility::xml2array($dataStructure);
698  }
699 
700  // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
701  // This also may happen if artificial identifiers were constructed which don't resolve. The
702  // flex form "exclude" access rights systems does that -> catchable
703  if (!is_array($dataStructure)) {
705  'Parse error: Data structure could not be resolved to a valid structure.',
706  1478106090
707  );
708  }
709 
710  // Create default sheet if there is none, yet.
711  if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
712  throw new \RuntimeException(
713  'Parsed data structure has both ROOT and sheets on top level. That is invalid.',
714  1440676540
715  );
716  }
717  if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
718  $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
719  unset($dataStructure['ROOT']);
720  }
721 
722  // Resolve FILE:EXT and EXT: for single sheets
723  if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
724  foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
725  if (!is_array($sheetStructure)) {
726  if (strpos(trim($sheetStructure), 'FILE:') === 0) {
727  $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
728  } else {
729  $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
730  }
731  if ($file && @is_file($file)) {
732  $sheetStructure = ‪GeneralUtility::xml2array((string)file_get_contents($file));
733  }
734  }
735  $dataStructure['sheets'][$sheetName] = $sheetStructure;
736 
737  if (is_array($dataStructure['sheets'][$sheetName])) {
738  $dataStructure['sheets'][$sheetName] = $this->‪prepareCategoryFields($dataStructure['sheets'][$sheetName]);
739  }
740  }
741  }
742 
743  // Hook to manipulate data structure further. This can be used to add or remove fields
744  // from given structure. Multiple hooks can be registered, all are called. They
745  // receive the parsed structure and the identifier array.
746  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'] ?? [] as $hookClass) {
747  $hookInstance = GeneralUtility::makeInstance($hookClass);
748  if (method_exists($hookClass, 'parseDataStructureByIdentifierPostProcess')) {
749  $dataStructure = $hookInstance->parseDataStructureByIdentifierPostProcess($dataStructure, $identifier);
750  if (!is_array($dataStructure)) {
751  // Programming error -> not catchable
752  throw new \RuntimeException(
753  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must return and array.',
754  1478350806
755  );
756  }
757  }
758  }
759 
760  return $dataStructure;
761  }
762 
773  public function ‪traverseFlexFormXMLData($table, $field, $row, ‪$callBackObj, $callBackMethod_value)
774  {
775  $PA = [];
776  if (!is_array(‪$GLOBALS['TCA'][$table]) || !is_array(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
777  return 'TCA table/field was not defined.';
778  }
779  $this->callBackObj = ‪$callBackObj;
780 
781  // Get data structure. The methods may throw various exceptions, with some of them being
782  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
783  // and substitute with a dummy DS.
784  $dataStructureArray = ['sheets' => ['sDEF' => []]];
785  try {
786  $dataStructureIdentifier = $this->‪getDataStructureIdentifier(‪$GLOBALS['TCA'][$table]['columns'][$field], $table, $field, $row);
787  $dataStructureArray = $this->‪parseDataStructureByIdentifier($dataStructureIdentifier);
789  }
790 
791  // Get flexform XML data
792  $editData = ‪GeneralUtility::xml2array($row[$field]);
793  if (!is_array($editData)) {
794  return 'Parsing error: ' . $editData;
795  }
796  // Check if $dataStructureArray['sheets'] is indeed an array before loop or it will crash with runtime error
797  if (!is_array($dataStructureArray['sheets'])) {
798  return 'Data Structure ERROR: sheets is defined but not an array for table ' . $table . (isset($row['uid']) ? ' and uid ' . $row['uid'] : '');
799  }
800  // Traverse languages:
801  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
802  // Render sheet:
803  if (isset($sheetData['ROOT']['el']) && is_array($sheetData['ROOT']['el'])) {
804  $PA['vKeys'] = ['DEF'];
805  $PA['lKey'] = 'lDEF';
806  $PA['callBackMethod_value'] = $callBackMethod_value;
807  $PA['table'] = $table;
808  $PA['field'] = $field;
809  $PA['uid'] = $row['uid'];
810  // Render flexform:
811  $this->‪traverseFlexFormXMLData_recurse($sheetData['ROOT']['el'], $editData['data'][$sheetKey]['lDEF'] ?? [], $PA, 'data/' . $sheetKey . '/lDEF');
812  } else {
813  return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheetKey . '".';
814  }
815  }
816  return true;
817  }
818 
827  public function ‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = ''): void
828  {
829  if (is_array($dataStruct)) {
830  foreach ($dataStruct as $key => $value) {
831  if (isset($value['type']) && $value['type'] === 'array') {
832  // Array (Section) traversal
833  if ($value['section'] ?? false) {
834  if (isset($editData[$key]['el']) && is_array($editData[$key]['el'])) {
835  if ($this->reNumberIndexesOfSectionData) {
836  $temp = [];
837  $c3 = 0;
838  foreach ($editData[$key]['el'] as $v3) {
839  $temp[++$c3] = $v3;
840  }
841  $editData[$key]['el'] = $temp;
842  }
843  foreach ($editData[$key]['el'] as $k3 => $v3) {
844  if (is_array($v3)) {
845  $cc = $k3;
846  $theType = key($v3);
847  $theDat = $v3[$theType];
848  $newSectionEl = $value['el'][$theType];
849  if (is_array($newSectionEl)) {
850  $this->‪traverseFlexFormXMLData_recurse([$theType => $newSectionEl], [$theType => $theDat], $PA, $path . '/' . $key . '/el/' . $cc);
851  }
852  }
853  }
854  }
855  } else {
856  // Array traversal
857  if (isset($editData[$key]['el'])) {
858  $this->‪traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
859  }
860  }
861  } elseif (isset($value['TCEforms']['config']) && is_array($value['TCEforms']['config'])) {
862  // Processing a field value:
863  foreach ($PA['vKeys'] as $vKey) {
864  $vKey = 'v' . $vKey;
865  // Call back
866  if (!empty($PA['callBackMethod_value']) && isset($editData[$key][$vKey])) {
867  $this->‪executeCallBackMethod($PA['callBackMethod_value'], [
868  $value,
869  $editData[$key][$vKey],
870  $PA,
871  $path . '/' . $key . '/' . $vKey,
872  $this,
873  ]);
874  }
875  }
876  }
877  }
878  }
879  }
880 
888  protected function ‪executeCallBackMethod($methodName, array $parameterArray)
889  {
890  return $this->callBackObj->$methodName(...$parameterArray);
891  }
892 
893  /***********************************
894  *
895  * Processing functions
896  *
897  ***********************************/
907  public function ‪cleanFlexFormXML($table, $field, $row)
908  {
909  // New structure:
910  $this->‪cleanFlexFormXML = [];
911  // Create and call iterator object:
912  $flexObj = GeneralUtility::makeInstance(\‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
913  $flexObj->reNumberIndexesOfSectionData = true;
914  $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
915  return $this->‪flexArray2Xml($this->‪cleanFlexFormXML, true);
916  }
917 
928  public function ‪cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
929  {
930  // Just setting value in our own result array, basically replicating the structure:
932  }
933 
934  /***********************************
935  *
936  * Multi purpose functions
937  *
938  ***********************************/
947  public function &‪getArrayValueByPath($pathArray, &$array)
948  {
949  trigger_error(
950  'Method ' . __METHOD__ . ' of class ' . __CLASS__ . ' is deprecated since v11 and will be removed in v12. Use ArrayUtility::getValueByPath() instead.',
951  E_USER_DEPRECATED
952  );
953  if (!is_array($pathArray)) {
954  $pathArray = explode('/', $pathArray);
955  }
956  if (is_array($array) && !empty($pathArray)) {
957  $key = array_shift($pathArray);
958  if (isset($array[$key])) {
959  if (empty($pathArray)) {
960  return $array[$key];
961  }
962  return $this->‪getArrayValueByPath($pathArray, $array[$key]);
963  }
964  return null;
965  }
966  }
967 
977  public function ‪setArrayValueByPath($pathArray, &$array, $value)
978  {
979  trigger_error(
980  'Method ' . __METHOD__ . ' of class ' . __CLASS__ . ' is deprecated since v11 and will be removed in v12. Use ArrayUtility::setValueByPath() instead.',
981  E_USER_DEPRECATED
982  );
983  if (isset($value)) {
984  if (!is_array($pathArray)) {
985  $pathArray = explode('/', $pathArray);
986  }
987  if (is_array($array) && !empty($pathArray)) {
988  $key = array_shift($pathArray);
989  if (empty($pathArray)) {
990  $array[$key] = $value;
991  return true;
992  }
993  if (!isset($array[$key])) {
994  $array[$key] = [];
995  }
996  return $this->‪setArrayValueByPath($pathArray, $array[$key], $value);
997  }
998  }
999  }
1000 
1008  public function ‪flexArray2Xml($array, $addPrologue = false)
1009  {
1010  if (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
1011  $this->flexArray2Xml_options['useCDATA'] = 1;
1012  }
1013  ‪$output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $this->flexArray2Xml_options);
1014  if ($addPrologue) {
1015  ‪$output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . ‪$output;
1016  }
1017  return ‪$output;
1018  }
1019 
1028  protected function ‪prepareCategoryFields(array $dataStructurSheets): array
1029  {
1030  if ($dataStructurSheets === []) {
1031  // Early return in case the no sheets are given
1032  return $dataStructurSheets;
1033  }
1034 
1035  foreach ($dataStructurSheets as &$structure) {
1036  if (!is_array($structure['el'] ?? false) || $structure['el'] === []) {
1037  // Skip if no elements (fields) are defined
1038  continue;
1039  }
1040  foreach ($structure['el'] as $fieldName => &$fieldConfig) {
1041  if (($fieldConfig['TCEforms']['config']['type'] ?? '') !== 'category') {
1042  // Skip if type is not "category"
1043  continue;
1044  }
1045 
1046  // Add a default label if none is defined
1047  if (!isset($fieldConfig['TCEforms']['label'])) {
1048  $fieldConfig['TCEforms']['label'] = 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
1049  }
1050 
1051  // Initialize default column configuration and merge it with already defined
1052  $fieldConfig['TCEforms']['config']['size'] ??= 20;
1053 
1054  // Force foreign_table_* fields for type category
1055  $fieldConfig['TCEforms']['config']['foreign_table'] = 'sys_category';
1056  $fieldConfig['TCEforms']['config']['foreign_table_where'] = ' AND {#sys_category}.{#sys_language_uid} IN (-1, 0)';
1057 
1058  if (empty($fieldConfig['TCEforms']['config']['relationship'])) {
1059  // Fall back to "oneToMany" when no relationship is given
1060  $fieldConfig['TCEforms']['config']['relationship'] = 'oneToMany';
1061  }
1062 
1063  if (!in_array($fieldConfig['TCEforms']['config']['relationship'], ['oneToOne', 'oneToMany'], true)) {
1064  throw new \UnexpectedValueException(
1065  '"relationship" must be one of "oneToOne" or "oneToMany", "manyToMany" is not supported as "relationship"' .
1066  ' for field ' . $fieldName . ' of type "category" in flexform.',
1067  1627640208
1068  );
1069  }
1070 
1071  // Set the maxitems value (necessary for DataHandling and FormEngine)
1072  if ($fieldConfig['TCEforms']['config']['relationship'] === 'oneToOne') {
1073  // In case relationship is set to "oneToOne", maxitems must be 1.
1074  if ((int)($fieldConfig['TCEforms']['config']['maxitems'] ?? 0) > 1) {
1075  throw new \UnexpectedValueException(
1076  $fieldName . ' is defined as type category with an "oneToOne" relationship. ' .
1077  'Therefore maxitems must be 1. Otherwise, use oneToMany as relationship instead.',
1078  1627640209
1079  );
1080  }
1081  $fieldConfig['TCEforms']['config']['maxitems'] = 1;
1082  } else {
1083  // In case maxitems is not set or set to 0, set the default value "99999"
1084  if (!($fieldConfig['TCEforms']['config']['maxitems'] ?? false)) {
1085  $fieldConfig['TCEforms']['config']['maxitems'] = 99999;
1086  } elseif ((int)($fieldConfig['TCEforms']['config']['maxitems'] ?? 0) === 1) {
1087  throw new \UnexpectedValueException(
1088  'Can not use maxitems=1 for field ' . $fieldName . ' with "relationship" set to "oneToMany". Use "oneToOne" instead.',
1089  1627640210
1090  );
1091  }
1092  }
1093 
1094  // Add the default value if not set
1095  if (!isset($fieldConfig['TCEforms']['config']['default'])
1096  && $fieldConfig['TCEforms']['config']['relationship'] !== 'oneToMany'
1097  ) {
1098  $fieldConfig['TCEforms']['config']['default'] = 0;
1099  }
1100  }
1101  }
1102 
1103  return $dataStructurSheets;
1104  }
1105 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static mixed xml2array($string, $NSprefix='', $reportDocTag=false)
Definition: GeneralUtility.php:1482
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException
Definition: InvalidParentRowException.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData
‪bool string traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
Definition: FlexFormTools.php:769
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$cleanFlexFormXML
‪array $cleanFlexFormXML
Definition: FlexFormTools.php:76
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\setArrayValueByPath
‪mixed setArrayValueByPath($pathArray, &$array, $value)
Definition: FlexFormTools.php:973
‪TYPO3
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidCombinedPointerFieldException
Definition: InvalidCombinedPointerFieldException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidSinglePointerFieldException
Definition: InvalidSinglePointerFieldException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\prepareCategoryFields
‪array prepareCategoryFields(array $dataStructurSheets)
Definition: FlexFormTools.php:1024
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifierFromRecord
‪array getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:268
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\flexArray2Xml
‪string flexArray2Xml($array, $addPrologue=false)
Definition: FlexFormTools.php:1004
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$flexArray2Xml_options
‪array $flexArray2Xml_options
Definition: FlexFormTools.php:52
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\executeCallBackMethod
‪mixed executeCallBackMethod($methodName, array $parameterArray)
Definition: FlexFormTools.php:884
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowRootException
Definition: InvalidParentRowRootException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\cleanFlexFormXML_callBackFunction
‪cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
Definition: FlexFormTools.php:924
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException
Definition: InvalidIdentifierException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:40
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\cleanFlexFormXML
‪string cleanFlexFormXML($table, $field, $row)
Definition: FlexFormTools.php:903
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifierFromTcaArray
‪array getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:433
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException
Definition: InvalidTcaException.php:23
‪$output
‪$output
Definition: annotationChecker.php:121
‪TYPO3\CMS\Core\Utility\ArrayUtility\setValueByPath
‪static array setValueByPath(array $array, $path, $value, $delimiter='/')
Definition: ArrayUtility.php:272
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException
Definition: InvalidPointerFieldValueException.php:21
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifier
‪string getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:116
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$reNumberIndexesOfSectionData
‪bool $reNumberIndexesOfSectionData
Definition: FlexFormTools.php:45
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:24
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException
Definition: InvalidParentRowLoopException.php:21
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$callBackObj
‪object $callBackObj
Definition: FlexFormTools.php:70
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\parseDataStructureByIdentifier
‪array parseDataStructureByIdentifier(string $identifier)
Definition: FlexFormTools.php:575
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData_recurse
‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path='')
Definition: FlexFormTools.php:823
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getArrayValueByPath
‪mixed & getArrayValueByPath($pathArray, &$array)
Definition: FlexFormTools.php:943