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