‪TYPO3CMS  9.5
FlexFormTools.php
Go to the documentation of this file.
1 <?php
2 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 
31 
36 {
42  public ‪$reNumberIndexesOfSectionData = false;
43 
50  public ‪$flexArray2Xml_options = [
51  'parentTagMap' => [
52  'data' => 'sheet',
53  'sheet' => 'language',
54  'language' => 'field',
55  'el' => 'field',
56  'field' => 'value',
57  'field:el' => 'el',
58  'el:_IS_NUM' => 'section',
59  'section' => 'itemType'
60  ],
61  'disableTypeAttrib' => 2
62  ];
63 
69  public ‪$callBackObj;
70 
76  public ‪$cleanFlexFormXML = [];
77 
116  public function ‪getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row): string
117  {
118  $dataStructureIdentifier = null;
119  // Hook to inject an own logic to point to a data structure elsewhere.
120  // A hook has to implement method getDataStructureIdentifierPreProcess() to be called here.
121  // All hooks are called in a row, each MUST return an array, and the FIRST one that
122  // returns a non-empty array is used as final identifier.
123  // It is important to restrict hooks as much as possible to give other hooks a chance to kick in.
124  // The returned identifier is later given to parseFlexFormDataStructureByIdentifier() and a hook in there MUST
125  // be used to handle this identifier again.
126  // Warning: If adding source record details like the uid or pid here, this may turn out to be fragile.
127  // Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may
128  // break in between different core versions.
129  // It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see
130  // the core internal 'tca' and 'record' return values below
131  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
132  && is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
133  $hookClasses = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
134  foreach ($hookClasses as $hookClass) {
135  $hookInstance = GeneralUtility::makeInstance($hookClass);
136  if (method_exists($hookClass, 'getDataStructureIdentifierPreProcess')) {
137  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPreProcess(
138  $fieldTca,
139  $tableName,
140  $fieldName,
141  $row
142  );
143  if (!is_array($dataStructureIdentifier)) {
144  throw new \RuntimeException(
145  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPreProcess must return an array',
146  1478096535
147  );
148  }
149  if (!empty($dataStructureIdentifier)) {
150  // Early break at first hook that returned something!
151  break;
152  }
153  }
154  }
155  }
156 
157  // If hooks didn't return something, kick in core logic
158  if (empty($dataStructureIdentifier)) {
159  $tcaDataStructureArray = $fieldTca['config']['ds'] ?? null;
160  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
161  if (!is_array($tcaDataStructureArray) && $tcaDataStructurePointerField) {
162  // "ds" is not an array, but "ds_pointerField" is set -> data structure is found in different table
163  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromRecord(
164  $fieldTca,
165  $tableName,
166  $fieldName,
167  $row
168  );
169  } elseif (is_array($tcaDataStructureArray)) {
170  $dataStructureIdentifier = $this->‪getDataStructureIdentifierFromTcaArray(
171  $fieldTca,
172  $tableName,
173  $fieldName,
174  $row
175  );
176  } else {
177  throw new \RuntimeException(
178  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
179  . ' The field is configured as type="flex" and no "ds_pointerField" is defined and "ds" is not an array.'
180  . ' Either configure a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
181  . ' that specifies the data structure',
182  1463826960
183  );
184  }
185  }
186 
187  // Second hook to manipulate identifier again. This can be used to add additional data to
188  // identifiers. Be careful here, especially if stuff from the source record like uid or pid
189  // is added! This may easily lead to issues with data handler details like copy or move records,
190  // localization and version overlays. Test this very well!
191  // Multiple hooks may add information to the same identifier here - take care to namespace array keys.
192  // Information added here can be later used in parseDataStructureByIdentifier post process hook again.
193  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
194  && is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
195  $hookClasses = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
196  foreach ($hookClasses as $hookClass) {
197  $hookInstance = GeneralUtility::makeInstance($hookClass);
198  if (method_exists($hookClass, 'getDataStructureIdentifierPostProcess')) {
199  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPostProcess(
200  $fieldTca,
201  $tableName,
202  $fieldName,
203  $row,
204  $dataStructureIdentifier
205  );
206  if (!is_array($dataStructureIdentifier) || empty($dataStructureIdentifier)) {
207  throw new \RuntimeException(
208  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPostProcess must return a non empty array',
209  1478350835
210  );
211  }
212  }
213  }
214  }
215 
216  return json_encode($dataStructureIdentifier);
217  }
218 
268  protected function ‪getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row): array
269  {
270  $pointerFieldName = $finalPointerFieldName = $fieldTca['config']['ds_pointerField'];
271  if (!array_key_exists($pointerFieldName, $row)) {
272  // Pointer field does not exist in row at all -> throw
273  throw new ‪InvalidTcaException(
274  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
275  . ' configured and given row does not have a field with ds_pointerField name "' . $pointerFieldName . '".',
276  1464115059
277  );
278  }
279  $pointerValue = $row[$pointerFieldName];
280  // If set, this is typically set to "pid"
281  $parentFieldName = $fieldTca['config']['ds_pointerField_searchParent'] ?? null;
282  $pointerSubFieldName = $fieldTca['config']['ds_pointerField_searchParent_subField'] ?? null;
283  if (!$pointerValue && $parentFieldName) {
284  // Fetch rootline until a valid pointer value is found
285  $handledUids = [];
286  while (!$pointerValue) {
287  $handledUids[$row['uid']] = 1;
288  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
289  $queryBuilder->getRestrictions()
290  ->removeAll()
291  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
292  $queryBuilder->select('uid', $parentFieldName, $pointerFieldName);
293  if (!empty($pointerSubFieldName)) {
294  $queryBuilder->addSelect($pointerSubFieldName);
295  }
296  $queryStatement = $queryBuilder->from($tableName)
297  ->where(
298  $queryBuilder->expr()->eq(
299  'uid',
300  $queryBuilder->createNamedParameter($row[$parentFieldName], \PDO::PARAM_INT)
301  )
302  )
303  ->execute();
304  $rowCount = $queryBuilder
305  ->count('uid')
306  ->execute()
307  ->fetchColumn(0);
308  if ($rowCount !== 1) {
310  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
311  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
312  . ' with uid "' . $row[$parentFieldName] . '" was done. This row however does not exist or was deleted.',
313  1463833794
314  );
315  }
316  $row = $queryStatement->fetch();
317  if (isset($handledUids[$row[$parentFieldName]])) {
318  // Row has been fetched before already -> loop detected!
320  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
321  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
322  . ' with uid "' . $row[$parentFieldName] . '" was done. A loop of records was detected, the tree is broken.',
323  1464110956
324  );
325  }
326  ‪BackendUtility::workspaceOL($tableName, $row);
327  ‪BackendUtility::fixVersioningPid($tableName, $row, true);
328  // New pointer value: This is the "subField" value if given, else the field value
329  // ds_pointerField_searchParent_subField is the "template on next level" structure from templavoila
330  if ($pointerSubFieldName && $row[$pointerSubFieldName]) {
331  $finalPointerFieldName = $pointerSubFieldName;
332  $pointerValue = $row[$pointerSubFieldName];
333  } else {
334  $pointerValue = $row[$pointerFieldName];
335  }
336  if (!$pointerValue && ((int)$row[$parentFieldName] === 0 || $row[$parentFieldName] === null)) {
337  // If on root level and still no valid pointer found -> exception
339  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
340  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
341  . ' with uid "' . $row[$parentFieldName] . '" was done. Root node with uid "' . $row['uid'] . '"'
342  . ' was fetched and still no valid pointer field value was found.',
343  1464112555
344  );
345  }
346  }
347  }
348  if (!$pointerValue) {
349  // Still no valid pointer value -> exception, This still can be a data integrity issue, so throw a catchable exception
351  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
352  . ' configured and data structure could be found by resolving parents. This is probably a TCA misconfiguration.',
353  1464114011
354  );
355  }
356  // Ok, finally we have the field value. This is now either a data structure directly, or a pointer to a file,
357  // or the value can be interpreted as integer (is an uid) and "ds_tableField" is set, so this is the table, uid and field
358  // where the final data structure can be found.
359  if (‪MathUtility::canBeInterpretedAsInteger($pointerValue)) {
360  if (!isset($fieldTca['config']['ds_tableField'])) {
361  throw new ‪InvalidTcaException(
362  'Invalid data structure pointer for field "' . $fieldName . '" in table "' . $tableName . '", the value'
363  . 'resolved to "' . $pointerValue . '" . which is an integer, so "ds_tableField" must be configured',
364  1464115639
365  );
366  }
367  if (substr_count($fieldTca['config']['ds_tableField'], ':') !== 1) {
368  // ds_tableField must be of the form "table:field"
369  throw new ‪InvalidTcaException(
370  'Invalid TCA configuration for field "' . $fieldName . '" in table "' . $tableName . '", the setting'
371  . '"ds_tableField" must be of the form "tableName:fieldName"',
372  1464116002
373  );
374  }
375  list($foreignTableName, $foreignFieldName) = GeneralUtility::trimExplode(':', $fieldTca['config']['ds_tableField']);
376  $dataStructureIdentifier = [
377  'type' => 'record',
378  'tableName' => $foreignTableName,
379  'uid' => (int)$pointerValue,
380  'fieldName' => $foreignFieldName,
381  ];
382  } else {
383  $dataStructureIdentifier = [
384  'type' => 'record',
385  'tableName' => $tableName,
386  'uid' => (int)$row['uid'],
387  'fieldName' => $finalPointerFieldName,
388  ];
389  }
390  return $dataStructureIdentifier;
391  }
392 
434  protected function ‪getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row): array
435  {
436  $dataStructureIdentifier = [
437  'type' => 'tca',
438  'tableName' => $tableName,
439  'fieldName' => $fieldName,
440  'dataStructureKey' => null,
441  ];
442  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
443  if ($tcaDataStructurePointerField === null) {
444  // No ds_pointerField set -> use 'default' as ds array key if exists.
445  if (isset($fieldTca['config']['ds']['default'])) {
446  $dataStructureIdentifier['dataStructureKey'] = 'default';
447  } else {
448  // A tca is configured as flex without ds_pointerField. A 'default' key must exist, otherwise
449  // this is a configuration error.
450  // May happen with an unloaded extension -> catchable
451  throw new ‪InvalidTcaException(
452  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
453  . ' The field is configured as type="flex" and no "ds_pointerField" is defined. Either configure'
454  . ' a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
455  . ' that specifies the data structure',
456  1463652560
457  );
458  }
459  } else {
460  // ds_pointerField is set, it can be a comma separated list of two fields, explode it.
461  $pointerFieldArray = GeneralUtility::trimExplode(',', $tcaDataStructurePointerField, true);
462  // Obvious configuration error, either one or two fields must be declared
463  $pointerFieldsCount = count($pointerFieldArray);
464  if ($pointerFieldsCount !== 1 && $pointerFieldsCount !== 2) {
465  // If it's there, it must be correct -> not catchable
466  throw new \RuntimeException(
467  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
468  . ' ds_pointerField must be either a single field name, or a comma separated list of two fields,'
469  . ' the invalid configuration string provided was: "' . $tcaDataStructurePointerField . '"',
470  1463577497
471  );
472  }
473  // Verify first field exists in row array. If not, this is a hard error: Any extension that sets a
474  // ds_pointerField to some field name should take care that field does exist, too. They are a pair,
475  // so there shouldn't be a situation where the field does not exist. Throw an exception if that is violated.
476  if (!isset($row[$pointerFieldArray[0]])) {
477  // If it's declared, it must exist -> not catchable
478  throw new \RuntimeException(
479  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
480  . ' ds_pointerField "' . $pointerFieldArray[0] . '" points to a field name that does not exist.',
481  1463578899
482  );
483  }
484  // Similar situation for the second field: If it is set, the field must exist.
485  if (isset($pointerFieldArray[1]) && !isset($row[$pointerFieldArray[1]])) {
486  // If it's declared, it must exist -> not catchable
487  throw new \RuntimeException(
488  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
489  . ' Second part "' . $pointerFieldArray[1] . '" of ds_pointerField with full value "'
490  . $tcaDataStructurePointerField . '" points to a field name that does not exist.',
491  1463578900
492  );
493  }
494  if ($pointerFieldsCount === 1) {
495  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
496  // Field value points directly to an existing key in tca ds
497  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
498  } elseif (isset($fieldTca['config']['ds']['default'])) {
499  // Field value does not exit in tca ds, fall back to default key if exists
500  $dataStructureIdentifier['dataStructureKey'] = 'default';
501  } else {
502  // The value of the ds_pointerField field points to a key in the ds array that does
503  // not exists, and there is no fallback either. This can happen if an extension brings
504  // new flex form definitions and that extension is unloaded later. "Old" records of the
505  // extension could then still point to the no longer existing key in ds. We throw a
506  // specific exception here to give controllers an opportunity to catch this case.
508  'Field value of field "' . $pointerFieldArray[0] . '" of database record with uid "'
509  . $row['uid'] . '" from table "' . $tableName . '" points to a "ds" key ' . $row[$pointerFieldArray[0]]
510  . ' but this key does not exist and there is no "default" fallback.',
511  1463653197
512  );
513  }
514  } else {
515  // Two comma separated field names
516  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]]])) {
517  // firstValue,secondValue
518  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]];
519  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',*'])) {
520  // firstValue,*
521  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',*';
522  } elseif (isset($fieldTca['config']['ds']['*,' . $row[$pointerFieldArray[1]]])) {
523  // *,secondValue
524  $dataStructureIdentifier['dataStructureKey'] = '*,' . $row[$pointerFieldArray[1]];
525  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
526  // firstValue
527  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
528  } elseif (isset($fieldTca['config']['ds']['default'])) {
529  // Fall back to default
530  $dataStructureIdentifier['dataStructureKey'] = 'default';
531  } else {
532  // No ds_pointerField value could be determined and 'default' does not exist as
533  // fallback. This is the same case as the above scenario, throw a
534  // InvalidCombinedPointerFieldException here, too.
536  'Field combination of fields "' . $pointerFieldArray[0] . '" and "' . $pointerFieldArray[1] . '" of database'
537  . 'record with uid "' . $row['uid'] . '" from table "' . $tableName . '" with values "' . $row[$pointerFieldArray[0]] . '"'
538  . ' and "' . $row[$pointerFieldArray[1]] . '" could not be resolved to any registered data structure and '
539  . ' no "default" fallback exists.',
540  1463678524
541  );
542  }
543  }
544  }
545  return $dataStructureIdentifier;
546  }
547 
577  public function ‪parseDataStructureByIdentifier(string $identifier): array
578  {
579  // Throw an exception for an empty string. This might be a valid use case for new
580  // records in some situations, so this is catchable to give callers a chance to deal with that.
581  if (empty($identifier)) {
583  'Empty string given to parseFlexFormDataStructureByIdentifier(). This exception might '
584  . ' be caught to handle some new record situations properly',
585  1478100828
586  );
587  }
588 
589  $identifier = json_decode($identifier, true);
590 
591  if (!is_array($identifier) || empty($identifier)) {
592  // If there is some identifier and it can't be decoded, programming error -> not catchable
593  throw new \RuntimeException(
594  'Identifier could not be decoded to an array.',
595  1478345642
596  );
597  }
598 
599  $dataStructure = '';
600 
601  // Hook to fetch data structure by given identifier.
602  // Method parseFlexFormDataStructureByIdentifier() must be implemented and returns either an
603  // empty string "not my business", or a string with the resolved data structure string, or FILE: reference,
604  // or a fully parsed data structure as aray.
605  // Result of the FIRST hook that gives an non-empty string is used, namespace your identifiers in
606  // a way that there is little chance they overlap (eg. prefix with extension name).
607  // If implemented, this hook should be paired with a hook in getDataStructureIdentifier() above.
608  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'] ?? [] as $hookClass) {
609  $hookInstance = GeneralUtility::makeInstance($hookClass);
610  if (method_exists($hookClass, 'parseDataStructureByIdentifierPreProcess')) {
611  $dataStructure = $hookInstance->parseDataStructureByIdentifierPreProcess($identifier);
612  if (!is_string($dataStructure) && !is_array($dataStructure)) {
613  // Programming error -> not catchable
614  throw new \RuntimeException(
615  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must either'
616  . ' return an empty string or a data structure string or a parsed data structure array.',
617  1478168512
618  );
619  }
620  if (!empty($dataStructure)) {
621  // Early break if a hook resolved to something!
622  break;
623  }
624  }
625  }
626 
627  // If hooks didn't resolve, try own methods
628  if (empty($dataStructure)) {
629  if ($identifier['type'] === 'record') {
630  // Handle "record" type, see getDataStructureIdentifierFromRecord()
631  if (empty($identifier['tableName']) || empty($identifier['uid']) || empty($identifier['fieldName'])) {
632  throw new \RuntimeException(
633  'Incomplete "record" based identifier: ' . json_encode($identifier),
634  1478113873
635  );
636  }
637  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($identifier['tableName']);
638  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
639  $dataStructure = $queryBuilder
640  ->select($identifier['fieldName'])
641  ->from($identifier['tableName'])
642  ->where(
643  $queryBuilder->expr()->eq(
644  'uid',
645  $queryBuilder->createNamedParameter($identifier['uid'], \PDO::PARAM_INT)
646  )
647  )
648  ->execute()
649  ->fetchColumn(0);
650  } elseif ($identifier['type'] === 'tca') {
651  // Handle "tca" type, see getDataStructureIdentifierFromTcaArray
652  if (empty($identifier['tableName']) || empty($identifier['fieldName']) || empty($identifier['dataStructureKey'])) {
653  throw new \RuntimeException(
654  'Incomplete "tca" based identifier: ' . json_encode($identifier),
655  1478113471
656  );
657  }
658  $table = $identifier['tableName'];
659  $field = $identifier['fieldName'];
660  $dataStructureKey = $identifier['dataStructureKey'];
661  if (!isset(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
662  || !is_string(‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
663  ) {
664  // This may happen for elements pointing to an unloaded extension -> catchable
666  'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid'
667  . ' TCA array value',
668  1478105491
669  );
670  }
671  $dataStructure = ‪$GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey];
672  } else {
674  'Identifier ' . json_encode($identifier) . ' could not be resolved',
675  1478104554
676  );
677  }
678  }
679 
680  // Hooks may have parse the data structure already to an array. If that is not the case, parse it now.
681  if (is_string($dataStructure)) {
682  // Resolve FILE: prefix pointing to a DS in a file
683  if (strpos(trim($dataStructure), 'FILE:') === 0) {
684  $file = GeneralUtility::getFileAbsFileName(substr(trim($dataStructure), 5));
685  if (empty($file) || !@is_file($file)) {
686  throw new \RuntimeException(
687  'Data structure file ' . $file . ' could not be resolved to an existing file',
688  1478105826
689  );
690  }
691  $dataStructure = file_get_contents($file);
692  }
693 
694  // Parse main structure
695  $dataStructure = GeneralUtility::xml2array($dataStructure);
696  }
697 
698  // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
699  // This also may happen if artificial identifiers were constructed which don't resolve. The
700  // flex form "exclude" access rights systems does that -> catchable
701  if (!is_array($dataStructure)) {
703  'Parse error: Data structure could not be resolved to a valid structure.',
704  1478106090
705  );
706  }
707 
708  // Create default sheet if there is none, yet.
709  if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
710  throw new \RuntimeException(
711  'Parsed data structure has both ROOT and sheets on top level. Thats invalid.',
712  1440676540
713  );
714  }
715  if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
716  $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
717  unset($dataStructure['ROOT']);
718  }
719 
720  // Resolve FILE:EXT and EXT: for single sheets
721  if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
722  foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
723  if (!is_array($sheetStructure)) {
724  if (strpos(trim($sheetStructure), 'FILE:') === 0) {
725  $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
726  } else {
727  $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
728  }
729  if ($file && @is_file($file)) {
730  $sheetStructure = GeneralUtility::xml2array(file_get_contents($file));
731  }
732  }
733  $dataStructure['sheets'][$sheetName] = $sheetStructure;
734  }
735  }
736 
737  // Hook to manipulate data structure further. This can be used to add or remove fields
738  // from given structure. Multiple hooks can be registered, all are called. They
739  // receive the parsed structure and the identifier array.
740  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'] ?? [] as $hookClass) {
741  $hookInstance = GeneralUtility::makeInstance($hookClass);
742  if (method_exists($hookClass, 'parseDataStructureByIdentifierPostProcess')) {
743  $dataStructure = $hookInstance->parseDataStructureByIdentifierPostProcess($dataStructure, $identifier);
744  if (!is_array($dataStructure)) {
745  // Programming error -> not catchable
746  throw new \RuntimeException(
747  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must return and array.',
748  1478350806
749  );
750  }
751  }
752  }
753 
754  return $dataStructure;
755  }
756 
767  public function ‪traverseFlexFormXMLData($table, $field, $row, ‪$callBackObj, $callBackMethod_value)
768  {
769  if (!is_array(‪$GLOBALS['TCA'][$table]) || !is_array(‪$GLOBALS['TCA'][$table]['columns'][$field])) {
770  return 'TCA table/field was not defined.';
771  }
772  $this->callBackObj = ‪$callBackObj;
773 
774  // Get data structure. The methods may throw various exceptions, with some of them being
775  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
776  // and substitute with a dummy DS.
777  $dataStructureArray = ['sheets' => ['sDEF' => []]];
778  try {
779  $dataStructureIdentifier = $this->‪getDataStructureIdentifier(‪$GLOBALS['TCA'][$table]['columns'][$field], $table, $field, $row);
780  $dataStructureArray = $this->‪parseDataStructureByIdentifier($dataStructureIdentifier);
781  } catch (‪InvalidParentRowException $e) {
782  } catch (‪InvalidParentRowLoopException $e) {
783  } catch (‪InvalidParentRowRootException $e) {
785  } catch (‪InvalidIdentifierException $e) {
786  }
787 
788  // Get flexform XML data
789  $editData = GeneralUtility::xml2array($row[$field]);
790  if (!is_array($editData)) {
791  return 'Parsing error: ' . $editData;
792  }
793  // Check if $dataStructureArray['sheets'] is indeed an array before loop or it will crash with runtime error
794  if (!is_array($dataStructureArray['sheets'])) {
795  return 'Data Structure ERROR: sheets is defined but not an array for table ' . $table . (isset($row['uid']) ? ' and uid ' . $row['uid'] : '');
796  }
797  // Traverse languages:
798  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
799  // Render sheet:
800  if (is_array($sheetData['ROOT']) && is_array($sheetData['ROOT']['el'])) {
801  $PA['vKeys'] = ['DEF'];
802  $PA['lKey'] = 'lDEF';
803  $PA['callBackMethod_value'] = $callBackMethod_value;
804  $PA['table'] = $table;
805  $PA['field'] = $field;
806  $PA['uid'] = $row['uid'];
807  // Render flexform:
808  $this->‪traverseFlexFormXMLData_recurse($sheetData['ROOT']['el'], $editData['data'][$sheetKey]['lDEF'], $PA, 'data/' . $sheetKey . '/lDEF');
809  } else {
810  return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheetKey . '".';
811  }
812  }
813  return true;
814  }
815 
824  public function ‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = ''): void
825  {
826  if (is_array($dataStruct)) {
827  foreach ($dataStruct as $key => $value) {
828  if (isset($value['type']) && $value['type'] === 'array') {
829  // Array (Section) traversal
830  if ($value['section']) {
831  $cc = 0;
832  if (isset($editData[$key]['el']) && is_array($editData[$key]['el'])) {
833  if ($this->reNumberIndexesOfSectionData) {
834  $temp = [];
835  $c3 = 0;
836  foreach ($editData[$key]['el'] as $v3) {
837  $temp[++$c3] = $v3;
838  }
839  $editData[$key]['el'] = $temp;
840  }
841  foreach ($editData[$key]['el'] as $k3 => $v3) {
842  if (is_array($v3)) {
843  $cc = $k3;
844  $theType = key($v3);
845  $theDat = $v3[$theType];
846  $newSectionEl = $value['el'][$theType];
847  if (is_array($newSectionEl)) {
848  $this->‪traverseFlexFormXMLData_recurse([$theType => $newSectionEl], [$theType => $theDat], $PA, $path . '/' . $key . '/el/' . $cc);
849  }
850  }
851  }
852  }
853  } else {
854  // Array traversal
855  if (isset($editData[$key]['el'])) {
856  $this->‪traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
857  }
858  }
859  } elseif (isset($value['TCEforms']['config']) && is_array($value['TCEforms']['config'])) {
860  // Processing a field value:
861  foreach ($PA['vKeys'] as $vKey) {
862  $vKey = 'v' . $vKey;
863  // Call back
864  if (!empty($PA['callBackMethod_value']) && isset($editData[$key][$vKey])) {
865  $this->‪executeCallBackMethod($PA['callBackMethod_value'], [
866  $value,
867  $editData[$key][$vKey],
868  $PA,
869  $path . '/' . $key . '/' . $vKey,
870  $this
871  ]);
872  }
873  }
874  }
875  }
876  }
877  }
878 
886  protected function ‪executeCallBackMethod($methodName, array $parameterArray)
887  {
888  return call_user_func_array([$this->callBackObj, $methodName], $parameterArray);
889  }
890 
891  /***********************************
892  *
893  * Processing functions
894  *
895  ***********************************/
905  public function ‪cleanFlexFormXML($table, $field, $row)
906  {
907  // New structure:
908  $this->‪cleanFlexFormXML = [];
909  // Create and call iterator object:
910  $flexObj = GeneralUtility::makeInstance(\‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
911  $flexObj->reNumberIndexesOfSectionData = true;
912  $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
913  return $this->‪flexArray2Xml($this->‪cleanFlexFormXML, true);
914  }
915 
926  public function ‪cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
927  {
928  // Just setting value in our own result array, basically replicating the structure:
929  $pObj->setArrayValueByPath($path, $this->‪cleanFlexFormXML, $data);
930  }
931 
932  /***********************************
933  *
934  * Multi purpose functions
935  *
936  ***********************************/
944  public function &‪getArrayValueByPath($pathArray, &$array)
945  {
946  if (!is_array($pathArray)) {
947  $pathArray = explode('/', $pathArray);
948  }
949  if (is_array($array) && !empty($pathArray)) {
950  $key = array_shift($pathArray);
951  if (isset($array[$key])) {
952  if (empty($pathArray)) {
953  return $array[$key];
954  }
955  return $this->‪getArrayValueByPath($pathArray, $array[$key]);
956  }
957  return null;
958  }
959  }
960 
969  public function ‪setArrayValueByPath($pathArray, &$array, $value)
970  {
971  if (isset($value)) {
972  if (!is_array($pathArray)) {
973  $pathArray = explode('/', $pathArray);
974  }
975  if (is_array($array) && !empty($pathArray)) {
976  $key = array_shift($pathArray);
977  if (empty($pathArray)) {
978  $array[$key] = $value;
979  return true;
980  }
981  if (!isset($array[$key])) {
982  $array[$key] = [];
983  }
984  return $this->‪setArrayValueByPath($pathArray, $array[$key], $value);
985  }
986  }
987  }
988 
996  public function ‪flexArray2Xml($array, $addPrologue = false)
997  {
998  if (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
999  $this->flexArray2Xml_options['useCDATA'] = 1;
1000  }
1001  ‪$output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $this->flexArray2Xml_options);
1002  if ($addPrologue) {
1003  ‪$output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . ‪$output;
1004  }
1005  return ‪$output;
1006  }
1007 }
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException
Definition: InvalidParentRowException.php:22
‪TYPO3\CMS\Core\Configuration\FlexForm
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData
‪bool string traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
Definition: FlexFormTools.php:763
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:73
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$cleanFlexFormXML
‪array $cleanFlexFormXML
Definition: FlexFormTools.php:72
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\setArrayValueByPath
‪mixed setArrayValueByPath($pathArray, &$array, $value)
Definition: FlexFormTools.php:965
‪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\getDataStructureIdentifierFromRecord
‪array getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:264
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\flexArray2Xml
‪string flexArray2Xml($array, $addPrologue=false)
Definition: FlexFormTools.php:992
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$flexArray2Xml_options
‪array $flexArray2Xml_options
Definition: FlexFormTools.php:48
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\executeCallBackMethod
‪mixed executeCallBackMethod($methodName, array $parameterArray)
Definition: FlexFormTools.php:882
‪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:922
‪TYPO3\CMS\Backend\Utility\BackendUtility\fixVersioningPid
‪static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)
Definition: BackendUtility.php:3986
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException
Definition: InvalidIdentifierException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:36
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\cleanFlexFormXML
‪string cleanFlexFormXML($table, $field, $row)
Definition: FlexFormTools.php:901
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifierFromTcaArray
‪array getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:430
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:72
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException
Definition: InvalidTcaException.php:23
‪$output
‪$output
Definition: annotationChecker.php:113
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException
Definition: InvalidPointerFieldValueException.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getDataStructureIdentifier
‪string getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
Definition: FlexFormTools.php:112
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$reNumberIndexesOfSectionData
‪bool $reNumberIndexesOfSectionData
Definition: FlexFormTools.php:41
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Backend\Utility\BackendUtility\workspaceOL
‪static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
Definition: BackendUtility.php:4048
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:26
‪TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException
Definition: InvalidParentRowLoopException.php:21
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:21
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\$callBackObj
‪object $callBackObj
Definition: FlexFormTools.php:66
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\parseDataStructureByIdentifier
‪array parseDataStructureByIdentifier(string $identifier)
Definition: FlexFormTools.php:573
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\traverseFlexFormXMLData_recurse
‪traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path='')
Definition: FlexFormTools.php:820
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools\getArrayValueByPath
‪mixed & getArrayValueByPath($pathArray, &$array)
Definition: FlexFormTools.php:940