[BUGFIX] Preserve order of finisher options in Form CE
[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\Messaging\AbstractMessage;
19 use TYPO3\CMS\Core\Messaging\FlashMessage;
20 use TYPO3\CMS\Core\Messaging\FlashMessageService;
21 use TYPO3\CMS\Core\Utility\ArrayUtility;
22 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Extbase\Object\ObjectManager;
25 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
26 use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
27 use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
28 use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
29 use TYPO3\CMS\Form\Service\TranslationService;
30 use TYPO3\CMS\Lang\LanguageService;
31
32 /**
33 * Hooks into flex form handling of backend for tt_content form elements:
34 *
35 * * Adds existing forms to flex form drop down list
36 * * Adds finisher settings if "override finishers" is active
37 *
38 * Scope: backend
39 * @internal
40 */
41 class DataStructureIdentifierHook
42 {
43
44 /**
45 * Localisation prefix
46 */
47 const L10N_PREFIX = 'LLL:EXT:form/Resources/Private/Language/Database.xlf:';
48
49 /**
50 * The data structure depends on a current form selection (persistenceIdentifier)
51 * and if the field "overrideFinishers" is active. Add both to the identifier to
52 * hand these information over to parseDataStructureByIdentifierPostProcess() hook.
53 *
54 * @param array $fieldTca Incoming field TCA
55 * @param string $tableName Handled table
56 * @param string $fieldName Handled field
57 * @param array $row Current data row
58 * @param array $identifier Already calculated identifier
59 * @return array Modified identifier
60 */
61 public function getDataStructureIdentifierPostProcess(
62 array $fieldTca,
63 string $tableName,
64 string $fieldName,
65 array $row,
66 array $identifier
67 ): array {
68 if ($tableName === 'tt_content' && $fieldName === 'pi_flexform' && $row['CType'] === 'form_formframework') {
69 $currentFlexData = [];
70 if (!empty($row['pi_flexform']) && !\is_array($row['pi_flexform'])) {
71 $currentFlexData = GeneralUtility::xml2array($row['pi_flexform']);
72 }
73
74 // Add selected form value
75 $identifier['ext-form-persistenceIdentifier'] = '';
76 if (!empty($currentFlexData['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'])) {
77 $identifier['ext-form-persistenceIdentifier'] = $currentFlexData['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'];
78 }
79
80 // Add bool - finisher override active or not
81 $identifier['ext-form-overrideFinishers'] = false;
82 if (
83 isset($currentFlexData['data']['sDEF']['lDEF']['settings.overrideFinishers']['vDEF'])
84 && (int)$currentFlexData['data']['sDEF']['lDEF']['settings.overrideFinishers']['vDEF'] === 1
85 ) {
86 $identifier['ext-form-overrideFinishers'] = true;
87 }
88 }
89 return $identifier;
90 }
91
92 /**
93 * Returns a modified flexform data array.
94 *
95 * This adds the list of existing form definitions to the form selection drop down
96 * and adds sheets to override finisher settings if requested.
97 *
98 * @param array $dataStructure
99 * @param array $identifier
100 * @return array
101 */
102 public function parseDataStructureByIdentifierPostProcess(array $dataStructure, array $identifier): array
103 {
104 if (isset($identifier['ext-form-persistenceIdentifier'])) {
105 try {
106 // Add list of existing forms to drop down if we find our key in the identifier
107 $formPersistenceManager = GeneralUtility::makeInstance(ObjectManager::class)->get(FormPersistenceManagerInterface::class);
108 $formIsAccessible = false;
109 foreach ($formPersistenceManager->listForms() as $form) {
110 $invalidFormDefinition = $form['invalid'] ?? false;
111 $hasDeprecatedFileExtension = $form['deprecatedFileExtension'] ?? false;
112
113 if ($form['location'] === 'storage' && $hasDeprecatedFileExtension) {
114 continue;
115 }
116
117 if ($form['persistenceIdentifier'] === $identifier['ext-form-persistenceIdentifier']) {
118 $formIsAccessible = true;
119 }
120
121 if ($invalidFormDefinition || $hasDeprecatedFileExtension) {
122 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
123 $form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
124 $form['persistenceIdentifier'],
125 'overlay-missing'
126 ];
127 } else {
128 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
129 $form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
130 $form['persistenceIdentifier'],
131 'content-form'
132 ];
133 }
134 }
135
136 if (!empty($identifier['ext-form-persistenceIdentifier']) && !$formIsAccessible) {
137 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
138 sprintf(
139 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.inaccessiblePersistenceIdentifier'),
140 $identifier['ext-form-persistenceIdentifier']
141 ),
142 $identifier['ext-form-persistenceIdentifier'],
143 ];
144 }
145
146 // If a specific form is selected and if finisher override is active, add finisher sheets
147 if (!empty($identifier['ext-form-persistenceIdentifier'])
148 && $formIsAccessible
149 && isset($identifier['ext-form-overrideFinishers'])
150 && $identifier['ext-form-overrideFinishers'] === true
151 ) {
152 $persistenceIdentifier = $identifier['ext-form-persistenceIdentifier'];
153 $formDefinition = $formPersistenceManager->load($persistenceIdentifier);
154 $newSheets = $this->getAdditionalFinisherSheets($persistenceIdentifier, $formDefinition);
155 ArrayUtility::mergeRecursiveWithOverrule(
156 $dataStructure,
157 $newSheets
158 );
159 }
160 } catch (NoSuchFileException $e) {
161 $dataStructure = $this->addSelectedPersistenceIdentifier($identifier['ext-form-persistenceIdentifier'], $dataStructure);
162 $this->addInvalidFrameworkConfigurationFlashMessage($e);
163 } catch (ParseErrorException $e) {
164 $dataStructure = $this->addSelectedPersistenceIdentifier($identifier['ext-form-persistenceIdentifier'], $dataStructure);
165 $this->addInvalidFrameworkConfigurationFlashMessage($e);
166 }
167 }
168 return $dataStructure;
169 }
170
171 /**
172 * Returns additional flexform sheets with finisher fields
173 *
174 * @param string $persistenceIdentifier Current persistence identifier
175 * @param array $formDefinition The form definition
176 * @return array
177 */
178 protected function getAdditionalFinisherSheets(string $persistenceIdentifier, array $formDefinition): array
179 {
180 if (!isset($formDefinition['finishers']) || empty($formDefinition['finishers'])) {
181 return [];
182 }
183
184 $prototypeName = $formDefinition['prototypeName'] ?? 'standard';
185 $prototypeConfiguration = GeneralUtility::makeInstance(ObjectManager::class)
186 ->get(ConfigurationService::class)
187 ->getPrototypeConfiguration($prototypeName);
188
189 if (!isset($prototypeConfiguration['finishersDefinition']) || empty($prototypeConfiguration['finishersDefinition'])) {
190 return [];
191 }
192
193 $formIdentifier = $formDefinition['identifier'];
194 $finishersDefinition = $prototypeConfiguration['finishersDefinition'];
195
196 $sheets = ['sheets' => []];
197 foreach ($formDefinition['finishers'] as $finisherValue) {
198 $finisherIdentifier = $finisherValue['identifier'];
199 if (!isset($finishersDefinition[$finisherIdentifier]['FormEngine']['elements'])) {
200 continue;
201 }
202 $sheetIdentifier = md5(
203 implode('', [
204 $persistenceIdentifier,
205 $prototypeName,
206 $formIdentifier,
207 $finisherIdentifier
208 ])
209 );
210
211 if (isset($finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'])) {
212 $translationFile = $finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'];
213 } else {
214 $translationFile = $prototypeConfiguration['formEngine']['translationFile'];
215 }
216
217 $finishersDefinition[$finisherIdentifier]['FormEngine'] = TranslationService::getInstance()->translateValuesRecursive(
218 $finishersDefinition[$finisherIdentifier]['FormEngine'],
219 $translationFile
220 );
221 $finisherLabel = $finishersDefinition[$finisherIdentifier]['FormEngine']['label'];
222 $sheet = $this->initializeNewSheetArray($sheetIdentifier, $finisherLabel);
223
224 $sheetElements = [];
225 foreach ($finisherValue['options'] as $optionKey => $optionValue) {
226 if (is_array($optionValue)) {
227 $optionKey = $optionKey . '.' . $this->implodeArrayKeys($finisherValue['options'][$optionKey]);
228 try {
229 $elementConfiguration = ArrayUtility::getValueByPath(
230 $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'],
231 $optionKey,
232 '.'
233 );
234 } catch (MissingArrayPathException $exception) {
235 $elementConfiguration = null;
236 }
237 try {
238 $optionValue = ArrayUtility::getValueByPath($finisherValue['options'], $optionKey, '.');
239 } catch (MissingArrayPathException $exception) {
240 $optionValue = null;
241 }
242 } else {
243 $elementConfiguration = $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'][$optionKey];
244 }
245
246 if (empty($elementConfiguration)) {
247 continue;
248 }
249
250 if (empty($optionValue)) {
251 $elementConfiguration['label'] .= ' (default: "[Empty]")';
252 } else {
253 $elementConfiguration['label'] .= ' (default: "' . $optionValue . '")';
254 }
255 $elementConfiguration['config']['default'] = $optionValue;
256 $sheetElements['settings.finishers.' . $finisherIdentifier . '.' . $optionKey] = $elementConfiguration;
257 }
258
259 $sheet[$sheetIdentifier]['ROOT']['el'] = $sheetElements;
260 ArrayUtility::mergeRecursiveWithOverrule($sheets['sheets'], $sheet);
261 }
262 if (empty($sheets['sheets'])) {
263 return [];
264 }
265
266 return $sheets;
267 }
268
269 /**
270 * Boilerplate XML array of a new sheet
271 *
272 * @param string $sheetIdentifier
273 * @param string $finisherName
274 * @throws \InvalidArgumentException
275 * @return array
276 */
277 protected function initializeNewSheetArray(string $sheetIdentifier, string $finisherName): array
278 {
279 if (empty($sheetIdentifier)) {
280 throw new \InvalidArgumentException('$sheetIdentifier must not be empty.', 1472060918);
281 }
282 if (empty($finisherName)) {
283 throw new \InvalidArgumentException('$finisherName must not be empty.', 1472060919);
284 }
285
286 return [
287 $sheetIdentifier => [
288 'ROOT' => [
289 'TCEforms' => [
290 'sheetTitle' => $finisherName,
291 ],
292 'type' => 'array',
293 'el' => [],
294 ],
295 ],
296 ];
297 }
298
299 /**
300 * Recursive helper to implode a nested array to a dotted path notation
301 *
302 * ['a' => [ 'b' => 42 ] ] becomes 'a.b'
303 *
304 * @param array $nestedArray
305 * @return string
306 */
307 protected function implodeArrayKeys(array $nestedArray): string
308 {
309 $dottedPath = (string)key($nestedArray);
310 if (is_array($nestedArray[$dottedPath])) {
311 $dottedPath .= '.' . $this->implodeArrayKeys($nestedArray[$dottedPath]);
312 }
313 return $dottedPath;
314 }
315
316 /**
317 * @param string $persistenceIdentifier
318 * @param array $dataStructure
319 * @return array
320 */
321 protected function addSelectedPersistenceIdentifier(string $persistenceIdentifier, array $dataStructure): array
322 {
323 if (!empty($persistenceIdentifier)) {
324 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
325 sprintf(
326 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.inaccessiblePersistenceIdentifier'),
327 $persistenceIdentifier
328 ),
329 $persistenceIdentifier,
330 ];
331 }
332
333 return $dataStructure;
334 }
335
336 /**
337 * @param \Exception $e
338 */
339 protected function addInvalidFrameworkConfigurationFlashMessage(\Exception $e)
340 {
341 $messageText = sprintf(
342 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.invalidFrameworkConfiguration.text'),
343 $e->getMessage()
344 );
345
346 GeneralUtility::makeInstance(ObjectManager::class)
347 ->get(FlashMessageService::class)
348 ->getMessageQueueByIdentifier('core.template.flashMessages')
349 ->enqueue(
350 GeneralUtility::makeInstance(
351 FlashMessage::class,
352 $messageText,
353 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.invalidFrameworkConfiguration.title'),
354 AbstractMessage::ERROR,
355 true
356 )
357 );
358 }
359
360 /**
361 * @return LanguageService
362 */
363 protected function getLanguageService(): LanguageService
364 {
365 return $GLOBALS['LANG'];
366 }
367 }