TYPO3 CMS  TYPO3_8-7
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 {
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 
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  if ($queryStatement->rowCount() !== 1) {
305  throw new InvalidParentRowException(
306  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
307  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
308  . ' with uid "' . $row[$parentFieldName] . '" was done. This row however does not exist or was deleted.',
309  1463833794
310  );
311  }
312  $row = $queryStatement->fetch();
313  if (isset($handledUids[$row[$parentFieldName]])) {
314  // Row has been fetched before already -> loop detected!
316  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
317  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
318  . ' with uid "' . $row[$parentFieldName] . '" was done. A loop of records was detected, the tree is broken.',
319  1464110956
320  );
321  }
322  BackendUtility::workspaceOL($tableName, $row);
323  BackendUtility::fixVersioningPid($tableName, $row, true);
324  // New pointer value: This is the "subField" value if given, else the field value
325  // ds_pointerField_searchParent_subField is the "template on next level" structure from templavoila
326  if ($pointerSubFieldName && $row[$pointerSubFieldName]) {
327  $finalPointerFieldName = $pointerSubFieldName;
328  $pointerValue = $row[$pointerSubFieldName];
329  } else {
330  $pointerValue = $row[$pointerFieldName];
331  }
332  if (!$pointerValue && ((int)$row[$parentFieldName] === 0 || $row[$parentFieldName] === null)) {
333  // If on root level and still no valid pointer found -> exception
335  'The data structure for field "' . $fieldName . '" in table "' . $tableName . '" has to be looked up'
336  . ' in field "' . $pointerFieldName . '". That field had no valid value, so a lookup in parent record'
337  . ' with uid "' . $row[$parentFieldName] . '" was done. Root node with uid "' . $row['uid'] . '"'
338  . ' was fetched and still no valid pointer field value was found.',
339  1464112555
340  );
341  }
342  }
343  }
344  if (!$pointerValue) {
345  // Still no valid pointer value -> exception, This still can be a data integrity issue, so throw a catchable exception
347  'No data structure for field "' . $fieldName . '" in table "' . $tableName . '" found, no "ds" array'
348  . ' configured and data structure could be found by resolving parents. This is probably a TCA misconfiguration.',
349  1464114011
350  );
351  }
352  // Ok, finally we have the field value. This is now either a data structure directly, or a pointer to a file,
353  // or the value can be interpreted as integer (is an uid) and "ds_tableField" is set, so this is the table, uid and field
354  // where the final data structure can be found.
355  if (MathUtility::canBeInterpretedAsInteger($pointerValue)) {
356  if (!isset($fieldTca['config']['ds_tableField'])) {
357  throw new InvalidTcaException(
358  'Invalid data structure pointer for field "' . $fieldName . '" in table "' . $tableName . '", the value'
359  . 'resolved to "' . $pointerValue . '" . which is an integer, so "ds_tableField" must be configured',
360  1464115639
361  );
362  }
363  if (substr_count($fieldTca['config']['ds_tableField'], ':') !== 1) {
364  // ds_tableField must be of the form "table:field"
365  throw new InvalidTcaException(
366  'Invalid TCA configuration for field "' . $fieldName . '" in table "' . $tableName . '", the setting'
367  . '"ds_tableField" must be of the form "tableName:fieldName"',
368  1464116002
369  );
370  }
371  list($foreignTableName, $foreignFieldName) = GeneralUtility::trimExplode(':', $fieldTca['config']['ds_tableField']);
372  $dataStructureIdentifier = [
373  'type' => 'record',
374  'tableName' => $foreignTableName,
375  'uid' => (int)$pointerValue,
376  'fieldName' => $foreignFieldName,
377  ];
378  } else {
379  $dataStructureIdentifier = [
380  'type' => 'record',
381  'tableName' => $tableName,
382  'uid' => (int)$row['uid'],
383  'fieldName' => $finalPointerFieldName,
384  ];
385  }
386  return $dataStructureIdentifier;
387  }
388 
430  protected function getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row): array
431  {
432  $dataStructureIdentifier = [
433  'type' => 'tca',
434  'tableName' => $tableName,
435  'fieldName' => $fieldName,
436  'dataStructureKey' => null,
437  ];
438  $tcaDataStructurePointerField = $fieldTca['config']['ds_pointerField'] ?? null;
439  if ($tcaDataStructurePointerField === null) {
440  // No ds_pointerField set -> use 'default' as ds array key if exists.
441  if (isset($fieldTca['config']['ds']['default'])) {
442  $dataStructureIdentifier['dataStructureKey'] = 'default';
443  } else {
444  // A tca is configured as flex without ds_pointerField. A 'default' key must exist, otherwise
445  // this is a configuration error.
446  // May happen with an unloaded extension -> catchable
447  throw new InvalidTcaException(
448  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
449  . ' The field is configured as type="flex" and no "ds_pointerField" is defined. Either configure'
450  . ' a default data structure in [\'ds\'][\'default\'] or add a "ds_pointerField" lookup mechanism'
451  . ' that specifies the data structure',
452  1463652560
453  );
454  }
455  } else {
456  // ds_pointerField is set, it can be a comma separated list of two fields, explode it.
457  $pointerFieldArray = GeneralUtility::trimExplode(',', $tcaDataStructurePointerField, true);
458  // Obvious configuration error, either one or two fields must be declared
459  $pointerFieldsCount = count($pointerFieldArray);
460  if ($pointerFieldsCount !== 1 && $pointerFieldsCount !== 2) {
461  // If it's there, it must be correct -> not catchable
462  throw new \RuntimeException(
463  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
464  . ' ds_pointerField must be either a single field name, or a comma separated list of two fields,'
465  . ' the invalid configuration string provided was: "' . $tcaDataStructurePointerField . '"',
466  1463577497
467  );
468  }
469  // Verify first field exists in row array. If not, this is a hard error: Any extension that sets a
470  // ds_pointerField to some field name should take care that field does exist, too. They are a pair,
471  // so there shouldn't be a situation where the field does not exist. Throw an exception if that is violated.
472  if (!isset($row[$pointerFieldArray[0]])) {
473  // If it's declared, it must exist -> not catchable
474  throw new \RuntimeException(
475  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
476  . ' ds_pointerField "' . $pointerFieldArray[0] . '" points to a field name that does not exist.',
477  1463578899
478  );
479  }
480  // Similar situation for the second field: If it is set, the field must exist.
481  if (isset($pointerFieldArray[1]) && !isset($row[$pointerFieldArray[1]])) {
482  // If it's declared, it must exist -> not catchable
483  throw new \RuntimeException(
484  'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
485  . ' Second part "' . $pointerFieldArray[1] . '" of ds_pointerField with full value "'
486  . $tcaDataStructurePointerField . '" points to a field name that does not exist.',
487  1463578900
488  );
489  }
490  if ($pointerFieldsCount === 1) {
491  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
492  // Field value points directly to an existing key in tca ds
493  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
494  } elseif (isset($fieldTca['config']['ds']['default'])) {
495  // Field value does not exit in tca ds, fall back to default key if exists
496  $dataStructureIdentifier['dataStructureKey'] = 'default';
497  } else {
498  // The value of the ds_pointerField field points to a key in the ds array that does
499  // not exists, and there is no fallback either. This can happen if an extension brings
500  // new flex form definitions and that extension is unloaded later. "Old" records of the
501  // extension could then still point to the no longer existing key in ds. We throw a
502  // specific exception here to give controllers an opportunity to catch this case.
504  'Field value of field "' . $pointerFieldArray[0] . '" of database record with uid "'
505  . $row['uid'] . '" from table "' . $tableName . '" points to a "ds" key ' . $row[$pointerFieldArray[0]]
506  . ' but this key does not exist and there is no "default" fallback.',
507  1463653197
508  );
509  }
510  } else {
511  // Two comma separated field names
512  if (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]]])) {
513  // firstValue,secondValue
514  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',' . $row[$pointerFieldArray[1]];
515  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[1]] . ',*'])) {
516  // secondValue,* ?!
517  // @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - just remove this elseif together with two unit tests
518  // This case is a wrong implementation - it matches "secondFieldValue,*", but it
519  // should match "*,secondFieldValue" only. Since this bug has been in the code for ages, it
520  // still works in v8 but is deprecated now.
521  // Try to log a meaningful deprecation message though, so devs can adapt
523  'TCA field "' . $fieldName . '" of table "' . $tableName . '" has a registered data structure'
524  . ' with name "' . $row[$pointerFieldArray[1]] . ',*". The ds_pointerField is set to "'
525  . $tcaDataStructurePointerField . '", with the matching value "' . $row[$pointerFieldArray[1]] . '"'
526  . ' for field "' . $pointerFieldArray[1] . '". This should be the other way round, so the name'
527  . ' should be: "*,' . $row[$pointerFieldArray[1]] . '" in the ds TCA array. Please change that'
528  . ' until TYPO3 v9, this matching code will be removed then.'
529  );
530  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[1]] . ',*';
531  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]] . ',*'])) {
532  // firstValue,*
533  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]] . ',*';
534  } elseif (isset($fieldTca['config']['ds']['*,' . $row[$pointerFieldArray[1]]])) {
535  // *,secondValue
536  $dataStructureIdentifier['dataStructureKey'] = '*,' . $row[$pointerFieldArray[1]];
537  } elseif (isset($fieldTca['config']['ds'][$row[$pointerFieldArray[0]]])) {
538  // firstValue
539  $dataStructureIdentifier['dataStructureKey'] = $row[$pointerFieldArray[0]];
540  } elseif (isset($fieldTca['config']['ds']['default'])) {
541  // Fall back to default
542  $dataStructureIdentifier['dataStructureKey'] = 'default';
543  } else {
544  // No ds_pointerField value could be determined and 'default' does not exist as
545  // fallback. This is the same case as the above scenario, throw a
546  // InvalidCombinedPointerFieldException here, too.
548  'Field combination of fields "' . $pointerFieldArray[0] . '" and "' . $pointerFieldArray[1] . '" of database'
549  . 'record with uid "' . $row['uid'] . '" from table "' . $tableName . '" with values "' . $row[$pointerFieldArray[0]] . '"'
550  . ' and "' . $row[$pointerFieldArray[1]] . '" could not be resolved to any registered data structure and '
551  . ' no "default" fallback exists.',
552  1463678524
553  );
554  }
555  }
556  }
557  return $dataStructureIdentifier;
558  }
559 
589  public function parseDataStructureByIdentifier(string $identifier): array
590  {
591  // Throw an exception for an empty string. This might be a valid use case for new
592  // records in some situations, so this is catchable to give callers a chance to deal with that.
593  if (empty($identifier)) {
594  throw new InvalidIdentifierException(
595  'Empty string given to parseFlexFormDataStructureByIdentifier(). This exception might '
596  . ' be caught to handle some new record situations properly',
597  1478100828
598  );
599  }
600 
601  $identifier = json_decode($identifier, true);
602 
603  if (!is_array($identifier) || empty($identifier)) {
604  // If there is some identifier and it can't be decoded, programming error -> not catchable
605  throw new \RuntimeException(
606  'Identifier could not be decoded to an array.',
607  1478345642
608  );
609  }
610 
611  $dataStructure = '';
612 
613  // Hook to fetch data structure by given identifier.
614  // Method parseFlexFormDataStructureByIdentifier() must be implemented and returns either an
615  // empty string "not my business", or a string with the resolved data structure string, or FILE: reference,
616  // or a fully parsed data structure as aray.
617  // Result of the FIRST hook that gives an non-empty string is used, namespace your identifiers in
618  // a way that there is little chance they overlap (eg. prefix with extension name).
619  // If implemented, this hook should be paired with a hook in getDataStructureIdentifier() above.
620  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
621  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
622  ) {
623  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
624  foreach ($hookClasses as $hookClass) {
625  $hookInstance = GeneralUtility::makeInstance($hookClass);
626  if (method_exists($hookClass, 'parseDataStructureByIdentifierPreProcess')) {
627  $dataStructure = $hookInstance->parseDataStructureByIdentifierPreProcess($identifier);
628  if (!is_string($dataStructure) && !is_array($dataStructure)) {
629  // Programming error -> not catchable
630  throw new \RuntimeException(
631  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must either'
632  . ' return an empty string or a data structure string or a parsed data structure array.',
633  1478168512
634  );
635  }
636  if (!empty($dataStructure)) {
637  // Early break if a hook resolved to something!
638  break;
639  }
640  }
641  }
642  }
643 
644  // If hooks didn't resolve, try own methods
645  if (empty($dataStructure)) {
646  if ($identifier['type'] === 'record') {
647  // Handle "record" type, see getDataStructureIdentifierFromRecord()
648  if (empty($identifier['tableName']) || empty($identifier['uid']) || empty($identifier['fieldName'])) {
649  throw new \RuntimeException(
650  'Incomplete "record" based identifier: ' . json_encode($identifier),
651  1478113873
652  );
653  }
654  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($identifier['tableName']);
655  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
656  $dataStructure = $queryBuilder
657  ->select($identifier['fieldName'])
658  ->from($identifier['tableName'])
659  ->where(
660  $queryBuilder->expr()->eq(
661  'uid',
662  $queryBuilder->createNamedParameter($identifier['uid'], \PDO::PARAM_INT)
663  )
664  )
665  ->execute()
666  ->fetchColumn(0);
667  } elseif ($identifier['type'] === 'tca') {
668  // Handle "tca" type, see getDataStructureIdentifierFromTcaArray
669  if (empty($identifier['tableName']) || empty($identifier['fieldName']) || empty($identifier['dataStructureKey'])) {
670  throw new \RuntimeException(
671  'Incomplete "tca" based identifier: ' . json_encode($identifier),
672  1478113471
673  );
674  }
675  $table = $identifier['tableName'];
676  $field = $identifier['fieldName'];
677  $dataStructureKey = $identifier['dataStructureKey'];
678  if (!isset($GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
679  || !is_string($GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey])
680  ) {
681  // This may happen for elements pointing to an unloaded extension -> catchable
682  throw new InvalidIdentifierException(
683  'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid'
684  . ' TCA array value',
685  1478105491
686  );
687  }
688  $dataStructure = $GLOBALS['TCA'][$table]['columns'][$field]['config']['ds'][$dataStructureKey];
689  } else {
690  throw new InvalidIdentifierException(
691  'Identifier ' . json_encode($identifier) . ' could not be resolved',
692  1478104554
693  );
694  }
695  }
696 
697  // Hooks may have parse the data structure already to an array. If that is not the case, parse it now.
698  if (is_string($dataStructure)) {
699  // Resolve FILE: prefix pointing to a DS in a file
700  if (strpos(trim($dataStructure), 'FILE:') === 0) {
701  $file = GeneralUtility::getFileAbsFileName(substr(trim($dataStructure), 5));
702  if (empty($file) || !@is_file($file)) {
703  throw new \RuntimeException(
704  'Data structure file ' . $file . ' could not be resolved to an existing file',
705  1478105826
706  );
707  }
708  $dataStructure = file_get_contents($file);
709  }
710 
711  // Parse main structure
712  $dataStructure = GeneralUtility::xml2array($dataStructure);
713  }
714 
715  // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
716  // This also may happen if artificial identifiers were constructed which don't resolve. The
717  // flex form "exclude" access rights systems does that -> catchable
718  if (!is_array($dataStructure)) {
719  throw new InvalidIdentifierException(
720  'Parse error: Data structure could not be resolved to a valid structure.',
721  1478106090
722  );
723  }
724 
725  // Create default sheet if there is none, yet.
726  if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
727  throw new \RuntimeException(
728  'Parsed data structure has both ROOT and sheets on top level. Thats invalid.',
729  1440676540
730  );
731  }
732  if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
733  $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
734  unset($dataStructure['ROOT']);
735  }
736 
737  // Resolve FILE:EXT and EXT: for single sheets
738  if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
739  foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
740  if (!is_array($sheetStructure)) {
741  if (strpos(trim($sheetStructure), 'FILE:') === 0) {
742  $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
743  } else {
744  $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
745  }
746  if ($file && @is_file($file)) {
747  $sheetStructure = GeneralUtility::xml2array(file_get_contents($file));
748  }
749  }
750  $dataStructure['sheets'][$sheetName] = $sheetStructure;
751  }
752  }
753 
754  // Hook to manipulate data structure further. This can be used to add or remove fields
755  // from given structure. Multiple hooks can be registered, all are called. They
756  // receive the parsed structure and the identifier array.
757  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
758  && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'])
759  ) {
760  $hookClasses = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['flexParsing'];
761  foreach ($hookClasses as $hookClass) {
762  $hookInstance = GeneralUtility::makeInstance($hookClass);
763  if (method_exists($hookClass, 'parseDataStructureByIdentifierPostProcess')) {
764  $dataStructure = $hookInstance->parseDataStructureByIdentifierPostProcess($dataStructure, $identifier);
765  if (!is_array($dataStructure)) {
766  // Programming error -> not catchable
767  throw new \RuntimeException(
768  'Hook class ' . $hookClass . ' method parseDataStructureByIdentifierPreProcess must return and array.',
769  1478350806
770  );
771  }
772  }
773  }
774  }
775 
776  return $dataStructure;
777  }
778 
789  public function traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
790  {
791  if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
792  return 'TCA table/field was not defined.';
793  }
794  $this->callBackObj = $callBackObj;
795 
796  // Get data structure. The methods may throw various exceptions, with some of them being
797  // ok in certain scenarios, for instance on new record rows. Those are ok to "eat" here
798  // and substitute with a dummy DS.
799  $dataStructureArray = [ 'sheets' => [ 'sDEF' => [] ] ];
800  try {
801  $dataStructureIdentifier = $this->getDataStructureIdentifier($GLOBALS['TCA'][$table]['columns'][$field], $table, $field, $row);
802  $dataStructureArray = $this->parseDataStructureByIdentifier($dataStructureIdentifier);
803  } catch (InvalidParentRowException $e) {
804  } catch (InvalidParentRowLoopException $e) {
805  } catch (InvalidParentRowRootException $e) {
806  } catch (InvalidPointerFieldValueException $e) {
807  } catch (InvalidIdentifierException $e) {
808  }
809 
810  // Get flexform XML data
811  $editData = GeneralUtility::xml2array($row[$field]);
812  if (!is_array($editData)) {
813  return 'Parsing error: ' . $editData;
814  }
815  // Check if $dataStructureArray['sheets'] is indeed an array before loop or it will crash with runtime error
816  if (!is_array($dataStructureArray['sheets'])) {
817  return 'Data Structure ERROR: sheets is defined but not an array for table ' . $table . (isset($row['uid']) ? ' and uid ' . $row['uid'] : '');
818  }
819  // Traverse languages:
820  foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
821  // Render sheet:
822  if (is_array($sheetData['ROOT']) && is_array($sheetData['ROOT']['el'])) {
823  $PA['vKeys'] = ['DEF'];
824  $PA['lKey'] = 'lDEF';
825  $PA['callBackMethod_value'] = $callBackMethod_value;
826  $PA['table'] = $table;
827  $PA['field'] = $field;
828  $PA['uid'] = $row['uid'];
829  // Render flexform:
830  $this->traverseFlexFormXMLData_recurse($sheetData['ROOT']['el'], $editData['data'][$sheetKey]['lDEF'], $PA, 'data/' . $sheetKey . '/lDEF');
831  } else {
832  return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheetKey . '".';
833  }
834  }
835  return true;
836  }
837 
847  public function traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = '')
848  {
849  if (is_array($dataStruct)) {
850  foreach ($dataStruct as $key => $value) {
851  // The value of each entry must be an array.
852  if (is_array($value)) {
853  if ($value['type'] === 'array') {
854  // Array (Section) traversal
855  if ($value['section']) {
856  $cc = 0;
857  if (is_array($editData[$key]['el'])) {
858  if ($this->reNumberIndexesOfSectionData) {
859  $temp = [];
860  $c3 = 0;
861  foreach ($editData[$key]['el'] as $v3) {
862  $temp[++$c3] = $v3;
863  }
864  $editData[$key]['el'] = $temp;
865  }
866  foreach ($editData[$key]['el'] as $k3 => $v3) {
867  if (is_array($v3)) {
868  $cc = $k3;
869  $theType = key($v3);
870  $theDat = $v3[$theType];
871  $newSectionEl = $value['el'][$theType];
872  if (is_array($newSectionEl)) {
873  $this->traverseFlexFormXMLData_recurse([$theType => $newSectionEl], [$theType => $theDat], $PA, $path . '/' . $key . '/el/' . $cc);
874  }
875  }
876  }
877  }
878  } else {
879  // Array traversal
880  if (is_array($editData) && is_array($editData[$key])) {
881  $this->traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
882  }
883  }
884  } elseif (is_array($value['TCEforms']['config'])) {
885  // Processing a field value:
886  foreach ($PA['vKeys'] as $vKey) {
887  $vKey = 'v' . $vKey;
888  // Call back
889  if ($PA['callBackMethod_value'] && is_array($editData) && is_array($editData[$key])) {
890  $this->executeCallBackMethod($PA['callBackMethod_value'], [$value, $editData[$key][$vKey], $PA, $path . '/' . $key . '/' . $vKey, $this]);
891  }
892  }
893  }
894  }
895  }
896  }
897  }
898 
906  protected function executeCallBackMethod($methodName, array $parameterArray)
907  {
908  return call_user_func_array([$this->callBackObj, $methodName], $parameterArray);
909  }
910 
911  /***********************************
912  *
913  * Processing functions
914  *
915  ***********************************/
925  public function cleanFlexFormXML($table, $field, $row)
926  {
927  // New structure:
928  $this->cleanFlexFormXML = [];
929  // Create and call iterator object:
930  $flexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
931  $flexObj->reNumberIndexesOfSectionData = true;
932  $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
933  return $this->flexArray2Xml($this->cleanFlexFormXML, true);
934  }
935 
946  public function cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
947  {
948  // Just setting value in our own result array, basically replicating the structure:
949  $pObj->setArrayValueByPath($path, $this->cleanFlexFormXML, $data);
950  }
951 
952  /***********************************
953  *
954  * Multi purpose functions
955  *
956  ***********************************/
964  public function &getArrayValueByPath($pathArray, &$array)
965  {
966  if (!is_array($pathArray)) {
967  $pathArray = explode('/', $pathArray);
968  }
969  if (is_array($array) && !empty($pathArray)) {
970  $key = array_shift($pathArray);
971  if (isset($array[$key])) {
972  if (empty($pathArray)) {
973  return $array[$key];
974  }
975  return $this->getArrayValueByPath($pathArray, $array[$key]);
976  }
977  return null;
978  }
979  }
980 
989  public function setArrayValueByPath($pathArray, &$array, $value)
990  {
991  if (isset($value)) {
992  if (!is_array($pathArray)) {
993  $pathArray = explode('/', $pathArray);
994  }
995  if (is_array($array) && !empty($pathArray)) {
996  $key = array_shift($pathArray);
997  if (empty($pathArray)) {
998  $array[$key] = $value;
999  return true;
1000  }
1001  if (!isset($array[$key])) {
1002  $array[$key] = [];
1003  }
1004  return $this->setArrayValueByPath($pathArray, $array[$key], $value);
1005  }
1006  }
1007  }
1008 
1016  public function flexArray2Xml($array, $addPrologue = false)
1017  {
1018  if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
1019  $this->flexArray2Xml_options['useCDATA'] = 1;
1020  }
1021  $output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $this->flexArray2Xml_options);
1022  if ($addPrologue) {
1023  $output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . $output;
1024  }
1025  return $output;
1026  }
1027 }
getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row)
static array2xml(array $array, $NSprefix='', $level=0, $docTag='phparray', $spaceInd=0, array $options=[], array $stackData=[])
getDataStructureIdentifierFromRecord(array $fieldTca, string $tableName, string $fieldName, array $row)
getDataStructureIdentifierFromTcaArray(array $fieldTca, string $tableName, string $fieldName, array $row)
static getFileAbsFileName($filename, $_=null, $_2=null)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
executeCallBackMethod($methodName, array $parameterArray)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
static makeInstance($className,... $constructorArguments)
traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path='')
static fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch=false)
cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj)
traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value)
static xml2array($string, $NSprefix='', $reportDocTag=false)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']