[BUGFIX] Exception in EXT:form due to invalid array lookup
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Hooks / DataStructureIdentifierHook.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Hooks;
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
18 use TYPO3\CMS\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Extbase\Object\ObjectManager;
21 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
22 use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
23 use TYPO3\CMS\Form\Service\TranslationService;
24
25 /**
26 * Hooks into flex form handling of backend for tt_content form elements:
27 *
28 * * Adds existing forms to flex form drop down list
29 * * Adds finisher settings if "override finishers" is active
30 *
31 * Scope: backend
32 * @internal
33 */
34 class DataStructureIdentifierHook
35 {
36
37 /**
38 * The data structure depends on a current form selection (persistenceIdentifier)
39 * and if the field "overrideFinishers" is active. Add both to the identifier to
40 * hand these information over to parseDataStructureByIdentifierPostProcess() hook.
41 *
42 * @param array $fieldTca Incoming field TCA
43 * @param string $tableName Handled table
44 * @param string $fieldName Handled field
45 * @param array $row Current data row
46 * @param array $identifier Already calculated identifier
47 * @return array Modified identifier
48 */
49 public function getDataStructureIdentifierPostProcess(
50 array $fieldTca,
51 string $tableName,
52 string $fieldName,
53 array $row,
54 array $identifier
55 ): array {
56 if ($tableName === 'tt_content' && $fieldName === 'pi_flexform' && $row['CType'] === 'form_formframework') {
57 $currentFlexData = [];
58 if (!is_array($row['pi_flexform']) && !empty($row['pi_flexform'])) {
59 $currentFlexData = GeneralUtility::xml2array($row['pi_flexform']);
60 }
61
62 // Add selected form value
63 $identifier['ext-form-persistenceIdentifier'] = '';
64 if (!empty($currentFlexData['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'])) {
65 $identifier['ext-form-persistenceIdentifier'] = $currentFlexData['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'];
66 }
67
68 // Add bool - finisher override active or not
69 $identifier['ext-form-overrideFinishers'] = false;
70 if (
71 isset($currentFlexData['data']['sDEF']['lDEF']['settings.overrideFinishers']['vDEF'])
72 && (int)$currentFlexData['data']['sDEF']['lDEF']['settings.overrideFinishers']['vDEF'] === 1
73 ) {
74 $identifier['ext-form-overrideFinishers'] = true;
75 }
76 }
77 return $identifier;
78 }
79
80 /**
81 * Returns a modified flexform data array.
82 *
83 * This adds the list of existing form definitions to the form selection drop down
84 * and adds sheets to override finisher settings if requested.
85 *
86 * @param array $dataStructure
87 * @param array $identifier
88 * @return array
89 */
90 public function parseDataStructureByIdentifierPostProcess(array $dataStructure, array $identifier): array
91 {
92 if (isset($identifier['ext-form-persistenceIdentifier'])) {
93 // Add list of existing forms to drop down if we find our key in the identifier
94 $formPersistenceManager = GeneralUtility::makeInstance(ObjectManager::class)->get(FormPersistenceManagerInterface::class);
95 foreach ($formPersistenceManager->listForms() as $form) {
96 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
97 $form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
98 $form['persistenceIdentifier'],
99 ];
100 }
101
102 // If a specific form is selected and if finisher override is active, add finisher sheets
103 if (!empty($identifier['ext-form-persistenceIdentifier'])
104 && isset($identifier['ext-form-overrideFinishers'])
105 && $identifier['ext-form-overrideFinishers'] === true
106 ) {
107 $persistenceIdentifier = $identifier['ext-form-persistenceIdentifier'];
108 $formDefinition = $formPersistenceManager->load($persistenceIdentifier);
109 $newSheets = $this->getAdditionalFinisherSheets($persistenceIdentifier, $formDefinition);
110 ArrayUtility::mergeRecursiveWithOverrule(
111 $dataStructure,
112 $newSheets
113 );
114 }
115 }
116 return $dataStructure;
117 }
118
119 /**
120 * Returns additional flexform sheets with finisher fields
121 *
122 * @param string $persistenceIdentifier Current persistence identifier
123 * @param array $formDefinition The form definition
124 * @return array
125 */
126 protected function getAdditionalFinisherSheets(string $persistenceIdentifier, array $formDefinition): array
127 {
128 if (!isset($formDefinition['finishers']) || empty($formDefinition['finishers'])) {
129 return [];
130 }
131
132 $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
133 $prototypeConfiguration = GeneralUtility::makeInstance(ObjectManager::class)
134 ->get(ConfigurationService::class)
135 ->getPrototypeConfiguration($prototypeName);
136
137 if (!isset($prototypeConfiguration['finishersDefinition']) || empty($prototypeConfiguration['finishersDefinition'])) {
138 return [];
139 }
140
141 $formIdentifier = $formDefinition['identifier'];
142 $finishersDefinition = $prototypeConfiguration['finishersDefinition'];
143
144 $sheets = ['sheets' => []];
145 foreach ($formDefinition['finishers'] as $finisherValue) {
146 $finisherIdentifier = $finisherValue['identifier'];
147 if (!isset($finishersDefinition[$finisherIdentifier]['FormEngine']['elements'])) {
148 continue;
149 }
150 $sheetIdentifier = md5(
151 implode('', [
152 $persistenceIdentifier,
153 $prototypeName,
154 $formIdentifier,
155 $finisherIdentifier
156 ])
157 );
158
159 $translationFile = $finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'];
160 $finishersDefinition[$finisherIdentifier]['FormEngine'] = TranslationService::getInstance()->translateValuesRecursive(
161 $finishersDefinition[$finisherIdentifier]['FormEngine'],
162 $translationFile
163 );
164 $finisherLabel = $finishersDefinition[$finisherIdentifier]['FormEngine']['label'];
165 $sheet = $this->initializeNewSheetArray($sheetIdentifier, $finisherLabel);
166
167 $sheetElements = [];
168 foreach ($finisherValue['options'] as $optionKey => $optionValue) {
169 if (is_array($optionValue)) {
170 $optionKey = $optionKey . '.' . $this->extractDottedPathToLastElement($finisherValue['options'][$optionKey]);
171 try {
172 $elementConfiguration = ArrayUtility::getValueByPath(
173 $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'],
174 $optionKey,
175 '.'
176 );
177 } catch (\RuntimeException $exception) {
178 $elementConfiguration = null;
179 }
180 try {
181 $optionValue = ArrayUtility::getValueByPath($finisherValue['options'], $optionKey, '.');
182 } catch (\RuntimeException $exception) {
183 $optionValue = null;
184 }
185 } else {
186 $elementConfiguration = $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'][$optionKey];
187 }
188
189 if (empty($elementConfiguration)) {
190 continue;
191 }
192
193 if (empty($optionValue)) {
194 $elementConfiguration['label'] .= ' (default: "[Empty]")';
195 } else {
196 $elementConfiguration['label'] .= ' (default: "' . $optionValue . '")';
197 }
198 $elementConfiguration['config']['default'] = $optionValue;
199 $sheetElements['settings.finishers.' . $finisherIdentifier . '.' . $optionKey] = $elementConfiguration;
200 }
201
202 ksort($sheetElements);
203
204 $sheet[$sheetIdentifier]['ROOT']['el'] = $sheetElements;
205 ArrayUtility::mergeRecursiveWithOverrule($sheets['sheets'], $sheet);
206 }
207 if (empty($sheets['sheets'])) {
208 return [];
209 }
210
211 return $sheets;
212 }
213
214 /**
215 * Boilerplate XML array of a new sheet
216 *
217 * @param string $sheetIdentifier
218 * @param string $finisherName
219 * @throws \InvalidArgumentException
220 * @return array
221 */
222 protected function initializeNewSheetArray(string $sheetIdentifier, string $finisherName): array
223 {
224 if (empty($sheetIdentifier)) {
225 throw new \InvalidArgumentException('$sheetIdentifier must not be empty.', 1472060918);
226 }
227 if (empty($finisherName)) {
228 throw new \InvalidArgumentException('$finisherName must not be empty.', 1472060919);
229 }
230
231 return [
232 $sheetIdentifier => [
233 'ROOT' => [
234 'TCEforms' => [
235 'sheetTitle' => $finisherName,
236 ],
237 'type' => 'array',
238 'el' => [],
239 ],
240 ],
241 ];
242 }
243
244 /**
245 * Recursive helper to implode a nested array to a dotted path notation
246 *
247 * @param array $array
248 * @return string
249 */
250 protected function extractDottedPathToLastElement(array $array): string
251 {
252 $dottedPath = key($array);
253 foreach ($array as $key => $value) {
254 if (is_array($value)) {
255 $dottedPath = $dottedPath . '.' . $this->extractDottedPathToLastElement($value);
256 }
257 }
258 return $dottedPath;
259 }
260 }