TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
FlexFormTools.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Configuration\FlexForm;
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 {
43 
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 = null;
70 
76  public $cleanFlexFormXML = [];
77 
111  public function getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row): string
112  {
113  // Hook to inject an own logic to point to a data structure elsewhere.
114  // A hook has to implement method getDataStructureIdentifierPreProcess() to be called here.
115  // All hooks are called in a row, each MUST return an array, and the FIRST one that
116  // returns a non-empty array is used as final identifier.
117  // It is important to restrict hooks as much as possible to give other hooks a chance to kick in.
118  // The returned identifier is later given to parseFlexFormDataStructureByIdentifier() and a hook in there MUST
119  // be used to handle this identifier again.
120  // Warning: If adding source record details like the uid or pid here, this may turn out to be fragile.
121  // Be sure to test scenarios like workspaces and data handler copy/move well, additionally, this may
122  // break in between different core versions.
123  // It is probably a good idea to return at least something like [ 'type' => 'myExtension', ... ], see
124  // the core internal 'tca' and 'record' return values below
125  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
126  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
127  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
128  foreach ($hookClasses as $hookClass) {
129  $hookInstance = GeneralUtility::makeInstance($hookClass);
130  if (method_exists($hookClass, 'getDataStructureIdentifierPreProcess')) {
131  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPreProcess(
132  $fieldTca, $tableName, $fieldName, $row
133  );
134  if (!is_array($dataStructureIdentifier)) {
135  throw new \RuntimeException(
136  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPreProcess must return an array',
137  1478096535
138  );
139  }
140  if (!empty($dataStructureIdentifier)) {
141  // Early break at first hook that returned something!
142  break;
143  }
144  }
145  }
146  }
147 
148  // If hooks didn't return something, kick in core logic
149  if (empty($dataStructureIdentifier)) {
150  $tcaDataStructureArray = $fieldTca['config']['ds'] ?? null;
151  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
152  if (!is_array($tcaDataStructureArray) && $tcaDataStructurePointerField) {
153  // "ds" is not an array, but "ds_pointerField" is set -> data structure is found in different table
154  $dataStructureIdentifier = $this->getDataStructureIdentifierFromRecord($fieldTca, $tableName,
155  $fieldName, $row);
156  } elseif (is_array($tcaDataStructureArray)) {
157  $dataStructureIdentifier = $this->getDataStructureIdentifierFromTcaArray($fieldTca, $tableName,
158  $fieldName, $row);
159  } else {
160  throw new \RuntimeException(
161  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
162  . ' The field is configured as type="flex" and no "ds_pointerField" is defined and "ds" is not an array.'
163  . ' Either configure a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
164  . ' that specifies the data structure',
165  1463826960
166  );
167  }
168  }
169 
170  // Second hook to manipulate identifier again. This can be used to add additional data to
171  // identifiers. Be careful here, especially if stuff from the source record like uid or pid
172  // is added! This may easily lead to issues with data handler details like copy or move records,
173  // localization and version overlays. Test this very well!
174  // Multiple hooks may add information to the same identifier here - take care to namespace array keys.
175  // Information added here can be later used in parseDataStructureByIdentifier post process hook again.
176  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
177  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])) {
178  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
179  foreach ($hookClasses as $hookClass) {
180  $hookInstance = GeneralUtility::makeInstance($hookClass);
181  if (method_exists($hookClass, 'getDataStructureIdentifierPostProcess')) {
182  $dataStructureIdentifier = $hookInstance->getDataStructureIdentifierPostProcess(
183  $fieldTca, $tableName, $fieldName, $row, $dataStructureIdentifier
184  );
185  if (!is_array($dataStructureIdentifier) || empty($dataStructureIdentifier)) {
186  throw new \RuntimeException(
187  'Hook class ' . $hookClass . ' method getDataStructureIdentifierPostProcess must return a non empty array',
188  1478350835
189  );
190  }
191  }
192  }
193  }
194 
195  return json_encode($dataStructureIdentifier);
196  }
197 
247  protected function getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row): array
248  {
249  $pointerFieldName = $finalPointerFieldName = $fieldTca['config']['ds_pointerField'];
250  if (!array_key_exists($pointerFieldName, $row)) {
251  // Pointer field does not exist in row at all -> throw
252  throw new InvalidTcaException(
253  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
254  . ' configured and given row does not have a field with ds_pointerField name "' . $pointerFieldName . '".',
255  1464115059
256  );
257  }
258  $pointerValue = $row[$pointerFieldName];
259  // If set, this is typically set to "pid"
260  $parentFieldName = $fieldTca['config']['ds_pointerField_searchParent'] ?? null;
261  $pointerSubFieldName = $fieldTca['config']['ds_pointerField_searchParent_subField'] ?? null;
262  if (!$pointerValue && $parentFieldName) {
263  // Fetch rootline until a valid pointer value is found
264  $handledUids = [];
265  while (!$pointerValue) {
266  $handledUids[$row['uid']] = 1;
267  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
268  $queryBuilder->getRestrictions()
269  ->removeAll()
270  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
271  $queryBuilder->select('uid', $parentFieldName, $pointerFieldName);
272  if (!empty($pointerSubFieldName)) {
273  $queryBuilder->addSelect($pointerSubFieldName);
274  }
275  $queryStatement = $queryBuilder->from($tableName)
276  ->where($queryBuilder->expr()->eq(
277  'uid',
278  $queryBuilder->createNamedParameter($row[$parentFieldName], \PDO::PARAM_INT))
279  )
280  ->execute();
281  if ($queryStatement->rowCount() !== 1) {
282  throw new InvalidParentRowException(
283  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
284  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
285  . ' with uid "' . $row[$parentFieldName] . '" was done. This row however does not exist or was deleted.',
286  1463833794
287  );
288  }
289  $row = $queryStatement->fetch();
290  if (isset($handledUids[$row[$parentFieldName]])) {
291  // Row has been fetched before already -> loop detected!
293  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
294  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
295  . ' with uid "' . $row[$parentFieldName] . '" was done. A loop of records was detected, the tree is broken.',
296  1464110956
297  );
298  }
299  BackendUtility::workspaceOL($tableName, $row);
300  BackendUtility::fixVersioningPid($tableName, $row, true);
301  // New pointer value: This is the "subField" value if given, else the field value
302  // ds_pointerField_searchParent_subField is the "template on next level" structure from templavoila
303  if ($pointerSubFieldName && $row[$pointerSubFieldName]) {
304  $finalPointerFieldName = $pointerSubFieldName;
305  $pointerValue = $row[$pointerSubFieldName];
306  } else {
307  $pointerValue = $row[$pointerFieldName];
308  }
309  if (!$pointerValue && ((int)$row[$parentFieldName] === 0 || $row[$parentFieldName] === null)) {
310  // If on root level and still no valid pointer found -> exception
312  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
313  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
314  . ' with uid "' . $row[$parentFieldName] . '" was done. Root node with uid "' . $row['uid'] . '"'
315  . ' was fetched and still no valid pointer field value was found.',
316  1464112555
317  );
318  }
319  }
320  }
321  if (!$pointerValue) {
322  // Still no valid pointer value -> exception, This still can be a data integrity issue, so throw a catchable exception
324  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
325  . ' configured and data structure could be found by resolving parents. This is probably a TCA misconfiguration.',
326  1464114011
327  );
328  }
329  // Ok, finally we have the field value. This is now either a data structure directly, or a pointer to a file,
330  // or the value can be interpreted as integer (is an uid) and "ds_tableField" is set, so this is the table, uid and field
331  // where the final data structure can be found.
332  if (MathUtility::canBeInterpretedAsInteger($pointerValue)) {
333  if (!isset($fieldTca['config']['ds_tableField'])) {
334  throw new InvalidTcaException(
335  'Invalid data structure pointer for field "' . $fieldName . '" in table "' . $tableName . '", the value'
336  . 'resolved to "' . $pointerValue . '" . which is an integer, so "ds_tableField" must be configured',
337  1464115639
338  );
339  }
340  if (substr_count($fieldTca['config']['ds_tableField'], ':') !== 1) {
341  // ds_tableField must be of the form "table:field"
342  throw new InvalidTcaException(
343  'Invalid TCA configuration for field "' . $fieldName . '" in table "' . $tableName . '", the setting'
344  . '"ds_tableField" must be of the form "tableName:fieldName"',
345  1464116002
346  );
347  }
348  list($foreignTableName, $foreignFieldName) = GeneralUtility::trimExplode(':', $fieldTca['config']['ds_tableField']);
349  $dataStructureIdentifier = [
350  'type' => 'record',
351  'tableName' => $foreignTableName,
352  'uid' => (int)$pointerValue,
353  'fieldName' => $foreignFieldName,
354  ];
355  } else {
356  $dataStructureIdentifier = [
357  'type' => 'record',
358  'tableName' => $tableName,
359  'uid' => (int)$row['uid'],
360  'fieldName' => $finalPointerFieldName,
361  ];
362  }
363  return $dataStructureIdentifier;
364  }
365 
407  protected function getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row): array
408  {
409  $dataStructureIdentifier = [
410  'type' => 'tca',
411  'tableName' => $tableName,
412  'fieldName' => $fieldName,
413  'dataStructureKey' => null,
414  ];
415  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
416  if ($tcaDataStructurePointerField === null) {
417  // No ds_pointerField set -> use 'default' as ds array key if exists.
418  if (isset($fieldTca['config']['ds']['default'])) {
419  $dataStructureIdentifier['dataStructureKey'] = 'default';
420  } else {
421  // A tca is configured as flex without ds_pointerField. A 'default' key must exist, otherwise
422  // this is a configuration error.
423  // May happen with an unloaded extension -> catchable
424  throw new InvalidTcaException(
425  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
426  . ' The field is configured as type="flex" and no "ds_pointerField" is defined. Either configure'
427  . ' a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
428  . ' that specifies the data structure',
429  1463652560
430  );
431  }
432  } else {
433  // ds_pointerField is set, it can be a comma separated list of two fields, explode it.
434  $pointerFieldArray = GeneralUtility::trimExplode(',', $tcaDataStructurePointerField, true);
435  // Obvious configuration error, either one or two fields must be declared
436  $pointerFieldsCount = count($pointerFieldArray);
437  if ($pointerFieldsCount !== 1 && $pointerFieldsCount !== 2) {
438  // If it's there, it must be correct -> not catchable
439  throw new \RuntimeException(
440  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
441  . ' ds_pointerField must be either a single field name, or a comma separated list of two fields,'
442  . ' the invalid configuration string provided was: "' . $tcaDataStructurePointerField . '"',
443  1463577497
444  );
445  }
446  // Verify first field exists in row array. If not, this is a hard error: Any extension that sets a
447  // ds_pointerField to some field name should take care that field does exist, too. They are a pair,
448  // so there shouldn't be a situation where the field does not exist. Throw an exception if that is violated.
449  if (!isset($row[$pointerFieldArray[0]])) {
450  // If it's declared, it must exist -> not catchable
451  throw new \RuntimeException(
452  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
453  . ' ds_pointerField "' . $pointerFieldArray[0] . '" points to a field name that does not exist.',
454  1463578899
455  );
456  }
457  // Similar situation for the second field: If it is set, the field must exist.
458  if (isset($pointerFieldArray[1]) && !isset($row[$pointerFieldArray[1]])) {
459  // If it's declared, it must exist -> not catchable
460  throw new \RuntimeException(
461  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
462  . ' Second part "' . $pointerFieldArray[1] . '" of ds_pointerField with full value "'
463  . $tcaDataStructurePointerField . '" points to a field name that does not exist.',
464  1463578900
465  );
466  }
467  if ($pointerFieldsCount === 1) {
468  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
469  // Field value points directly to an existing key in tca ds
470  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
471  } elseif (isset($fieldTca['config']['ds']['default'])) {
472  // Field value does not exit in tca ds, fall back to default key if exists
473  $dataStructureIdentifier['dataStructureKey'] = 'default';
474  } else {
475  // The value of the ds_pointerField field points to a key in the ds array that does
476  // not exists, and there is no fallback either. This can happen if an extension brings
477  // new flex form definitions and that extension is unloaded later. "Old" records of the
478  // extension could then still point to the no longer existing key in ds. We throw a
479  // specific exception here to give controllers an opportunity to catch this case.
481  'Field value of field "' . $pointerFieldArray[0] . '" of database record with uid "'
482  . $row['uid'] . '" from table "' . $tableName . '" points to a "ds" key ' . $row[$pointerFieldArray[0]]
483  . ' but this key does not exist and there is no "default" fallback.',
484  1463653197
485  );
486  }
487  } else {
488  // Two comma separated field names
489  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]]])) {
490  // firstValue,secondValue
491  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]];
492  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[1]] . ',*'])) {
493  // secondValue,* ?!
494  // @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - just remove this elseif together with two unit tests
495  // This case is a wrong implementation - it matches "secondFieldValue,*", but it
496  // should match "*,secondFieldValue" only. Since this bug has been in the code for ages, it
497  // still works in v8 but is deprecated now.
498  // Try to log a meaningful deprecation message though, so devs can adapt
500  'TCA field "' . $fieldName . '" of table "' . $tableName . '" has a registered data structure'
501  . ' with name "' . $row[$pointerFieldArray[1]] . ',*". The ds_pointerField is set to "'
502  . $tcaDataStructurePointerField . '", with the matching value "' . $row[$pointerFieldArray[1]] . '"'
503  . ' for field "' . $pointerFieldArray[1] . '". This should be the other way round, so the name'
504  . ' should be: "*,' . $row[$pointerFieldArray[1]] . '" in the ds TCA array. Please change that'
505  . ' until TYPO3 v9, this matching code will be removed then.'
506  );
507  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[1]] . ',*';
508  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',*'])) {
509  // firstValue,*
510  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',*';
511  } elseif (isset($fieldTca['config']['ds']['*,' . $row[$pointerFieldArray[1]]])) {
512  // *,secondValue
513  $dataStructureIdentifier['dataStructureKey'] = '*,' . $row[$pointerFieldArray[1]];
514  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
515  // firstValue
516  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
517  } elseif (isset($fieldTca['config']['ds']['default'])) {
518  // Fall back to default
519  $dataStructureIdentifier['dataStructureKey'] = 'default';
520  } else {
521  // No ds_pointerField value could be determined and 'default' does not exist as
522  // fallback. This is the same case as the above scenario, throw a
523  // InvalidCombinedPointerFieldException here, too.
525  'Field combination of fields "' . $pointerFieldArray[0] . '" and "' . $pointerFieldArray[1] . '" of database'
526  . 'record with uid "' . $row['uid'] . '" from table "' . $tableName . '" with values "' . $row[$pointerFieldArray[0]] . '"'
527  . ' and "' . $row[$pointerFieldArray[1]] . '" could not be resolved to any registered data structure and '
528  . ' no "default" fallback exists.',
529  1463678524
530  );
531  }
532  }
533  }
534  return $dataStructureIdentifier;
535  }
536 
566  public function parseDataStructureByIdentifier(string $identifier): array
567  {
568  // Throw an exception for an empty string. This might be a valid use case for new
569  // records in some situations, so this is catchable to give callers a chance to deal with that.
570  if (empty($identifier)) {
571  throw new InvalidIdentifierException(
572  'Empty string given to parseFlexFormDataStructureByIdentifier(). This exception might '
573  . ' be caught to handle some new record situations properly',
574  1478100828
575  );
576  }
577 
578  $identifier = json_decode($identifier, true);
579 
580  if (!is_array($identifier) || empty($identifier)) {
581  // If there is some identifier and it can't be decoded, programming error -> not catchable
582  throw new \RuntimeException(
583  'Identifier could not be decoded to an array.',
584  1478345642
585  );
586  }
587 
588  $dataStructure = '';
589 
590  // Hook to fetch data structure by given identifier.
591  // Method parseFlexFormDataStructureByIdentifier() must be implemented and returns either an
592  // empty string "not my business", or a string with the resolved data structure string, or FILE: reference,
593  // or a fully parsed data structure as aray.
594  // Result of the FIRST hook that gives an non-empty string is used, namespace your identifiers in
595  // a way that there is little chance they overlap (eg. prefix with extension name).
596  // If implemented, this hook should be paired with a hook in getDataStructureIdentifier() above.
597  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
598  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
599  ) {
600  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
601  foreach ($hookClasses as $hookClass) {
602  $hookInstance = GeneralUtility::makeInstance($hookClass);
603  if (method_exists($hookClass, 'parseDataStructureByIdentifierPreProcess')) {
604  $dataStructure = $hookInstance->parseDataStructureByIdentifierPreProcess($identifier);
605  if (!is_string($dataStructure) && !is_array($dataStructure)) {
606  // Programming error -> not catchable
607  throw new \RuntimeException(
608  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must either'
609  . ' return an empty string or a data structure string or a parsed data structure array.',
610  1478168512
611  );
612  }
613  if (!empty($dataStructure)) {
614  // Early break if a hook resolved to something!
615  break;
616  }
617  }
618  }
619  }
620 
621  // If hooks didn't resolve, try own methods
622  if (empty($dataStructure)) {
623  if ($identifier['type'] === 'record') {
624  // Handle "record" type, see getDataStructureIdentifierFromRecord()
625  if (empty($identifier['tableName']) || empty($identifier['uid']) || empty($identifier['fieldName'])) {
626  throw new \RuntimeException(
627  'Incomplete "record" based identifier: ' . json_encode($identifier),
628  1478113873
629  );
630  }
631  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($identifier['tableName']);
632  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
633  $dataStructure = $queryBuilder
634  ->select($identifier['fieldName'])
635  ->from($identifier['tableName'])
636  ->where(
637  $queryBuilder->expr()->eq(
638  'uid',
639  $queryBuilder->createNamedParameter($identifier['uid'], \PDO::PARAM_INT)
640  )
641  )
642  ->execute()
643  ->fetchColumn(0);
644  } elseif ($identifier['type'] === 'tca') {
645  // Handle "tca" type, see getDataStructureIdentifierFromTcaArray
646  if (empty($identifier['tableName']) || empty($identifier['fieldName']) || empty($identifier['dataStructureKey'])) {
647  throw new \RuntimeException(
648  'Incomplete "tca" based identifier: ' . json_encode($identifier),
649  1478113471
650  );
651  }
652  $table = $identifier['tableName'];
653  $field = $identifier['fieldName'];
654  $dataStructureKey = $identifier['dataStructureKey'];
655  if (!isset($GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
656  || !is_string($GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
657  ) {
658  // This may happen for elements pointing to an unloaded extension -> catchable
659  throw new InvalidIdentifierException(
660  'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid'
661  . ' TCA array value',
662  1478105491
663  );
664  }
665  $dataStructure = $GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey];
666  } else {
667  throw new InvalidIdentifierException(
668  'Identifier ' . json_encode($identifier) . ' could not be resolved',
669  1478104554
670  );
671  }
672  }
673 
674  // Hooks may have parse the data structure already to an array. If that is not the case, parse it now.
675  if (is_string($dataStructure)) {
676  // Resolve FILE: prefix pointing to a DS in a file
677  if (strpos(trim($dataStructure), 'FILE:') === 0) {
678  $file = GeneralUtility::getFileAbsFileName(substr(trim($dataStructure), 5));
679  if (empty($file) || !@is_file($file)) {
680  throw new \RuntimeException(
681  'Data structure file ' . $file . ' could not be resolved to an existing file',
682  1478105826
683  );
684  }
685  $dataStructure = file_get_contents($file);
686  }
687 
688  // Parse main structure
689  $dataStructure = GeneralUtility::xml2array($dataStructure);
690  }
691 
692  // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
693  // This also may happen if artificial identifiers were constructed which don't resolve. The
694  // flex form "exclude" access rights systems does that -> catchable
695  if (!is_array($dataStructure)) {
696  throw new InvalidIdentifierException(
697  'Parse error: Data structure could not be resolved to a valid structure.',
698  1478106090
699  );
700  }
701 
702  // Create default sheet if there is none, yet.
703  if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
704  throw new \RuntimeException(
705  'Parsed data structure has both ROOT and sheets on top level. Thats invalid.',
706  1440676540
707  );
708  }
709  if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
710  $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
711  unset($dataStructure['ROOT']);
712  }
713 
714  // Resolve FILE:EXT and EXT: for single sheets
715  if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
716  foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
717  if (!is_array($sheetStructure)) {
718  if (strpos(trim($sheetStructure), 'FILE:') === 0) {
719  $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
720  } else {
721  $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
722  }
723  if ($file && @is_file($file)) {
724  $sheetStructure = GeneralUtility::xml2array(file_get_contents($file));
725  }
726  }
727  $dataStructure['sheets'][$sheetName] = $sheetStructure;
728  }
729  }
730 
731  // Hook to manipulate data structure further. This can be used to add or remove fields
732  // from given structure. Multiple hooks can be registered, all are called. They
733  // receive the parsed structure and the identifier array.
734  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
735  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
736  ) {
737  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
738  foreach ($hookClasses as $hookClass) {
739  $hookInstance = GeneralUtility::makeInstance($hookClass);
740  if (method_exists($hookClass, 'parseDataStructureByIdentifierPostProcess')) {
741  $dataStructure = $hookInstance->parseDataStructureByIdentifierPostProcess($dataStructure, $identifier);
742  if (!is_array($dataStructure)) {
743  // Programming error -> not catchable
744  throw new \RuntimeException(
745  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must return and array.',
746  1478350806
747  );
748  }
749  }
750  }
751  }
752 
753  return $dataStructure;
754  }
755 
766  public function traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
767  {
768  if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
769  return 'TCA table/field was not defined.';
770  }
771  $this->callBackObj = $callBackObj;
772  // Get Data Structure:
773  $dataStructureIdentifier = $this->getDataStructureIdentifier($GLOBALS['TCA'][$table]['columns'][$field], $table, $field, $row);
774  $dataStructureArray = $this->parseDataStructureByIdentifier($dataStructureIdentifier);
775  // Get flexform XML data
776  $editData = GeneralUtility::xml2array($row[$field]);
777  if (!is_array($editData)) {
778  return 'Parsing error: ' . $editData;
779  }
780  // Traverse languages:
781  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
782  // Render sheet:
783  if (is_array($sheetData['ROOT']) && is_array($sheetData['ROOT']['el'])) {
784  $PA['vKeys'] = ['DEF'];
785  $PA['lKey'] = 'lDEF';
786  $PA['callBackMethod_value'] = $callBackMethod_value;
787  $PA['table'] = $table;
788  $PA['field'] = $field;
789  $PA['uid'] = $row['uid'];
790  // Render flexform:
791  $this->traverseFlexFormXMLData_recurse($sheetData['ROOT']['el'], $editData['data'][$sheetKey]['lDEF'], $PA, 'data/' . $sheetKey . '/lDEF');
792  } else {
793  return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheetKey . '".';
794  }
795  }
796  return true;
797  }
798 
808  public function traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = '')
809  {
810  if (is_array($dataStruct)) {
811  foreach ($dataStruct as $key => $value) {
812  // The value of each entry must be an array.
813  if (is_array($value)) {
814  if ($value['type'] == 'array') {
815  // Array (Section) traversal
816  if ($value['section']) {
817  $cc = 0;
818  if (is_array($editData[$key]['el'])) {
819  if ($this->reNumberIndexesOfSectionData) {
820  $temp = [];
821  $c3 = 0;
822  foreach ($editData[$key]['el'] as $v3) {
823  $temp[++$c3] = $v3;
824  }
825  $editData[$key]['el'] = $temp;
826  }
827  foreach ($editData[$key]['el'] as $k3 => $v3) {
828  if (is_array($v3)) {
829  $cc = $k3;
830  $theType = key($v3);
831  $theDat = $v3[$theType];
832  $newSectionEl = $value['el'][$theType];
833  if (is_array($newSectionEl)) {
834  $this->traverseFlexFormXMLData_recurse([$theType => $newSectionEl], [$theType => $theDat], $PA, $path . '/' . $key . '/el/' . $cc);
835  }
836  }
837  }
838  }
839  } else {
840  // Array traversal
841  if (is_array($editData) && is_array($editData[$key])) {
842  $this->traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
843  }
844  }
845  } elseif (is_array($value['TCEforms']['config'])) {
846  // Processing a field value:
847  foreach ($PA['vKeys'] as $vKey) {
848  $vKey = 'v' . $vKey;
849  // Call back
850  if ($PA['callBackMethod_value'] && is_array($editData) && is_array($editData[$key])) {
851  $this->executeCallBackMethod($PA['callBackMethod_value'], [$value, $editData[$key][$vKey], $PA, $path . '/' . $key . '/' . $vKey, $this]);
852  }
853  }
854  }
855  }
856  }
857  }
858  }
859 
867  protected function executeCallBackMethod($methodName, array $parameterArray)
868  {
869  return call_user_func_array([$this->callBackObj, $methodName], $parameterArray);
870  }
871 
872  /***********************************
873  *
874  * Processing functions
875  *
876  ***********************************/
886  public function cleanFlexFormXML($table, $field, $row)
887  {
888  // New structure:
889  $this->cleanFlexFormXML = [];
890  // Create and call iterator object:
891  $flexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
892  $flexObj->reNumberIndexesOfSectionData = true;
893  $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
894  return $this->flexArray2Xml($this->cleanFlexFormXML, true);
895  }
896 
908  public function cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
909  {
910  // Just setting value in our own result array, basically replicating the structure:
911  $pObj->setArrayValueByPath($path, $this->cleanFlexFormXML, $data);
912  }
913 
914  /***********************************
915  *
916  * Multi purpose functions
917  *
918  ***********************************/
926  public function &getArrayValueByPath($pathArray, &$array)
927  {
928  if (!is_array($pathArray)) {
929  $pathArray = explode('/', $pathArray);
930  }
931  if (is_array($array) && !empty($pathArray)) {
932  $key = array_shift($pathArray);
933  if (isset($array[$key])) {
934  if (empty($pathArray)) {
935  return $array[$key];
936  }
937  return $this->getArrayValueByPath($pathArray, $array[$key]);
938  }
939  return null;
940  }
941  }
942 
951  public function setArrayValueByPath($pathArray, &$array, $value)
952  {
953  if (isset($value)) {
954  if (!is_array($pathArray)) {
955  $pathArray = explode('/', $pathArray);
956  }
957  if (is_array($array) && !empty($pathArray)) {
958  $key = array_shift($pathArray);
959  if (empty($pathArray)) {
960  $array[$key] = $value;
961  return true;
962  }
963  if (!isset($array[$key])) {
964  $array[$key] = [];
965  }
966  return $this->setArrayValueByPath($pathArray, $array[$key], $value);
967  }
968  }
969  }
970 
978  public function flexArray2Xml($array, $addPrologue = false)
979  {
980  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
981  $this->flexArray2Xml_options['useCDATA'] = 1;
982  }
983  $output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $this->flexArray2Xml_options);
984  if ($addPrologue) {
985  $output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . $output;
986  }
987  return $output;
988  }
989 }
traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path= '')
getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row)
static xml2array($string, $NSprefix= '', $reportDocTag=false)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static array2xml(array $array, $NSprefix= '', $level=0, $docTag= 'phparray', $spaceInd=0, array $options=[], array $stackData=[])
traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
executeCallBackMethod($methodName, array $parameterArray)
cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static getFileAbsFileName($filename, $_=null, $_2=null)
getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row)
static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)