‪TYPO3CMS  ‪main
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 Psr\EventDispatcher\EventDispatcherInterface;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
42 
57 {
63  public ‪$reNumberIndexesOfSectionData = false;
64 
71  public ‪$flexArray2Xml_options = [
72  'parentTagMap' => [
73  'data' => 'sheet',
74  'sheet' => 'language',
75  'language' => 'field',
76  'el' => 'field',
77  'field' => 'value',
78  'field:el' => 'el',
79  'el:_IS_NUM' => 'section',
80  'section' => 'itemType',
81  ],
82  'disableTypeAttrib' => 2,
83  ];
84 
90  public ‪$callBackObj;
91 
97  public ‪$cleanFlexFormXML = [];
98 
99  public function ‪__construct(
100  private ?EventDispatcherInterface $eventDispatcher = null,
101  ) {
102  $this->eventDispatcher ??= GeneralUtility::makeInstance(EventDispatcherInterface::class);
103  }
104 
143  public function ‪getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row): string
144  {
145  $dataStructureIdentifier = $this->eventDispatcher
146  ->dispatch(new ‪BeforeFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row))
147  ->getIdentifier() ?? $this->‪getDefaultIdentifier($fieldTca, $tableName, $fieldName, $row);
148 
149  $dataStructureIdentifier = $this->eventDispatcher
150  ->dispatch(new ‪AfterFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row, $dataStructureIdentifier))
151  ->getIdentifier();
152 
153  return json_encode($dataStructureIdentifier, JSON_THROW_ON_ERROR);
154  }
155 
164  protected function ‪getDefaultIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row): array
165  {
166  $tcaDataStructureArray = $fieldTca['config']['ds'] ?? null;
167  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
168  if (!is_array($tcaDataStructureArray) && $tcaDataStructurePointerField) {
169  // "ds" is not an array, but "ds_pointerField" is set -> data structure is found in different table
170  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromRecord(
171  $fieldTca,
172  $tableName,
173  $fieldName,
174  $row
175  );
176  } elseif (is_array($tcaDataStructureArray)) {
177  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromTcaArray(
178  $fieldTca,
179  $tableName,
180  $fieldName,
181  $row
182  );
183  } else {
184  throw new \RuntimeException(
185  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
186  . ' The field is configured as type="flex" and no "ds_pointerField" is defined and "ds" is not an array.'
187  . ' Either configure a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
188  . ' that specifies the data structure',
189  1463826960
190  );
191  }
192 
193  return $dataStructureIdentifier;
194  }
195 
245  protected function ‪getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row): array
246  {
247  $pointerFieldName = $finalPointerFieldName = $fieldTca['config']['ds_pointerField'];
248  if (!array_key_exists($pointerFieldName, $row)) {
249  // Pointer field does not exist in row at all -> throw
250  throw new ‪InvalidTcaException(
251  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
252  . ' configured and given row does not have a field with ds_pointerField name "' . $pointerFieldName . '".',
253  1464115059
254  );
255  }
256  $pointerValue = $row[$pointerFieldName];
257  // If set, this is typically set to "pid"
258  $parentFieldName = $fieldTca['config']['ds_pointerField_searchParent'] ?? null;
259  $pointerSubFieldName = $fieldTca['config']['ds_pointerField_searchParent_subField'] ?? null;
260  if (!$pointerValue && $parentFieldName) {
261  // Fetch rootline until a valid pointer value is found
262  $handledUids = [];
263  while (!$pointerValue) {
264  $handledUids[$row['uid']] = 1;
265  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
266  $queryBuilder->getRestrictions()
267  ->removeAll()
268  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
269  $queryBuilder->select('uid', $parentFieldName, $pointerFieldName);
270  if (!empty($pointerSubFieldName)) {
271  $queryBuilder->addSelect($pointerSubFieldName);
272  }
273  $queryStatement = $queryBuilder->from($tableName)
274  ->where(
275  $queryBuilder->expr()->eq(
276  'uid',
277  $queryBuilder->createNamedParameter($row[$parentFieldName], ‪Connection::PARAM_INT)
278  )
279  )
280  ->executeQuery();
281  $rowCount = $queryBuilder
282  ->count('uid')
283  ->executeQuery()
284  ->fetchOne();
285  if ($rowCount !== 1) {
287  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
288  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
289  . ' with uid "' . $row[$parentFieldName] . '" was done. This row however does not exist or was deleted.',
290  1463833794
291  );
292  }
293  $row = $queryStatement->fetchAssociative();
294  if (isset($handledUids[$row[$parentFieldName]])) {
295  // Row has been fetched before already -> loop detected!
297  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
298  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
299  . ' with uid "' . $row[$parentFieldName] . '" was done. A loop of records was detected, the tree is broken.',
300  1464110956
301  );
302  }
303  BackendUtility::workspaceOL($tableName, $row);
304  // New pointer value: This is the "subField" value if given, else the field value
305  // ds_pointerField_searchParent_subField is the "template on next level" structure from templavoila
306  if ($pointerSubFieldName && $row[$pointerSubFieldName]) {
307  $finalPointerFieldName = $pointerSubFieldName;
308  $pointerValue = $row[$pointerSubFieldName];
309  } else {
310  $pointerValue = $row[$pointerFieldName];
311  }
312  if (!$pointerValue && ((int)$row[$parentFieldName] === 0 || $row[$parentFieldName] === null)) {
313  // If on root level and still no valid pointer found -> exception
315  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
316  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
317  . ' with uid "' . $row[$parentFieldName] . '" was done. Root node with uid "' . $row['uid'] . '"'
318  . ' was fetched and still no valid pointer field value was found.',
319  1464112555
320  );
321  }
322  }
323  }
324  if (!$pointerValue) {
325  // Still no valid pointer value -> exception, This still can be a data integrity issue, so throw a catchable exception
327  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
328  . ' configured and data structure could be found by resolving parents. This is probably a TCA misconfiguration.',
329  1464114011
330  );
331  }
332  // Ok, finally we have the field value. This is now either a data structure directly, or a pointer to a file,
333  // or the value can be interpreted as integer (is a uid) and "ds_tableField" is set, so this is the table, uid and field
334  // where the final data structure can be found.
335  if (‪MathUtility::canBeInterpretedAsInteger($pointerValue)) {
336  if (!isset($fieldTca['config']['ds_tableField'])) {
337  throw new ‪InvalidTcaException(
338  'Invalid data structure pointer for field "' . $fieldName . '" in table "' . $tableName . '", the value'
339  . 'resolved to "' . $pointerValue . '" . which is an integer, so "ds_tableField" must be configured',
340  1464115639
341  );
342  }
343  if (substr_count($fieldTca['config']['ds_tableField'], ':') !== 1) {
344  // ds_tableField must be of the form "table:field"
345  throw new ‪InvalidTcaException(
346  'Invalid TCA configuration for field "' . $fieldName . '" in table "' . $tableName . '", the setting'
347  . '"ds_tableField" must be of the form "tableName:fieldName"',
348  1464116002
349  );
350  }
351  [$foreignTableName, $foreignFieldName] = ‪GeneralUtility::trimExplode(':', $fieldTca['config']['ds_tableField']);
352  $dataStructureIdentifier = [
353  'type' => 'record',
354  'tableName' => $foreignTableName,
355  'uid' => (int)$pointerValue,
356  'fieldName' => $foreignFieldName,
357  ];
358  } else {
359  $dataStructureIdentifier = [
360  'type' => 'record',
361  'tableName' => $tableName,
362  'uid' => (int)$row['uid'],
363  'fieldName' => $finalPointerFieldName,
364  ];
365  }
366  return $dataStructureIdentifier;
367  }
368 
410  protected function ‪getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row): array
411  {
412  $dataStructureIdentifier = [
413  'type' => 'tca',
414  'tableName' => $tableName,
415  'fieldName' => $fieldName,
416  'dataStructureKey' => null,
417  ];
418  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
419  if ($tcaDataStructurePointerField === null) {
420  // No ds_pointerField set -> use 'default' as ds array key if exists.
421  if (isset($fieldTca['config']['ds']['default'])) {
422  $dataStructureIdentifier['dataStructureKey'] = 'default';
423  } else {
424  // A tca is configured as flex without ds_pointerField. A 'default' key must exist, otherwise
425  // this is a configuration error.
426  // May happen with an unloaded extension -> catchable
427  throw new ‪InvalidTcaException(
428  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
429  . ' The field is configured as type="flex" and no "ds_pointerField" is defined. Either configure'
430  . ' a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
431  . ' that specifies the data structure',
432  1463652560
433  );
434  }
435  } else {
436  // ds_pointerField is set, it can be a comma separated list of two fields, explode it.
437  $pointerFieldArray = ‪GeneralUtility::trimExplode(',', $tcaDataStructurePointerField, true);
438  // Obvious configuration error, either one or two fields must be declared
439  $pointerFieldsCount = count($pointerFieldArray);
440  if ($pointerFieldsCount !== 1 && $pointerFieldsCount !== 2) {
441  // If it's there, it must be correct -> not catchable
442  throw new \RuntimeException(
443  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
444  . ' ds_pointerField must be either a single field name, or a comma separated list of two fields,'
445  . ' the invalid configuration string provided was: "' . $tcaDataStructurePointerField . '"',
446  1463577497
447  );
448  }
449  // Verify first field exists in row array. If not, this is a hard error: Any extension that sets a
450  // ds_pointerField to some field name should take care that field does exist, too. They are a pair,
451  // so there shouldn't be a situation where the field does not exist. Throw an exception if that is violated.
452  if (!isset($row[$pointerFieldArray[0]])) {
453  // If it's declared, it must exist -> not catchable
454  throw new \RuntimeException(
455  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
456  . ' ds_pointerField "' . $pointerFieldArray[0] . '" points to a field name that does not exist.',
457  1463578899
458  );
459  }
460  // Similar situation for the second field: If it is set, the field must exist.
461  if (isset($pointerFieldArray[1]) && !isset($row[$pointerFieldArray[1]])) {
462  // If it's declared, it must exist -> not catchable
463  throw new \RuntimeException(
464  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
465  . ' Second part "' . $pointerFieldArray[1] . '" of ds_pointerField with full value "'
466  . $tcaDataStructurePointerField . '" points to a field name that does not exist.',
467  1463578900
468  );
469  }
470  if ($pointerFieldsCount === 1) {
471  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
472  // Field value points directly to an existing key in tca ds
473  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
474  } elseif (isset($fieldTca['config']['ds']['default'])) {
475  // Field value does not exit in tca ds, fall back to default key if exists
476  $dataStructureIdentifier['dataStructureKey'] = 'default';
477  } else {
478  // The value of the ds_pointerField field points to a key in the ds array that does
479  // not exist, and there is no fallback either. This can happen if an extension brings
480  // new flex form definitions and that extension is unloaded later. "Old" records of the
481  // extension could then still point to the no longer existing key in ds. We throw a
482  // specific exception here to give controllers an opportunity to catch this case.
484  'Field value of field "' . $pointerFieldArray[0] . '" of database record with uid "'
485  . $row['uid'] . '" from table "' . $tableName . '" points to a "ds" key ' . $row[$pointerFieldArray[0]]
486  . ' but this key does not exist and there is no "default" fallback.',
487  1463653197
488  );
489  }
490  } else {
491  // Two comma separated field names
492  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]]])) {
493  // firstValue,secondValue
494  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]];
495  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',*'])) {
496  // firstValue,*
497  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',*';
498  } elseif (isset($fieldTca['config']['ds']['*,' . $row[$pointerFieldArray[1]]])) {
499  // *,secondValue
500  $dataStructureIdentifier['dataStructureKey'] = '*,' . $row[$pointerFieldArray[1]];
501  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
502  // firstValue
503  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
504  } elseif (isset($fieldTca['config']['ds']['default'])) {
505  // Fall back to default
506  $dataStructureIdentifier['dataStructureKey'] = 'default';
507  } else {
508  // No ds_pointerField value could be determined and 'default' does not exist as
509  // fallback. This is the same case as the above scenario, throw a
510  // InvalidCombinedPointerFieldException here, too.
512  'Field combination of fields "' . $pointerFieldArray[0] . '" and "' . $pointerFieldArray[1] . '" of database'
513  . 'record with uid "' . $row['uid'] . '" from table "' . $tableName . '" with values "' . $row[$pointerFieldArray[0]] . '"'
514  . ' and "' . $row[$pointerFieldArray[1]] . '" could not be resolved to any registered data structure and '
515  . ' no "default" fallback exists.',
516  1463678524
517  );
518  }
519  }
520  }
521  return $dataStructureIdentifier;
522  }
523 
552  public function ‪parseDataStructureByIdentifier(string ‪$identifier): array
553  {
554  // Throw an exception for an empty string. This might be a valid use case for new
555  // records in some situations, so this is catchable to give callers a chance to deal with that.
556  if (empty(‪$identifier)) {
558  'Empty string given to parseFlexFormDataStructureByIdentifier(). This exception might '
559  . ' be caught to handle some new record situations properly',
560  1478100828
561  );
562  }
563 
564  $parsedIdentifier = json_decode(‪$identifier, true);
565 
566  if (!is_array($parsedIdentifier) || $parsedIdentifier === []) {
567  // If there is some identifier and it can't be decoded, programming error -> not catchable
568  throw new \RuntimeException(
569  'Identifier could not be decoded to an array.',
570  1478345642
571  );
572  }
573 
574  $dataStructure = $this->eventDispatcher
575  ->dispatch(new ‪BeforeFlexFormDataStructureParsedEvent($parsedIdentifier))
576  ->getDataStructure() ?? $this->‪getDefaultStructureForIdentifier($parsedIdentifier);
577 
578  $dataStructure = $this->‪convertDataStructureToArray($dataStructure);
579  $dataStructure = $this->‪ensureDefaultSheet($dataStructure);
580  $dataStructure = $this->‪resolveFileDirectives($dataStructure);
581 
582  return $this->eventDispatcher
583  ->dispatch(new ‪AfterFlexFormDataStructureParsedEvent($dataStructure, $parsedIdentifier))
584  ->getDataStructure();
585  }
586 
587  protected function ‪convertDataStructureToArray(string|array $dataStructure): array
588  {
589  if (is_array($dataStructure)) {
590  return $dataStructure;
591  }
592 
593  // Resolve FILE: prefix pointing to a DS in a file
594  if (str_starts_with(trim($dataStructure), 'FILE:')) {
595  $fileName = substr(trim($dataStructure), 5);
596  $file = GeneralUtility::getFileAbsFileName($fileName);
597  if (empty($file) || !is_file($file)) {
598  throw new \RuntimeException(
599  'Data structure file "' . $fileName . '" could not be resolved to an existing file',
600  1478105826
601  );
602  }
603  $dataStructure = (string)file_get_contents($file);
604  }
605 
606  // Parse main structure
607  $dataStructure = ‪GeneralUtility::xml2array($dataStructure);
608 
609  // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
610  // This also may happen if artificial identifiers were constructed which don't resolve. The
611  // flex form "exclude" access rights systems does that -> catchable
612  if (!is_array($dataStructure)) {
613  throw new InvalidIdentifierException(
614  'Parse error: Data structure could not be resolved to a valid structure.',
615  1478106090
616  );
617  }
618 
619  return $dataStructure;
620  }
621 
622  protected function ‪getDefaultStructureForIdentifier(array ‪$identifier): string
623  {
624  if ((‪$identifier['type'] ?? '') === 'record') {
625  // Handle "record" type, see getDataStructureIdentifierFromRecord()
626  if (empty(‪$identifier['tableName']) || empty(‪$identifier['uid']) || empty(‪$identifier['fieldName'])) {
627  throw new \RuntimeException(
628  'Incomplete "record" based identifier: ' . json_encode(‪$identifier),
629  1478113873
630  );
631  }
632  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(‪$identifier['tableName']);
633  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
634  $dataStructure = $queryBuilder
635  ->select(‪$identifier['fieldName'])
636  ->from(‪$identifier['tableName'])
637  ->where(
638  $queryBuilder->expr()->eq(
639  'uid',
640  $queryBuilder->createNamedParameter(‪$identifier['uid'], ‪Connection::PARAM_INT)
641  )
642  )
643  ->executeQuery()
644  ->fetchOne();
645  } elseif ((‪$identifier['type'] ?? '') === 'tca') {
646  // Handle "tca" type, see getDataStructureIdentifierFromTcaArray
647  if (empty(‪$identifier['tableName']) || empty(‪$identifier['fieldName']) || empty(‪$identifier['dataStructureKey'])) {
648  throw new \RuntimeException(
649  'Incomplete "tca" based identifier: ' . json_encode(‪$identifier),
650  1478113471
651  );
652  }
653  $table = ‪$identifier['tableName'];
654  $field = ‪$identifier['fieldName'];
655  $dataStructureKey = ‪$identifier['dataStructureKey'];
656  if (!isset(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
657  || !is_string(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
658  ) {
659  // This may happen for elements pointing to an unloaded extension -> catchable
660  throw new InvalidIdentifierException(
661  'Specified identifier ' . json_encode(‪$identifier) . ' does not resolve to a valid'
662  . ' TCA array value',
663  1478105491
664  );
665  }
666  $dataStructure = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey];
667  } else {
668  throw new InvalidIdentifierException(
669  'Identifier ' . json_encode(‪$identifier) . ' could not be resolved',
670  1478104554
671  );
672  }
673  return $dataStructure;
674  }
675 
679  protected function ‪ensureDefaultSheet(array $dataStructure): array
680  {
681  if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
682  throw new \RuntimeException(
683  'Parsed data structure has both ROOT and sheets on top level. That is invalid.',
684  1440676540
685  );
686  }
687  if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
688  $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
689  unset($dataStructure['ROOT']);
690  }
691  return $dataStructure;
692  }
693 
697  protected function ‪resolveFileDirectives(array $dataStructure): array
698  {
699  if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
700  foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
701  if (!is_array($sheetStructure)) {
702  if (str_starts_with(trim($sheetStructure), 'FILE:')) {
703  $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
704  } else {
705  $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
706  }
707  if ($file && @is_file($file)) {
708  $sheetStructure = ‪GeneralUtility::xml2array((string)file_get_contents($file));
709  }
710  }
711  $dataStructure['sheets'][$sheetName] = $sheetStructure;
712  $dataStructure = $this->‪removeElementTceFormsRecursive($dataStructure);
713 
714  if (is_array($dataStructure['sheets'][$sheetName])) {
715  // @todo Use TcaPreparation instead of duplicating the code.
716  // @todo Actually, the type category preparation is different for FlexForm as is doesn't support manyToMany.
717  // @todo The difficulty for type file is the difference of the field name. For FlexForm it is not the column name of TCA, but the sub key.
718  $dataStructure['sheets'][$sheetName] = $this->‪migrateFlexFormTcaRecursive($dataStructure['sheets'][$sheetName]);
719  $dataStructure['sheets'][$sheetName] = $this->‪prepareCategoryFields($dataStructure['sheets'][$sheetName]);
720  $dataStructure['sheets'][$sheetName] = $this->‪prepareFileFields($dataStructure['sheets'][$sheetName]);
721  }
722  }
723  }
724  return $dataStructure;
725  }
726 
737  public function ‪traverseFlexFormXMLData($table, $field, $row, ‪$callBackObj, $callBackMethod_value)
738  {
739  $PA = [];
740  if (!is_array(‪$GLOBALS['TCA'][$table]) || !is_array(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
741  return 'TCA table/field was not defined.';
742  }
743  $this->callBackObj = ‪$callBackObj;
744 
745  // Get data structure. The methods may throw various exceptions, with some of them being
746  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
747  // and substitute with a dummy DS.
748  $dataStructureArray = ['sheets' => ['sDEF' => []]];
749  try {
750  $dataStructureIdentifier = $this->‪getDataStructureIdentifier(‪$GLOBALS['TCA'][$table]['columns'][$field], $table, $field, $row);
751  $dataStructureArray = $this->‪parseDataStructureByIdentifier($dataStructureIdentifier);
752  } catch (InvalidParentRowException|InvalidParentRowLoopException|InvalidParentRowRootException|InvalidPointerFieldValueException|InvalidIdentifierException $e) {
753  }
754 
755  // Get flexform XML data
756  $editData = ‪GeneralUtility::xml2array($row[$field]);
757  if (!is_array($editData)) {
758  return 'Parsing error: ' . $editData;
759  }
760  // Check if $dataStructureArray['sheets'] is indeed an array before loop or it will crash with runtime error
761  if (!is_array($dataStructureArray['sheets'])) {
762  return 'Data Structure ERROR: sheets is defined but not an array for table ' . $table . (isset($row['uid']) ? ' and uid ' . $row['uid'] : '');
763  }
764  // Traverse languages:
765  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
766  // Render sheet:
767  if (isset($sheetData['ROOT']['el']) && is_array($sheetData['ROOT']['el'])) {
768  $PA['vKeys'] = ['DEF'];
769  $PA['lKey'] = 'lDEF';
770  $PA['callBackMethod_value'] = $callBackMethod_value;
771  $PA['table'] = $table;
772  $PA['field'] = $field;
773  $PA['uid'] = $row['uid'];
774  // Render flexform:
775  $this->‪traverseFlexFormXMLData_recurse($sheetData['ROOT']['el'], $editData['data'][$sheetKey]['lDEF'] ?? [], $PA, 'data/' . $sheetKey . '/lDEF');
776  } else {
777  return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheetKey . '".';
778  }
779  }
780  return true;
781  }
782 
791  public function ‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = ''): void
792  {
793  if (is_array($dataStruct)) {
794  foreach ($dataStruct as $key => $value) {
795  if (isset($value['type']) && $value['type'] === 'array') {
796  // Array (Section) traversal
797  if ($value['section'] ?? false) {
798  if (isset($editData[$key]['el']) && is_array($editData[$key]['el'])) {
799  if ($this->reNumberIndexesOfSectionData) {
800  $temp = [];
801  $c3 = 0;
802  foreach ($editData[$key]['el'] as $v3) {
803  $temp[++$c3] = $v3;
804  }
805  $editData[$key]['el'] = $temp;
806  }
807  foreach ($editData[$key]['el'] as $k3 => $v3) {
808  if (is_array($v3)) {
809  $cc = $k3;
810  $theType = key($v3);
811  $theDat = $v3[$theType];
812  $newSectionEl = $value['el'][$theType];
813  if (is_array($newSectionEl)) {
814  $this->‪traverseFlexFormXMLData_recurse([$theType => $newSectionEl], [$theType => $theDat], $PA, $path . '/' . $key . '/el/' . $cc);
815  }
816  }
817  }
818  }
819  } else {
820  // Array traversal
821  if (isset($editData[$key]['el'])) {
822  $this->‪traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
823  }
824  }
825  } elseif (isset($value['config']) && is_array($value['config'])) {
826  // Processing a field value:
827  foreach ($PA['vKeys'] as $vKey) {
828  $vKey = 'v' . $vKey;
829  // Call back
830  if (!empty($PA['callBackMethod_value']) && isset($editData[$key][$vKey])) {
831  $this->‪executeCallBackMethod($PA['callBackMethod_value'], [
832  $value,
833  $editData[$key][$vKey],
834  $PA,
835  $path . '/' . $key . '/' . $vKey,
836  $this,
837  ]);
838  }
839  }
840  }
841  }
842  }
843  }
844 
852  protected function ‪executeCallBackMethod($methodName, array $parameterArray)
853  {
854  return $this->callBackObj->$methodName(...$parameterArray);
855  }
856 
857  /***********************************
858  *
859  * Processing functions
860  *
861  ***********************************/
871  public function ‪cleanFlexFormXML($table, $field, $row)
872  {
873  // New structure:
874  $this->‪cleanFlexFormXML = [];
875  // Create and call iterator object:
876  $flexObj = GeneralUtility::makeInstance(\‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
877  $flexObj->reNumberIndexesOfSectionData = true;
878  $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
879  return $this->‪flexArray2Xml($this->‪cleanFlexFormXML, true);
880  }
881 
892  public function ‪cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
893  {
894  // Just setting value in our own result array, basically replicating the structure:
896  }
897 
905  public function ‪flexArray2Xml($array, $addPrologue = false)
906  {
907  if (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
908  $this->flexArray2Xml_options['useCDATA'] = 1;
909  }
910  ‪$output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $this->flexArray2Xml_options);
911  if ($addPrologue) {
912  ‪$output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . ‪$output;
913  }
914  return ‪$output;
915  }
916 
925  protected function ‪prepareCategoryFields(array $dataStructureSheets): array
926  {
927  if ($dataStructureSheets === []) {
928  // Early return in case the no sheets are given
929  return $dataStructureSheets;
930  }
931 
932  foreach ($dataStructureSheets as &$structure) {
933  if (!is_array($structure['el'] ?? false) || $structure['el'] === []) {
934  // Skip if no elements (fields) are defined
935  continue;
936  }
937  foreach ($structure['el'] as $fieldName => &$fieldConfig) {
938  if (($fieldConfig['config']['type'] ?? '') !== 'category') {
939  // Skip if type is not "category"
940  continue;
941  }
942 
943  // Add a default label if none is defined
944  if (!isset($fieldConfig['label'])) {
945  $fieldConfig['label'] = 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
946  }
947 
948  // Initialize default column configuration and merge it with already defined
949  $fieldConfig['config']['size'] ??= 20;
950 
951  // Force foreign_table_* fields for type category
952  $fieldConfig['config']['foreign_table'] = 'sys_category';
953  $fieldConfig['config']['foreign_table_where'] = ' AND {#sys_category}.{#sys_language_uid} IN (-1, 0)';
954 
955  if (empty($fieldConfig['config']['relationship'])) {
956  // Fall back to "oneToMany" when no relationship is given
957  $fieldConfig['config']['relationship'] = 'oneToMany';
958  }
959 
960  if (!in_array($fieldConfig['config']['relationship'], ['oneToOne', 'oneToMany'], true)) {
961  throw new \UnexpectedValueException(
962  '"relationship" must be one of "oneToOne" or "oneToMany", "manyToMany" is not supported as "relationship"' .
963  ' for field ' . $fieldName . ' of type "category" in flexform.',
964  1627640208
965  );
966  }
967 
968  // Set the maxitems value (necessary for DataHandling and FormEngine)
969  if ($fieldConfig['config']['relationship'] === 'oneToOne') {
970  // In case relationship is set to "oneToOne", maxitems must be 1.
971  if ((int)($fieldConfig['config']['maxitems'] ?? 0) > 1) {
972  throw new \UnexpectedValueException(
973  $fieldName . ' is defined as type category with an "oneToOne" relationship. ' .
974  'Therefore maxitems must be 1. Otherwise, use oneToMany as relationship instead.',
975  1627640209
976  );
977  }
978  $fieldConfig['config']['maxitems'] = 1;
979  } elseif ($fieldConfig['config']['relationship'] === 'oneToMany') {
980  // In case maxitems is not set or set to 0, set the default value "99999"
981  if (!($fieldConfig['config']['maxitems'] ?? false)) {
982  $fieldConfig['config']['maxitems'] = 99999;
983  } elseif ((int)($fieldConfig['config']['maxitems'] ?? 0) === 1) {
984  throw new \UnexpectedValueException(
985  'Can not use maxitems=1 for field ' . $fieldName . ' with "relationship" set to "oneToMany". Use "oneToOne" instead.',
986  1627640210
987  );
988  }
989  }
990 
991  // Add the default value if not set
992  if (!isset($fieldConfig['config']['default'])
993  && $fieldConfig['config']['relationship'] !== 'oneToMany'
994  ) {
995  $fieldConfig['config']['default'] = 0;
996  }
997  }
998  }
999 
1000  return $dataStructureSheets;
1001  }
1002 
1008  protected function ‪prepareFileFields(array $dataStructureSheets): array
1009  {
1010  if ($dataStructureSheets === []) {
1011  // Early return in case the no sheets are given
1012  return $dataStructureSheets;
1013  }
1014 
1015  foreach ($dataStructureSheets as &$structure) {
1016  if (!is_array($structure['el'] ?? false) || $structure['el'] === []) {
1017  // Skip if no elements (fields) are defined
1018  continue;
1019  }
1020  foreach ($structure['el'] as $fieldName => &$fieldConfig) {
1021  if (($fieldConfig['config']['type'] ?? '') !== 'file') {
1022  // Skip if type is not "file"
1023  continue;
1024  }
1025 
1026  $fieldConfig['config'] = array_replace_recursive(
1027  $fieldConfig['config'],
1028  [
1029  'foreign_table' => 'sys_file_reference',
1030  'foreign_field' => 'uid_foreign',
1031  'foreign_sortby' => 'sorting_foreign',
1032  'foreign_table_field' => 'tablenames',
1033  'foreign_match_fields' => [
1034  'fieldname' => $fieldName,
1035  ],
1036  'foreign_label' => 'uid_local',
1037  'foreign_selector' => 'uid_local',
1038  ]
1039  );
1040 
1041  if (!empty(($allowed = ($fieldConfig['config']['allowed'] ?? null)))) {
1042  $fieldConfig['config']['allowed'] = ‪TcaPreparation::prepareFileExtensions($allowed);
1043  }
1044  if (!empty(($disallowed = ($fieldConfig['config']['disallowed'] ?? null)))) {
1045  $fieldConfig['config']['disallowed'] = ‪TcaPreparation::prepareFileExtensions($disallowed);
1046  }
1047  }
1048  }
1049 
1050  return $dataStructureSheets;
1051  }
1052 
1067  public function ‪removeElementTceFormsRecursive(array $structure): array
1068  {
1069  $newStructure = [];
1070  foreach ($structure as $key => $value) {
1071  if ($key === 'ROOT' && is_array($value) && isset($value['TCEforms'])) {
1072  trigger_error(
1073  'The tag "<TCEforms>" should not be set under the FlexForm definition "<ROOT>" anymore. It should be omitted while the underlying configuration ascends one level up. This compatibility layer will be removed in TYPO3 v13.',
1074  E_USER_DEPRECATED
1075  );
1076  $value = array_merge($value, $value['TCEforms']);
1077  unset($value['TCEforms']);
1078  }
1079  if ($key === 'el' && is_array($value)) {
1080  $newSubStructure = [];
1081  foreach ($value as $subKey => $subValue) {
1082  if (is_array($subValue) && count($subValue) === 1 && isset($subValue['TCEforms'])) {
1083  trigger_error(
1084  'The tag "<TCEforms>" was found in a FlexForm definition for the field "<' . $subKey . '>". It should be omitted while the underlying configuration ascends one level up. This compatibility layer will be removed in TYPO3 v13.',
1085  E_USER_DEPRECATED
1086  );
1087  $newSubStructure[$subKey] = $subValue['TCEforms'];
1088  } else {
1089  $newSubStructure[$subKey] = $subValue;
1090  }
1091  }
1092  $value = $newSubStructure;
1093  }
1094  if (is_array($value)) {
1095  $value = $this->‪removeElementTceFormsRecursive($value);
1096  }
1097  $newStructure[$key] = $value;
1098  }
1099  return $newStructure;
1100  }
1105  public function ‪migrateFlexFormTcaRecursive(array $structure): array
1106  {
1107  $newStructure = [];
1108  foreach ($structure as $key => $value) {
1109  if ($key === 'el' && is_array($value)) {
1110  $newSubStructure = [];
1111  $tcaMigration = GeneralUtility::makeInstance(TcaMigration::class);
1112  foreach ($value as $subKey => $subValue) {
1113  // On-the-fly migration for flex form "TCA". Call the TcaMigration and log any deprecations.
1114  $dummyTca = [
1115  'dummyTable' => [
1116  'columns' => [
1117  'dummyField' => $subValue,
1118  ],
1119  ],
1120  ];
1121  $migratedTca = $tcaMigration->migrate($dummyTca);
1122  $messages = $tcaMigration->getMessages();
1123  if (!empty($messages)) {
1124  $context = 'FlexFormTools did an on-the-fly migration of a flex form data structure. This is deprecated and will be removed.'
1125  . ' Merge the following changes into the flex form definition "' . $subKey . '":';
1126  array_unshift($messages, $context);
1127  trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
1128  }
1129  $newSubStructure[$subKey] = $migratedTca['dummyTable']['columns']['dummyField'];
1130  }
1131  $value = $newSubStructure;
1132  }
1133  if (is_array($value)) {
1134  $value = $this->‪migrateFlexFormTcaRecursive($value);
1135  }
1136  $newStructure[$key] = $value;
1137  }
1138  return $newStructure;
1139  }
1140 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:916
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static mixed xml2array($string, $NSprefix='', $reportDocTag=false)
Definition: GeneralUtility.php:1363
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException
Definition: InvalidParentRowException.php:23
‪TYPO3\CMS\Core\Migrations\TcaMigration
Definition: TcaMigration.php:31
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDefaultStructureForIdentifier
‪getDefaultStructureForIdentifier(array $identifier)
Definition: FlexFormTools.php:618
‪TYPO3\CMS\Core\Configuration\FlexForm
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:47
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData
‪bool string traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
Definition: FlexFormTools.php:733
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$cleanFlexFormXML
‪array $cleanFlexFormXML
Definition: FlexFormTools.php:93
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\prepareFileFields
‪array prepareFileFields(array $dataStructureSheets)
Definition: FlexFormTools.php:1004
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\convertDataStructureToArray
‪convertDataStructureToArray(string|array $dataStructure)
Definition: FlexFormTools.php:583
‪TYPO3
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidCombinedPointerFieldException
Definition: InvalidCombinedPointerFieldException.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\__construct
‪__construct(private ?EventDispatcherInterface $eventDispatcher=null,)
Definition: FlexFormTools.php:95
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidSinglePointerFieldException
Definition: InvalidSinglePointerFieldException.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifierFromRecord
‪array getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:241
‪TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent
Definition: BeforeFlexFormDataStructureParsedEvent.php:35
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\flexArray2Xml
‪string flexArray2Xml($array, $addPrologue=false)
Definition: FlexFormTools.php:901
‪TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent
Definition: AfterFlexFormDataStructureParsedEvent.php:32
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$flexArray2Xml_options
‪array $flexArray2Xml_options
Definition: FlexFormTools.php:69
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\executeCallBackMethod
‪mixed executeCallBackMethod($methodName, array $parameterArray)
Definition: FlexFormTools.php:848
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowRootException
Definition: InvalidParentRowRootException.php:22
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\cleanFlexFormXML_callBackFunction
‪cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
Definition: FlexFormTools.php:888
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\ensureDefaultSheet
‪ensureDefaultSheet(array $dataStructure)
Definition: FlexFormTools.php:675
‪TYPO3\CMS\Core\Preparations\TcaPreparation\prepareFileExtensions
‪static prepareFileExtensions(mixed $fileExtensions)
Definition: TcaPreparation.php:216
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException
Definition: InvalidIdentifierException.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\resolveFileDirectives
‪resolveFileDirectives(array $dataStructure)
Definition: FlexFormTools.php:693
‪TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent
Definition: AfterFlexFormDataStructureIdentifierInitializedEvent.php:35
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:57
‪TYPO3\CMS\Core\Preparations\TcaPreparation
Definition: TcaPreparation.php:28
‪TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent
Definition: BeforeFlexFormDataStructureIdentifierInitializedEvent.php:44
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\cleanFlexFormXML
‪string cleanFlexFormXML($table, $field, $row)
Definition: FlexFormTools.php:867
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifierFromTcaArray
‪array getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:406
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\migrateFlexFormTcaRecursive
‪migrateFlexFormTcaRecursive(array $structure)
Definition: FlexFormTools.php:1101
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException
Definition: InvalidTcaException.php:24
‪$output
‪$output
Definition: annotationChecker.php:119
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException
Definition: InvalidPointerFieldValueException.php:22
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifier
‪string getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:139
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$reNumberIndexesOfSectionData
‪bool $reNumberIndexesOfSectionData
Definition: FlexFormTools.php:62
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDefaultIdentifier
‪getDefaultIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:160
‪$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\FlexFormTools\prepareCategoryFields
‪array prepareCategoryFields(array $dataStructureSheets)
Definition: FlexFormTools.php:921
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException
Definition: InvalidParentRowLoopException.php:22
‪TYPO3\CMS\Core\Utility\ArrayUtility\setValueByPath
‪static array setValueByPath(array $array, string|array|\ArrayAccess $path, mixed $value, string $delimiter='/')
Definition: ArrayUtility.php:261
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\removeElementTceFormsRecursive
‪removeElementTceFormsRecursive(array $structure)
Definition: FlexFormTools.php:1063
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$callBackObj
‪object $callBackObj
Definition: FlexFormTools.php:87
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\parseDataStructureByIdentifier
‪array parseDataStructureByIdentifier(string $identifier)
Definition: FlexFormTools.php:548
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:51
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData_recurse
‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path='')
Definition: FlexFormTools.php:787
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37